diff --git a/.eslintrc.js b/.eslintrc.js index fc82e75ce2..bc2a142c2d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,7 +19,7 @@ module.exports = { }, overrides: [{ - "files": ["src/**/*.{ts, tsx}"], + "files": ["src/**/*.{ts,tsx}"], "extends": ["matrix-org/ts"], "rules": { // We disable this while we're transitioning diff --git a/.stylelintrc.js b/.stylelintrc.js index 1690f2186f..313102ea83 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -17,7 +17,7 @@ module.exports = { "at-rule-no-unknown": null, "no-descending-specificity": null, "scss/at-rule-no-unknown": [true, { - // https://github.com/vector-im/riot-web/issues/10544 + // https://github.com/vector-im/element-web/issues/10544 "ignoreAtRules": ["define-mixin"], }], } diff --git a/CHANGELOG.md b/CHANGELOG.md index d944d58f36..47bffe432f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,206 @@ +Changes in [3.3.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.3.0) (2020-09-01) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.3.0-rc.1...v3.3.0) + + * Upgrade to JS SDK 8.2.0 + +Changes in [3.3.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.3.0-rc.1) (2020-08-26) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.2.0...v3.3.0-rc.1) + + * Upgrade to JS SDK 8.2.0-rc.1 + * Update from Weblate + [\#5146](https://github.com/matrix-org/matrix-react-sdk/pull/5146) + * BaseAvatar avoid initial render with default avatar + [\#5142](https://github.com/matrix-org/matrix-react-sdk/pull/5142) + * Enforce Secure Backup completion when requested by HS + [\#5130](https://github.com/matrix-org/matrix-react-sdk/pull/5130) + * Communities v2 prototype: Explore rooms, global state, and default room + [\#5139](https://github.com/matrix-org/matrix-react-sdk/pull/5139) + * Add communities v2 prototyping feature flag + initial tag panel prototypes + [\#5133](https://github.com/matrix-org/matrix-react-sdk/pull/5133) + * Remove some unused components + [\#5134](https://github.com/matrix-org/matrix-react-sdk/pull/5134) + * Allow avatar image view for 1:1 rooms + [\#5137](https://github.com/matrix-org/matrix-react-sdk/pull/5137) + * Send mx_local_settings in rageshake + [\#5136](https://github.com/matrix-org/matrix-react-sdk/pull/5136) + * Run all room leaving behaviour through a single function + [\#5132](https://github.com/matrix-org/matrix-react-sdk/pull/5132) + * Add clarifying comment in media device selection + [\#5131](https://github.com/matrix-org/matrix-react-sdk/pull/5131) + * Settings v3: Feature flag changes + [\#5124](https://github.com/matrix-org/matrix-react-sdk/pull/5124) + * Clear url previews if they all get edited out of the event + [\#5129](https://github.com/matrix-org/matrix-react-sdk/pull/5129) + * Consider tab completions as modifications for editing purposes to unlock + sending + [\#5128](https://github.com/matrix-org/matrix-react-sdk/pull/5128) + * Use matrix-doc for SAS emoji translations + [\#5125](https://github.com/matrix-org/matrix-react-sdk/pull/5125) + * Add a rageshake function to download the logs locally + [\#3849](https://github.com/matrix-org/matrix-react-sdk/pull/3849) + * Room List filtering visual tweaks + [\#5123](https://github.com/matrix-org/matrix-react-sdk/pull/5123) + * Make reply preview not an overlay so you can see new messages + [\#5072](https://github.com/matrix-org/matrix-react-sdk/pull/5072) + * Allow room tile context menu when minimized using right click + [\#5113](https://github.com/matrix-org/matrix-react-sdk/pull/5113) + * Add null guard to group inviter for corrupted groups + [\#5121](https://github.com/matrix-org/matrix-react-sdk/pull/5121) + * Room List styling tweaks + [\#5118](https://github.com/matrix-org/matrix-react-sdk/pull/5118) + * Fix corner rounding on images not always affecting right side + [\#5120](https://github.com/matrix-org/matrix-react-sdk/pull/5120) + * Change add room action for rooms to context menu + [\#5108](https://github.com/matrix-org/matrix-react-sdk/pull/5108) + * Switch out the globe icon and colour it depending on theme + [\#5106](https://github.com/matrix-org/matrix-react-sdk/pull/5106) + * Message Action Bar watch for event send changes + [\#5115](https://github.com/matrix-org/matrix-react-sdk/pull/5115) + * Put message previews for Emoji behind Labs + [\#5110](https://github.com/matrix-org/matrix-react-sdk/pull/5110) + * Fix styling for selected community marker + [\#5107](https://github.com/matrix-org/matrix-react-sdk/pull/5107) + * Fix action bar safe area regression + [\#5111](https://github.com/matrix-org/matrix-react-sdk/pull/5111) + * Fix /op slash command + [\#5109](https://github.com/matrix-org/matrix-react-sdk/pull/5109) + +Changes in [3.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.2.0) (2020-08-17) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.2.0-rc.1...v3.2.0) + + * Upgrade to JS SDK 8.1.0 + * [Release] Fix corner rounding on images not always affecting right side + [\#5122](https://github.com/matrix-org/matrix-react-sdk/pull/5122) + * [Release] Message Action Bar watch for event send changes + [\#5116](https://github.com/matrix-org/matrix-react-sdk/pull/5116) + * Fix /op slash command to release + [\#5114](https://github.com/matrix-org/matrix-react-sdk/pull/5114) + * Fix action bar safe area regression + [\#5112](https://github.com/matrix-org/matrix-react-sdk/pull/5112) + +Changes in [3.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.2.0-rc.1) (2020-08-13) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.1.0...v3.2.0-rc.1) + + * Upgrade to JS SDK 8.1.0-rc.1 + * Update from Weblate + [\#5105](https://github.com/matrix-org/matrix-react-sdk/pull/5105) + * padding the timeline so that its scrollbar has its own space from the + resizer + [\#5103](https://github.com/matrix-org/matrix-react-sdk/pull/5103) + * Try to close notification on all platforms which support it, not just + electron + [\#5102](https://github.com/matrix-org/matrix-react-sdk/pull/5102) + * Fix exception when stripping replies from an event with a non-string body + [\#5101](https://github.com/matrix-org/matrix-react-sdk/pull/5101) + * Quick win session 24/07/2020 + [\#5056](https://github.com/matrix-org/matrix-react-sdk/pull/5056) + * Remove rebranding toast + [\#5100](https://github.com/matrix-org/matrix-react-sdk/pull/5100) + * Generate previews for rooms when the option changes + [\#5098](https://github.com/matrix-org/matrix-react-sdk/pull/5098) + * Fix Bridge Settings tab + [\#5095](https://github.com/matrix-org/matrix-react-sdk/pull/5095) + * get screen type from app prop + [\#5081](https://github.com/matrix-org/matrix-react-sdk/pull/5081) + * Update rageshake app name + [\#5093](https://github.com/matrix-org/matrix-react-sdk/pull/5093) + * Factor out Iconized Context menu for reusability + [\#5085](https://github.com/matrix-org/matrix-react-sdk/pull/5085) + * Decouple Audible notifications from Desktop notifications + [\#5088](https://github.com/matrix-org/matrix-react-sdk/pull/5088) + * Make the room sublist show more/less buttons treeitems + [\#5087](https://github.com/matrix-org/matrix-react-sdk/pull/5087) + * Share and debug master cross-signing key + [\#5092](https://github.com/matrix-org/matrix-react-sdk/pull/5092) + * Create Map comparison utilities and convert Hooks to Typescript + [\#5086](https://github.com/matrix-org/matrix-react-sdk/pull/5086) + * Fix room list scrolling in Safari + [\#5090](https://github.com/matrix-org/matrix-react-sdk/pull/5090) + * Replace Riot with Element in docs and comments + [\#5083](https://github.com/matrix-org/matrix-react-sdk/pull/5083) + * When the room view isn't active don't highlight it in room list + [\#5027](https://github.com/matrix-org/matrix-react-sdk/pull/5027) + * remove emoji icons in autocomplete/reply by designer request + [\#5073](https://github.com/matrix-org/matrix-react-sdk/pull/5073) + * Add title and icon to empty state of file and notification panel + [\#5079](https://github.com/matrix-org/matrix-react-sdk/pull/5079) + * Mass redact ignore room creation events + [\#5045](https://github.com/matrix-org/matrix-react-sdk/pull/5045) + * Replace all chevrons with a single icon + [\#5067](https://github.com/matrix-org/matrix-react-sdk/pull/5067) + * Replace i18n generation script with something matching our project + [\#5077](https://github.com/matrix-org/matrix-react-sdk/pull/5077) + * Handle tag changes in sticky room updates + [\#5078](https://github.com/matrix-org/matrix-react-sdk/pull/5078) + * Remove leftover bits of TSLint + [\#5075](https://github.com/matrix-org/matrix-react-sdk/pull/5075) + * Clean up documentation of Whenable + fix other code concerns + [\#5076](https://github.com/matrix-org/matrix-react-sdk/pull/5076) + * Center the jump down/up icon, looks misaligned + [\#5074](https://github.com/matrix-org/matrix-react-sdk/pull/5074) + * [WIP] Support a new settings structure + [\#5058](https://github.com/matrix-org/matrix-react-sdk/pull/5058) + * Convert SettingsStore to TypeScript + [\#5062](https://github.com/matrix-org/matrix-react-sdk/pull/5062) + +Changes in [3.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.1.0) (2020-08-05) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.1.0-rc.1...v3.1.0) + + * Upgrade JS SDK to 8.0.1 + * Fix room list scrolling in Safari + [\#5091](https://github.com/matrix-org/matrix-react-sdk/pull/5091) + * Add null guard in InviteDialog + [\#5084](https://github.com/matrix-org/matrix-react-sdk/pull/5084) + * Add null guard in InviteDialog + [\#5082](https://github.com/matrix-org/matrix-react-sdk/pull/5082) + * Handle tag changes in sticky room updates + [\#5080](https://github.com/matrix-org/matrix-react-sdk/pull/5080) + +Changes in [3.1.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.1.0-rc.1) (2020-07-31) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.0.0...v3.1.0-rc.1) + + * Upgrade JS SDK to 8.0.1-rc.1 + * Update from Weblate + [\#5071](https://github.com/matrix-org/matrix-react-sdk/pull/5071) + * Add local echo for notifications in the new room list + [\#5065](https://github.com/matrix-org/matrix-react-sdk/pull/5065) + * Fix various small regressions in the room list's behaviour + [\#5070](https://github.com/matrix-org/matrix-react-sdk/pull/5070) + * Remove redundant lint dependencies + [\#5059](https://github.com/matrix-org/matrix-react-sdk/pull/5059) + * Fix key backup warning on soft logout page + [\#5069](https://github.com/matrix-org/matrix-react-sdk/pull/5069) + * Bump elliptic from 6.5.2 to 6.5.3 + [\#5066](https://github.com/matrix-org/matrix-react-sdk/pull/5066) + * Fix crash on logging in again after soft logout + [\#5068](https://github.com/matrix-org/matrix-react-sdk/pull/5068) + * Convert right_panel to TS + [\#5036](https://github.com/matrix-org/matrix-react-sdk/pull/5036) + * Remove all unreferenced images + [\#5063](https://github.com/matrix-org/matrix-react-sdk/pull/5063) + * Provide nicer error for no known servers error when accepting an invite + [\#5061](https://github.com/matrix-org/matrix-react-sdk/pull/5061) + * add logging for keytar/pickle key + [\#5057](https://github.com/matrix-org/matrix-react-sdk/pull/5057) + * Don't speak the outgoing message if it is in the Sending state. + [\#4075](https://github.com/matrix-org/matrix-react-sdk/pull/4075) + * Remove poorly contrasted "dark style" heading in Room Preview Bar + [\#5052](https://github.com/matrix-org/matrix-react-sdk/pull/5052) + * Fix Query Matcher regression with certain unhomoglyph'd characters + [\#5050](https://github.com/matrix-org/matrix-react-sdk/pull/5050) + * Fix handlebar interaction + [\#4989](https://github.com/matrix-org/matrix-react-sdk/pull/4989) + * Minor improvements to filtering performance + [\#5054](https://github.com/matrix-org/matrix-react-sdk/pull/5054) + * Fix TextWithTooltip "leaking" tooltip wrappers + [\#5055](https://github.com/matrix-org/matrix-react-sdk/pull/5055) + Changes in [3.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.0.0) (2020-07-27) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.1...v3.0.0) diff --git a/README.md b/README.md index 5f5da9a40d..e468d272d0 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ a 'skin'. A skin provides: * The containing application * Zero or more 'modules' containing non-UI functionality -As of Aug 2018, the only skin that exists is `vector-im/riot-web`; it and +As of Aug 2018, the only skin that exists is `vector-im/element-web`; it and `matrix-org/matrix-react-sdk` should effectively be considered as a single project (for instance, matrix-react-sdk bugs -are currently filed against vector-im/riot-web rather than this project). +are currently filed against vector-im/element-web rather than this project). Translation Status ================== -[![Translation status](https://translate.riot.im/widgets/riot-web/-/multi-auto.svg)](https://translate.riot.im/engage/riot-web/?utm_source=widget) +[![Translation status](https://translate.riot.im/widgets/element-web/-/multi-auto.svg)](https://translate.riot.im/engage/element-web/?utm_source=widget) Developer Guide =============== @@ -41,10 +41,10 @@ https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md Code should be committed as follows: * All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components - * Riot-specific components: https://github.com/vector-im/riot-web/tree/master/src/components + * Element-specific components: https://github.com/vector-im/element-web/tree/master/src/components * In practice, `matrix-react-sdk` is still evolving so fast that the maintenance - burden of customising and overriding these components for Riot can seriously - impede development. So right now, there should be very few (if any) customisations for Riot. + burden of customising and overriding these components for Element can seriously + impede development. So right now, there should be very few (if any) customisations for Element. * CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css * Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes @@ -71,7 +71,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold: * The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css). CSS for matrix-react-sdk currently resides in - https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk. + https://github.com/vector-im/element-web/tree/master/src/skins/vector/css/matrix-react-sdk. * Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual for any but @@ -125,7 +125,7 @@ from it. Github Issues ============= -All issues should be filed under https://github.com/vector-im/riot-web/issues +All issues should be filed under https://github.com/vector-im/element-web/issues for now. Development @@ -174,5 +174,5 @@ yarn test ## End-to-End tests -Make sure you've got your Riot development server running (by doing `yarn start` in riot-web), and then in this project, run `yarn run e2etests`. +Make sure you've got your Element development server running (by doing `yarn start` in element-web), and then in this project, run `yarn run e2etests`. See `test/end-to-end-tests/README.md` for more information. diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index 00033b5b8c..f522dc2fc4 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -1,6 +1,6 @@ # The CIDER (Contenteditable-Input-Diff-Error-Reconcile) editor -The CIDER editor is a custom editor written for Riot. +The CIDER editor is a custom editor written for Element. Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project. It is used to power the composer main composer (both to send and edit messages), and might be used for other usecases where autocomplete is desired (invite box, ...). diff --git a/docs/jitsi.md b/docs/jitsi.md index 779ef79d3a..2b63ce0f72 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -25,7 +25,7 @@ which takes several parameters: be null. The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently -being served. For example, `https://riot.im/develop/jitsi.html` or `vector://webapp/jitsi.html`. +being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`. The `jitsi.html` wrapper can use the react-sdk's `WidgetApi` to communicate, making it easier to actually implement the feature. diff --git a/docs/room-list-store.md b/docs/room-list-store.md index 53f0527209..fa849e2505 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -55,7 +55,7 @@ timestamp contained within the event (generated server-side by the sender's serv This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list. -Historically, it's been the only option in Riot and extremely common in most chat applications due to +Historically, it's been the only option in Element and extremely common in most chat applications due to its relative deterministic behaviour. ### List ordering algorithm: Importance diff --git a/docs/scrolling.md b/docs/scrolling.md index 71329e5c32..a5232359a7 100644 --- a/docs/scrolling.md +++ b/docs/scrolling.md @@ -13,7 +13,7 @@ ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a j BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842. -The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/riot-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles. +The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/element-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles. The approach taken instead is to vertically align the timeline tiles to the bottom of the scroll container (using flexbox) and give the timeline inside the scroll container an explicit height, initially set to a multiple of the PAGE_SIZE (400px at time of writing) as needed by the content. When scrolled up, we can compensate for anything that grew below the viewport by changing the height of the timeline to maintain what's currently visible in the viewport without adjusting the scrollTop and hence without jumping. diff --git a/docs/settings.md b/docs/settings.md index 46e4a68fdb..4172c72c15 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -9,7 +9,7 @@ of dealing with the different levels and exposes easy to use getters and setters ## Levels Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in -order of prioirty, are: +order of priority, are: * `device` - The current user's device * `room-device` - The current user's device, but only when in a specific room * `room-account` - The current user's account, but only when in a specific room @@ -25,33 +25,10 @@ that room administrators cannot force account-only settings upon participants. ## Settings Settings are the different options a user may set or experience in the application. These are pre-defined in -`src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements: -``` -// The ID is used to reference the setting throughout the application. This must be unique. -"theSettingId": { - // The levels this setting supports is required. In `src/settings/Settings.js` there are various pre-set arrays - // for this option - they should be used where possible to avoid copy/pasting arrays across settings. - supportedLevels: [...], +`src/settings/Settings.ts` under the `SETTINGS` constant, and match the `ISetting` interface as defined there. - // The default for this setting serves two purposes: It provides a value if the setting is not defined at other - // levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it - // should be respected throughout the code. The default may be any data type. - default: false, - - // The display name has two notations: string and object. The object notation allows for different translatable - // strings to be used for different levels, while the string notation represents the string for all levels. - - displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }` - displayName: { - "room": _td("Change something for participants of this room"), - - // Note: the default will be used if the level requested (such as `device`) does not have a string defined here. - "default": _td("Change something"), - } -} -``` - -Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some settings, like the "theme" setting, are special cased in the config file): +Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some +settings, like the "theme" setting, are special cased in the config file): ```json { ... @@ -119,38 +96,29 @@ for you. If a display name cannot be found, it will return `null`. ## Features -Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are -commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and -look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting -definition and should go through the helper functions on `SettingsStore`. +Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually +a feature flag is used when a portion of the application is under development or not ready for full release yet, such +as new functionality or experimental ideas. In these cases, the feature name *should* be named with the `feature_*` +convention and must be tagged with `isFeature: true` in the setting definition. By doing so, the feature will automatically +appear in the "labs" section of the user's settings. -Although features have levels and a default value, the calculation of those options is blocked by the feature's state. -A feature's state is determined from the `SdkConfig` and is a little complex. If `enableLabs` (a legacy flag) is `true` -then the feature's state is `labs`, if it is `false`, the state is `disable`. If `enableLabs` is not set then the state -is determined from the `features` config, such as in the following: +Features can be controlled at the config level using the following structure: ```json "features": { - "feature_lazyloading": "labs" + "feature_lazyloading": true } ``` -In this example, `feature_lazyloading` is in the `labs` state. It may also be in the `enable` or `disable` state with a -similar approach. If the state is invalid, the feature is in the `disable` state. A feature's levels are only calculated -if it is in the `labs` state, therefore the default only applies in that scenario. If the state is `enable`, the feature -is always-on. -Once a feature flag has served its purpose, it is generally recommended to remove it and the associated feature flag -checks. This would enable the feature implicitly as it is part of the application now. +When `true`, the user will see the feature as enabled. Similarly, when `false` the user will see the feature as disabled. +The user will only be able to change/see these states if `showLabsSettings: true` is in the config. ### Determining if a feature is enabled -A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the -required calculations to determine if the feature is enabled based upon the configuration and user selection. +Call `SettingsStore.getValue()` as you would for any other setting. ### Enabling a feature -Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set -of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call -`SettingsStore.setFeatureEnabled`. +Call `SettingsStore.setValue("feature_name", null, SettingLevel.DEVICE, true)`. ## Setting controllers @@ -162,7 +130,7 @@ kept up to date with the setting where it is otherwise not possible. An example they can only be considered enabled if the platform supports notifications, and enabling notifications requires additional steps to actually enable notifications. -For more information, see `src/settings/controllers/SettingController.js`. +For more information, see `src/settings/controllers/SettingController.ts`. ## Local echo @@ -222,7 +190,7 @@ The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each level should be defined in this array, including `default`. -Handlers (`src/settings/handlers/SettingsHandler.js`) represent a single level and are responsible for getting and +Handlers (`src/settings/handlers/SettingsHandler.ts`) represent a single level and are responsible for getting and setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for @@ -230,7 +198,7 @@ their level (for example, a setting being renamed or using a different key from Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by only considering handlers that are supported on the platform. -Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` which acts as a wrapper around a given +Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.ts` which acts as a wrapper around a given handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated immediately upon the proxied save call succeeding or failing. @@ -240,20 +208,7 @@ Controllers are notified of changes by the `SettingsStore`, and are given the op ### Features -Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enableLabs` is -false/not set. Features are always checked against the configuration before going through the level order as they have -the option of being forced-on or forced-off for the application. This is done by the `features` section and looks -something like this: - -``` -"features": { - "feature_groups": "enable", - "feature_pinning": "disable", // the default - "feature_presence": "labs" -} -``` - -If `enableLabs` is true in the configuration, the default for features becomes `"labs"`. +See above for feature reference. ### Watchers diff --git a/docs/usercontent.md b/docs/usercontent.md index e54851dd0d..db0e34e5fa 100644 --- a/docs/usercontent.md +++ b/docs/usercontent.md @@ -5,9 +5,9 @@ letting the browser and user interact with the resulting data may be dangerous, previously `usercontent.riot.im` was used to act as a sandbox on a different origin to close the attack surface, it is now possible to do by using a combination of a sandboxed iframe and some code written into the app which consumes this SDK. -Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your riot session out from under you. +Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your Element session out from under you. -Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the riot instance to protect against XSS. +Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the Element instance to protect against XSS. It exposes a function over a postMessage API, when sent an object with the matching fields to render a download link with the Object URL: @@ -24,4 +24,4 @@ It exposes a function over a postMessage API, when sent an object with the match If only imgSrc, imgStyle and style are passed then just update the existing link without overwriting other things about it. -It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in riot-web's webpack config. +It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in element-web's webpack config. diff --git a/package.json b/package.json index 61a9a21815..9b7d80ca73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.0.0", + "version": "3.3.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -61,7 +61,6 @@ "classnames": "^2.2.6", "commonmark": "^0.29.1", "counterpart": "^0.18.6", - "create-react-class": "^15.6.3", "diff-dom": "^4.1.6", "diff-match-patch": "^1.0.5", "emojibase-data": "^5.0.1", @@ -96,6 +95,7 @@ "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.1", "sanitize-html": "^1.27.1", + "tar-js": "^0.3.0", "text-encoding-utf-8": "^1.0.2", "url": "^0.11.0", "velocity-animate": "^1.5.2", @@ -127,6 +127,7 @@ "@types/lodash": "^4.14.158", "@types/modernizr": "^3.5.3", "@types/node": "^12.12.51", + "@types/pako": "^1.0.1", "@types/qrcode": "^1.3.4", "@types/react": "^16.9", "@types/react-dom": "^16.9.8", @@ -161,9 +162,7 @@ "stylelint-config-standard": "^18.3.0", "stylelint-scss": "^3.18.0", "typescript": "^3.9.7", - "walk": "^2.3.14", - "webpack": "^4.43.0", - "webpack-cli": "^3.3.12" + "walk": "^2.3.14" }, "jest": { "testMatch": [ diff --git a/res/css/_common.scss b/res/css/_common.scss index f2d3a0e54b..a22d77f3d3 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -38,7 +38,7 @@ body { margin: 0px; // needed to match the designs correctly on macOS - // see https://github.com/vector-im/riot-web/issues/11425 + // see https://github.com/vector-im/element-web/issues/11425 -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -436,7 +436,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // TODO: Review mx_GeneralButton usage to see if it can use a different class // These classes were brought in from the old UserSettings and are included here to avoid // breaking the app. -// Ref: https://github.com/vector-im/riot-web/issues/8420 +// Ref: https://github.com/vector-im/element-web/issues/8420 .mx_GeneralButton { @mixin mx_DialogButton; display: inline; @@ -585,93 +585,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { max-width: 120px; } -// A context menu that largely fits the | [icon] [label] | format. -.mx_IconizedContextMenu { - min-width: 146px; - - .mx_IconizedContextMenu_optionList { - & > * { - padding-left: 20px; - padding-right: 20px; - } - - // the notFirst class is for cases where the optionList might be under a header of sorts. - &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { - // This is a bit of a hack when we could just use a simple border-top property, - // however we have a (kinda) good reason for doing it this way: we need opacity. - // To get the right color, we need an opacity modifier which means we have to work - // around the problem. PostCSS doesn't support the opacity() function, and if we - // use something like postcss-functions we quickly run into an issue where the - // function we would define gets passed a CSS variable for custom themes, which - // can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379 - // - // Therefore, we just hack in a line and border the thing ourselves - &::before { - border-top: 1px solid $primary-fg-color; - opacity: 0.1; - content: ''; - - // Counteract the padding problems (width: 100% ignores the 40px padding, - // unless we position it absolutely then it does the right thing). - width: 100%; - position: absolute; - left: 0; - } - } - - // round the top corners of the top button for the hover effect to be bounded - &:first-child .mx_AccessibleButton:first-child { - border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu - } - - // round the bottom corners of the bottom button for the hover effect to be bounded - &:last-child .mx_AccessibleButton:last-child { - border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu - } - - .mx_AccessibleButton { - // pad the inside of the button so that the hover background is padded too - padding-top: 12px; - padding-bottom: 12px; - text-decoration: none; - color: $primary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - - // Create a flexbox to more easily define the list items - display: flex; - align-items: center; - - &:hover { - background-color: $menu-selected-color; - } - - img, .mx_IconizedContextMenu_icon { // icons - width: 16px; - min-width: 16px; - max-width: 16px; - } - - span.mx_IconizedContextMenu_label { // labels - padding-left: 14px; - width: 100%; - flex: 1; - - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - } - } - - &.mx_IconizedContextMenu_compact { - .mx_IconizedContextMenu_optionList > * { - padding: 8px 16px 8px 11px; - } - } -} - @define-mixin ProgressBarColour $colour { color: $colour; &::-moz-progress-bar { @@ -692,3 +605,15 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border-radius: $radius; } } + +@define-mixin unreal-focus { + outline-width: 2px; + outline-style: solid; + outline-color: Highlight; + + /* WebKit gets its native focus styles. */ + @media (-webkit-min-device-pixel-ratio: 0) { + outline-color: -webkit-focus-ring-color; + outline-style: auto; + } +} diff --git a/res/css/_components.scss b/res/css/_components.scss index 1f86bb67a6..a4873090f5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -1,6 +1,7 @@ // autogenerated by rethemendex.sh @import "./_common.scss"; @import "./_font-sizes.scss"; +@import "./_font-weights.scss"; @import "./structures/_AutoHideScrollbar.scss"; @import "./structures/_CompatibilityPage.scss"; @import "./structures/_ContextualMenu.scss"; @@ -50,28 +51,30 @@ @import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss"; +@import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; -@import "./views/context_menus/_RoomTileContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; -@import "./views/context_menus/_TopLeftMenu.scss"; @import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; +@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; +@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; +@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; +@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; -@import "./views/dialogs/_RebrandDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @@ -106,6 +109,7 @@ @import "./views/elements/_FormButton.scss"; @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; +@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_PowerSelector.scss"; @@ -156,7 +160,6 @@ @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; @import "./views/room_settings/_AliasSettings.scss"; -@import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_Autocomplete.scss"; @import "./views/rooms/_AuxPanel.scss"; @@ -186,7 +189,6 @@ @import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSublist.scss"; @import "./views/rooms/_RoomTile.scss"; -@import "./views/rooms/_RoomTileIcon.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SendMessageComposer.scss"; diff --git a/res/css/_font-weights.scss b/res/css/_font-weights.scss new file mode 100644 index 0000000000..3e2b19d516 --- /dev/null +++ b/res/css/_font-weights.scss @@ -0,0 +1,17 @@ +/* +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. +*/ + +$font-semi-bold: 600; diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 50b01b4a14..21b30d804a 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -41,13 +41,19 @@ limitations under the License. .mx_FilePanel .mx_EventTile { word-break: break-word; + margin-top: 32px; } .mx_FilePanel .mx_EventTile .mx_MImageBody { margin-right: 0px; } +.mx_FilePanel .mx_EventTile .mx_MFileBody { + line-height: 2.4rem; +} + .mx_FilePanel .mx_EventTile .mx_MFileBody_download { + padding-top: 8px; display: flex; font-size: $font-14px; color: $event-timestamp-color; @@ -60,7 +66,7 @@ limitations under the License. .mx_FilePanel .mx_EventTile .mx_MImageBody_size { flex: 1 0 0; - font-size: $font-11px; + font-size: $font-14px; text-align: right; white-space: nowrap; } @@ -80,7 +86,7 @@ limitations under the License. flex: 1 1 auto; line-height: initial; padding: 0px; - font-size: $font-11px; + font-size: $font-14px; opacity: 1.0; color: $event-timestamp-color; } @@ -90,7 +96,7 @@ limitations under the License. text-align: right; visibility: visible; position: initial; - font-size: $font-11px; + font-size: $font-14px; opacity: 1.0; color: $event-timestamp-color; } diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index db531cf088..5112d07c46 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -97,23 +97,25 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations display: flex; align-items: center; - .mx_RoomSearch_expanded + .mx_LeftPanel_exploreButton { - // Cheaty way to return the occupied space to the filter input - flex-basis: 0; - margin: 0; - width: 0; + .mx_RoomSearch_focused, .mx_RoomSearch_hasQuery { + & + .mx_LeftPanel_exploreButton { + // Cheaty way to return the occupied space to the filter input + flex-basis: 0; + margin: 0; + width: 0; - // Don't forget to hide the masked ::before icon, - // using display:none or visibility:hidden would break accessibility - &::before { - content: none; + // Don't forget to hide the masked ::before icon, + // using display:none or visibility:hidden would break accessibility + &::before { + content: none; + } } } .mx_LeftPanel_exploreButton { - width: 28px; - height: 28px; - border-radius: 20px; + width: 32px; + height: 32px; + border-radius: 8px; background-color: $roomlist-button-bg-color; position: relative; margin-left: 8px; @@ -121,22 +123,31 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations &::before { content: ''; position: absolute; - top: 6px; - left: 6px; + top: 8px; + left: 8px; width: 16px; height: 16px; - mask-image: url('$(res)/img/feather-customised/compass.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background: $secondary-fg-color; } } } + .mx_LeftPanel_roomListFilterCount { + font-size: $font-13px; + font-weight: $font-semi-bold; + margin-left: 12px; + margin-top: 14px; + margin-bottom: -4px; // to counteract the normal roomListWrapper margin-top + } + .mx_LeftPanel_roomListWrapper { overflow: hidden; margin-top: 10px; // so we're not up against the search/filter + flex: 1 0 0; // needed in Safari to properly set flex-basis &.mx_LeftPanel_roomListWrapper_stickyBottom { padding-bottom: 32px; diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index aee7b5a154..dc62cb8218 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -23,6 +23,8 @@ limitations under the License. .mx_MainSplit > .mx_RightPanel_ResizeWrapper { padding: 5px; + // margin left to not allow the handle to not encroach on the space for the scrollbar + margin-left: 8px; &:hover .mx_RightPanel_ResizeHandle { // Need to use important to override element style attributes diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index af6f6c79e9..f4e46a8e94 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -72,7 +72,7 @@ limitations under the License. flex: 1 1 0; min-width: 0; - /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari + /* To fix https://github.com/vector-im/element-web/issues/3298 where Safari needed height 100% all the way down to the HomePage. Height does not have to be auto, empirically. */ diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 120f44db90..c7c0d6fac4 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -64,7 +64,7 @@ limitations under the License. left: 4px; // center with parent of 32px height: 24px; width: 24px; - background-color: $rightpanel-button-color; + background-color: $icon-button-color; mask-repeat: no-repeat; mask-size: contain; } @@ -99,7 +99,7 @@ limitations under the License. background: rgba($accent-color, 0.25); // make the icon the accent color too &::before { - background-color: $accent-color; + background-color: $accent-color !important; } } diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss index 39a3dee30b..c33a3c0ff9 100644 --- a/res/css/structures/_RoomSearch.scss +++ b/res/css/structures/_RoomSearch.scss @@ -17,8 +17,10 @@ limitations under the License. // Note: this component expects to be contained within a flexbox .mx_RoomSearch { flex: 1; - border-radius: 20px; + border-radius: 8px; background-color: $roomlist-button-bg-color; + // keep border thickness consistent to prevent movement + border: 1px solid transparent; height: 28px; padding: 2px; @@ -29,9 +31,9 @@ limitations under the License. .mx_RoomSearch_icon { width: 16px; height: 16px; - mask: url('$(res)/img/feather-customised/search-input.svg'); + mask: url('$(res)/img/element-icons/roomlist/search.svg'); mask-repeat: no-repeat; - background: $primary-fg-color; + background-color: $secondary-fg-color; margin-left: 7px; } @@ -46,19 +48,30 @@ limitations under the License. line-height: $font-16px; &:not(.mx_RoomSearch_inputExpanded)::placeholder { - color: $primary-fg-color !important; // !important to override default app-wide styles + color: $tertiary-fg-color !important; // !important to override default app-wide styles } } - &.mx_RoomSearch_expanded { + &.mx_RoomSearch_hasQuery { + border-color: $secondary-fg-color; + } + + &.mx_RoomSearch_focused { + box-shadow: 0 0 4px 4px rgba(0, 132, 255, 0.5); + border-color: transparent; + } + + &.mx_RoomSearch_focused, &.mx_RoomSearch_hasQuery { + background-color: $roomlist-filter-active-bg-color; + .mx_RoomSearch_clearButton { width: 16px; height: 16px; - mask-image: url('$(res)/img/feather-customised/x.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/search-clear.svg'); mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background-color: $secondary-fg-color; margin-right: 8px; } } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 78e8326772..cdca1f0764 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -30,30 +30,11 @@ limitations under the License. cursor: pointer; } -.mx_TagPanel .mx_TagPanel_clearButton_container { - /* Constant height within flex mx_TagPanel */ - height: 70px; - width: 56px; - - flex: none; - - justify-content: center; - align-items: flex-start; - - display: none; -} - -.mx_TagPanel .mx_TagPanel_clearButton object { - /* Same as .mx_SearchBox padding-top */ - margin-top: 24px; - pointer-events: none; -} - .mx_TagPanel .mx_TagPanel_divider { height: 0px; - width: 34px; - border-bottom: 1px solid $panel-divider-color; - display: none; + width: 90%; + border: none; + border-bottom: 1px solid $tagpanel-divider-color; } .mx_TagPanel .mx_TagPanel_scroller { @@ -76,12 +57,57 @@ limitations under the License. // opacity: 0.5; position: relative; } + +.mx_TagPanel .mx_TagTile.mx_TagTile_prototype { + padding: 3px; +} + .mx_TagPanel .mx_TagTile:focus, .mx_TagPanel .mx_TagTile:hover, .mx_TagPanel .mx_TagTile.mx_TagTile_selected { // opacity: 1; } +.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype { + background-color: $primary-bg-color; + border-radius: 6px; +} + +.mx_TagTile_selected_prototype { + .mx_TagTile_homeIcon::before { + background-color: $primary-fg-color; // dark-on-light + } +} + +.mx_TagTile:not(.mx_TagTile_selected_prototype) .mx_TagTile_homeIcon { + background-color: $roomheader-addroom-bg-color; + border-radius: 48px; + + &::before { + background-color: $roomheader-addroom-fg-color; + } +} + +.mx_TagTile_homeIcon { + width: 32px; + height: 32px; + position: relative; + + &::before { + mask-image: url('$(res)/img/element-icons/home.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 21px; + content: ''; + display: inline-block; + width: 32px; + height: 32px; + position: absolute; + top: calc(50% - 16px); + left: calc(50% - 16px); + } +} + .mx_TagPanel .mx_TagTile_plus { margin-bottom: 12px; height: 32px; @@ -108,13 +134,12 @@ limitations under the License. .mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { content: ''; - height: calc(100% + 16px); + height: 100%; background-color: $accent-color; - width: 5px; + width: 4px; position: absolute; - left: -15px; + left: -12px; border-radius: 0 3px 3px 0; - top: -8px; // (16px from height / 2) } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index e798e4ac52..544dcbc180 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -80,10 +80,6 @@ limitations under the License. } } - &.mx_Toast_icon_element_logo::after { - background-image: url("$(res)/img/element-logo.svg"); - } - .mx_Toast_title, .mx_Toast_body { grid-column: 2; } diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 81a10ee1d0..6fa2f2578e 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -16,9 +16,33 @@ limitations under the License. .mx_UserMenu { - // to make the ... button sort of aligned with the explore button below + // to make the menu button sort of aligned with the explore button below padding-right: 6px; + &.mx_UserMenu_prototype { + // The margin & padding combination between here and the ::after is to + // align the border line with the tag panel. + margin-bottom: 6px; + + padding-right: 0; // make the right edge line up with the explore button + + .mx_UserMenu_headerButtons { + // considering we've eliminated right padding on the menu itself, we need to + // push the chevron in slightly (roughly lining up with the center of the + // plus buttons) + margin-right: 2px; + } + + // we cheat opacity on the theme colour with an after selector here + &::after { + content: ''; + border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse + opacity: 0.2; + display: block; + padding-top: 8px; + } + } + .mx_UserMenu_headerButtons { width: 16px; height: 16px; @@ -36,7 +60,7 @@ limitations under the License. mask-size: contain; mask-repeat: no-repeat; background: $primary-fg-color; - mask-image: url('$(res)/img/element-icons/context-menu.svg'); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } } @@ -56,6 +80,28 @@ limitations under the License. } } + .mx_UserMenu_doubleName { + flex: 1; + min-width: 0; // make flexbox aware that it can crush this to a tiny width + + .mx_UserMenu_userName, + .mx_UserMenu_subUserName { + display: block; + } + + .mx_UserMenu_subUserName { + color: $muted-fg-color; + font-size: $font-13px; + line-height: $font-18px; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + .mx_UserMenu_userName { font-weight: 600; font-size: $font-15px; @@ -89,15 +135,48 @@ limitations under the License. .mx_UserMenu_contextMenu { width: 247px; - .mx_UserMenu_contextMenu_redRow { + // These override the styles already present on the user menu rather than try to + // define a new menu. They are specifically for the stacked menu when a community + // is being represented as a prototype. + &.mx_UserMenu_contextMenu_prototype { + padding-bottom: 16px; + + .mx_UserMenu_contextMenu_header { + padding-bottom: 0; + padding-top: 16px; + + &:nth-child(n + 2) { + padding-top: 8px; + } + } + + hr { + width: 85%; + opacity: 0.2; + border: none; + border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse + } + + &.mx_IconizedContextMenu { + > .mx_IconizedContextMenu_optionList { + margin-top: 4px; + + &::before { + border: none; + } + + > .mx_AccessibleButton { + padding-top: 2px; + padding-bottom: 2px; + } + } + } + } + + &.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red { .mx_AccessibleButton { padding-top: 16px; padding-bottom: 16px; - color: $warning-color !important; // !important to override styles from context menu - } - - .mx_IconizedContextMenu_icon::before { - background-color: $warning-color; } } @@ -198,4 +277,12 @@ limitations under the License. .mx_UserMenu_iconSignOut::before { mask-image: url('$(res)/img/element-icons/leave.svg'); } + + .mx_UserMenu_iconMembers::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_UserMenu_iconInvite::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } } diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index e59598278f..1a1e14e7ac 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -22,7 +22,7 @@ limitations under the License. // different results during full reflow of the page vs. incremental reflow // of small portions. While that's surely a browser bug, we can avoid it by // using `inline-block` instead of the default `inline`. - // https://github.com/vector-im/riot-web/issues/5594 + // https://github.com/vector-im/element-web/issues/5594 // https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 // https://bugzilla.mozilla.org/show_bug.cgi?id=255139 display: inline-block; diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index 48d72131b5..e0afd9de66 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -18,10 +18,49 @@ limitations under the License. .mx_DecoratedRoomAvatar, .mx_TemporaryTile { position: relative; - .mx_RoomTileIcon { + &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { + mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + } + + .mx_DecoratedRoomAvatar_icon { position: absolute; - bottom: 0; - right: 0; + bottom: -2px; + right: -2px; + margin: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + } + + .mx_DecoratedRoomAvatar_icon::before { + content: ''; + width: 8px; + height: 8px; + position: absolute; + border-radius: 8px; + } + + .mx_DecoratedRoomAvatar_icon_globe::before { + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $secondary-fg-color; + mask-image: url('$(res)/img/globe.svg'); + } + + .mx_DecoratedRoomAvatar_icon_offline::before { + background-color: $presence-offline; + } + + .mx_DecoratedRoomAvatar_icon_online::before { + background-color: $presence-online; + } + + .mx_DecoratedRoomAvatar_icon_away::before { + background-color: $presence-away; } .mx_NotificationBadge, .mx_RoomTile_badgeContainer { diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss new file mode 100644 index 0000000000..7913058995 --- /dev/null +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -0,0 +1,148 @@ +/* +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. +*/ + +// A context menu that largely fits the | [icon] [label] | format. +.mx_IconizedContextMenu { + min-width: 146px; + + .mx_IconizedContextMenu_optionList { + & > * { + padding-left: 20px; + padding-right: 20px; + } + + // the notFirst class is for cases where the optionList might be under a header of sorts. + &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { + // This is a bit of a hack when we could just use a simple border-top property, + // however we have a (kinda) good reason for doing it this way: we need opacity. + // To get the right color, we need an opacity modifier which means we have to work + // around the problem. PostCSS doesn't support the opacity() function, and if we + // use something like postcss-functions we quickly run into an issue where the + // function we would define gets passed a CSS variable for custom themes, which + // can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379 + // + // Therefore, we just hack in a line and border the thing ourselves + &::before { + border-top: 1px solid $primary-fg-color; + opacity: 0.1; + content: ''; + + // Counteract the padding problems (width: 100% ignores the 40px padding, + // unless we position it absolutely then it does the right thing). + width: 100%; + position: absolute; + left: 0; + } + } + + // round the top corners of the top button for the hover effect to be bounded + &:first-child .mx_AccessibleButton:first-child { + border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu + } + + // round the bottom corners of the bottom button for the hover effect to be bounded + &:last-child .mx_AccessibleButton:last-child { + border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu + } + + .mx_AccessibleButton { + // pad the inside of the button so that the hover background is padded too + padding-top: 12px; + padding-bottom: 12px; + text-decoration: none; + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + // Create a flexbox to more easily define the list items + display: flex; + align-items: center; + + &:hover { + background-color: $menu-selected-color; + } + + img, .mx_IconizedContextMenu_icon { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span.mx_IconizedContextMenu_label { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + + .mx_IconizedContextMenu_icon { + position: relative; + width: 16px; + height: 16px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_IconizedContextMenu_optionList_red { + .mx_AccessibleButton { + color: $warning-color !important; + } + + .mx_IconizedContextMenu_icon::before { + background-color: $warning-color; + } + } + + .mx_IconizedContextMenu_active { + &.mx_AccessibleButton, .mx_AccessibleButton { + color: $accent-color !important; + } + + .mx_IconizedContextMenu_icon::before { + background-color: $accent-color; + } + } + + &.mx_IconizedContextMenu_compact { + .mx_IconizedContextMenu_optionList > * { + padding: 8px 16px 8px 11px; + } + } + + .mx_IconizedContextMenu_checked { + margin-left: 16px; + margin-right: -5px; + + &::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } +} diff --git a/res/css/views/context_menus/_RoomTileContextMenu.scss b/res/css/views/context_menus/_RoomTileContextMenu.scss deleted file mode 100644 index 9697ac9bef..0000000000 --- a/res/css/views/context_menus/_RoomTileContextMenu.scss +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket 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. -*/ - -.mx_RoomTileContextMenu { - padding: 6px; -} - -.mx_RoomTileContextMenu_tag_icon { - padding-right: 8px; - padding-left: 4px; - display: inline-block; -} - -.mx_RoomTileContextMenu_tag_icon_set { - padding-right: 8px; - padding-left: 4px; - display: none; -} - -.mx_RoomTileContextMenu_tag_field, .mx_RoomTileContextMenu_leave { - padding-top: 8px; - padding-right: 20px; - padding-bottom: 8px; - cursor: pointer; - white-space: nowrap; - display: flex; - align-items: center; - line-height: $font-16px; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet { - font-weight: bold; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon { - display: none; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon_set { - display: inline-block; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldDisabled { - color: rgba(0, 0, 0, 0.2); -} - -.mx_RoomTileContextMenu_separator { - margin-top: 0; - margin-bottom: 0; - border-bottom-style: none; - border-left-style: none; - border-right-style: none; - border-top-style: solid; - border-top-width: 1px; - border-color: $menu-border-color; -} - -.mx_RoomTileContextMenu_leave { - color: $warning-color; -} - -.mx_RoomTileContextMenu_notif_picker { - position: absolute; - top: 16px; - left: 5px; -} - -.mx_RoomTileContextMenu_notif_field { - padding-top: 4px; - padding-right: 6px; - padding-bottom: 10px; - padding-left: 8px; /* 20px */ - cursor: pointer; - white-space: nowrap; - display: flex; - align-items: center; -} - -.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldSet { - font-weight: bold; -} - -.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldDisabled { - color: rgba(0, 0, 0, 0.2); -} - -.mx_RoomTileContextMenu_notif_icon { - padding-right: 4px; - padding-left: 4px; -} - -.mx_RoomTileContextMenu_notif_activeIcon { - display: inline-block; - opacity: 0; - position: relative; - left: -5px; -} - -.mx_RoomTileContextMenu_notif_fieldSet .mx_RoomTileContextMenu_notif_activeIcon { - opacity: 1; -} diff --git a/res/css/views/context_menus/_TopLeftMenu.scss b/res/css/views/context_menus/_TopLeftMenu.scss deleted file mode 100644 index e0f5dd47bd..0000000000 --- a/res/css/views/context_menus/_TopLeftMenu.scss +++ /dev/null @@ -1,96 +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. -*/ - -.mx_TopLeftMenu { - min-width: 210px; - border-radius: 4px; - - .mx_TopLeftMenu_greyedText { - font-size: $font-12px; - opacity: 0.5; - } - - .mx_TopLeftMenu_upgradeLink { - font-size: $font-12px; - - img { - margin-left: 5px; - } - } - - .mx_TopLeftMenu_section:not(:last-child) { - border-bottom: 1px solid $menu-border-color; - } - - .mx_TopLeftMenu_section_noIcon { - margin: 5px 0; - padding: 5px 20px 5px 15px; - - div:not(:first-child) { - margin-top: 5px; - } - } - - .mx_TopLeftMenu_section_withIcon { - margin: 5px 0; - padding: 0; - list-style: none; - - .mx_TopLeftMenu_icon_home::after { - mask-image: url('$(res)/img/feather-customised/home.svg'); - } - - .mx_TopLeftMenu_icon_help::after { - mask-image: url('$(res)/img/feather-customised/life-buoy.svg'); - } - - .mx_TopLeftMenu_icon_settings::after { - mask-image: url('$(res)/img/feather-customised/settings.svg'); - } - - .mx_TopLeftMenu_icon_signin::after { - mask-image: url('$(res)/img/feather-customised/sign-in.svg'); - } - - .mx_TopLeftMenu_icon_signout::after { - mask-image: url('$(res)/img/feather-customised/sign-out.svg'); - } - - .mx_AccessibleButton::after { - mask-repeat: no-repeat; - mask-position: 0 center; - mask-size: $font-16px; - position: absolute; - width: $font-16px; - height: $font-16px; - content: ""; - top: 5px; - left: 14px; - background-color: $primary-fg-color; - } - - .mx_AccessibleButton { - position: relative; - cursor: pointer; - white-space: nowrap; - padding: 5px 20px 5px 43px; - } - - .mx_AccessibleButton:hover { - background-color: $menu-selected-color; - } - } -} diff --git a/res/css/views/dialogs/_BugReportDialog.scss b/res/css/views/dialogs/_BugReportDialog.scss new file mode 100644 index 0000000000..1920ac33ea --- /dev/null +++ b/res/css/views/dialogs/_BugReportDialog.scss @@ -0,0 +1,23 @@ +/* +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_BugReportDialog { + .mx_BugReportDialog_download { + .mx_AccessibleButton_kind_link { + padding-left: 0; + } + } +} diff --git a/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss new file mode 100644 index 0000000000..beae03f00f --- /dev/null +++ b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss @@ -0,0 +1,88 @@ +/* +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_CommunityPrototypeInviteDialog { + &.mx_Dialog_fixedWidth { + width: 360px; + } + + .mx_Dialog_content { + margin-bottom: 0; + + .mx_CommunityPrototypeInviteDialog_people { + position: relative; + margin-bottom: 4px; + + .mx_AccessibleButton { + display: inline-block; + background-color: $focus-bg-color; // XXX: Abuse of variables + border-radius: 4px; + padding: 3px 5px; + font-size: $font-12px; + float: right; + } + } + + .mx_CommunityPrototypeInviteDialog_morePeople { + margin-top: 8px; + } + + .mx_CommunityPrototypeInviteDialog_person { + position: relative; + margin-top: 4px; + + & > * { + vertical-align: middle; + } + + .mx_Checkbox { + position: absolute; + right: 0; + top: calc(50% - 8px); // checkbox is 16px high + width: 16px; // to force a square + } + + .mx_CommunityPrototypeInviteDialog_personIdentifiers { + display: inline-block; + + & > * { + display: block; + } + + .mx_CommunityPrototypeInviteDialog_personName { + font-weight: 600; + font-size: $font-14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_CommunityPrototypeInviteDialog_personId { + font-size: $font-12px; + color: $muted-fg-color; + margin-left: 7px; + } + } + } + + .mx_CommunityPrototypeInviteDialog_primaryButton { + display: block; + font-size: $font-13px; + line-height: 20px; + height: 20px; + margin-top: 24px; + } + } +} diff --git a/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss new file mode 100644 index 0000000000..81babc4c38 --- /dev/null +++ b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss @@ -0,0 +1,102 @@ +/* +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_CreateCommunityPrototypeDialog { + .mx_Dialog_content { + display: flex; + flex-direction: row; + margin-bottom: 12px; + + .mx_CreateCommunityPrototypeDialog_colName { + flex-basis: 66.66%; + padding-right: 100px; + + .mx_Field input { + font-size: $font-16px; + line-height: $font-20px; + } + + .mx_CreateCommunityPrototypeDialog_subtext { + display: block; + color: $muted-fg-color; + margin-bottom: 16px; + + &:last-child { + margin-top: 16px; + } + + &.mx_CreateCommunityPrototypeDialog_subtext_error { + color: $warning-color; + } + } + + .mx_CreateCommunityPrototypeDialog_communityId { + position: relative; + + .mx_InfoTooltip { + float: right; + } + } + + .mx_AccessibleButton { + display: block; + height: 32px; + font-size: $font-16px; + line-height: 32px; + } + } + + .mx_CreateCommunityPrototypeDialog_colAvatar { + flex-basis: 33.33%; + + .mx_CreateCommunityPrototypeDialog_avatarContainer { + margin-top: 12px; + margin-bottom: 20px; + + .mx_CreateCommunityPrototypeDialog_avatar, + .mx_CreateCommunityPrototypeDialog_placeholderAvatar { + width: 96px; + height: 96px; + border-radius: 96px; + } + + .mx_CreateCommunityPrototypeDialog_placeholderAvatar { + background-color: #368bd6; // hardcoded for both themes + + &::before { + display: inline-block; + background-color: #fff; // hardcoded because the background is + mask-repeat: no-repeat; + mask-size: 96px; + width: 96px; + height: 96px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/add-photo.svg'); + } + } + } + + .mx_CreateCommunityPrototypeDialog_tip { + & > b, & > span { + display: block; + color: $muted-fg-color; + } + } + } + } +} diff --git a/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss b/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss new file mode 100644 index 0000000000..75a56bf6b3 --- /dev/null +++ b/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss @@ -0,0 +1,77 @@ +/* +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. +*/ + +// XXX: many of these styles are shared with the create dialog +.mx_EditCommunityPrototypeDialog { + &.mx_Dialog_fixedWidth { + width: 360px; + } + + .mx_Dialog_content { + margin-bottom: 12px; + + .mx_AccessibleButton.mx_AccessibleButton_kind_primary { + display: block; + height: 32px; + font-size: $font-16px; + line-height: 32px; + } + + .mx_EditCommunityPrototypeDialog_rowAvatar { + display: flex; + flex-direction: row; + align-items: center; + } + + .mx_EditCommunityPrototypeDialog_avatarContainer { + margin-top: 20px; + margin-bottom: 20px; + + .mx_EditCommunityPrototypeDialog_avatar, + .mx_EditCommunityPrototypeDialog_placeholderAvatar { + width: 96px; + height: 96px; + border-radius: 96px; + } + + .mx_EditCommunityPrototypeDialog_placeholderAvatar { + background-color: #368bd6; // hardcoded for both themes + + &::before { + display: inline-block; + background-color: #fff; // hardcoded because the background is + mask-repeat: no-repeat; + mask-size: 96px; + width: 96px; + height: 96px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/add-photo.svg'); + } + } + } + + .mx_EditCommunityPrototypeDialog_tip { + margin-left: 20px; + + & > b, & > span { + display: block; + color: $muted-fg-color; + } + } + } +} diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index a77d0bfbba..b9063f46b9 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -89,6 +89,13 @@ limitations under the License. font-weight: bold; text-transform: uppercase; } + + .mx_InviteDialog_subname { + margin-bottom: 10px; + margin-top: -10px; // HACK: Positioning with margins is bad + font-size: $font-12px; + color: $muted-fg-color; + } } .mx_InviteDialog_roomTile { @@ -226,3 +233,7 @@ limitations under the License. .mx_InviteDialog_addressBar { margin-right: 45px; } + +.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { + padding: 0; +} diff --git a/res/css/views/dialogs/_RebrandDialog.scss b/res/css/views/dialogs/_RebrandDialog.scss deleted file mode 100644 index 534584ae2a..0000000000 --- a/res/css/views/dialogs/_RebrandDialog.scss +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_RebrandDialog { - text-align: center; - - a:link, - a:hover, - a:visited { - @mixin mx_Dialog_link; - } - - .mx_Dialog_buttons { - margin-top: 43px; - text-align: center; - } -} - -.mx_RebrandDialog_body { - width: 550px; - margin-left: auto; - margin-right: auto; -} - -.mx_RebrandDialog_logoContainer { - margin-top: 35px; - margin-bottom: 20px; - display: flex; - align-items: center; - justify-content: center; -} - -.mx_RebrandDialog_logo { - margin-left: 28px; - margin-right: 28px; - width: 64px; - height: 64px; -} - -.mx_RebrandDialog_chevron::after { - content: ''; - display: inline-block; - width: 30px; - height: 30px; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: $muted-fg-color; - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); - transform: rotate(-90deg); -} diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index d2fe98e8f9..c343b872fd 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -51,7 +51,8 @@ limitations under the License. display: inherit; } .mx_ShareDialog_matrixto_copy > div { - background-image: url($copy-button-url); + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; margin-left: 5px; width: 20px; height: 20px; diff --git a/res/css/views/room_settings/_ColorSettings.scss b/res/css/views/elements/_InfoTooltip.scss similarity index 56% rename from res/css/views/room_settings/_ColorSettings.scss rename to res/css/views/elements/_InfoTooltip.scss index fc6a4443ad..5858a60629 100644 --- a/res/css/views/room_settings/_ColorSettings.scss +++ b/res/css/views/elements/_InfoTooltip.scss @@ -1,5 +1,5 @@ /* -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. @@ -14,26 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ColorSettings_roomColor { +.mx_InfoTooltip_icon { + width: 16px; + height: 16px; display: inline-block; - position: relative; - width: 37px; - height: 37px; - border: 1px solid #979797; - margin-right: 13px; - cursor: pointer; } -.mx_ColorSettings_roomColor_selected { - position: absolute; - left: 10px; - top: 4px; - cursor: default !important; -} - -.mx_ColorSettings_roomColorPrimary { - height: 10px; - position: absolute; - bottom: 0px; - width: 100%; +.mx_InfoTooltip_icon::before { + display: inline-block; + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/info.svg'); } diff --git a/res/css/views/elements/_StyledCheckbox.scss b/res/css/views/elements/_StyledCheckbox.scss index 60f1bf0277..e2d61c033b 100644 --- a/res/css/views/elements/_StyledCheckbox.scss +++ b/res/css/views/elements/_StyledCheckbox.scss @@ -80,5 +80,11 @@ limitations under the License. background-color: $accent-color; border-color: $accent-color; } + + &.focus-visible { + & + label .mx_Checkbox_background { + @mixin unreal-focus; + } + } } } diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss index ffa1337ebb..62fb5c5512 100644 --- a/res/css/views/elements/_StyledRadioButton.scss +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -63,6 +63,7 @@ limitations under the License. box-sizing: border-box; height: $font-16px; width: $font-16px; + margin-left: 2px; // For the highlight on focus border: $font-1-5px solid $radio-circle-color; border-radius: $font-16px; @@ -77,6 +78,12 @@ limitations under the License. } } + &.focus-visible { + & + div { + @mixin unreal-focus; + } + } + &:checked { & + div { border-color: $active-radio-circle-color; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 547b16e9ad..1c773c2f06 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -25,6 +25,7 @@ limitations under the License. height: 100%; left: 0; top: 0; + border-radius: 4px; } .mx_MImageBody_thumbnail_container { diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index e3ccd99611..1254b496b5 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -24,7 +24,7 @@ limitations under the License. line-height: $font-24px; border-radius: 4px; background: $message-action-bar-bg-color; - top: -18px; + top: -26px; right: 8px; user-select: none; // Ensure the action bar appears above over things, like the read marker. @@ -41,7 +41,7 @@ limitations under the License. width: calc(10px + 48px + 100% + 8px); // safe area + action bar height: calc(20px + 100%); - top: -20px; + top: -12px; left: -58px; z-index: -1; cursor: initial; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 2a2191b799..eb0e1dd7b0 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -536,11 +536,12 @@ $left-gutter: 64px; display: inline-block; visibility: hidden; cursor: pointer; - top: 8px; + top: 6px; right: 6px; width: 19px; height: 19px; - background-image: url($copy-button-url); + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; } .mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton, diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index ed60c220e7..958d718b11 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -54,7 +54,7 @@ $irc-line-height: $font-18px; flex-shrink: 0; width: var(--name-width); text-overflow: ellipsis; - text-align: right; + text-align: left; display: flex; align-items: center; overflow: visible; diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index ec95403262..a403a8dc4c 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -21,6 +21,7 @@ limitations under the License. border-top: 1px solid $primary-hairline-color; position: relative; padding-left: 82px; + padding-right: 6px; } .mx_MessageComposer_replaced_wrapper { @@ -178,25 +179,44 @@ limitations under the License. color: $accent-color; } +.mx_MessageComposer_button_highlight { + background: rgba($accent-color, 0.25); + // make the icon the accent color too + &::before { + background-color: $accent-color !important; + } +} + .mx_MessageComposer_button { position: relative; - margin-right: 12px; + margin-right: 6px; cursor: pointer; - height: 20px; - width: 20px; + height: 26px; + width: 26px; + border-radius: 100%; &::before { content: ''; position: absolute; + top: 3px; + left: 3px; height: 20px; width: 20px; - background-color: $composer-button-color; + background-color: $icon-button-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; } + &:hover { + background: rgba($accent-color, 0.1); + + &::before { + background-color: $accent-color; + } + } + &.mx_MessageComposer_hangup::before { background-color: $warning-color; } @@ -288,7 +308,7 @@ limitations under the License. mask-size: contain; mask-position: center; mask-repeat: no-repeat; - background-color: $composer-button-color; + background-color: $icon-button-color; &.mx_MessageComposer_markdownDisabled { opacity: 0.2; diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 229b4291db..c1fe1d9a8b 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -15,10 +15,6 @@ limitations under the License. */ .mx_ReplyPreview { - position: absolute; - bottom: 0; - z-index: 1000; - width: 100%; border: 1px solid $primary-hairline-color; background: $primary-bg-color; border-bottom: none; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index ba46100ea6..a880a7bee2 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -222,7 +222,7 @@ limitations under the License. left: 4px; // center with parent of 32px height: 24px; width: 24px; - background-color: $roomheader-button-color; + background-color: $icon-button-color; mask-repeat: no-repeat; mask-size: contain; } diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 89ab85e146..78e7307bc0 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -17,3 +17,43 @@ limitations under the License. .mx_RoomList { padding-right: 7px; // width of the scrollbar, to line things up } + +.mx_RoomList_iconPlus::before { + mask-image: url('$(res)/img/element-icons/roomlist/plus.svg'); +} +.mx_RoomList_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); +} + +.mx_RoomList_explorePrompt { + margin: 4px 12px 4px; + padding-top: 12px; + border-top: 1px solid $tertiary-fg-color; + font-size: $font-13px; + + div:first-child { + font-weight: $font-semi-bold; + margin-bottom: 8px; + } + + .mx_AccessibleButton { + color: $secondary-fg-color; + position: relative; + padding: 0 0 0 24px; + font-size: inherit; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 0; + left: 0; + background: $secondary-fg-color; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } + } +} diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index d3c9b79c69..543940fb78 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -120,7 +120,7 @@ limitations under the License. } .mx_RoomSublist_auxButton::before { - mask-image: url('$(res)/img/feather-customised/plus.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/plus.svg'); } .mx_RoomSublist_menuButton::before { @@ -169,7 +169,7 @@ limitations under the License. // that the sublists below them do not jump. However, that leaves a gap // when scrolled to the top above the first sublist (whose header can only // ever stick to top), so we force height to 0 for only that first header. - // See also https://github.com/vector-im/riot-web/issues/14429. + // See also https://github.com/vector-im/element-web/issues/14429. &:first-child .mx_RoomSublist_headerContainer { height: 0; padding-bottom: 4px; diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index f22228602d..8eca3f1efa 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -175,48 +175,8 @@ limitations under the License. .mx_RoomTile_iconBellMentions::before { mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg'); } -.mx_RoomTile_iconCheck::before { - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); -} .mx_RoomTile_contextMenu { - .mx_RoomTile_contextMenu_redRow { - .mx_AccessibleButton { - color: $warning-color !important; // !important to override styles from context menu - } - - .mx_IconizedContextMenu_icon::before { - background-color: $warning-color; - } - } - - .mx_RoomTile_contextMenu_activeRow { - &.mx_AccessibleButton, .mx_AccessibleButton { - color: $accent-color !important; // !important to override styles from context menu - } - - .mx_IconizedContextMenu_icon::before { - background-color: $accent-color; - } - } - - .mx_IconizedContextMenu_icon { - position: relative; - width: 16px; - height: 16px; - - &::before { - content: ''; - width: 16px; - height: 16px; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $primary-fg-color; - } - } - .mx_RoomTile_iconStar::before { mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); } diff --git a/res/css/views/rooms/_RoomTileIcon.scss b/res/css/views/rooms/_RoomTileIcon.scss deleted file mode 100644 index 2f3afdd446..0000000000 --- a/res/css/views/rooms/_RoomTileIcon.scss +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_RoomTileIcon { - width: 12px; - height: 12px; - border-radius: 12px; - background-color: $roomlist-bg-color; // to match the room list itself -} - -.mx_RoomTileIcon_globe::before { - content: ''; - width: 8px; - height: 8px; - top: 2px; - left: 2px; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $primary-fg-color; - mask-image: url('$(res)/img/globe.svg'); -} - -.mx_RoomTileIcon_offline::before { - content: ''; - width: 8px; - height: 8px; - top: 2px; - left: 2px; - position: absolute; - border-radius: 8px; - background-color: $presence-offline; -} - -.mx_RoomTileIcon_online::before { - content: ''; - width: 8px; - height: 8px; - top: 2px; - left: 2px; - position: absolute; - border-radius: 8px; - background-color: $presence-online; -} - -.mx_RoomTileIcon_away::before { - content: ''; - width: 8px; - height: 8px; - top: 2px; - left: 2px; - position: absolute; - border-radius: 8px; - background-color: $presence-away; -} diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index 0b646666e7..9f6a8d52ce 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -44,10 +44,5 @@ limitations under the License. overflow-y: auto; } } - - .mx_SendMessageComposer_overlayWrapper { - position: relative; - height: 0; - } } diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss index b5a57dfefb..23dcc532b2 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SecurityRoomSettingsTab label { - display: block; -} - .mx_SecurityRoomSettingsTab_warning { display: block; diff --git a/res/img/element-icons/add-photo.svg b/res/img/element-icons/add-photo.svg new file mode 100644 index 0000000000..bde5253bea --- /dev/null +++ b/res/img/element-icons/add-photo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/home.svg b/res/img/element-icons/home.svg new file mode 100644 index 0000000000..a6c15456ff --- /dev/null +++ b/res/img/element-icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/info.svg b/res/img/element-icons/info.svg new file mode 100644 index 0000000000..b5769074ab --- /dev/null +++ b/res/img/element-icons/info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/roomlist/decorated-avatar-mask.svg b/res/img/element-icons/roomlist/decorated-avatar-mask.svg new file mode 100644 index 0000000000..fb09c16bba --- /dev/null +++ b/res/img/element-icons/roomlist/decorated-avatar-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/explore.svg b/res/img/element-icons/roomlist/explore.svg new file mode 100644 index 0000000000..3786ce1153 --- /dev/null +++ b/res/img/element-icons/roomlist/explore.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/roomlist/plus.svg b/res/img/element-icons/roomlist/plus.svg new file mode 100644 index 0000000000..f6d80ac7ef --- /dev/null +++ b/res/img/element-icons/roomlist/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/search-clear.svg b/res/img/element-icons/roomlist/search-clear.svg new file mode 100644 index 0000000000..29fc097600 --- /dev/null +++ b/res/img/element-icons/roomlist/search-clear.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/search.svg b/res/img/element-icons/roomlist/search.svg new file mode 100644 index 0000000000..b706092a5c --- /dev/null +++ b/res/img/element-icons/roomlist/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-logo.svg b/res/img/element-logo.svg deleted file mode 100644 index 2cd11ed193..0000000000 --- a/res/img/element-logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/clipboard.svg b/res/img/feather-customised/clipboard.svg new file mode 100644 index 0000000000..b25b97176c --- /dev/null +++ b/res/img/feather-customised/clipboard.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/feather-customised/compass.svg b/res/img/feather-customised/compass.svg deleted file mode 100644 index 3296260803..0000000000 --- a/res/img/feather-customised/compass.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/globe.svg b/res/img/globe.svg index cc22bc6e66..635fa91cce 100644 --- a/res/img/globe.svg +++ b/res/img/globe.svg @@ -1,6 +1,3 @@ - - - - + diff --git a/res/img/icon_copy_message.svg b/res/img/icon_copy_message.svg deleted file mode 100644 index 8d8887bb22..0000000000 --- a/res/img/icon_copy_message.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - image/svg+xml - - ED5D3E59-2561-4AC1-9B43-82FBC51767FC - - - - - - ED5D3E59-2561-4AC1-9B43-82FBC51767FC - Created with sketchtool. - - - - - - - - - diff --git a/res/img/riot-logo.svg b/res/img/riot-logo.svg deleted file mode 100644 index ac1e547234..0000000000 --- a/res/img/riot-logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index e39bb29044..a3b03c777e 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -46,7 +46,7 @@ $inverted-bg-color: $base-color; $selected-color: $room-highlight-color; // selected for hoverover & selected event tiles -$event-selected-color: $header-panel-bg-color; +$event-selected-color: #21262c; // used for the hairline dividers in RoomView $primary-hairline-color: transparent; @@ -100,10 +100,9 @@ $roomheader-bg-color: $bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3); $roomheader-addroom-fg-color: $text-primary-color; $tagpanel-button-color: $header-panel-text-primary-color; -$roomheader-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; -$composer-button-color: $header-panel-text-primary-color; +$icon-button-color: #8E99A4; $roomtopic-color: $text-secondary-color; $eventtile-meta-color: $roomtopic-color; @@ -115,10 +114,13 @@ $composer-e2e-icon-color: $header-panel-text-primary-color; $theme-button-bg-color: #e3e8f0; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: $bg-color; $roomlist-bg-color: rgba(33, 38, 44, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; +$tagpanel-divider-color: $roomlist-header-color; + $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: rgba(141, 151, 165, 0.2); diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 7ecfcf13d9..2741dcebf8 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -15,6 +15,8 @@ $room-highlight-color: #343a46; // typical text (dark-on-white in light skin) $primary-fg-color: $text-primary-color; +$secondary-fg-color: $primary-fg-color; +$tertiary-fg-color: $primary-fg-color; $primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; @@ -95,10 +97,9 @@ $roomheader-color: $text-primary-color; $roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity $roomheader-addroom-fg-color: $text-primary-color; $tagpanel-button-color: $header-panel-text-primary-color; -$roomheader-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; -$composer-button-color: $header-panel-text-primary-color; +$icon-button-color: $header-panel-text-primary-color; $roomtopic-color: $text-secondary-color; $eventtile-meta-color: $roomtopic-color; @@ -110,10 +111,13 @@ $composer-e2e-icon-color: $header-panel-text-primary-color; $theme-button-bg-color: #e3e8f0; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-bg-color: $header-panel-bg-color; $roomsublist-divider-color: $primary-fg-color; +$tagpanel-divider-color: $roomlist-header-color; + $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #1A1D23; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 3465aa307e..4fd2a3615b 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -23,6 +23,8 @@ $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) $primary-fg-color: #2e2f32; +$secondary-fg-color: $primary-fg-color; +$tertiary-fg-color: $primary-fg-color; $primary-bg-color: #ffffff; $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text @@ -162,10 +164,9 @@ $roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: #91a1c0; $roomheader-addroom-fg-color: $accent-fg-color; $tagpanel-button-color: #91a1c0; -$roomheader-button-color: #91a1c0; $groupheader-button-color: #91a1c0; $rightpanel-button-color: #91a1c0; -$composer-button-color: #91a1c0; +$icon-button-color: #91a1c0; $roomtopic-color: #9e9e9e; $eventtile-meta-color: $roomtopic-color; @@ -177,10 +178,13 @@ $header-divider-color: #91a1c0; $theme-button-bg-color: #e3e8f0; $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-bg-color: $header-panel-bg-color; $roomlist-header-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color; +$tagpanel-divider-color: $roomlist-header-color; + $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #fff; @@ -228,7 +232,8 @@ $event-redacted-border-color: #cccccc; // event timestamp $event-timestamp-color: #acacac; -$copy-button-url: "$(res)/img/icon_copy_message.svg"; +$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; + // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color diff --git a/res/themes/legacy-light/css/_paths.scss b/res/themes/legacy-light/css/_paths.scss index 0744347826..3944076004 100644 --- a/res/themes/legacy-light/css/_paths.scss +++ b/res/themes/legacy-light/css/_paths.scss @@ -1,3 +1,3 @@ // Path from root SCSS file (such as `light.scss`) to `res` dir in the source tree -// This value is overridden by external themes in `riot-web`. +// This value is overridden by external themes in `element-web`. $res: ../../..; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index e67bcdf89a..05302a2a80 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -74,7 +74,7 @@ $droptarget-bg-color: rgba(255,255,255,0.5); $selected-color: $secondary-accent-color; // selected for hoverover & selected event tiles -$event-selected-color: $header-panel-bg-color; +$event-selected-color: #f6f7f8; // used for the hairline dividers in RoomView $primary-hairline-color: transparent; @@ -158,10 +158,9 @@ $roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); $roomheader-addroom-fg-color: #5c6470; $tagpanel-button-color: #91A1C0; -$roomheader-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; -$composer-button-color: #91A1C0; +$icon-button-color: #C1C6CD; $roomtopic-color: #9e9e9e; $eventtile-meta-color: $roomtopic-color; @@ -172,11 +171,14 @@ $header-divider-color: #91A1C0; $theme-button-bg-color: #e3e8f0; -$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons +$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: #ffffff; $roomlist-bg-color: rgba(245, 245, 245, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; +$tagpanel-divider-color: $roomlist-header-color; + $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #FFF; @@ -230,7 +232,7 @@ $event-redacted-border-color: #cccccc; // event timestamp $event-timestamp-color: #acacac; -$copy-button-url: "$(res)/img/icon_copy_message.svg"; +$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color diff --git a/res/themes/light/css/_paths.scss b/res/themes/light/css/_paths.scss index 0744347826..3944076004 100644 --- a/res/themes/light/css/_paths.scss +++ b/res/themes/light/css/_paths.scss @@ -1,3 +1,3 @@ // Path from root SCSS file (such as `light.scss`) to `res` dir in the source tree -// This value is overridden by external themes in `riot-web`. +// This value is overridden by external themes in `element-web`. $res: ../../..; diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 1233677db4..7a62c03b12 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -13,7 +13,7 @@ handle_error() { trap 'handle_error' ERR -echo "--- Building Riot" +echo "--- Building Element" scripts/ci/layered-riot-web.sh cd ../riot-web riot_web_dir=`pwd` diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index c30ac62e3b..91733469f7 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -217,7 +217,7 @@ function getTranslationsOther(file) { const trs = new Set(); - // Taken from riot-web src/components/structures/HomePage.js + // Taken from element-web src/components/structures/HomePage.js const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; let matches; while (matches = translationsRegex.exec(contents)) { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 6510c02160..84340d8219 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -19,17 +19,20 @@ import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; -import RebrandListener from "../RebrandListener"; import { RoomListStoreClass } from "../stores/room-list/RoomListStore"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {ModalManager} from "../Modal"; import SettingsStore from "../settings/SettingsStore"; +import {ActiveRoomObserver} from "../ActiveRoomObserver"; +import {Notifier} from "../Notifier"; +import type {Renderer} from "react-dom"; declare global { interface Window { Modernizr: ModernizrStatic; + matrixChat: ReturnType; mxMatrixClientPeg: IMatrixClientPeg; Olm: { init: () => Promise; @@ -38,18 +41,14 @@ declare global { mxContentMessages: ContentMessages; mxToastStore: ToastStore; mxDeviceListener: DeviceListener; - mxRebrandListener: RebrandListener; mxRoomListStore: RoomListStoreClass; mxRoomListLayoutStore: RoomListLayoutStore; + mxActiveRoomObserver: ActiveRoomObserver; mxPlatformPeg: PlatformPeg; mxIntegrationManagers: typeof IntegrationManagers; singletonModalManager: ModalManager; mxSettingsStore: SettingsStore; - } - - // workaround for https://github.com/microsoft/TypeScript/issues/30933 - interface ObjectConstructor { - fromEntries?(xs: [string|number|symbol, any][]): object; + mxNotifier: typeof Notifier; } interface Document { @@ -77,4 +76,8 @@ declare global { interface PromiseConstructor { allSettled(promises: Promise[]): Promise | ISettledRejected>>; } + + interface HTMLAudioElement { + type?: string; + } } diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.ts similarity index 54% rename from src/ActiveRoomObserver.js rename to src/ActiveRoomObserver.ts index b7695d401d..1126dc9496 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.ts @@ -16,6 +16,8 @@ limitations under the License. import RoomViewStore from './stores/RoomViewStore'; +type Listener = (isActive: boolean) => void; + /** * Consumes changes from the RoomViewStore and notifies specific things * about when the active room changes. Unlike listening for RoomViewStore @@ -25,57 +27,57 @@ import RoomViewStore from './stores/RoomViewStore'; * TODO: If we introduce an observer for something else, factor out * the adding / removing of listeners & emitting into a common class. */ -class ActiveRoomObserver { - constructor() { - this._listeners = {}; // key=roomId, value=function(isActive:boolean) +export class ActiveRoomObserver { + private listeners: {[key: string]: Listener[]} = {}; + private _activeRoomId = RoomViewStore.getRoomId(); + private readonly roomStoreToken: string; - this._activeRoomId = RoomViewStore.getRoomId(); - // TODO: We could self-destruct when the last listener goes away, or at least - // stop listening. - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); + constructor() { + // TODO: We could self-destruct when the last listener goes away, or at least stop listening. + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); } - get activeRoomId(): string { + public get activeRoomId(): string { return this._activeRoomId; } - addListener(roomId, listener) { - if (!this._listeners[roomId]) this._listeners[roomId] = []; - this._listeners[roomId].push(listener); + public addListener(roomId, listener) { + if (!this.listeners[roomId]) this.listeners[roomId] = []; + this.listeners[roomId].push(listener); } - removeListener(roomId, listener) { - if (this._listeners[roomId]) { - const i = this._listeners[roomId].indexOf(listener); + public removeListener(roomId, listener) { + if (this.listeners[roomId]) { + const i = this.listeners[roomId].indexOf(listener); if (i > -1) { - this._listeners[roomId].splice(i, 1); + this.listeners[roomId].splice(i, 1); } } else { console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")"); } } - _emit(roomId, isActive: boolean) { - if (!this._listeners[roomId]) return; + private emit(roomId, isActive: boolean) { + if (!this.listeners[roomId]) return; - for (const l of this._listeners[roomId]) { + for (const l of this.listeners[roomId]) { l.call(null, isActive); } } - _onRoomViewStoreUpdate() { + private onRoomViewStoreUpdate = () => { // emit for the old room ID - if (this._activeRoomId) this._emit(this._activeRoomId, false); + if (this._activeRoomId) this.emit(this._activeRoomId, false); // update our cache this._activeRoomId = RoomViewStore.getRoomId(); // and emit for the new one - if (this._activeRoomId) this._emit(this._activeRoomId, true); - } + if (this._activeRoomId) this.emit(this._activeRoomId, true); + }; } -if (global.mx_ActiveRoomObserver === undefined) { - global.mx_ActiveRoomObserver = new ActiveRoomObserver(); +if (window.mxActiveRoomObserver === undefined) { + window.mxActiveRoomObserver = new ActiveRoomObserver(); } -export default global.mx_ActiveRoomObserver; +export default window.mxActiveRoomObserver; diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js index 05054cf63a..359828b312 100644 --- a/src/AsyncWrapper.js +++ b/src/AsyncWrapper.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import createReactClass from 'create-react-class'; +import React from "react"; import * as sdk from './index'; import PropTypes from 'prop-types'; import { _t } from './languageHandler'; @@ -24,24 +24,22 @@ import { _t } from './languageHandler'; * Wrap an asynchronous loader function with a react component which shows a * spinner until the real component loads. */ -export default createReactClass({ - propTypes: { +export default class AsyncWrapper extends React.Component { + static propTypes = { /** A promise which resolves with the real component */ prom: PropTypes.object.isRequired, - }, + }; - getInitialState: function() { - return { - component: null, - error: null, - }; - }, + state = { + component: null, + error: null, + }; - componentDidMount: function() { + componentDidMount() { this._unmounted = false; // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 + // https://github.com/vector-im/element-web/issues/3148 console.log('Starting load of AsyncWrapper for modal'); this.props.prom.then((result) => { if (this._unmounted) { @@ -56,17 +54,17 @@ export default createReactClass({ console.warn('AsyncWrapper promise failed', e); this.setState({error: e}); }); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._unmounted = true; - }, + } - _onWrapperCancelClick: function() { + _onWrapperCancelClick = () => { this.props.onFinished(false); - }, + }; - render: function() { + render() { if (this.state.component) { const Component = this.state.component; return ; @@ -87,6 +85,6 @@ export default createReactClass({ const Spinner = sdk.getComponent("elements.Spinner"); return ; } - }, -}); + } +} diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index acf72a986c..4d06c5df73 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -156,6 +156,14 @@ export default abstract class BasePlatform { loudNotification(ev: Event, room: Object) { } + clearNotification(notif: Notification) { + // Some browsers don't support this, e.g Safari on iOS + // https://developer.mozilla.org/en-US/docs/Web/API/Notification/close + if (notif.close) { + notif.close(); + } + } + /** * Returns a promise that resolves to a string representing the current version of the application. */ diff --git a/src/CallHandler.js b/src/CallHandler.js index d5e058ef1e..18f6aeb98a 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -90,7 +90,7 @@ function play(audioId) { // This is usually because the user hasn't interacted with the document, // or chrome doesn't think so and is denying the request. Not sure what // we can really do here... - // https://github.com/vector-im/riot-web/issues/7657 + // https://github.com/vector-im/element-web/issues/7657 console.log("Unable to play audio clip", e); } }; @@ -474,15 +474,15 @@ const callHandler = { /** * The conference handler is a module that deals with implementation-specific - * multi-party calling implementations. Riot passes in its own which creates + * multi-party calling implementations. Element passes in its own which creates * a one-to-one call with a freeswitch conference bridge. As of July 2018, * the de-facto way of conference calling is a Jitsi widget, so this is * deprecated. It reamins here for two reasons: - * 1. So Riot still supports joining existing freeswitch conference calls + * 1. So Element still supports joining existing freeswitch conference calls * (but doesn't support creating them). After a transition period, we can * remove support for joining them too. * 2. To hide the one-to-one rooms that old-style conferencing creates. This - * is much harder to remove: probably either we make Riot leave & forget these + * is much harder to remove: probably either we make Element leave & forget these * rooms after we remove support for joining freeswitch conferences, or we * accept that random rooms with cryptic users will suddently appear for * anyone who's ever used conference calling, or we are stuck with this diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 6f55a75d0c..eb8fff0eb1 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -70,6 +70,7 @@ interface IContent { interface IThumbnail { info: { + // eslint-disable-next-line camelcase thumbnail_info: { w: number; h: number; @@ -104,7 +105,12 @@ interface IAbortablePromise extends Promise { * @return {Promise} A promise that resolves with an object with an info key * and a thumbnail key. */ -function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise { +function createThumbnail( + element: ThumbnailableElement, + inputWidth: number, + inputHeight: number, + mimeType: string, +): Promise { return new Promise((resolve) => { let targetWidth = inputWidth; let targetHeight = inputHeight; @@ -437,11 +443,13 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { - const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, { - file, - currentIndex: i, - totalFiles: okFiles.length, - }); + const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', + '', UploadConfirmDialog, { + file, + currentIndex: i, + totalFiles: okFiles.length, + }, + ); const [shouldContinue, shouldUploadAll] = await finished; if (!shouldContinue) break; if (shouldUploadAll) { diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index a584a69d35..0353bfc5ae 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -21,6 +21,7 @@ import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; +import { isSecureBackupRequired } from './utils/WellKnownUtils'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -34,6 +35,17 @@ function isCachingAllowed() { return secretStorageBeingAccessed; } +/** + * This can be used by other components to check if secret storage access is in + * progress, so that we can e.g. avoid intermittently showing toasts during + * secret storage setup. + * + * @returns {bool} + */ +export function isSecretStorageBeingAccessed() { + return secretStorageBeingAccessed; +} + export class AccessCancelledError extends Error { constructor() { super("Secret storage access canceled"); @@ -57,19 +69,19 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); } - const [name, info] = keyInfoEntries[0]; + const [keyId, keyInfo] = keyInfoEntries[0]; // Check the in-memory cache - if (isCachingAllowed() && secretStorageKeys[name]) { - return [name, secretStorageKeys[name]]; + if (isCachingAllowed() && secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; } const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( passphrase, - info.passphrase.salt, - info.passphrase.iterations, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, ); } else { return decodeRecoveryKey(recoveryKey); @@ -81,10 +93,10 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { AccessSecretStorageDialog, /* props= */ { - keyInfo: info, + keyInfo, checkPrivateKey: async (input) => { const key = await inputToKey(input); - return await MatrixClientPeg.get().checkSecretStorageKey(key, info); + return await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo); }, }, /* className= */ null, @@ -106,11 +118,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - if (isCachingAllowed()) { - secretStorageKeys[name] = key; - } + cacheSecretStorageKey(keyId, key); - return [name, key]; + return [keyId, key]; +} + +function cacheSecretStorageKey(keyId, key) { + if (isCachingAllowed()) { + secretStorageKeys[keyId] = key; + } } const onSecretRequested = async function({ @@ -129,27 +145,21 @@ const onSecretRequested = async function({ console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); return; } - if (name.startsWith("m.cross_signing")) { + if ( + name === "m.cross_signing.master" || + name === "m.cross_signing.self_signing" || + name === "m.cross_signing.user_signing" + ) { const callbacks = client.getCrossSigningCacheCallbacks(); if (!callbacks.getCrossSigningKeyCache) return; - /* Explicit enumeration here is deliberate – never share the master key! */ - if (name === "m.cross_signing.self_signing") { - const key = await callbacks.getCrossSigningKeyCache("self_signing"); - if (!key) { - console.log( - `self_signing requested by ${deviceId}, but not found in cache`, - ); - } - return key && encodeBase64(key); - } else if (name === "m.cross_signing.user_signing") { - const key = await callbacks.getCrossSigningKeyCache("user_signing"); - if (!key) { - console.log( - `user_signing requested by ${deviceId}, but not found in cache`, - ); - } - return key && encodeBase64(key); + const keyId = name.replace("m.cross_signing.", ""); + const key = await callbacks.getCrossSigningKeyCache(keyId); + if (!key) { + console.log( + `${keyId} requested by ${deviceId}, but not found in cache`, + ); } + return key && encodeBase64(key); } else if (name === "m.megolm_backup.v1") { const key = await client._crypto.getSessionBackupPrivateKey(); if (!key) { @@ -164,6 +174,7 @@ const onSecretRequested = async function({ export const crossSigningCallbacks = { getSecretStorageKey, + cacheSecretStorageKey, onSecretRequested, }; @@ -212,9 +223,20 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), { - force: forceReset, + forceReset, + }, + null, + /* priority = */ false, + /* static = */ true, + /* options = */ { + onBeforeClose(reason) { + // If Secure Backup is required, you cannot leave the modal. + if (reason === "backgroundClick") { + return !isSecureBackupRequired(); + } + return true; + }, }, - null, /* priority = */ false, /* static = */ true, ); const [confirmed] = await finished; if (!confirmed) { @@ -222,7 +244,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f } } else { const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await cli.bootstrapSecretStorage({ + await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest) => { const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, @@ -237,7 +259,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f throw new Error("Cross-signing key upload auth canceled"); } }, - getBackupPassphrase: promptForBackupPassphrase, + }); + await cli.bootstrapSecretStorage({ + getKeyBackupPassphrase: promptForBackupPassphrase, }); } diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index a37521118f..156d8db61b 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -15,6 +15,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; +import dis from "./dispatcher/dispatcher"; import { hideToast as hideBulkUnverifiedSessionsToast, showToast as showBulkUnverifiedSessionsToast, @@ -28,11 +29,16 @@ import { hideToast as hideUnverifiedSessionsToast, showToast as showUnverifiedSessionsToast, } from "./toasts/UnverifiedSessionToast"; -import {privateShouldBeEncrypted} from "./createRoom"; +import { privateShouldBeEncrypted } from "./createRoom"; +import { isSecretStorageBeingAccessed, accessSecretStorage } from "./CrossSigningManager"; +import { isSecureBackupRequired } from './utils/WellKnownUtils'; +import { isLoggedIn } from './components/structures/MatrixChat'; + const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; export default class DeviceListener { + private dispatcherRef: string; // device IDs for which the user has dismissed the verify toast ('Later') private dismissed = new Set(); // has the user dismissed any of the various nag toasts to setup encryption on this device? @@ -60,6 +66,7 @@ export default class DeviceListener { MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); MatrixClientPeg.get().on('accountData', this._onAccountData); MatrixClientPeg.get().on('sync', this._onSync); + this.dispatcherRef = dis.register(this._onAction); this._recheck(); } @@ -73,6 +80,10 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('sync', this._onSync); } + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; + } this.dismissed.clear(); this.dismissedThisDeviceToast = false; this.keyBackupInfo = null; @@ -158,6 +169,11 @@ export default class DeviceListener { if (state === 'PREPARED' && prevState === null) this._recheck(); }; + _onAction = ({ action }) => { + if (action !== "on_logged_in") return; + this._recheck(); + }; + // The server doesn't tell us when key backup is set up, so we poll // & cache the result async _getKeyBackupInfo() { @@ -170,6 +186,9 @@ export default class DeviceListener { } private shouldShowSetupEncryptionToast() { + // If we're in the middle of a secret storage operation, we're likely + // modifying the state involved here, so don't add new toasts to setup. + if (isSecretStorageBeingAccessed()) return false; // In a default configuration, show the toasts. If the well-known config causes e2ee default to be false // then do not show the toasts until user is in at least one encrypted room. if (privateShouldBeEncrypted()) return true; @@ -188,16 +207,23 @@ export default class DeviceListener { // (we add a listener on sync to do once check after the initial sync is done) if (!cli.isInitialSyncComplete()) return; + // JRS: This will change again in the next PR which moves secret storage + // later in the process. const crossSigningReady = await cli.isCrossSigningReady(); + const secretStorageReady = await cli.isSecretStorageReady(); + const allSystemsReady = crossSigningReady && secretStorageReady; - if (this.dismissedThisDeviceToast || crossSigningReady) { + if (this.dismissedThisDeviceToast || allSystemsReady) { hideSetupEncryptionToast(); } else if (this.shouldShowSetupEncryptionToast()) { // make sure our keys are finished downloading await cli.downloadKeys([cli.getUserId()]); // cross signing isn't enabled - nag to enable it // There are 3 different toasts for: - if (cli.getStoredCrossSigningForUser(cli.getUserId())) { + if ( + !cli.getCrossSigningId() && + cli.getStoredCrossSigningForUser(cli.getUserId()) + ) { // Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); } else { @@ -207,7 +233,15 @@ export default class DeviceListener { showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); } else { // No cross-signing or key backup on account (set up encryption) - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired() && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); + } else { + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + } } } } diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 1b4aa19ebf..d5d7c08d50 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -197,7 +197,7 @@ export default class FromWidgetPostMessageApi { const integId = (data && data.integId) ? data.integId : null; // TODO: Open the right integration manager for the widget - if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll( MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), `type_${integType}`, diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 77a9579f2c..bd314c2e5f 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -339,33 +339,9 @@ class HtmlHighlighter extends BaseHighlighter { } } -class TextHighlighter extends BaseHighlighter { - private key = 0; - - /* create a node to hold the given content - * - * snippet: content of the span - * highlight: true to highlight as a search match - * - * returns a React node - */ - protected processSnippet(snippet: string, highlight: boolean): React.ReactNode { - const key = this.key++; - - let node = - { snippet } - ; - - if (highlight && this.highlightLink) { - node = { node }; - } - - return node; - } -} - interface IContent { format?: string; + // eslint-disable-next-line camelcase formatted_body?: string; body: string; } @@ -421,7 +397,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts } let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null; - const plainBody = typeof content.body === 'string' ? content.body : null; + const plainBody = typeof content.body === 'string' ? content.body : ""; if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(plainBody) : plainBody; @@ -474,8 +450,13 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts }); return isDisplayedWithHtml ? - : - { strippedBody }; + : { strippedBody }; } /** diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 4a830d6506..fbdb6812ee 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -177,7 +177,7 @@ export default class IdentityAuthClient { // appropriately. We already clear storage on sign out, but we'll need // additional clearing when changing ISes in settings as part of future // privacy work. - // See also https://github.com/vector-im/riot-web/issues/10455. + // See also https://github.com/vector-im/element-web/issues/10455. } async registerForToken(check=true) { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 2bebe22f14..d2de31eb80 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -40,7 +40,6 @@ import ToastStore from "./stores/ToastStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; -import RebrandListener from "./RebrandListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; @@ -647,8 +646,6 @@ async function startMatrixClient(startSyncing=true) { // Now that we have a MatrixClientPeg, update the Jitsi info await Jitsi.getInstance().start(); - RebrandListener.sharedInstance().start(); - // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. dis.dispatch({action: 'client_started'}); @@ -710,7 +707,6 @@ export function stopMatrixClient(unsetClient=true) { IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); DeviceListener.sharedInstance().stop(); - RebrandListener.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); diff --git a/src/Markdown.js b/src/Markdown.js index d312b7c5bd..492450e87d 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -15,7 +15,7 @@ limitations under the License. */ import commonmark from 'commonmark'; -import escape from 'lodash/escape'; +import {escape} from "lodash"; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; @@ -99,7 +99,7 @@ export default class Markdown { // puts softbreaks in for multiple lines in a blockquote, // so if these are just newline characters then the // block quote ends up all on one line - // (https://github.com/vector-im/riot-web/issues/3154) + // (https://github.com/vector-im/element-web/issues/3154) softbreak: '
', }); @@ -166,7 +166,7 @@ export default class Markdown { * Render the markdown message to plain text. That is, essentially * just remove any backslashes escaping what would otherwise be * markdown syntax - * (to fix https://github.com/vector-im/riot-web/issues/2870). + * (to fix https://github.com/vector-im/element-web/issues/2870). * * N.B. this does **NOT** render arbitrary MD to plain text - only MD * which has no formatting. Otherwise it emits HTML(!). diff --git a/src/Modal.tsx b/src/Modal.tsx index b744dbacf4..0a36813961 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -151,7 +151,7 @@ export class ModalManager { prom: Promise, props?: IProps, className?: string, - options?: IOptions + options?: IOptions, ) { const modal: IModal = { onFinished: props ? props.onFinished : null, @@ -182,7 +182,7 @@ export class ModalManager { private getCloseFn( modal: IModal, - props: IProps + props: IProps, ): [IHandle["close"], IHandle["finished"]] { const deferred = defer(); return [async (...args: T) => { @@ -264,7 +264,7 @@ export class ModalManager { className?: string, isPriorityModal = false, isStaticModal = false, - options: IOptions = {} + options: IOptions = {}, ): IHandle { const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, options); if (isPriorityModal) { @@ -287,7 +287,7 @@ export class ModalManager { private appendDialogAsync( prom: Promise, props?: IProps, - className?: string + className?: string, ): IHandle { const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, {}); @@ -319,7 +319,7 @@ export class ModalManager { private reRender() { if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) { - // If there is no modal to render, make all of Riot available + // If there is no modal to render, make all of Element available // to screen reader users again dis.dispatch({ action: 'aria_unhide_main_app', diff --git a/src/Notifier.js b/src/Notifier.ts similarity index 91% rename from src/Notifier.js rename to src/Notifier.ts index 2ed302267e..473de6c161 100644 --- a/src/Notifier.js +++ b/src/Notifier.ts @@ -17,6 +17,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; + import { MatrixClientPeg } from './MatrixClientPeg'; import SdkConfig from './SdkConfig'; import PlatformPeg from './PlatformPeg'; @@ -28,9 +31,7 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import SettingsStore from "./settings/SettingsStore"; -import { - hideToast as hideNotificationsToast, -} from "./toasts/DesktopNotificationsToast"; +import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast"; import {SettingLevel} from "./settings/SettingLevel"; /* @@ -55,7 +56,7 @@ const typehandlers = { }, }; -const Notifier = { +export const Notifier = { notifsByRoom: {}, // A list of event IDs that we've received but need to wait until @@ -63,14 +64,14 @@ const Notifier = { // or not pendingEncryptedEventIds: [], - notificationMessageForEvent: function(ev) { + notificationMessageForEvent: function(ev: MatrixEvent) { if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { return typehandlers[ev.getContent().msgtype](ev); } return TextForEvent.textForEvent(ev); }, - _displayPopupNotification: function(ev, room) { + _displayPopupNotification: function(ev: MatrixEvent, room: Room) { const plaf = PlatformPeg.get(); if (!plaf) { return; @@ -125,7 +126,7 @@ const Notifier = { } }, - getSoundForRoom: function(roomId) { + getSoundForRoom: function(roomId: string) { // We do no caching here because the SDK caches setting // and the browser will cache the sound. const content = SettingsStore.getValue("notificationSound", roomId); @@ -153,12 +154,13 @@ const Notifier = { }; }, - _playAudioNotification: async function(ev, room) { + _playAudioNotification: async function(ev: MatrixEvent, room: Room) { const sound = this.getSoundForRoom(room.roomId); console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); try { - const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); + const selector = + document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); let audioElement = selector; if (!selector) { if (!sound) { @@ -207,7 +209,7 @@ const Notifier = { return plaf && plaf.supportsNotifications(); }, - setEnabled: function(enable, callback) { + setEnabled: function(enable: boolean, callback?: () => void) { const plaf = PlatformPeg.get(); if (!plaf) return; @@ -277,10 +279,11 @@ const Notifier = { }, isAudioEnabled: function() { - return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled"); + // We don't route Audio via the HTML Notifications API so it is possible regardless of other things + return SettingsStore.getValue("audioNotificationsEnabled"); }, - setToolbarHidden: function(hidden, persistent = true) { + setToolbarHidden: function(hidden: boolean, persistent = true) { this.toolbarHidden = hidden; Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); @@ -289,7 +292,7 @@ const Notifier = { // update the info to localStorage for persistent settings if (persistent && global.localStorage) { - global.localStorage.setItem("notifications_hidden", hidden); + global.localStorage.setItem("notifications_hidden", String(hidden)); } }, @@ -312,7 +315,7 @@ const Notifier = { return this.toolbarHidden; }, - onSyncStateChange: function(state) { + onSyncStateChange: function(state: string) { if (state === "SYNCING") { this.isSyncing = true; } else if (state === "STOPPED" || state === "ERROR") { @@ -320,7 +323,7 @@ const Notifier = { } }, - onEvent: function(ev) { + onEvent: function(ev: MatrixEvent) { if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; @@ -338,7 +341,7 @@ const Notifier = { this._evaluateEvent(ev); }, - onEventDecrypted: function(ev) { + onEventDecrypted: function(ev: MatrixEvent) { // 'decrypted' means the decryption process has finished: it may have failed, // in which case it might decrypt soon if the keys arrive if (ev.isDecryptionFailure()) return; @@ -350,7 +353,7 @@ const Notifier = { this._evaluateEvent(ev); }, - onRoomReceipt: function(ev, room) { + onRoomReceipt: function(ev: MatrixEvent, room: Room) { if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether @@ -383,8 +386,8 @@ const Notifier = { }, }; -if (!global.mxNotifier) { - global.mxNotifier = Notifier; +if (!window.mxNotifier) { + window.mxNotifier = Notifier; } -export default global.mxNotifier; +export default window.mxNotifier; diff --git a/src/RebrandListener.tsx b/src/RebrandListener.tsx deleted file mode 100644 index 47b883cf35..0000000000 --- a/src/RebrandListener.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import SdkConfig from "./SdkConfig"; -import ToastStore from "./stores/ToastStore"; -import GenericToast from "./components/views/toasts/GenericToast"; -import RebrandDialog from "./components/views/dialogs/RebrandDialog"; -import { RebrandDialogKind } from "./components/views/dialogs/RebrandDialog"; -import Modal from './Modal'; -import { _t } from './languageHandler'; - -const TOAST_KEY = 'rebrand'; -const NAG_INTERVAL = 24 * 60 * 60 * 1000; - -function getRedirectUrl(url): string { - const redirectUrl = new URL(url); - redirectUrl.hash = ''; - - if (SdkConfig.get()['redirectToNewBrandUrl']) { - const newUrl = new URL(SdkConfig.get()['redirectToNewBrandUrl']); - if (url.hostname !== newUrl.hostname || url.pathname !== newUrl.pathname) { - redirectUrl.hostname = newUrl.hostname; - redirectUrl.pathname = newUrl.pathname; - return redirectUrl.toString(); - } - return null; - } else if (url.hostname === 'riot.im') { - if (url.pathname.startsWith('/app')) { - redirectUrl.hostname = 'app.element.io'; - redirectUrl.pathname = '/'; - } else if (url.pathname.startsWith('/staging')) { - redirectUrl.hostname = 'staging.element.io'; - redirectUrl.pathname = '/'; - } else if (url.pathname.startsWith('/develop')) { - redirectUrl.hostname = 'develop.element.io'; - redirectUrl.pathname = '/'; - } - - return redirectUrl.href; - } else if (url.hostname.endsWith('.riot.im')) { - redirectUrl.hostname = url.hostname.substr(0, url.hostname.length - '.riot.im'.length) + '.element.io'; - return redirectUrl.href; - } else { - return null; - } -} - -/** - * Shows toasts informing the user that the name of the app has changed and, - * potentially, that they should head to a different URL and log in there - */ -export default class RebrandListener { - private _reshowTimer?: number; - private nagAgainAt?: number = null; - - static sharedInstance() { - if (!window.mxRebrandListener) window.mxRebrandListener = new RebrandListener(); - return window.mxRebrandListener; - } - - constructor() { - this._reshowTimer = null; - } - - start() { - this.recheck(); - } - - stop() { - if (this._reshowTimer) { - clearTimeout(this._reshowTimer); - this._reshowTimer = null; - } - } - - onNagToastLearnMore = async () => { - const [doneClicked] = await Modal.createDialog(RebrandDialog, { - kind: RebrandDialogKind.NAG, - targetUrl: getRedirectUrl(window.location), - }).finished; - if (doneClicked) { - // open in new tab: they should come back here & log out - window.open(getRedirectUrl(window.location), '_blank'); - } - - // whatever the user clicks, we go away & nag again after however long: - // If they went to the new URL, we want to nag them to log out if they - // come back to this tab, and if they clicked, 'remind me later' we want - // to, well, remind them later. - this.nagAgainAt = Date.now() + NAG_INTERVAL; - this.recheck(); - }; - - onOneTimeToastLearnMore = async () => { - const [doneClicked] = await Modal.createDialog(RebrandDialog, { - kind: RebrandDialogKind.ONE_TIME, - }).finished; - if (doneClicked) { - localStorage.setItem('mx_rename_dialog_dismissed', 'true'); - this.recheck(); - } - }; - - onOneTimeToastDismiss = async () => { - localStorage.setItem('mx_rename_dialog_dismissed', 'true'); - this.recheck(); - }; - - onNagTimerFired = () => { - this._reshowTimer = null; - this.nagAgainAt = null; - this.recheck(); - }; - - private async recheck() { - // There are two types of toast/dialog we show: a 'one time' informing the user that - // the app is now called a different thing but no action is required from them (they - // may need to look for a different name name/icon to launch the app but don't need to - // log in again) and a nag toast where they need to log in to the app on a different domain. - let nagToast = false; - let oneTimeToast = false; - - if (getRedirectUrl(window.location)) { - if (!this.nagAgainAt) { - // if we have redirectUrl, show the nag toast - nagToast = true; - } - } else { - // otherwise we show the 'one time' toast / dialog - const renameDialogDismissed = localStorage.getItem('mx_rename_dialog_dismissed'); - if (renameDialogDismissed !== 'true') { - oneTimeToast = true; - } - } - - if (nagToast || oneTimeToast) { - let description; - let rejectLabel = null; - let onReject = null; - if (nagToast) { - description = _t("Use your account to sign in to the latest version"); - } else { - description = _t("We’re excited to announce Riot is now Element"); - rejectLabel = _t("Dismiss"); - onReject = this.onOneTimeToastDismiss; - } - - ToastStore.sharedInstance().addOrReplaceToast({ - key: TOAST_KEY, - title: _t("Riot is now Element!"), - icon: 'element_logo', - props: { - description, - acceptLabel: _t("Learn More"), - onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore, - rejectLabel, - onReject, - }, - component: GenericToast, - priority: 20, - }); - } else { - ToastStore.sharedInstance().dismissToast(TOAST_KEY); - } - - if (!this._reshowTimer && this.nagAgainAt) { - // XXX: Our build system picks up NodeJS bindings when we need browser bindings. - this._reshowTimer = setTimeout(this.onNagTimerFired, (this.nagAgainAt - Date.now()) + 100) as any as number; - } - } -} diff --git a/src/Registration.js b/src/Registration.js index 32c3d9cc35..9c0264c067 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -52,7 +52,7 @@ export async function startAnyRegistrationFlow(options) { // caution though. // XXX: ILAG is disabled for now, - // see https://github.com/vector-im/riot-web/issues/8222 + // see https://github.com/vector-im/element-web/issues/8222 // const flows = await _getRegistrationFlows(); // const hasIlagFlow = flows.some((flow) => { diff --git a/src/Resend.js b/src/Resend.js index f5f24bffa5..5638313306 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -45,7 +45,7 @@ export default class Resend { }); }, function(err) { // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 + // https://github.com/vector-im/element-web/issues/3148 console.log('Resend got send failure: ' + err.name + '(' + err + ')'); dis.dispatch({ diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 839d677069..7eb7f5dbb2 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -23,6 +23,8 @@ import Modal from './Modal'; import * as sdk from './'; import { _t } from './languageHandler'; import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; +import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; +import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; /** * Invites multiple addresses to a room @@ -56,6 +58,23 @@ export function showRoomInviteDialog(roomId) { ); } +export function showCommunityRoomInviteDialog(roomId, communityName) { + Modal.createTrackedDialog( + 'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); +} + +export function showCommunityInviteDialog(communityId) { + const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); + if (chat) { + const name = CommunityPrototypeStore.instance.getCommunityName(communityId); + showCommunityRoomInviteDialog(chat.roomId, name); + } else { + throw new Error("Failed to locate appropriate room to start an invite in"); + } +} + /** * Checks if the given MatrixEvent is a valid 3rd party user invite. * @param {MatrixEvent} event The event to check @@ -77,7 +96,7 @@ export function isValid3pidInvite(event) { export function inviteUsersToRoom(roomId, userIds) { return inviteMultipleToRoom(roomId, userIds).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(result.states, room, result.inviter); + showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -88,7 +107,7 @@ export function inviteUsersToRoom(roomId, userIds) { }); } -function _showAnyInviteErrors(addrs, room, inviter) { +export function showAnyInviteErrors(addrs, room, inviter) { // Show user any errors const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); if (failedUsers.length === 1 && inviter.fatal) { @@ -100,6 +119,7 @@ function _showAnyInviteErrors(addrs, room, inviter) { title: _t("Failed to invite users to the room:", {roomName: room.name}), description: inviter.getErrorText(failedUsers[0]), }); + return false; } else { const errorList = []; for (const addr of failedUsers) { @@ -118,8 +138,9 @@ function _showAnyInviteErrors(addrs, room, inviter) { title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), description, }); + return false; } } - return addrs; + return true; } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index b33aa57359..896e27d92c 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -174,7 +174,7 @@ Request: Response: [ { - // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) type: "im.vector.modular.widgets", state_key: "wid1", content: { @@ -193,7 +193,7 @@ Example: room_id: "!foo:bar", response: [ { - // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) type: "im.vector.modular.widgets", state_key: "wid1", content: { diff --git a/src/SendHistoryManager.js b/src/SendHistoryManager.js index 794a58ad6f..d9955727a4 100644 --- a/src/SendHistoryManager.js +++ b/src/SendHistoryManager.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import _clamp from 'lodash/clamp'; +import {clamp} from "lodash"; export default class SendHistoryManager { history: Array = []; @@ -54,7 +54,7 @@ export default class SendHistoryManager { } getItem(offset: number): ?HistoryItem { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); + this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1); return this.history[this.currentIndex]; } } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index ad3dc7002a..661ab74e6f 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -43,7 +43,7 @@ import SdkConfig from "./SdkConfig"; import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; import { Action } from "./dispatcher/actions"; -import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; +import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -479,7 +479,7 @@ export const Commands = [ const parsedUrl = new URL(params[0]); const hostname = parsedUrl.host || parsedUrl.hostname; // takes first non-falsey value - // if we're using a Riot permalink handler, this will catch it before we get much further. + // if we're using a Element permalink handler, this will catch it before we get much further. // see below where we make assumptions about parsing the URL. if (isPermalinkHost(hostname)) { isPermalink = true; @@ -601,11 +601,7 @@ export const Commands = [ } if (!targetRoomId) targetRoomId = roomId; - return success( - cli.leaveRoomChain(targetRoomId).then(function() { - dis.dispatch({action: 'view_next_room'}); - }), - ); + return success(leaveRoomBehaviour(targetRoomId)); }, category: CommandCategories.actions, }), @@ -733,7 +729,7 @@ export const Commands = [ const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); if (!room) return reject(_t("Command failed")); - const member = room.getMember(args); + const member = room.getMember(userId); if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) { return reject(_t("Could not find user in room")); } @@ -864,12 +860,12 @@ export const Commands = [ _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' + ' %(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, - })); + { + fprint, + userId, + deviceId, + fingerprint, + })); } await cli.setDeviceVerified(userId, deviceId, true); @@ -883,7 +879,7 @@ export const Commands = [ { _t('The signing key you provided matches the signing key you received ' + 'from %(userId)s\'s session %(deviceId)s. Session marked as verified.', - {userId, deviceId}) + {userId, deviceId}) }

, diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3607d7a676..c55380bd9b 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -345,7 +345,7 @@ function textForCallHangupEvent(event) { } else if (eventContent.reason === "invite_timeout") { reason = _t('(no answer)'); } else if (eventContent.reason === "user hangup") { - // workaround for https://github.com/vector-im/riot-web/issues/5178 + // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( // https://github.com/vector-im/riot-android/issues/2623 @@ -603,7 +603,7 @@ const stateHandlers = { 'm.room.guest_access': textForGuestAccessEvent, 'm.room.related_groups': textForRelatedGroupsEvent, - // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) 'im.vector.modular.widgets': textForWidgetEvent, }; diff --git a/src/Tinter.js b/src/Tinter.js index 24a4d25a00..ca5a460e16 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -327,7 +327,7 @@ class Tinter { // Vector Green as its primary color? // --richvdh - // Yes, tinting assumes that you are using the Riot skin for now. + // Yes, tinting assumes that you are using the Element skin for now. // The right solution will be to move the CSS over to react-sdk. // And yes, the default assets for the base skin might as well use // Vector Green as any other colour. diff --git a/src/Unread.js b/src/Unread.js index ca713b05e4..cf131cac00 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -52,10 +52,10 @@ export function doesRoomHaveUnreadMessages(room) { // 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 + // Should fix: https://github.com/vector-im/element-web/issues/3263 + // https://github.com/vector-im/element-web/issues/2427 // ...and possibly some of the others at - // https://github.com/vector-im/riot-web/issues/3363 + // https://github.com/vector-im/element-web/issues/3363 if (room.timeline.length && room.timeline[room.timeline.length - 1].sender && room.timeline[room.timeline.length - 1].sender.userId === myUserId) { diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index 180dad876b..c10bc659ae 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -19,13 +19,13 @@ import {createNewMatrixCall as jsCreateNewMatrixCall, Room} from "matrix-js-sdk" import CallHandler from './CallHandler'; 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. +// FIXME: this is Element specific code, but will be removed shortly when we +// switch over to Jitsi entirely for video conferencing. -// FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing. -// This is bad because it prevents people running their own ASes from being used. -// This isn't permanent and will be customisable in the future: see the proposal -// at docs/conferencing.md for more info. +// FIXME: This currently forces Element to try to hit the matrix.org AS for +// conferencing. This is bad because it prevents people running their own ASes +// from being used. This isn't permanent and will be customisable in the future: +// see the proposal at docs/conferencing.md for more info. const USER_PREFIX = "fs_"; const DOMAIN = "matrix.org"; diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index c89a0ceeeb..6aed08c39d 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -76,7 +76,7 @@ export default class WidgetMessaging { console.error(err._error); } // Potential XSS attack if 'msg' is not appropriately sanitized, - // as it is untrusted input by our parent window (which we assume is Riot). + // as it is untrusted input by our parent window (which we assume is Element). // We can't aggressively sanitize [A-z0-9] since it might be a translation. throw new Error(msg); } diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index f527ab4a14..58d8124122 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -168,7 +168,7 @@ const shortcuts: Record = { key: Key.U, }], description: _td("Upload a file"), - } + }, ], [Categories.ROOM_LIST]: [ diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 5a650d4b6e..b1dbb56a01 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -190,7 +190,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn ev.preventDefault(); ev.stopPropagation(); } else if (onKeyDown) { - return onKeyDown(ev, state); + return onKeyDown(ev, context.state); } }, [context.state, onKeyDown, handleHomeEnd]); diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 0e968461a8..cc2a1769c7 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -30,6 +30,7 @@ const Toolbar: React.FC = ({children, ...props}) => { const target = ev.target as HTMLElement; let handled = true; + // HOME and END are handled by RovingTabIndexProvider switch (ev.key) { case Key.ARROW_UP: case Key.ARROW_DOWN: @@ -47,8 +48,6 @@ const Toolbar: React.FC = ({children, ...props}) => { } break; - // HOME and END are handled by RovingTabIndexProvider - default: handled = false; } diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index abc5412100..49f57ca7b6 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -20,7 +20,7 @@ import React from "react"; import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { // whether or not the context menu is currently open isExpanded: boolean; } diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 64233e51ad..0bb169abf8 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -26,8 +26,9 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a role=menuitem export const MenuItem: React.FC = ({children, label, ...props}) => { + const ariaLabel = props["aria-label"] || label; return ( - + { children } ); diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index cc824fef22..2cb974d60e 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -20,7 +20,8 @@ import AccessibleTooltipButton from "../../components/views/elements/AccessibleT import {useRovingTabIndex} from "../RovingTabIndex"; import {Ref} from "./types"; -interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { +type ATBProps = React.ComponentProps; +interface IProps extends Omit { inputRef?: Ref; } diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx index c826b74497..5211f30215 100644 --- a/src/accessibility/roving/RovingTabIndexWrapper.tsx +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; import {useRovingTabIndex} from "../RovingTabIndex"; import {FocusHandler, Ref} from "./types"; diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 7ec9da39de..406ffd8749 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -17,7 +17,6 @@ limitations under the License. import FileSaver from 'file-saver'; import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import { MatrixClient } from 'matrix-js-sdk'; @@ -27,34 +26,31 @@ import * as sdk from '../../../index'; const PHASE_EDIT = 1; const PHASE_EXPORTING = 2; -export default createReactClass({ - displayName: 'ExportE2eKeysDialog', - - propTypes: { +export default class ExportE2eKeysDialog extends React.Component { + static propTypes = { matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, onFinished: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - phase: PHASE_EDIT, - errStr: null, - }; - }, + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { this._unmounted = false; this._passphrase1 = createRef(); this._passphrase2 = createRef(); - }, - componentWillUnmount: function() { + this.state = { + phase: PHASE_EDIT, + errStr: null, + }; + } + + componentWillUnmount() { this._unmounted = true; - }, + } - _onPassphraseFormSubmit: function(ev) { + _onPassphraseFormSubmit = (ev) => { ev.preventDefault(); const passphrase = this._passphrase1.current.value; @@ -69,9 +65,9 @@ export default createReactClass({ this._startExport(passphrase); return false; - }, + }; - _startExport: function(passphrase) { + _startExport(passphrase) { // extra Promise.resolve() to turn synchronous exceptions into // asynchronous ones. Promise.resolve().then(() => { @@ -84,7 +80,7 @@ export default createReactClass({ const blob = new Blob([f], { type: 'text/plain;charset=us-ascii', }); - FileSaver.saveAs(blob, 'riot-keys.txt'); + FileSaver.saveAs(blob, 'element-keys.txt'); this.props.onFinished(true); }).catch((e) => { console.error("Error exporting e2e keys:", e); @@ -102,15 +98,15 @@ export default createReactClass({ errStr: null, phase: PHASE_EXPORTING, }); - }, + } - _onCancelClick: function(ev) { + _onCancelClick = (ev) => { ev.preventDefault(); this.props.onFinished(false); return false; - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const disableForm = (this.state.phase === PHASE_EXPORTING); @@ -184,5 +180,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 6b9d2c7e45..c2d17f681d 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -16,7 +16,6 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -38,48 +37,45 @@ function readFileAsArrayBuffer(file) { const PHASE_EDIT = 1; const PHASE_IMPORTING = 2; -export default createReactClass({ - displayName: 'ImportE2eKeysDialog', - - propTypes: { +export default class ImportE2eKeysDialog extends React.Component { + static propTypes = { matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, onFinished: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - enableSubmit: false, - phase: PHASE_EDIT, - errStr: null, - }; - }, + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { this._unmounted = false; this._file = createRef(); this._passphrase = createRef(); - }, - componentWillUnmount: function() { + this.state = { + enableSubmit: false, + phase: PHASE_EDIT, + errStr: null, + }; + } + + componentWillUnmount() { this._unmounted = true; - }, + } - _onFormChange: function(ev) { + _onFormChange = (ev) => { const files = this._file.current.files || []; this.setState({ enableSubmit: (this._passphrase.current.value !== "" && files.length > 0), }); - }, + }; - _onFormSubmit: function(ev) { + _onFormSubmit = (ev) => { ev.preventDefault(); this._startImport(this._file.current.files[0], this._passphrase.current.value); return false; - }, + }; - _startImport: function(file, passphrase) { + _startImport(file, passphrase) { this.setState({ errStr: null, phase: PHASE_IMPORTING, @@ -105,15 +101,15 @@ export default createReactClass({ phase: PHASE_EDIT, }); }); - }, + } - _onCancelClick: function(ev) { + _onCancelClick = (ev) => { ev.preventDefault(); this.props.onFinished(false); return false; - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const disableForm = (this.state.phase !== PHASE_EDIT); @@ -188,5 +184,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 79fbb98c7b..c3aef9109a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -286,7 +286,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { changeText = _t("Use a different passphrase?"); } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { // only tell them they're wrong if they've actually gone wrong. - // Security concious readers will note that if you left riot-web unattended + // Security concious readers will note that if you left element-web unattended // on this screen, this would make it easy for a malicious person to guess // your passphrase one letter at a time, but they could get this faster by // just opening the browser's developer tools and reading it. diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 4cef817a38..0a1a0b02b3 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -30,6 +30,7 @@ import StyledRadioButton from '../../../../components/views/elements/StyledRadio import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; +import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -55,12 +56,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { static propTypes = { hasCancel: PropTypes.bool, accountPassword: PropTypes.string, - force: PropTypes.bool, + forceReset: PropTypes.bool, }; static defaultProps = { hasCancel: true, - force: false, + forceReset: false, }; constructor(props) { @@ -85,8 +86,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { canUploadKeysWithPasswordOnly: null, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY, + canSkip: !isSecureBackupRequired(), }; this._passphraseField = createRef(); @@ -117,8 +118,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) ); - const { force } = this.props; - const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; + const { forceReset } = this.props; + const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; this.setState({ phase, @@ -276,20 +277,25 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const cli = MatrixClientPeg.get(); - const { force } = this.props; + const { forceReset } = this.props; try { - if (force) { - console.log("Forcing secret storage reset"); // log something so we can debug this later + if (forceReset) { + console.log("Forcing cross-signing and secret storage reset"); await cli.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); - } else { - await cli.bootstrapSecretStorage({ + await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + setupNewCrossSigning: true, + }); + } else { + await cli.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + }); + await cli.bootstrapSecretStorage({ createSecretStorageKey: async () => this._recoveryKey, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo, @@ -470,7 +476,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { primaryButton={_t("Continue")} onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit} onCancel={this._onCancelClick} - hasCancel={true} + hasCancel={this.state.canSkip} /> ; } @@ -480,7 +486,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // click the button are aware they're making a change to their account. // Once we're confident enough in this (and it's supported enough) we can do // it automatically. - // https://github.com/vector-im/riot-web/issues/11696 + // https://github.com/vector-im/element-web/issues/11696 const Field = sdk.getComponent('views.elements.Field'); let authPrompt; @@ -575,7 +581,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { changeText = _t("Use a different passphrase?"); } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { // only tell them they're wrong if they've actually gone wrong. - // Security concious readers will note that if you left riot-web unattended + // Security concious readers will note that if you left element-web unattended // on this screen, this would make it easy for a malicious person to guess // your passphrase one letter at a time, but they could get this faster by // just opening the browser's developer tools and reading it. @@ -687,7 +693,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
@@ -714,7 +720,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { case PHASE_CHOOSE_KEY_PASSPHRASE: - return _t('Set up Secure backup'); + return _t('Set up Secure Backup'); case PHASE_MIGRATE: return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: @@ -742,7 +748,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index e7a6f44536..3ff8ff0469 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -89,7 +89,11 @@ export default class CommandProvider extends AutocompleteProvider { renderCompletions(completions: React.ReactNode[]): React.ReactNode { return ( -
+
{ completions }
); diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index d7eac59f91..ebf5d536ec 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -23,7 +23,7 @@ import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import * as sdk from '../index'; -import _sortBy from 'lodash/sortBy'; +import {sortBy} from "lodash"; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; @@ -53,7 +53,7 @@ export default class CommunityProvider extends AutocompleteProvider { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); // Disable autocompletions when composing commands because of various issues - // (see https://github.com/vector-im/riot-web/issues/4762) + // (see https://github.com/vector-im/element-web/issues/4762) if (/^(\/join|\/leave)/.test(query)) { return []; } @@ -81,7 +81,7 @@ export default class CommunityProvider extends AutocompleteProvider { const matchedString = command[0]; completions = this.matcher.match(matchedString); - completions = _sortBy(completions, [ + completions = sortBy(completions, [ (c) => score(matchedString, c.groupId), (c) => c.groupId.length, ]).map(({avatarUrl, groupId, name}) => ({ @@ -91,15 +91,15 @@ export default class CommunityProvider extends AutocompleteProvider { href: makeGroupPermalink(groupId), component: ( - + ), range, - })) - .slice(0, 4); + })).slice(0, 4); } return completions; } diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 6ac2f4db14..4b0d35698d 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -34,9 +34,9 @@ export const TextualCompletion = forwardRef((props const {title, subtitle, description, className, ...restProps} = props; return (
{ title } { subtitle } @@ -53,9 +53,9 @@ export const PillCompletion = forwardRef((props, ref) const {title, subtitle, description, className, children, ...restProps} = props; return (
{ children } { title } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 147d68f5ff..705474f8d0 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -23,8 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import {ICompletion, ISelectionRange} from './Autocompleter'; -import _uniq from 'lodash/uniq'; -import _sortBy from 'lodash/sortBy'; +import {uniq, sortBy} from 'lodash'; import SettingsStore from "../settings/SettingsStore"; import { shortcodeToUnicode } from '../HtmlUtils'; import { EMOJI, IEmoji } from '../emoji'; @@ -115,7 +114,7 @@ export default class EmojiProvider extends AutocompleteProvider { } // Finally, sort by original ordering sorters.push((c) => c._orderBy); - completions = _sortBy(_uniq(completions), sorters); + completions = sortBy(uniq(completions), sorters); completions = completions.map(({shortname}) => { const unicode = shortcodeToUnicode(shortname); @@ -139,7 +138,11 @@ export default class EmojiProvider extends AutocompleteProvider { renderCompletions(completions: React.ReactNode[]): React.ReactNode { return ( -
+
{ completions }
); diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 9c91414556..a07ed29c7e 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -16,8 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import _at from 'lodash/at'; -import _uniq from 'lodash/uniq'; +import {at, uniq} from 'lodash'; import {removeHiddenChars} from "matrix-js-sdk/src/utils"; interface IOptions { @@ -73,7 +72,7 @@ export default class QueryMatcher { // type for their values. We assume that those values who's keys have // been specified will be string. Also, we cannot infer all the // types of the keys of the objects at compile. - const keyValues = _at(object, this._options.keys); + const keyValues = at(object, this._options.keys); if (this._options.funcs) { for (const f of this._options.funcs) { @@ -137,7 +136,7 @@ export default class QueryMatcher { }); // Now map the keys to the result objects. Also remove any duplicates. - return _uniq(matches.map((match) => match.object)); + return uniq(matches.map((match) => match.object)); } private processQuery(query: string): string { diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index f14fa3bbfa..74deacf61f 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -27,7 +27,7 @@ import {PillCompletion} from './Components'; import * as sdk from '../index'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; -import { uniqBy, sortBy } from 'lodash'; +import {uniqBy, sortBy} from "lodash"; const ROOM_REGEX = /\B#\S*/g; @@ -110,15 +110,13 @@ export default class RoomProvider extends AutocompleteProvider { ), range, }; - }) - .filter((completion) => !!completion.completion && completion.completion.length > 0) - .slice(0, 4); + }).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4); } return completions; } getName() { - return '💬 ' + _t('Rooms'); + return _t('Rooms'); } renderCompletions(completions: React.ReactNode[]): React.ReactNode { diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index eeb6c7a522..32eea55b0b 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -23,7 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider'; import {PillCompletion} from './Components'; import * as sdk from '../index'; import QueryMatcher from './QueryMatcher'; -import _sortBy from 'lodash/sortBy'; +import {sortBy} from 'lodash'; import {MatrixClientPeg} from '../MatrixClientPeg'; import MatrixEvent from "matrix-js-sdk/src/models/event"; @@ -71,8 +71,13 @@ export default class UserProvider extends AutocompleteProvider { } } - private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, - data: IRoomTimelineData) => { + private onRoomTimeline = ( + ev: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: IRoomTimelineData, + ) => { if (!room) return; if (removed) return; if (room.roomId !== this.room.roomId) return; @@ -137,7 +142,7 @@ export default class UserProvider extends AutocompleteProvider { } getName(): string { - return '👥 ' + _t('Users'); + return _t('Users'); } _makeUsers() { @@ -151,7 +156,7 @@ export default class UserProvider extends AutocompleteProvider { const currentUserId = MatrixClientPeg.get().credentials.userId; this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId); - this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); + this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); this.matcher.setObjects(this.users); } @@ -171,7 +176,11 @@ export default class UserProvider extends AutocompleteProvider { renderCompletions(completions: React.ReactNode[]): React.ReactNode { return ( -
+
{ completions }
); diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js deleted file mode 100644 index 1fa6068675..0000000000 --- a/src/components/structures/CompatibilityPage.js +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -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 from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { _t } from '../../languageHandler'; -import SdkConfig from '../../SdkConfig'; - -export default createReactClass({ - displayName: 'CompatibilityPage', - propTypes: { - onAccept: PropTypes.func, - }, - - getDefaultProps: function() { - return { - onAccept: function() {}, // NOP - }; - }, - - onAccept: function() { - this.props.onAccept(); - }, - - render: function() { - const brand = SdkConfig.get().brand; - - return ( -
-
-

{_t( - "Sorry, your browser is not able to run %(brand)s.", - { - brand, - }, - { - 'b': (sub) => {sub}, - }) - }

-

- { _t( - "%(brand)s uses many advanced browser features, some of which are not available " + - "or experimental in your current browser.", - { brand }, - ) } -

-

- { _t( - 'Please install Chrome, Firefox, ' + - 'or Safari for the best experience.', - {}, - { - 'chromeLink': (sub) => {sub}, - 'firefoxLink': (sub) => {sub}, - 'safariLink': (sub) => {sub}, - }, - )} -

-

- { _t( - "With your current browser, the look and feel of the application may be " + - "completely incorrect, and some or all features may not function. " + - "If you want to try it anyway you can continue, but you are on your own in terms " + - "of any issues you may encounter!", - ) } -

- -
-
- ); - }, -}); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index f1bd297730..64e0160d83 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -58,7 +58,7 @@ export enum ChevronFace { None = "none", } -interface IProps extends IPosition { +export interface IProps extends IPosition { menuWidth?: number; menuHeight?: number; @@ -233,8 +233,7 @@ export class ContextMenu extends React.PureComponent { switch (ev.key) { case Key.TAB: case Key.ESCAPE: - // close on left and right arrows too for when it is a context menu on a - case Key.ARROW_LEFT: + case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a case Key.ARROW_RIGHT: this.props.onFinished(); break; diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 49ba3d1227..cbfeff7582 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -43,8 +43,8 @@ export default class EmbeddedPage extends React.PureComponent { static contextType = MatrixClientContext; - constructor(props) { - super(props); + constructor(props, context) { + super(props, context); this._dispatcherRef = null; diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index d873dd4094..8aa1192458 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {Filter} from 'matrix-js-sdk'; @@ -28,23 +27,20 @@ import { _t } from '../../languageHandler'; /* * Component which shows the filtered file using a TimelinePanel */ -const FilePanel = createReactClass({ - displayName: 'FilePanel', +class FilePanel extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + }; + // This is used to track if a decrypted event was a live event and should be // added to the timeline. - decryptingEvents: new Set(), + decryptingEvents = new Set(); - propTypes: { - roomId: PropTypes.string.isRequired, - }, + state = { + timelineSet: null, + }; - getInitialState: function() { - return { - timelineSet: null, - }; - }, - - onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { if (room.roomId !== this.props.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; @@ -53,9 +49,9 @@ const FilePanel = createReactClass({ } else { this.addEncryptedLiveEvent(ev); } - }, + }; - onEventDecrypted(ev, err) { + onEventDecrypted = (ev, err) => { if (ev.getRoomId() !== this.props.roomId) return; const eventId = ev.getId(); @@ -63,7 +59,7 @@ const FilePanel = createReactClass({ if (err) return; this.addEncryptedLiveEvent(ev); - }, + }; addEncryptedLiveEvent(ev, toStartOfTimeline) { if (!this.state.timelineSet) return; @@ -77,7 +73,7 @@ const FilePanel = createReactClass({ if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) { this.state.timelineSet.addEventToTimeline(ev, timeline, false); } - }, + } async componentDidMount() { const client = MatrixClientPeg.get(); @@ -98,7 +94,7 @@ const FilePanel = createReactClass({ client.on('Room.timeline', this.onRoomTimeline); client.on('Event.decrypted', this.onEventDecrypted); } - }, + } componentWillUnmount() { const client = MatrixClientPeg.get(); @@ -110,7 +106,7 @@ const FilePanel = createReactClass({ client.removeListener('Room.timeline', this.onRoomTimeline); client.removeListener('Event.decrypted', this.onEventDecrypted); } - }, + } async fetchFileEventsServer(room) { const client = MatrixClientPeg.get(); @@ -134,9 +130,9 @@ const FilePanel = createReactClass({ const timelineSet = room.getOrCreateFilteredTimelineSet(filter); return timelineSet; - }, + } - onPaginationRequest(timelineWindow, direction, limit) { + onPaginationRequest = (timelineWindow, direction, limit) => { const client = MatrixClientPeg.get(); const eventIndex = EventIndexPeg.get(); const roomId = this.props.roomId; @@ -152,7 +148,7 @@ const FilePanel = createReactClass({ } else { return timelineWindow.paginate(direction, limit); } - }, + }; async updateTimelineSet(roomId: string) { const client = MatrixClientPeg.get(); @@ -188,9 +184,9 @@ const FilePanel = createReactClass({ } else { console.error("Failed to add filtered timelineSet for FilePanel as no room!"); } - }, + } - render: function() { + render() { if (MatrixClientPeg.get().isGuest()) { return
@@ -220,7 +216,7 @@ const FilePanel = createReactClass({ // "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId); return (
- ); } - }, -}); + } +} export default FilePanel; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index a946d16319..83f70eb72a 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import * as sdk from '../../index'; @@ -70,10 +69,8 @@ const UserSummaryType = PropTypes.shape({ }).isRequired, }); -const CategoryRoomList = createReactClass({ - displayName: 'CategoryRoomList', - - props: { +class CategoryRoomList extends React.Component { + static propTypes = { rooms: PropTypes.arrayOf(RoomSummaryType).isRequired, category: PropTypes.shape({ profile: PropTypes.shape({ @@ -84,9 +81,9 @@ const CategoryRoomList = createReactClass({ // Whether the list should be editable editing: PropTypes.bool.isRequired, - }, + }; - onAddRoomsToSummaryClicked: function(ev) { + onAddRoomsToSummaryClicked = (ev) => { ev.preventDefault(); const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, { @@ -122,9 +119,9 @@ const CategoryRoomList = createReactClass({ }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); - }, + }; - render: function() { + render() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const addButton = this.props.editing ? (; - }, -}); + } +} -const FeaturedRoom = createReactClass({ - displayName: 'FeaturedRoom', - - props: { +class FeaturedRoom extends React.Component { + static propTypes = { summaryInfo: RoomSummaryType.isRequired, editing: PropTypes.bool.isRequired, groupId: PropTypes.string.isRequired, - }, + }; - onClick: function(e) { + onClick = (e) => { e.preventDefault(); e.stopPropagation(); @@ -176,9 +171,9 @@ const FeaturedRoom = createReactClass({ room_alias: this.props.summaryInfo.profile.canonical_alias, room_id: this.props.summaryInfo.room_id, }); - }, + }; - onDeleteClicked: function(e) { + onDeleteClicked = (e) => { e.preventDefault(); e.stopPropagation(); GroupStore.removeRoomFromGroupSummary( @@ -201,9 +196,9 @@ const FeaturedRoom = createReactClass({ description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), }); }); - }, + }; - render: function() { + render() { const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); const roomName = this.props.summaryInfo.profile.name || @@ -243,13 +238,11 @@ const FeaturedRoom = createReactClass({
{ roomNameNode }
{ deleteButton }
; - }, -}); + } +} -const RoleUserList = createReactClass({ - displayName: 'RoleUserList', - - props: { +class RoleUserList extends React.Component { + static propTypes = { users: PropTypes.arrayOf(UserSummaryType).isRequired, role: PropTypes.shape({ profile: PropTypes.shape({ @@ -260,9 +253,9 @@ const RoleUserList = createReactClass({ // Whether the list should be editable editing: PropTypes.bool.isRequired, - }, + }; - onAddUsersClicked: function(ev) { + onAddUsersClicked = (ev) => { ev.preventDefault(); const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, { @@ -298,9 +291,9 @@ const RoleUserList = createReactClass({ }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); - }, + }; - render: function() { + render() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const addButton = this.props.editing ? ( @@ -325,19 +318,17 @@ const RoleUserList = createReactClass({ { userNodes } { addButton }
; - }, -}); + } +} -const FeaturedUser = createReactClass({ - displayName: 'FeaturedUser', - - props: { +class FeaturedUser extends React.Component { + static propTypes = { summaryInfo: UserSummaryType.isRequired, editing: PropTypes.bool.isRequired, groupId: PropTypes.string.isRequired, - }, + }; - onClick: function(e) { + onClick = (e) => { e.preventDefault(); e.stopPropagation(); @@ -345,9 +336,9 @@ const FeaturedUser = createReactClass({ action: 'view_start_chat_or_reuse', user_id: this.props.summaryInfo.user_id, }); - }, + }; - onDeleteClicked: function(e) { + onDeleteClicked = (e) => { e.preventDefault(); e.stopPropagation(); GroupStore.removeUserFromGroupSummary( @@ -368,9 +359,9 @@ const FeaturedUser = createReactClass({ description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}), }); }); - }, + }; - render: function() { + render() { const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id; @@ -394,41 +385,37 @@ const FeaturedUser = createReactClass({
{ userNameNode }
{ deleteButton } ; - }, -}); + } +} const GROUP_JOINPOLICY_OPEN = "open"; const GROUP_JOINPOLICY_INVITE = "invite"; -export default createReactClass({ - displayName: 'GroupView', - - propTypes: { +export default class GroupView extends React.Component { + static propTypes = { groupId: PropTypes.string.isRequired, // Whether this is the first time the group admin is viewing the group groupIsNew: PropTypes.bool, - }, + }; - getInitialState: function() { - return { - summary: null, - isGroupPublicised: null, - isUserPrivileged: null, - groupRooms: null, - groupRoomsLoading: null, - error: null, - editing: false, - saving: false, - uploadingAvatar: false, - avatarChanged: false, - membershipBusy: false, - publicityBusy: false, - inviterProfile: null, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, - }; - }, + state = { + summary: null, + isGroupPublicised: null, + isUserPrivileged: null, + groupRooms: null, + groupRoomsLoading: null, + error: null, + editing: false, + saving: false, + uploadingAvatar: false, + avatarChanged: false, + membershipBusy: false, + publicityBusy: false, + inviterProfile: null, + showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + }; - componentDidMount: function() { + componentDidMount() { this._unmounted = false; this._matrixClient = MatrixClientPeg.get(); this._matrixClient.on("Group.myMembership", this._onGroupMyMembership); @@ -437,9 +424,9 @@ export default createReactClass({ this._dispatcherRef = dis.register(this._onAction); this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._unmounted = true; this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); dis.unregister(this._dispatcherRef); @@ -448,10 +435,11 @@ export default createReactClass({ if (this._rightPanelStoreToken) { this._rightPanelStoreToken.remove(); } - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(newProps) { if (this.props.groupId !== newProps.groupId) { this.setState({ summary: null, @@ -460,24 +448,24 @@ export default createReactClass({ this._initGroupStore(newProps.groupId); }); } - }, + } - _onRightPanelStoreUpdate: function() { + _onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, }); - }, + }; - _onGroupMyMembership: function(group) { + _onGroupMyMembership = (group) => { if (this._unmounted || group.groupId !== this.props.groupId) return; if (group.myMembership === 'leave') { // Leave settings - the user might have clicked the "Leave" button this._closeSettings(); } this.setState({membershipBusy: false}); - }, + }; - _initGroupStore: function(groupId, firstInit) { + _initGroupStore(groupId, firstInit) { const group = this._matrixClient.getGroup(groupId); if (group && group.inviter && group.inviter.userId) { this._fetchInviterProfile(group.inviter.userId); @@ -506,9 +494,9 @@ export default createReactClass({ }); } }); - }, + } - onGroupStoreUpdated(firstInit) { + onGroupStoreUpdated = (firstInit) => { if (this._unmounted) return; const summary = GroupStore.getSummary(this.props.groupId); if (summary.profile) { @@ -533,7 +521,7 @@ export default createReactClass({ if (this.props.groupIsNew && firstInit) { this._onEditClick(); } - }, + }; _fetchInviterProfile(userId) { this.setState({ @@ -555,9 +543,9 @@ export default createReactClass({ inviterProfileBusy: false, }); }); - }, + } - _onEditClick: function() { + _onEditClick = () => { this.setState({ editing: true, profileForm: Object.assign({}, this.state.summary.profile), @@ -568,20 +556,20 @@ export default createReactClass({ GROUP_JOINPOLICY_INVITE, }, }); - }, + }; - _onShareClick: function() { + _onShareClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share community dialog', '', ShareDialog, { target: this._matrixClient.getGroup(this.props.groupId) || new Group(this.props.groupId), }); - }, + }; - _onCancelClick: function() { + _onCancelClick = () => { this._closeSettings(); - }, + }; - _onAction(payload) { + _onAction = (payload) => { switch (payload.action) { // NOTE: close_settings is an app-wide dispatch; as it is dispatched from MatrixChat case 'close_settings': @@ -593,34 +581,34 @@ export default createReactClass({ default: break; } - }, + }; - _closeSettings() { + _closeSettings = () => { dis.dispatch({action: 'close_settings'}); - }, + }; - _onNameChange: function(value) { + _onNameChange = (value) => { const newProfileForm = Object.assign(this.state.profileForm, { name: value }); this.setState({ profileForm: newProfileForm, }); - }, + }; - _onShortDescChange: function(value) { + _onShortDescChange = (value) => { const newProfileForm = Object.assign(this.state.profileForm, { short_description: value }); this.setState({ profileForm: newProfileForm, }); - }, + }; - _onLongDescChange: function(e) { + _onLongDescChange = (e) => { const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value }); this.setState({ profileForm: newProfileForm, }); - }, + }; - _onAvatarSelected: function(ev) { + _onAvatarSelected = ev => { const file = ev.target.files[0]; if (!file) return; @@ -644,15 +632,15 @@ export default createReactClass({ description: _t('Failed to upload image'), }); }); - }, + }; - _onJoinableChange: function(ev) { + _onJoinableChange = ev => { this.setState({ joinableForm: { policyType: ev.target.value }, }); - }, + }; - _onSaveClick: function() { + _onSaveClick = () => { this.setState({saving: true}); const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve(); savePromise.then((result) => { @@ -683,16 +671,16 @@ export default createReactClass({ avatarChanged: false, }); }); - }, + }; - _saveGroup: async function() { + async _saveGroup() { await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm); await this._matrixClient.setGroupJoinPolicy(this.props.groupId, { type: this.state.joinableForm.policyType, }); - }, + } - _onAcceptInviteClick: async function() { + _onAcceptInviteClick = async () => { this.setState({membershipBusy: true}); // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the @@ -709,9 +697,9 @@ export default createReactClass({ description: _t("Unable to accept invite"), }); }); - }, + }; - _onRejectInviteClick: async function() { + _onRejectInviteClick = async () => { this.setState({membershipBusy: true}); // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the @@ -728,9 +716,9 @@ export default createReactClass({ description: _t("Unable to reject invite"), }); }); - }, + }; - _onJoinClick: async function() { + _onJoinClick = async () => { if (this._matrixClient.isGuest()) { dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}}); return; @@ -752,9 +740,9 @@ export default createReactClass({ description: _t("Unable to join community"), }); }); - }, + }; - _leaveGroupWarnings: function() { + _leaveGroupWarnings() { const warnings = []; if (this.state.isUserPrivileged) { @@ -768,10 +756,9 @@ export default createReactClass({ } return warnings; - }, + } - - _onLeaveClick: function() { + _onLeaveClick = () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const warnings = this._leaveGroupWarnings(); @@ -806,13 +793,13 @@ export default createReactClass({ }); }, }); - }, + }; - _onAddRoomsClick: function() { + _onAddRoomsClick = () => { showGroupAddRoomDialog(this.props.groupId); - }, + }; - _getGroupSection: function() { + _getGroupSection() { const groupSettingsSectionClasses = classnames({ "mx_GroupView_group": this.state.editing, "mx_GroupView_group_disabled": this.state.editing && !this.state.isUserPrivileged, @@ -856,9 +843,9 @@ export default createReactClass({ { this._getLongDescriptionNode() } { this._getRoomsNode() }
; - }, + } - _getRoomsNode: function() { + _getRoomsNode() { const RoomDetailList = sdk.getComponent('rooms.RoomDetailList'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); @@ -902,9 +889,9 @@ export default createReactClass({ className={roomDetailListClassName} /> }
; - }, + } - _getFeaturedRoomsNode: function() { + _getFeaturedRoomsNode() { const summary = this.state.summary; const defaultCategoryRooms = []; @@ -943,9 +930,9 @@ export default createReactClass({ { defaultCategoryNode } { categoryRoomNodes }
; - }, + } - _getFeaturedUsersNode: function() { + _getFeaturedUsersNode() { const summary = this.state.summary; const noRoleUsers = []; @@ -984,9 +971,9 @@ export default createReactClass({ { noRoleNode } { roleUserNodes }
; - }, + } - _getMembershipSection: function() { + _getMembershipSection() { const Spinner = sdk.getComponent("elements.Spinner"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); @@ -1003,9 +990,10 @@ export default createReactClass({ this.state.inviterProfile.avatarUrl, 36, 36, ) : null; - let inviterName = group.inviter.userId; + const inviter = group.inviter || {}; + let inviterName = inviter.userId; if (this.state.inviterProfile) { - inviterName = this.state.inviterProfile.displayName || group.inviter.userId; + inviterName = this.state.inviterProfile.displayName || inviter.userId; } return
@@ -1016,7 +1004,7 @@ export default createReactClass({ height={36} /> { _t("%(inviter)s has invited you to join this community", { - inviter: inviterName, + inviter: inviterName || _t("Someone"), }) }
@@ -1099,9 +1087,9 @@ export default createReactClass({
; - }, + } - _getJoinableNode: function() { + _getJoinableNode() { const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); return this.state.editing ?

@@ -1135,9 +1123,9 @@ export default createReactClass({

: null; - }, + } - _getLongDescriptionNode: function() { + _getLongDescriptionNode() { const summary = this.state.summary; let description = null; if (summary.profile && summary.profile.long_description) { @@ -1174,9 +1162,9 @@ export default createReactClass({
{ description }
; - }, + } - render: function() { + render() { const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const Spinner = sdk.getComponent("elements.Spinner"); @@ -1365,5 +1353,5 @@ export default createReactClass({ console.error("Invalid state for GroupView"); return
; } - }, -}); + } +} diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 7414a44f11..a42032c9fe 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -46,8 +46,8 @@ const HomePage = () => { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); return
- {config.brand -

{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }

+ {config.brand +

{ _t("Welcome to %(appName)s", { appName: config.brand || "Element" }) }

{ _t("Liberate your communication") }

diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 27f7fbb301..cd5510de9d 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -158,7 +158,7 @@ export default class IndicatorScrollbar extends React.Component { } // don't mess with the horizontal scroll for trackpad users - // See https://github.com/vector-im/riot-web/issues/10005 + // See https://github.com/vector-im/element-web/issues/10005 if (this._likelyTrackpadUser) { return; } diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 351e3bbad0..c8fcd7e9ca 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -17,7 +17,6 @@ limitations under the License. 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'; @@ -26,10 +25,8 @@ import * as sdk from '../../index'; export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); -export default createReactClass({ - displayName: 'InteractiveAuth', - - propTypes: { +export default class InteractiveAuthComponent extends React.Component { + static propTypes = { // matrix client to use for UI auth requests matrixClient: PropTypes.object.isRequired, @@ -86,20 +83,19 @@ export default createReactClass({ // continueText and continueKind are passed straight through to the AuthEntryComponent. continueText: PropTypes.string, continueKind: PropTypes.string, - }, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this.state = { authStage: null, busy: false, errorText: null, stageErrorText: null, submitButtonEnabled: false, }; - }, - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { this._unmounted = false; this._authLogic = new InteractiveAuth({ authData: this.props.authData, @@ -114,6 +110,18 @@ export default createReactClass({ requestEmailToken: this._requestEmailToken, }); + this._intervalId = null; + if (this.props.poll) { + this._intervalId = setInterval(() => { + this._authLogic.poll(); + }, 2000); + } + + this._stageComponent = createRef(); + } + + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount() { // eslint-disable-line camelcase this._authLogic.attemptAuth().then((result) => { const extra = { emailSid: this._authLogic.getEmailSid(), @@ -132,26 +140,17 @@ export default createReactClass({ errorText: msg, }); }); + } - this._intervalId = null; - if (this.props.poll) { - this._intervalId = setInterval(() => { - this._authLogic.poll(); - }, 2000); - } - - this._stageComponent = createRef(); - }, - - componentWillUnmount: function() { + componentWillUnmount() { this._unmounted = true; if (this._intervalId !== null) { clearInterval(this._intervalId); } - }, + } - _requestEmailToken: async function(...args) { + _requestEmailToken = async (...args) => { this.setState({ busy: true, }); @@ -162,15 +161,15 @@ export default createReactClass({ busy: false, }); } - }, + }; - tryContinue: function() { + tryContinue = () => { if (this._stageComponent.current && this._stageComponent.current.tryContinue) { this._stageComponent.current.tryContinue(); } - }, + }; - _authStateUpdated: function(stageType, stageState) { + _authStateUpdated = (stageType, stageState) => { const oldStage = this.state.authStage; this.setState({ busy: false, @@ -180,16 +179,16 @@ export default createReactClass({ }, () => { if (oldStage != stageType) this._setFocus(); }); - }, + }; - _requestCallback: function(auth) { + _requestCallback = (auth) => { // This wrapper just exists because the js-sdk passes a second // 'busy' param for backwards compat. This throws the tests off // so discard it here. return this.props.makeRequest(auth); - }, + }; - _onBusyChanged: function(busy) { + _onBusyChanged = (busy) => { // if we've started doing stuff, reset the error messages if (busy) { this.setState({ @@ -203,30 +202,30 @@ export default createReactClass({ // the UI layer, so we ignore this signal and show a spinner until // there's a new screen to show the user. This is implemented by setting // `busy: false` in `_authStateUpdated`. - // See also https://github.com/vector-im/riot-web/issues/12546 - }, + // See also https://github.com/vector-im/element-web/issues/12546 + }; - _setFocus: function() { + _setFocus() { if (this._stageComponent.current && this._stageComponent.current.focus) { this._stageComponent.current.focus(); } - }, + } - _submitAuthDict: function(authData) { + _submitAuthDict = authData => { this._authLogic.submitAuthDict(authData); - }, + }; - _onPhaseChange: function(newPhase) { + _onPhaseChange = newPhase => { if (this.props.onStagePhaseChange) { this.props.onStagePhaseChange(this.state.authStage, newPhase || 0); } - }, + }; - _onStageCancel: function() { + _onStageCancel = () => { this.props.onAuthFinished(false, ERROR_USER_CANCELLED); - }, + }; - _renderCurrentStage: function() { + _renderCurrentStage() { const stage = this.state.authStage; if (!stage) { if (this.state.busy) { @@ -260,16 +259,17 @@ export default createReactClass({ onCancel={this._onStageCancel} /> ); - }, + } - _onAuthStageFailed: function(e) { + _onAuthStageFailed = e => { this.props.onAuthFinished(false, e); - }, - _setEmailSid: function(sid) { - this._authLogic.setEmailSid(sid); - }, + }; - render: function() { + _setEmailSid = sid => { + this._authLogic.setEmailSid(sid); + }; + + render() { let error = null; if (this.state.errorText) { error = ( @@ -287,5 +287,5 @@ export default createReactClass({
); - }, -}); + } +} diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index bc17bbe23f..1c2295384c 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -37,6 +37,7 @@ import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; +import RoomListNumResults from "../views/rooms/RoomListNumResults"; interface IProps { isMinimized: boolean; @@ -376,8 +377,8 @@ export default class LeftPanel extends React.Component { public render(): React.ReactNode { const tagPanel = !this.state.showTagPanel ? null : (
- - {SettingsStore.isFeatureEnabled("feature_custom_tags") ? : null} + + {SettingsStore.getValue("feature_custom_tags") ? : null}
); @@ -409,6 +410,7 @@ export default class LeftPanel extends React.Component { {this.renderHeader()} {this.renderSearchExplore()} {this.renderBreadcrumbs()} +
{ } }; - _calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { - const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; + _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { - usageLimitEventContent = syncErrorData.error.data; + usageLimitEventContent = syncError.error.data; } if (usageLimitEventContent) { @@ -596,7 +599,7 @@ class LoggedInView extends React.Component { const maxRadius = 5; // People shouldn't be straying too far, hopefully // Note: we track how far the user moved their mouse to help - // combat against https://github.com/vector-im/riot-web/issues/7158 + // combat against https://github.com/vector-im/element-web/issues/7158 if (distance < maxRadius) { // This is probably a real click, and not a drag @@ -620,18 +623,18 @@ class LoggedInView extends React.Component { switch (this.props.page_type) { case PageTypes.RoomView: pageElement = ; + ref={this._roomView} + autoJoin={this.props.autoJoin} + onRegistered={this.props.onRegistered} + thirdPartyInvite={this.props.thirdPartyInvite} + oobData={this.props.roomOobData} + viaServers={this.props.viaServers} + eventPixelOffset={this.props.initialEventPixelOffset} + key={this.props.currentRoomId || 'roomview'} + disabled={this.props.middleDisabled} + ConferenceHandler={this.props.ConferenceHandler} + resizeNotifier={this.props.resizeNotifier} + />; break; case PageTypes.MyGroups: diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a66d4c043f..176aaf95a3 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -69,13 +69,15 @@ import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload"; import { Action } from "../../dispatcher/actions"; import { showToast as showAnalyticsToast, - hideToast as hideAnalyticsToast + hideToast as hideAnalyticsToast, } from "../../toasts/AnalyticsToast"; import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import ErrorDialog from "../views/dialogs/ErrorDialog"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { SettingLevel } from "../../settings/SettingLevel"; +import { leaveRoomBehaviour } from "../../utils/membership"; +import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; /** constants for MatrixChat.state.view */ export enum Views { @@ -127,6 +129,7 @@ interface IScreen { params?: object; } +/* eslint-disable camelcase */ interface IRoomInfo { room_id?: string; room_alias?: string; @@ -138,6 +141,7 @@ interface IRoomInfo { oob_data?: object; via_servers?: string[]; } +/* eslint-enable camelcase */ interface IProps { // TODO type things better config: Record; @@ -163,6 +167,7 @@ interface IState { // the master view we are showing. view: Views; // What the LoggedInView would be showing if visible + // eslint-disable-next-line camelcase page_type?: PageTypes; // The ID of the room we're viewing. This is either populated directly // in the case where we view a room by ID or by RoomView when it resolves @@ -178,8 +183,11 @@ interface IState { middleDisabled: boolean; // the right panel's disabled state is tracked in its store. // Parameters used in the registration dance with the IS + // eslint-disable-next-line camelcase register_client_secret?: string; + // eslint-disable-next-line camelcase register_session_id?: string; + // eslint-disable-next-line camelcase register_id_sid?: string; // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs @@ -339,6 +347,7 @@ export default class MatrixChat extends React.PureComponent { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage + // eslint-disable-next-line camelcase UNSAFE_componentWillUpdate(props, state) { if (this.shouldTrackPageChange(this.state, state)) { this.startPageChangeTimer(); @@ -415,7 +424,7 @@ export default class MatrixChat extends React.PureComponent { return; } this.pageChanging = true; - performance.mark('riot_MatrixChat_page_change_start'); + performance.mark('element_MatrixChat_page_change_start'); } stopPageChangeTimer() { @@ -427,15 +436,15 @@ export default class MatrixChat extends React.PureComponent { return; } this.pageChanging = false; - performance.mark('riot_MatrixChat_page_change_stop'); + performance.mark('element_MatrixChat_page_change_stop'); performance.measure( - 'riot_MatrixChat_page_change_delta', - 'riot_MatrixChat_page_change_start', - 'riot_MatrixChat_page_change_stop', + 'element_MatrixChat_page_change_delta', + 'element_MatrixChat_page_change_start', + 'element_MatrixChat_page_change_stop', ); - performance.clearMarks('riot_MatrixChat_page_change_start'); - performance.clearMarks('riot_MatrixChat_page_change_stop'); - const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop(); + performance.clearMarks('element_MatrixChat_page_change_start'); + performance.clearMarks('element_MatrixChat_page_change_stop'); + const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop(); // In practice, sometimes the entries list is empty, so we get no measurement if (!measurement) return null; @@ -608,8 +617,7 @@ export default class MatrixChat extends React.PureComponent { const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {initialTabId: tabPayload.initialTabId}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true - ); + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); @@ -619,7 +627,10 @@ export default class MatrixChat extends React.PureComponent { this.createRoom(payload.public); break; case 'view_create_group': { - const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); + let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog") + if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + CreateGroupDialog = CreateCommunityPrototypeDialog; + } Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); break; } @@ -1075,57 +1086,20 @@ export default class MatrixChat extends React.PureComponent { title: _t("Leave room"), description: ( - { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } + { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } { warnings } ), button: _t("Leave"), onFinished: (shouldLeave) => { if (shouldLeave) { - const d = MatrixClientPeg.get().leaveRoomChain(roomId); + const d = leaveRoomBehaviour(roomId); // FIXME: controller shouldn't be loading a view :( const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - d.then((errors) => { - modal.close(); - - for (const leftRoomId of Object.keys(errors)) { - const err = errors[leftRoomId]; - if (!err) continue; - - console.error("Failed to leave room " + leftRoomId + " " + err); - let title = _t("Failed to leave room"); - let message = _t("Server may be unavailable, overloaded, or you hit a bug."); - if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { - title = _t("Can't leave Server Notices room"); - message = _t( - "This room is used for important messages from the Homeserver, " + - "so you cannot leave it.", - ); - } else if (err && err.message) { - message = err.message; - } - Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, { - title: title, - description: message, - }); - return; - } - - if (this.state.currentRoomId === roomId) { - dis.dispatch({action: 'view_next_room'}); - } - }, (err) => { - // This should only happen if something went seriously wrong with leaving the chain. - modal.close(); - console.error("Failed to leave room " + roomId + " " + err); - Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, { - title: _t("Failed to leave room"), - description: _t("Unknown error"), - }); - }); + d.finally(() => modal.close()); } }, }); @@ -1323,7 +1297,7 @@ export default class MatrixChat extends React.PureComponent { // state (each of which can be 10s of MBs) for each DISJOINT timeline. This is // particularly noticeable when there are lots of 'limited' /sync responses // such as when laptops unsleep. - // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 + // https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568 cli.setCanResetTimelineCallback((roomId) => { console.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId); if (roomId !== this.state.currentRoomId) { @@ -1465,7 +1439,6 @@ export default class MatrixChat extends React.PureComponent { cli.on("crypto.warning", (type) => { switch (type) { case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': - const brand = SdkConfig.get().brand; Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { title: _t('Old cryptography data detected'), description: _t( @@ -1476,7 +1449,7 @@ export default class MatrixChat extends React.PureComponent { "in this version. This may also cause messages exchanged with this " + "version to fail. If you experience problems, log out and back in " + "again. To retain message history, export and re-import your keys.", - { brand }, + { brand: SdkConfig.get().brand }, ), }); break; @@ -1661,7 +1634,7 @@ export default class MatrixChat extends React.PureComponent { // of the app, we coerce the eventId to be undefined where applicable. if (!eventId) eventId = undefined; - // TODO: Handle encoded room/event IDs: https://github.com/vector-im/riot-web/issues/9149 + // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149 // FIXME: sort_out caseConsistency const thirdPartyInvite = { @@ -1935,7 +1908,7 @@ export default class MatrixChat extends React.PureComponent { let fragmentAfterLogin = ""; const initialScreenAfterLogin = this.props.initialScreenAfterLogin; if (initialScreenAfterLogin && - // XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop + // XXX: workaround for https://github.com/vector-im/element-web/issues/11643 causing a login-loop !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen) ) { fragmentAfterLogin = `/${initialScreenAfterLogin.screen}`; @@ -2085,3 +2058,12 @@ export default class MatrixChat extends React.PureComponent { ; } } + +export function isLoggedIn(): boolean { + // JRS: Maybe we should move the step that writes this to the window out of + // `element-web` and into this file? Better yet, we should probably create a + // store to hold this state. + // See also https://github.com/vector-im/element-web/issues/15034. + const app = window.matrixChat; + return app && (app as MatrixChat).state.view === Views.LOGGED_IN; +} diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 7043c7f38a..e0551eecdb 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import * as sdk from '../../index'; import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; @@ -26,29 +25,23 @@ import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; -export default createReactClass({ - displayName: 'MyGroups', +export default class MyGroups extends React.Component { + static contextType = MatrixClientContext; - getInitialState: function() { - return { - groups: null, - error: null, - }; - }, + state = { + groups: null, + error: null, + }; - statics: { - contextType: MatrixClientContext, - }, - - componentDidMount: function() { + componentDidMount() { this._fetch(); - }, + } - _onCreateGroupClick: function() { + _onCreateGroupClick = () => { dis.dispatch({action: 'view_create_group'}); - }, + }; - _fetch: function() { + _fetch() { this.context.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { @@ -59,9 +52,9 @@ export default createReactClass({ } this.setState({groups: null, error: err}); }); - }, + } - render: function() { + render() { const brand = SdkConfig.get().brand; const Loader = sdk.getComponent("elements.Spinner"); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); @@ -149,5 +142,5 @@ export default createReactClass({ { content }
; - }, -}); + } +} diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index c1f78cffda..6ae7f91142 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import { _t } from '../../languageHandler'; import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; @@ -25,13 +24,8 @@ import * as sdk from "../../index"; /* * Component which shows the global notification list using a TimelinePanel */ -const NotificationPanel = createReactClass({ - displayName: 'NotificationPanel', - - propTypes: { - }, - - render: function() { +class NotificationPanel extends React.Component { + render() { // wrap a TimelinePanel with the jump-to-event bits turned off. const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const Loader = sdk.getComponent("elements.Spinner"); @@ -45,7 +39,7 @@ const NotificationPanel = createReactClass({ if (timelineSet) { return (
- ); } - }, -}); + } +} export default NotificationPanel; diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index a4e3254e4c..24fee80c2a 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -42,8 +42,8 @@ export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - constructor(props) { - super(props); + constructor(props, context) { + super(props, context); this.state = { phase: this._getPhaseFromProps(), isUserPrivilegedInGroup: null, diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 5b12dae7df..16ab8edbed 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; @@ -30,6 +29,10 @@ import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/Di import Analytics from '../../Analytics'; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; +import SettingsStore from "../../settings/SettingsStore"; +import TagOrderStore from "../../stores/TagOrderStore"; +import GroupStore from "../../stores/GroupStore"; +import FlairStore from "../../stores/FlairStore"; const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 160; @@ -38,15 +41,16 @@ function track(action) { Analytics.trackEvent('RoomDirectory', action); } -export default createReactClass({ - displayName: 'RoomDirectory', - - propTypes: { +export default class RoomDirectory extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + const selectedCommunityId = TagOrderStore.getSelectedTags()[0]; + this.state = { publicRooms: [], loading: true, protocolsLoading: true, @@ -54,11 +58,12 @@ export default createReactClass({ instanceId: undefined, roomServer: MatrixClientPeg.getHomeserverName(), filterString: null, + selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") + ? selectedCommunityId + : null, + communityName: null, }; - }, - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount: function() { this._unmounted = false; this.nextBatch = null; this.filterTimeout = null; @@ -71,49 +76,88 @@ export default createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { - this.protocols = response; - this.setState({protocolsLoading: false}); - }, (err) => { - console.warn(`error loading third party protocols: ${err}`); - this.setState({protocolsLoading: false}); - if (MatrixClientPeg.get().isGuest()) { - // Guests currently aren't allowed to use this API, so - // ignore this as otherwise this error is literally the - // thing you see when loading the client! - return; - } - track('Failed to get protocol list from homeserver'); - const brand = SdkConfig.get().brand; - this.setState({ - error: _t( - '%(brand)s failed to get the protocol list from the homeserver. ' + - 'The homeserver may be too old to support third party networks.', - { brand }, - ), + + if (!this.state.selectedCommunityId) { + MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { + this.protocols = response; + this.setState({protocolsLoading: false}); + }, (err) => { + console.warn(`error loading third party protocols: ${err}`); + this.setState({protocolsLoading: false}); + if (MatrixClientPeg.get().isGuest()) { + // Guests currently aren't allowed to use this API, so + // ignore this as otherwise this error is literally the + // thing you see when loading the client! + return; + } + track('Failed to get protocol list from homeserver'); + const brand = SdkConfig.get().brand; + this.setState({ + error: _t( + '%(brand)s failed to get the protocol list from the homeserver. ' + + 'The homeserver may be too old to support third party networks.', + {brand}, + ), + }); }); - }); + } else { + // We don't use the protocols in the communities v2 prototype experience + this.setState({protocolsLoading: false}); + + // Grab the profile info async + FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => { + this.setState({communityName: profile.name}); + }); + } this.refreshRoomList(); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { if (this.filterTimeout) { clearTimeout(this.filterTimeout); } this._unmounted = true; - }, + } + + refreshRoomList = () => { + if (this.state.selectedCommunityId) { + this.setState({ + publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => { + return { + // Translate all the group properties to the directory format + room_id: r.roomId, + name: r.name, + topic: r.topic, + canonical_alias: r.canonicalAlias, + num_joined_members: r.numJoinedMembers, + avatarUrl: r.avatarUrl, + world_readable: r.worldReadable, + guest_can_join: r.guestsCanJoin, + }; + }).filter(r => { + const filterString = this.state.filterString; + if (filterString) { + const containedIn = (s: string) => (s || "").toLowerCase().includes(filterString.toLowerCase()); + return containedIn(r.name) || containedIn(r.topic) || containedIn(r.canonical_alias); + } + return true; + }), + loading: false, + }); + return; + } - refreshRoomList: function() { this.nextBatch = null; this.setState({ publicRooms: [], loading: true, }); this.getMoreRooms(); - }, + }; - getMoreRooms: function() { + getMoreRooms() { + if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms if (!MatrixClientPeg.get()) return Promise.resolve(); this.setState({ @@ -185,7 +229,7 @@ export default createReactClass({ ), }); }); - }, + } /** * A limited interface for removing rooms from the directory. @@ -194,7 +238,7 @@ export default createReactClass({ * HS admins to do this through the RoomSettings interface, but * this needs SPEC-417. */ - removeFromDirectory: function(room) { + removeFromDirectory(room) { const alias = get_display_alias_for_room(room); const name = room.name || alias || _t('Unnamed room'); @@ -236,18 +280,18 @@ export default createReactClass({ }); }, }); - }, + } - onRoomClicked: function(room, ev) { - if (ev.shiftKey) { + onRoomClicked = (room, ev) => { + if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); this.removeFromDirectory(room); } else { this.showRoom(room); } - }, + }; - onOptionChange: function(server, instanceId) { + onOptionChange = (server, instanceId) => { // clear next batch so we don't try to load more rooms this.nextBatch = null; this.setState({ @@ -265,15 +309,15 @@ export default createReactClass({ // find the five gitter ones, at which point we do not want // to render all those rooms when switching back to 'all networks'. // Easiest to just blow away the state & re-fetch. - }, + }; - onFillRequest: function(backwards) { + onFillRequest = (backwards) => { if (backwards || !this.nextBatch) return Promise.resolve(false); return this.getMoreRooms(); - }, + }; - onFilterChange: function(alias) { + onFilterChange = (alias) => { this.setState({ filterString: alias || null, }); @@ -289,9 +333,9 @@ export default createReactClass({ this.filterTimeout = null; this.refreshRoomList(); }, 700); - }, + }; - onFilterClear: function() { + onFilterClear = () => { // update immediately this.setState({ filterString: null, @@ -300,9 +344,9 @@ export default createReactClass({ if (this.filterTimeout) { clearTimeout(this.filterTimeout); } - }, + }; - onJoinFromSearchClick: function(alias) { + onJoinFromSearchClick = (alias) => { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { // If the user specified an alias without a domain, add on whichever server is selected @@ -343,9 +387,9 @@ export default createReactClass({ }); }); } - }, + }; - onPreviewClick: function(ev, room) { + onPreviewClick = (ev, room) => { this.props.onFinished(); dis.dispatch({ action: 'view_room', @@ -353,9 +397,9 @@ export default createReactClass({ should_peek: true, }); ev.stopPropagation(); - }, + }; - onViewClick: function(ev, room) { + onViewClick = (ev, room) => { this.props.onFinished(); dis.dispatch({ action: 'view_room', @@ -363,26 +407,26 @@ export default createReactClass({ should_peek: false, }); ev.stopPropagation(); - }, + }; - onJoinClick: function(ev, room) { + onJoinClick = (ev, room) => { this.showRoom(room, null, true); ev.stopPropagation(); - }, + }; - onCreateRoomClick: function(room) { + onCreateRoomClick = room => { this.props.onFinished(); dis.dispatch({ action: 'view_create_room', public: true, }); - }, + }; - showRoomAlias: function(alias, autoJoin=false) { + showRoomAlias(alias, autoJoin=false) { this.showRoom(null, alias, autoJoin); - }, + } - showRoom: function(room, room_alias, autoJoin=false) { + showRoom(room, room_alias, autoJoin=false) { this.props.onFinished(); const payload = { action: 'view_room', @@ -426,7 +470,7 @@ export default createReactClass({ payload.room_id = room.room_id; } dis.dispatch(payload); - }, + } getRow(room) { const client = MatrixClientPeg.get(); @@ -492,22 +536,22 @@ export default createReactClass({ {joinOrViewButton} ); - }, + } - collectScrollPanel: function(element) { + collectScrollPanel = (element) => { this.scrollPanel = element; - }, + }; - _stringLooksLikeId: function(s, field_type) { + _stringLooksLikeId(s, field_type) { let pat = /^#[^\s]+:[^\s]/; if (field_type && field_type.regexp) { pat = new RegExp(field_type.regexp); } return pat.test(s); - }, + } - _getFieldsForThirdPartyLocation: function(userInput, protocol, instance) { + _getFieldsForThirdPartyLocation(userInput, protocol, instance) { // make an object with the fields specified by that protocol. We // require that the values of all but the last field come from the // instance. The last is the user input. @@ -521,20 +565,20 @@ export default createReactClass({ } fields[requiredFields[requiredFields.length - 1]] = userInput; return fields; - }, + } /** * called by the parent component when PageUp/Down/etc is pressed. * * We pass it down to the scroll panel. */ - handleScrollKey: function(ev) { + handleScrollKey = ev => { if (this.scrollPanel) { this.scrollPanel.handleScrollKey(ev); } - }, + }; - render: function() { + render() { const Loader = sdk.getComponent("elements.Spinner"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -610,6 +654,18 @@ export default createReactClass({ } } + let dropdown = ( + + ); + if (this.state.selectedCommunityId) { + dropdown = null; + } + listHeader =
- + {dropdown}
; } const explanation = @@ -637,12 +688,16 @@ export default createReactClass({ }}, ); + const title = this.state.selectedCommunityId + ? _t("Explore rooms in %(communityName)s", { + communityName: this.state.communityName || this.state.selectedCommunityId, + }) : _t("Explore rooms"); return (
{explanation} @@ -653,8 +708,8 @@ export default createReactClass({
); - }, -}); + } +} // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // but works with the objects we get from the public room list diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 69504e9ab8..768bc38d23 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -20,7 +20,6 @@ import classNames from "classnames"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { ActionPayload } from "../../dispatcher/payloads"; -import { throttle } from 'lodash'; import { Key } from "../../Keyboard"; import AccessibleButton from "../views/elements/AccessibleButton"; import { Action } from "../../dispatcher/actions"; @@ -126,7 +125,8 @@ export default class RoomSearch extends React.PureComponent { public render(): React.ReactNode { const classes = classNames({ 'mx_RoomSearch': true, - 'mx_RoomSearch_expanded': this.state.query || this.state.focused, + 'mx_RoomSearch_hasQuery': this.state.query, + 'mx_RoomSearch_focused': this.state.focused, 'mx_RoomSearch_minimized': this.props.isMinimized, }); @@ -136,7 +136,7 @@ export default class RoomSearch extends React.PureComponent { }); let icon = ( -
+
); let input = ( { if (state === "SYNCING" && prevState === "SYNCING") { return; } @@ -124,39 +119,39 @@ export default createReactClass({ syncState: state, syncStateData: data, }); - }, + }; - _onResendAllClick: function() { + _onResendAllClick = () => { Resend.resendUnsentEvents(this.props.room); dis.fire(Action.FocusComposer); - }, + }; - _onCancelAllClick: function() { + _onCancelAllClick = () => { Resend.cancelUnsentEvents(this.props.room); dis.fire(Action.FocusComposer); - }, + }; - _onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { + _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { if (room.roomId !== this.props.room.roomId) return; this.setState({ unsentMessages: getUnsentMessages(this.props.room), }); - }, + }; // Check whether current size is greater than 0, if yes call props.onVisible - _checkSize: function() { + _checkSize() { if (this._getSize()) { if (this.props.onVisible) this.props.onVisible(); } else { if (this.props.onHidden) this.props.onHidden(); } - }, + } // We don't need the actual height - just whether it is likely to have // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. - _getSize: function() { + _getSize() { if (this._shouldShowConnectionError() || this.props.hasActiveCall || this.props.sentMessageAndIsAlone @@ -166,10 +161,10 @@ export default createReactClass({ return STATUS_BAR_EXPANDED_LARGE; } return STATUS_BAR_HIDDEN; - }, + } // return suitable content for the image on the left of the status bar. - _getIndicator: function() { + _getIndicator() { if (this.props.hasActiveCall) { const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( @@ -182,9 +177,9 @@ export default createReactClass({ } return null; - }, + } - _shouldShowConnectionError: function() { + _shouldShowConnectionError() { // no conn bar trumps the "some not sent" msg since you can't resend without // a connection! // There's one situation in which we don't show this 'no connection' bar, and that's @@ -195,9 +190,9 @@ export default createReactClass({ this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED', ); return this.state.syncState === "ERROR" && !errorIsMauError; - }, + } - _getUnsentMessageContent: function() { + _getUnsentMessageContent() { const unsentMessages = this.state.unsentMessages; if (!unsentMessages.length) return null; @@ -272,10 +267,10 @@ export default createReactClass({
; - }, + } // return suitable content for the main (text) part of the status bar. - _getContent: function() { + _getContent() { if (this._shouldShowConnectionError()) { return (
@@ -323,9 +318,9 @@ export default createReactClass({ } return null; - }, + } - render: function() { + render() { const content = this._getContent(); const indicator = this._getIndicator(); @@ -339,5 +334,5 @@ export default createReactClass({
); - }, -}); + } +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f585a97fde..e1a075f770 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -24,7 +24,6 @@ limitations under the License. import shouldHideEvent from '../../shouldHideEvent'; import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../languageHandler'; @@ -68,9 +67,8 @@ if (DEBUG) { debuglog = console.log.bind(console); } -export default createReactClass({ - displayName: 'RoomView', - propTypes: { +export default class RoomView extends React.Component { + static propTypes = { ConferenceHandler: PropTypes.any, // Called with the credentials of a registered user (if they were a ROU that @@ -97,15 +95,15 @@ export default createReactClass({ // Servers the RoomView can use to try and assist joins viaServers: PropTypes.arrayOf(PropTypes.string), - }, + }; - statics: { - contextType: MatrixClientContext, - }, + static contextType = MatrixClientContext; + + constructor(props, context) { + super(props, context); - getInitialState: function() { const llMembers = this.context.hasLazyLoadMembersEnabled(); - return { + this.state = { room: null, roomId: null, roomLoading: true, @@ -171,10 +169,7 @@ export default createReactClass({ matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; - }, - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { this.dispatcherRef = dis.register(this.onAction); this.context.on("Room", this.onRoom); this.context.on("Room.timeline", this.onRoomTimeline); @@ -191,7 +186,6 @@ export default createReactClass({ // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); - this._onRoomViewStoreUpdate(true); WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); this._showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, @@ -201,15 +195,20 @@ export default createReactClass({ this._searchResultsPanel = createRef(); this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); - }, + } - _onReadReceiptsChange: function() { + // TODO: [REACT-WARNING] Move into constructor + UNSAFE_componentWillMount() { + this._onRoomViewStoreUpdate(true); + } + + _onReadReceiptsChange = () => { this.setState({ showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), }); - }, + }; - _onRoomViewStoreUpdate: function(initial) { + _onRoomViewStoreUpdate = initial => { if (this.unmounted) { return; } @@ -251,7 +250,7 @@ export default createReactClass({ this.context.stopPeeking(); } - // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 + // Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307 console.log( 'RVS update:', newState.roomId, @@ -303,7 +302,7 @@ export default createReactClass({ if (initial) { this._setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek); } - }, + }; _getRoomId() { // According to `_onRoomViewStoreUpdate`, `state.roomId` can be null @@ -312,9 +311,9 @@ export default createReactClass({ // the bare room ID. (We may want to update `state.roomId` after // resolving aliases, so we could always trust it.) return this.state.room ? this.state.room.roomId : this.state.roomId; - }, + } - _getPermalinkCreatorForRoom: function(room) { + _getPermalinkCreatorForRoom(room) { if (!this._permalinkCreators) this._permalinkCreators = {}; if (this._permalinkCreators[room.roomId]) return this._permalinkCreators[room.roomId]; @@ -327,22 +326,22 @@ export default createReactClass({ this._permalinkCreators[room.roomId].load(); } return this._permalinkCreators[room.roomId]; - }, + } - _stopAllPermalinkCreators: function() { + _stopAllPermalinkCreators() { if (!this._permalinkCreators) return; for (const roomId of Object.keys(this._permalinkCreators)) { this._permalinkCreators[roomId].stop(); } - }, + } - _onWidgetEchoStoreUpdate: function() { + _onWidgetEchoStoreUpdate = () => { this.setState({ showApps: this._shouldShowApps(this.state.room), }); - }, + }; - _setupRoom: function(room, roomId, joining, shouldPeek) { + _setupRoom(room, roomId, joining, shouldPeek) { // if this is an unknown room then we're in one of three states: // - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can publicly join or were invited to. (we can /join) @@ -404,9 +403,9 @@ export default createReactClass({ this.setState({isPeeking: false}); } } - }, + } - _shouldShowApps: function(room) { + _shouldShowApps(room) { if (!BROWSER_SUPPORTS_SANDBOX) return false; // Check if user has previously chosen to hide the app drawer for this @@ -417,9 +416,9 @@ export default createReactClass({ // This is confusing, but it means to say that we default to the tray being // hidden unless the user clicked to open it. return hideWidgetDrawer === "false"; - }, + } - componentDidMount: function() { + componentDidMount() { const call = this._getCallForRoom(); const callState = call ? call.call_state : "ended"; this.setState({ @@ -435,14 +434,14 @@ export default createReactClass({ this.onResize(); document.addEventListener("keydown", this.onNativeKeyDown); - }, + } - shouldComponentUpdate: function(nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { return (!ObjectUtils.shallowEqual(this.props, nextProps) || !ObjectUtils.shallowEqual(this.state, nextState)); - }, + } - componentDidUpdate: function() { + componentDidUpdate() { if (this._roomView.current) { const roomView = this._roomView.current; if (!roomView.ondrop) { @@ -464,9 +463,9 @@ export default createReactClass({ atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(), }); } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -543,21 +542,21 @@ export default createReactClass({ // Tinter.tint(); // reset colourscheme SettingsStore.unwatchSetting(this._layoutWatcherRef); - }, + } - onLayoutChange: function() { + onLayoutChange = () => { this.setState({ useIRCLayout: SettingsStore.getValue("useIRCLayout"), }); - }, + }; - _onRightPanelStoreUpdate: function() { + _onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, }); - }, + }; - onPageUnload(event) { + onPageUnload = event => { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return event.returnValue = _t("You seem to be uploading files, are you sure you want to quit?"); @@ -565,10 +564,10 @@ export default createReactClass({ return event.returnValue = _t("You seem to be in a call, are you sure you want to quit?"); } - }, + }; // we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire - onNativeKeyDown: function(ev) { + onNativeKeyDown = ev => { let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); @@ -592,9 +591,9 @@ export default createReactClass({ ev.stopPropagation(); ev.preventDefault(); } - }, + }; - onReactKeyDown: function(ev) { + onReactKeyDown = ev => { let handled = false; switch (ev.key) { @@ -613,7 +612,7 @@ export default createReactClass({ break; case Key.U.toUpperCase(): if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) { - dis.dispatch({ action: "upload_file" }) + dis.dispatch({ action: "upload_file" }); handled = true; } break; @@ -623,9 +622,9 @@ export default createReactClass({ ev.stopPropagation(); ev.preventDefault(); } - }, + }; - onAction: function(payload) { + onAction = payload => { switch (payload.action) { case 'message_send_failed': case 'message_sent': @@ -709,9 +708,9 @@ export default createReactClass({ } break; } - }, + }; - onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { + onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { if (this.unmounted) return; // ignore events for other rooms @@ -747,51 +746,51 @@ export default createReactClass({ }); } } - }, + }; - onRoomName: function(room) { + onRoomName = room => { if (this.state.room && room.roomId == this.state.room.roomId) { this.forceUpdate(); } - }, + }; - onRoomRecoveryReminderDontAskAgain: function() { + onRoomRecoveryReminderDontAskAgain = () => { // Called when the option to not ask again is set: // force an update to hide the recovery reminder this.forceUpdate(); - }, + }; - onKeyBackupStatus() { + onKeyBackupStatus = () => { // Key backup status changes affect whether the in-room recovery // reminder is displayed. this.forceUpdate(); - }, + }; - canResetTimeline: function() { + canResetTimeline = () => { if (!this._messagePanel) { return true; } return this._messagePanel.canResetTimeline(); - }, + }; // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). - _onRoomLoaded: function(room) { + _onRoomLoaded = room => { this._calculatePeekRules(room); this._updatePreviewUrlVisibility(room); this._loadMembersIfJoined(room); this._calculateRecommendedVersion(room); this._updateE2EStatus(room); this._updatePermissions(room); - }, + }; - _calculateRecommendedVersion: async function(room) { + async _calculateRecommendedVersion(room) { this.setState({ upgradeRecommendation: await room.getRecommendedVersion(), }); - }, + } - _loadMembersIfJoined: async function(room) { + async _loadMembersIfJoined(room) { // lazy load members if enabled if (this.context.hasLazyLoadMembersEnabled()) { if (room && room.getMyMembership() === 'join') { @@ -808,9 +807,9 @@ export default createReactClass({ } } } - }, + } - _calculatePeekRules: function(room) { + _calculatePeekRules(room) { const guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", ""); if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") { this.setState({ @@ -824,17 +823,17 @@ export default createReactClass({ canPeek: true, }); } - }, + } - _updatePreviewUrlVisibility: function({roomId}) { + _updatePreviewUrlVisibility({roomId}) { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'; this.setState({ showUrlPreview: SettingsStore.getValue(key, roomId), }); - }, + } - onRoom: function(room) { + onRoom = room => { if (!room || room.roomId !== this.state.roomId) { return; } @@ -843,32 +842,32 @@ export default createReactClass({ }, () => { this._onRoomLoaded(room); }); - }, + }; - onDeviceVerificationChanged: function(userId, device) { + onDeviceVerificationChanged = (userId, device) => { const room = this.state.room; if (!room.currentState.getMember(userId)) { return; } this._updateE2EStatus(room); - }, + }; - onUserVerificationChanged: function(userId, _trustStatus) { + onUserVerificationChanged = (userId, _trustStatus) => { const room = this.state.room; if (!room || !room.currentState.getMember(userId)) { return; } this._updateE2EStatus(room); - }, + }; - onCrossSigningKeysChanged: function() { + onCrossSigningKeysChanged = () => { const room = this.state.room; if (room) { this._updateE2EStatus(room); } - }, + }; - _updateE2EStatus: async function(room) { + async _updateE2EStatus(room) { if (!this.context.isRoomEncrypted(room.roomId)) { return; } @@ -886,26 +885,26 @@ export default createReactClass({ this.setState({ e2eStatus: await shieldStatusForRoom(this.context, room), }); - }, + } - updateTint: function() { + updateTint() { const room = this.state.room; if (!room) return; console.log("Tinter.tint from updateTint"); const colorScheme = SettingsStore.getValue("roomColor", room.roomId); Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color); - }, + } - onAccountData: function(event) { + onAccountData = event => { const type = event.getType(); if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) { // non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls` this._updatePreviewUrlVisibility(this.state.room); } - }, + }; - onRoomAccountData: function(event, room) { + onRoomAccountData = (event, room) => { if (room.roomId == this.state.roomId) { const type = event.getType(); if (type === "org.matrix.room.color_scheme") { @@ -918,18 +917,18 @@ export default createReactClass({ this._updatePreviewUrlVisibility(room); } } - }, + }; - onRoomStateEvents: function(ev, state) { + onRoomStateEvents = (ev, state) => { // ignore if we don't have a room yet if (!this.state.room || this.state.room.roomId !== state.roomId) { return; } this._updatePermissions(this.state.room); - }, + }; - onRoomStateMember: function(ev, state, member) { + onRoomStateMember = (ev, state, member) => { // ignore if we don't have a room yet if (!this.state.room) { return; @@ -941,17 +940,17 @@ export default createReactClass({ } this._updateRoomMembers(member); - }, + }; - onMyMembership: function(room, membership, oldMembership) { + onMyMembership = (room, membership, oldMembership) => { if (room.roomId === this.state.roomId) { this.forceUpdate(); this._loadMembersIfJoined(room); this._updatePermissions(room); } - }, + }; - _updatePermissions: function(room) { + _updatePermissions(room) { if (room) { const me = this.context.getUserId(); const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me); @@ -959,11 +958,11 @@ export default createReactClass({ this.setState({canReact, canReply}); } - }, + } // rate limited because a power level change will emit an event for every // member in the room. - _updateRoomMembers: rate_limited_func(function(dueToMember) { + _updateRoomMembers = rate_limited_func((dueToMember) => { // a member state changed in this room // refresh the conf call notification state this._updateConfCallNotification(); @@ -978,9 +977,9 @@ export default createReactClass({ this._checkIfAlone(this.state.room, memberCountInfluence); this._updateE2EStatus(this.state.room); - }, 500), + }, 500); - _checkIfAlone: function(room, countInfluence) { + _checkIfAlone(room, countInfluence) { let warnedAboutLonelyRoom = false; if (localStorage) { warnedAboutLonelyRoom = localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId); @@ -993,9 +992,9 @@ export default createReactClass({ let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount(); if (countInfluence) joinedOrInvitedMemberCount += countInfluence; this.setState({isAlone: joinedOrInvitedMemberCount === 1}); - }, + } - _updateConfCallNotification: function() { + _updateConfCallNotification() { const room = this.state.room; if (!room || !this.props.ConferenceHandler) { return; @@ -1017,7 +1016,7 @@ export default createReactClass({ confMember.membership === "join" ), }); - }, + } _updateDMState() { const room = this.state.room; @@ -1028,9 +1027,9 @@ export default createReactClass({ if (dmInviter) { Rooms.setDMRoom(room.roomId, dmInviter); } - }, + } - onSearchResultsFillRequest: function(backwards) { + onSearchResultsFillRequest = backwards => { if (!backwards) { return Promise.resolve(false); } @@ -1043,25 +1042,25 @@ export default createReactClass({ debuglog("no more search results"); return Promise.resolve(false); } - }, + }; - onInviteButtonClick: function() { + onInviteButtonClick = () => { // call AddressPickerDialog dis.dispatch({ action: 'view_invite', roomId: this.state.room.roomId, }); this.setState({isAlone: false}); // there's a good chance they'll invite someone - }, + }; - onStopAloneWarningClick: function() { + onStopAloneWarningClick = () => { if (localStorage) { localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, true); } this.setState({isAlone: false}); - }, + }; - onJoinButtonClicked: function(ev) { + onJoinButtonClicked = ev => { // If the user is a ROU, allow them to transition to a PWLU if (this.context && this.context.isGuest()) { // Join this room once the user has registered and logged in @@ -1078,7 +1077,7 @@ export default createReactClass({ // Do this by indicating our intention to join // XXX: ILAG is disabled for now, - // see https://github.com/vector-im/riot-web/issues/8222 + // see https://github.com/vector-im/element-web/issues/8222 dis.dispatch({action: 'require_registration'}); // dis.dispatch({ // action: 'will_join', @@ -1120,10 +1119,9 @@ export default createReactClass({ return Promise.resolve(); }); } + }; - }, - - onMessageListScroll: function(ev) { + onMessageListScroll = ev => { if (this._messagePanel.isAtEndOfLiveTimeline()) { this.setState({ numUnreadMessages: 0, @@ -1135,9 +1133,9 @@ export default createReactClass({ }); } this._updateTopUnreadMessagesBar(); - }, + }; - onDragOver: function(ev) { + onDragOver = ev => { ev.stopPropagation(); ev.preventDefault(); @@ -1154,9 +1152,9 @@ export default createReactClass({ ev.dataTransfer.dropEffect = 'copy'; } } - }, + }; - onDrop: function(ev) { + onDrop = ev => { ev.stopPropagation(); ev.preventDefault(); ContentMessages.sharedInstance().sendContentListToRoom( @@ -1164,15 +1162,15 @@ export default createReactClass({ ); this.setState({ draggingFile: false }); dis.fire(Action.FocusComposer); - }, + }; - onDragLeaveOrEnd: function(ev) { + onDragLeaveOrEnd = ev => { ev.stopPropagation(); ev.preventDefault(); this.setState({ draggingFile: false }); - }, + }; - injectSticker: function(url, info, text) { + injectSticker(url, info, text) { if (this.context.isGuest()) { dis.dispatch({action: 'require_registration'}); return; @@ -1185,9 +1183,9 @@ export default createReactClass({ return; } }); - }, + } - onSearch: function(term, scope) { + onSearch = (term, scope) => { this.setState({ searchTerm: term, searchScope: scope, @@ -1213,9 +1211,9 @@ export default createReactClass({ debuglog("sending search request"); const searchPromise = eventSearch(term, roomId); this._handleSearchResult(searchPromise); - }, + }; - _handleSearchResult: function(searchPromise) { + _handleSearchResult(searchPromise) { const self = this; // keep a record of the current search id, so that if the search terms @@ -1266,9 +1264,9 @@ export default createReactClass({ searchInProgress: false, }); }); - }, + } - getSearchResultTiles: function() { + getSearchResultTiles() { const SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); const Spinner = sdk.getComponent("elements.Spinner"); @@ -1348,20 +1346,20 @@ export default createReactClass({ onHeightChanged={onHeightChanged} />); } return ret; - }, + } - onPinnedClick: function() { + onPinnedClick = () => { const nowShowingPinned = !this.state.showingPinned; const roomId = this.state.room.roomId; this.setState({showingPinned: nowShowingPinned, searching: false}); SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned); - }, + }; - onSettingsClick: function() { + onSettingsClick = () => { dis.dispatch({ action: 'open_room_settings' }); - }, + }; - onCancelClick: function() { + onCancelClick = () => { console.log("updateTint from onCancelClick"); this.updateTint(); if (this.state.forwardingEvent) { @@ -1371,23 +1369,23 @@ export default createReactClass({ }); } dis.fire(Action.FocusComposer); - }, + }; - onLeaveClick: function() { + onLeaveClick = () => { dis.dispatch({ action: 'leave_room', room_id: this.state.room.roomId, }); - }, + }; - onForgetClick: function() { + onForgetClick = () => { dis.dispatch({ action: 'forget_room', room_id: this.state.room.roomId, }); - }, + }; - onRejectButtonClicked: function(ev) { + onRejectButtonClicked = ev => { const self = this; this.setState({ rejecting: true, @@ -1412,9 +1410,9 @@ export default createReactClass({ rejectError: error, }); }); - }, + }; - onRejectAndIgnoreClick: async function() { + onRejectAndIgnoreClick = async () => { this.setState({ rejecting: true, }); @@ -1446,49 +1444,49 @@ export default createReactClass({ rejectError: error, }); } - }, + }; - onRejectThreepidInviteButtonClicked: function(ev) { + onRejectThreepidInviteButtonClicked = ev => { // We can reject 3pid invites in the same way that we accept them, // using /leave rather than /join. In the short term though, we // just ignore them. // https://github.com/vector-im/vector-web/issues/1134 dis.fire(Action.ViewRoomDirectory); - }, + }; - onSearchClick: function() { + onSearchClick = () => { this.setState({ searching: !this.state.searching, showingPinned: false, }); - }, + }; - onCancelSearchClick: function() { + onCancelSearchClick = () => { this.setState({ searching: false, searchResults: null, }); - }, + }; // jump down to the bottom of this room, where new events are arriving - jumpToLiveTimeline: function() { + jumpToLiveTimeline = () => { this._messagePanel.jumpToLiveTimeline(); dis.fire(Action.FocusComposer); - }, + }; // jump up to wherever our read marker is - jumpToReadMarker: function() { + jumpToReadMarker = () => { this._messagePanel.jumpToReadMarker(); - }, + }; // update the read marker to match the read-receipt - forgetReadMarker: function(ev) { + forgetReadMarker = ev => { ev.stopPropagation(); this._messagePanel.forgetReadMarker(); - }, + }; // decide whether or not the top 'unread messages' bar should be shown - _updateTopUnreadMessagesBar: function() { + _updateTopUnreadMessagesBar = () => { if (!this._messagePanel) { return; } @@ -1497,12 +1495,12 @@ export default createReactClass({ if (this.state.showTopUnreadMessagesBar != showBar) { this.setState({showTopUnreadMessagesBar: showBar}); } - }, + }; // get the current scroll position of the room, so that it can be // restored when we switch back to it. // - _getScrollState: function() { + _getScrollState() { const messagePanel = this._messagePanel; if (!messagePanel) return null; @@ -1537,9 +1535,9 @@ export default createReactClass({ focussedEvent: scrollState.trackedScrollToken, pixelOffset: scrollState.pixelOffset, }; - }, + } - onResize: function() { + onResize = () => { // It seems flexbox doesn't give us a way to constrain the auxPanel height to have // a minimum of the height of the video element, whilst also capping it from pushing out the page // so we have to do it via JS instead. In this implementation we cap the height by putting @@ -1557,16 +1555,16 @@ export default createReactClass({ if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); - }, + }; - onFullscreenClick: function() { + onFullscreenClick = () => { dis.dispatch({ action: 'video_fullscreen', fullscreen: true, }, true); - }, + }; - onMuteAudioClick: function() { + onMuteAudioClick = () => { const call = this._getCallForRoom(); if (!call) { return; @@ -1574,9 +1572,9 @@ export default createReactClass({ const newState = !call.isMicrophoneMuted(); call.setMicrophoneMuted(newState); this.forceUpdate(); // TODO: just update the voip buttons - }, + }; - onMuteVideoClick: function() { + onMuteVideoClick = () => { const call = this._getCallForRoom(); if (!call) { return; @@ -1584,29 +1582,29 @@ export default createReactClass({ const newState = !call.isLocalVideoMuted(); call.setLocalVideoMuted(newState); this.forceUpdate(); // TODO: just update the voip buttons - }, + }; - onStatusBarVisible: function() { + onStatusBarVisible = () => { if (this.unmounted) return; this.setState({ statusBarVisible: true, }); - }, + }; - onStatusBarHidden: function() { + onStatusBarHidden = () => { // This is currently not desired as it is annoying if it keeps expanding and collapsing if (this.unmounted) return; this.setState({ statusBarVisible: false, }); - }, + }; /** * called by the parent component when PageUp/Down/etc is pressed. * * We pass it down to the scroll panel. */ - handleScrollKey: function(ev) { + handleScrollKey = ev => { let panel; if (this._searchResultsPanel.current) { panel = this._searchResultsPanel.current; @@ -1617,48 +1615,48 @@ export default createReactClass({ if (panel) { panel.handleScrollKey(ev); } - }, + }; /** * get any current call for this room */ - _getCallForRoom: function() { + _getCallForRoom() { if (!this.state.room) { return null; } return CallHandler.getCallForRoom(this.state.room.roomId); - }, + } // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. - _gatherTimelinePanelRef: function(r) { + _gatherTimelinePanelRef = r => { this._messagePanel = r; if (r) { console.log("updateTint from RoomView._gatherTimelinePanelRef"); this.updateTint(); } - }, + }; - _getOldRoom: function() { + _getOldRoom() { const createEvent = this.state.room.currentState.getStateEvents("m.room.create", ""); if (!createEvent || !createEvent.getContent()['predecessor']) return null; return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']); - }, + } - _getHiddenHighlightCount: function() { + _getHiddenHighlightCount() { const oldRoom = this._getOldRoom(); if (!oldRoom) return 0; return oldRoom.getUnreadNotificationCount('highlight'); - }, + } - _onHiddenHighlightsClick: function() { + _onHiddenHighlightsClick = () => { const oldRoom = this._getOldRoom(); if (!oldRoom) return; dis.dispatch({action: "view_room", room_id: oldRoom.roomId}); - }, + }; - render: function() { + render() { const RoomHeader = sdk.getComponent('rooms.RoomHeader'); const ForwardMessage = sdk.getComponent("rooms.ForwardMessage"); const AuxPanel = sdk.getComponent("rooms.AuxPanel"); @@ -1913,6 +1911,7 @@ export default createReactClass({ disabled={this.props.disabled} showApps={this.state.showApps} e2eStatus={this.state.e2eStatus} + resizeNotifier={this.props.resizeNotifier} permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)} />; } @@ -2117,5 +2116,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 51113f4f56..ef21b5d787 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -15,7 +15,6 @@ limitations under the License. */ import React, {createRef} from "react"; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; @@ -84,10 +83,8 @@ if (DEBUG_SCROLL) { * offset as normal. */ -export default createReactClass({ - displayName: 'ScrollPanel', - - propTypes: { +export default class ScrollPanel extends React.Component { + static propTypes = { /* stickyBottom: if set to true, then once the user hits the bottom of * the list, any new children added to the list will cause the list to * scroll down to show the new element, rather than preserving the @@ -149,20 +146,19 @@ export default createReactClass({ * of the wrapper */ fixedChildren: PropTypes.node, - }, + }; - getDefaultProps: function() { - return { - stickyBottom: true, - startAtBottom: true, - onFillRequest: function(backwards) { return Promise.resolve(false); }, - onUnfillRequest: function(backwards, scrollToken) {}, - onScroll: function() {}, - }; - }, + static defaultProps = { + stickyBottom: true, + startAtBottom: true, + onFillRequest: function(backwards) { return Promise.resolve(false); }, + onUnfillRequest: function(backwards, scrollToken) {}, + onScroll: function() {}, + }; + + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { this._pendingFillRequests = {b: null, f: null}; if (this.props.resizeNotifier) { @@ -172,13 +168,13 @@ export default createReactClass({ this.resetScrollState(); this._itemlist = createRef(); - }, + } - componentDidMount: function() { + componentDidMount() { this.checkScroll(); - }, + } - componentDidUpdate: function() { + componentDidUpdate() { // after adding event tiles, we may need to tweak the scroll (either to // keep at the bottom of the timeline, or to maintain the view after // adding events to the top). @@ -186,9 +182,9 @@ export default createReactClass({ // This will also re-check the fill state, in case the paginate was inadequate this.checkScroll(); this.updatePreventShrinking(); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -198,41 +194,41 @@ export default createReactClass({ if (this.props.resizeNotifier) { this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize); } - }, + } - onScroll: function(ev) { + onScroll = ev => { debuglog("onScroll", this._getScrollNode().scrollTop); this._scrollTimeout.restart(); this._saveScrollState(); this.updatePreventShrinking(); this.props.onScroll(ev); this.checkFillState(); - }, + }; - onResize: function() { + onResize = () => { this.checkScroll(); // update preventShrinkingState if present if (this.preventShrinkingState) { this.preventShrinking(); } - }, + }; // after an update to the contents of the panel, check that the scroll is // where it ought to be, and set off pagination requests if necessary. - checkScroll: function() { + checkScroll = () => { if (this.unmounted) { return; } this._restoreSavedScrollState(); this.checkFillState(); - }, + }; // return true if the content is fully scrolled down right now; else false. // // note that this is independent of the 'stuckAtBottom' state - it is simply // about whether the content is scrolled down right now, irrespective of // whether it will stay that way when the children update. - isAtBottom: function() { + isAtBottom = () => { const sn = this._getScrollNode(); // fractional values (both too big and too small) // for scrollTop happen on certain browsers/platforms @@ -240,7 +236,7 @@ export default createReactClass({ // so check difference <= 1; return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1; - }, + }; // returns the vertical height in the given direction that can be removed from // the content box (which has a height of scrollHeight, see checkFillState) without @@ -273,7 +269,7 @@ export default createReactClass({ // |#########| - | // |#########| | // `---------' - - _getExcessHeight: function(backwards) { + _getExcessHeight(backwards) { const sn = this._getScrollNode(); const contentHeight = this._getMessagesHeight(); const listHeight = this._getListHeight(); @@ -285,10 +281,10 @@ export default createReactClass({ } else { return contentHeight - (unclippedScrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING; } - }, + } // check the scroll state and send out backfill requests if necessary. - checkFillState: async function(depth=0) { + checkFillState = async (depth=0) => { if (this.unmounted) { return; } @@ -368,10 +364,10 @@ export default createReactClass({ this._fillRequestWhileRunning = false; this.checkFillState(); } - }, + }; // check if unfilling is possible and send an unfill request if necessary - _checkUnfillState: function(backwards) { + _checkUnfillState(backwards) { let excessHeight = this._getExcessHeight(backwards); if (excessHeight <= 0) { return; @@ -417,10 +413,10 @@ export default createReactClass({ this.props.onUnfillRequest(backwards, markerScrollToken); }, UNFILL_REQUEST_DEBOUNCE_MS); } - }, + } // check if there is already a pending fill request. If not, set one off. - _maybeFill: function(depth, backwards) { + _maybeFill(depth, backwards) { const dir = backwards ? 'b' : 'f'; if (this._pendingFillRequests[dir]) { debuglog("Already a "+dir+" fill in progress - not starting another"); @@ -456,7 +452,7 @@ export default createReactClass({ return this.checkFillState(depth + 1); } }); - }, + } /* get the current scroll state. This returns an object with the following * properties: @@ -472,9 +468,7 @@ export default createReactClass({ * the number of pixels the bottom of the tracked child is above the * bottom of the scroll panel. */ - getScrollState: function() { - return this.scrollState; - }, + getScrollState = () => this.scrollState; /* reset the saved scroll state. * @@ -488,7 +482,7 @@ export default createReactClass({ * no use if no children exist yet, or if you are about to replace the * child list.) */ - resetScrollState: function() { + resetScrollState = () => { this.scrollState = { stuckAtBottom: this.props.startAtBottom, }; @@ -496,20 +490,20 @@ export default createReactClass({ this._pages = 0; this._scrollTimeout = new Timer(100); this._heightUpdateInProgress = false; - }, + }; /** * jump to the top of the content. */ - scrollToTop: function() { + scrollToTop = () => { this._getScrollNode().scrollTop = 0; this._saveScrollState(); - }, + }; /** * jump to the bottom of the content. */ - scrollToBottom: function() { + scrollToBottom = () => { // the easiest way to make sure that the scroll state is correctly // saved is to do the scroll, then save the updated state. (Calculating // it ourselves is hard, and we can't rely on an onScroll callback @@ -517,25 +511,25 @@ export default createReactClass({ const sn = this._getScrollNode(); sn.scrollTop = sn.scrollHeight; this._saveScrollState(); - }, + }; /** * Page up/down. * * @param {number} mult: -1 to page up, +1 to page down */ - scrollRelative: function(mult) { + scrollRelative = mult => { const scrollNode = this._getScrollNode(); const delta = mult * scrollNode.clientHeight * 0.5; scrollNode.scrollBy(0, delta); this._saveScrollState(); - }, + }; /** * Scroll up/down in response to a scroll key * @param {object} ev the keyboard event */ - handleScrollKey: function(ev) { + handleScrollKey = ev => { switch (ev.key) { case Key.PAGE_UP: if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { @@ -561,7 +555,7 @@ export default createReactClass({ } break; } - }, + }; /* Scroll the panel to bring the DOM node with the scroll token * `scrollToken` into view. @@ -574,7 +568,7 @@ export default createReactClass({ * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - scrollToToken: function(scrollToken, pixelOffset, offsetBase) { + scrollToToken = (scrollToken, pixelOffset, offsetBase) => { pixelOffset = pixelOffset || 0; offsetBase = offsetBase || 0; @@ -596,9 +590,9 @@ export default createReactClass({ scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset; this._saveScrollState(); } - }, + }; - _saveScrollState: function() { + _saveScrollState() { if (this.props.stickyBottom && this.isAtBottom()) { this.scrollState = { stuckAtBottom: true }; debuglog("saved stuckAtBottom state"); @@ -641,9 +635,9 @@ export default createReactClass({ bottomOffset: bottomOffset, pixelOffset: bottomOffset - viewportBottom, //needed for restoring the scroll position when coming back to the room }; - }, + } - _restoreSavedScrollState: async function() { + async _restoreSavedScrollState() { const scrollState = this.scrollState; if (scrollState.stuckAtBottom) { @@ -676,7 +670,8 @@ export default createReactClass({ } else { debuglog("not updating height because request already in progress"); } - }, + } + // need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content? async _updateHeight() { // wait until user has stopped scrolling @@ -731,7 +726,7 @@ export default createReactClass({ debuglog("updateHeight to", {newHeight, topDiff}); } } - }, + } _getTrackedNode() { const scrollState = this.scrollState; @@ -764,11 +759,11 @@ export default createReactClass({ } return scrollState.trackedNode; - }, + } _getListHeight() { return this._bottomGrowth + (this._pages * PAGE_SIZE); - }, + } _getMessagesHeight() { const itemlist = this._itemlist.current; @@ -777,17 +772,17 @@ export default createReactClass({ const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0; // 18 is itemlist padding return lastNodeBottom - firstNodeTop + (18 * 2); - }, + } _topFromBottom(node) { // current capped height - distance from top = distance from bottom of container to top of tracked element return this._itemlist.current.clientHeight - node.offsetTop; - }, + } /* get the DOM node which has the scrollTop property we care about for our * message panel. */ - _getScrollNode: function() { + _getScrollNode() { if (this.unmounted) { // this shouldn't happen, but when it does, turn the NPE into // something more meaningful. @@ -801,18 +796,18 @@ export default createReactClass({ } return this._divScroll; - }, + } - _collectScroll: function(divScroll) { + _collectScroll = divScroll => { this._divScroll = divScroll; - }, + }; /** Mark the bottom offset of the last tile so we can balance it out when anything below it changes, by calling updatePreventShrinking, to keep the same minimum bottom offset, effectively preventing the timeline to shrink. */ - preventShrinking: function() { + preventShrinking = () => { const messageList = this._itemlist.current; const tiles = messageList && messageList.children; if (!messageList) { @@ -836,16 +831,16 @@ export default createReactClass({ offsetNode: lastTileNode, }; debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom"); - }, + }; /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */ - clearPreventShrinking: function() { + clearPreventShrinking = () => { const messageList = this._itemlist.current; const balanceElement = messageList && messageList.parentElement; if (balanceElement) balanceElement.style.paddingBottom = null; this.preventShrinkingState = null; debuglog("prevent shrinking cleared"); - }, + }; /** update the container padding to balance @@ -855,7 +850,7 @@ export default createReactClass({ from the bottom of the marked tile grows larger than what it was when marking. */ - updatePreventShrinking: function() { + updatePreventShrinking = () => { if (this.preventShrinkingState) { const sn = this._getScrollNode(); const scrollState = this.scrollState; @@ -885,9 +880,9 @@ export default createReactClass({ this.clearPreventShrinking(); } } - }, + }; - render: function() { + render() { // TODO: the classnames on the div and ol could do with being updated to // reflect the fact that we don't necessarily contain a list of messages. // it's not obvious why we have a separate div and ol anyway. @@ -905,5 +900,5 @@ export default createReactClass({
); - }, -}); + } +} diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 7e9d290bce..c1e3ad0cf2 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -16,18 +16,15 @@ limitations under the License. */ import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { Key } from '../../Keyboard'; import dis from '../../dispatcher/dispatcher'; -import { throttle } from 'lodash'; +import {throttle} from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import classNames from 'classnames'; -export default createReactClass({ - displayName: 'SearchBox', - - propTypes: { +export default class SearchBox extends React.Component { + static propTypes = { onSearch: PropTypes.func, onCleared: PropTypes.func, onKeyDown: PropTypes.func, @@ -38,35 +35,32 @@ export default createReactClass({ // on room search focus action (it would be nicer to take // this functionality out, but not obvious how that would work) enableRoomSearchFocus: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - enableRoomSearchFocus: false, - }; - }, + static defaultProps = { + enableRoomSearchFocus: false, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this._search = createRef(); + + this.state = { searchTerm: "", blurred: true, }; - }, + } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { - this._search = createRef(); - }, - - componentDidMount: function() { + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { dis.unregister(this.dispatcherRef); - }, + } - onAction: function(payload) { + onAction = payload => { if (!this.props.enableRoomSearchFocus) return; switch (payload.action) { @@ -81,51 +75,51 @@ export default createReactClass({ } break; } - }, + }; - onChange: function() { + onChange = () => { if (!this._search.current) return; this.setState({ searchTerm: this._search.current.value }); this.onSearch(); - }, + }; - onSearch: throttle(function() { + onSearch = throttle(() => { this.props.onSearch(this._search.current.value); - }, 200, {trailing: true, leading: true}), + }, 200, {trailing: true, leading: true}); - _onKeyDown: function(ev) { + _onKeyDown = ev => { switch (ev.key) { case Key.ESCAPE: this._clearSearch("keyboard"); break; } if (this.props.onKeyDown) this.props.onKeyDown(ev); - }, + }; - _onFocus: function(ev) { + _onFocus = ev => { this.setState({blurred: false}); ev.target.select(); if (this.props.onFocus) { this.props.onFocus(ev); } - }, + }; - _onBlur: function(ev) { + _onBlur = ev => { this.setState({blurred: true}); if (this.props.onBlur) { this.props.onBlur(ev); } - }, + }; - _clearSearch: function(source) { + _clearSearch(source) { this._search.current.value = ""; this.onChange(); if (this.props.onCleared) { this.props.onCleared(source); } - }, + } - render: function() { + render() { // check for collapsed here and // not at parent so we keep // searchTerm in our state @@ -166,5 +160,5 @@ export default createReactClass({ { clearButton } ); - }, -}); + } +} diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 704dbf8832..6bc35eb2a4 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -18,7 +18,6 @@ limitations under the License. import * as React from "react"; import {_t} from '../../languageHandler'; -import * as PropTypes from "prop-types"; import * as sdk from "../../index"; import AutoHideScrollbar from './AutoHideScrollbar'; import { ReactNode } from "react"; diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 713ed004b0..135b2a1c5c 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; @@ -29,22 +28,18 @@ import { Droppable } from 'react-beautiful-dnd'; import classNames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; +import SettingsStore from "../../settings/SettingsStore"; +import UserTagTile from "../views/elements/UserTagTile"; -const TagPanel = createReactClass({ - displayName: 'TagPanel', +class TagPanel extends React.Component { + static contextType = MatrixClientContext; - statics: { - contextType: MatrixClientContext, - }, + state = { + orderedTags: [], + selectedTags: [], + }; - getInitialState() { - return { - orderedTags: [], - selectedTags: [], - }; - }, - - componentDidMount: function() { + componentDidMount() { this.unmounted = false; this.context.on("Group.myMembership", this._onGroupMyMembership); this.context.on("sync", this._onClientSync); @@ -60,7 +55,7 @@ const TagPanel = createReactClass({ }); // This could be done by anything with a matrix client dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); - }, + } componentWillUnmount() { this.unmounted = true; @@ -69,14 +64,14 @@ const TagPanel = createReactClass({ if (this._tagOrderStoreToken) { this._tagOrderStoreToken.remove(); } - }, + } - _onGroupMyMembership() { + _onGroupMyMembership = () => { if (this.unmounted) return; dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); - }, + }; - _onClientSync(syncState, prevState) { + _onClientSync = (syncState, prevState) => { // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. const reconnected = syncState !== "ERROR" && prevState !== syncState; @@ -84,29 +79,33 @@ const TagPanel = createReactClass({ // Load joined groups dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); } - }, + }; - onMouseDown(e) { + onMouseDown = e => { // only dispatch if its not a no-op if (this.state.selectedTags.length > 0) { dis.dispatch({action: 'deselect_tags'}); } - }, + }; - onCreateGroupClick(ev) { - ev.stopPropagation(); - dis.dispatch({action: 'view_create_group'}); - }, - - onClearFilterClick(ev) { + onClearFilterClick = ev => { dis.dispatch({action: 'deselect_tags'}); - }, + }; + + renderGlobalIcon() { + if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null; + + return ( +
+ +
+
+ ); + } render() { const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const ActionButton = sdk.getComponent('elements.ActionButton'); - const TintableSvg = sdk.getComponent('elements.TintableSvg'); const tags = this.state.orderedTags.map((tag, index) => { return 0; - - let clearButton; - if (itemsSelected) { - clearButton = - - ; - } - const classes = classNames('mx_TagPanel', { mx_TagPanel_items_selected: itemsSelected, }); - return
-
- { clearButton } -
-
+ let createButton = ( + + ); + + if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + createButton = ( + + ); + } + + return
+ { this.renderGlobalIcon() } { tags }
- + {createButton}
{ provided.placeholder }
@@ -167,6 +166,6 @@ const TagPanel = createReactClass({
; - }, -}); + } +} export default TagPanel; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index d469a41cc8..daa18bb290 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -19,7 +19,6 @@ 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'; import {EventTimeline} from "matrix-js-sdk"; @@ -54,10 +53,8 @@ if (DEBUG) { * * Also responsible for handling and sending read receipts. */ -const TimelinePanel = createReactClass({ - displayName: 'TimelinePanel', - - propTypes: { +class TimelinePanel extends React.Component { + static propTypes = { // The js-sdk EventTimelineSet object for the timeline sequence we are // representing. This may or may not have a room, depending on what it's // a timeline representing. If it has a room, we maintain RRs etc for @@ -115,23 +112,28 @@ const TimelinePanel = createReactClass({ // whether to use the irc layout useIRCLayout: PropTypes.bool, - }, + } - statics: { - // a map from room id to read marker event timestamp - roomReadMarkerTsMap: {}, - }, + // a map from room id to read marker event timestamp + static roomReadMarkerTsMap = {}; - getDefaultProps: function() { - return { - // By default, disable the timelineCap in favour of unpaginating based on - // event tile heights. (See _unpaginateEvents) - timelineCap: Number.MAX_VALUE, - className: 'mx_RoomView_messagePanel', - }; - }, + static defaultProps = { + // By default, disable the timelineCap in favour of unpaginating based on + // event tile heights. (See _unpaginateEvents) + timelineCap: Number.MAX_VALUE, + className: 'mx_RoomView_messagePanel', + }; + + constructor(props) { + super(props); + + debuglog("TimelinePanel: mounting"); + + this.lastRRSentEventId = undefined; + this.lastRMSentEventId = undefined; + + this._messagePanel = createRef(); - getInitialState: function() { // XXX: we could track RM per TimelineSet rather than per Room. // but for now we just do it per room for simplicity. let initialReadMarker = null; @@ -144,7 +146,7 @@ const TimelinePanel = createReactClass({ } } - return { + this.state = { events: [], liveEvents: [], timelineLoading: true, // track whether our room timeline is loading @@ -203,24 +205,6 @@ const TimelinePanel = createReactClass({ // how long to show the RM for when it's scrolled off-screen readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"), }; - }, - - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { - debuglog("TimelinePanel: mounting"); - - this.lastRRSentEventId = undefined; - this.lastRMSentEventId = undefined; - - this._messagePanel = createRef(); - - if (this.props.manageReadReceipts) { - this.updateReadReceiptOnUserActivity(); - } - if (this.props.manageReadMarkers) { - this.updateReadMarkerOnUserActivity(); - } - this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); @@ -234,12 +218,24 @@ const TimelinePanel = createReactClass({ MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced); MatrixClientPeg.get().on("sync", this.onSync); + } + + // TODO: [REACT-WARNING] Move into constructor + // eslint-disable-next-line camelcase + UNSAFE_componentWillMount() { + if (this.props.manageReadReceipts) { + this.updateReadReceiptOnUserActivity(); + } + if (this.props.manageReadMarkers) { + this.updateReadMarkerOnUserActivity(); + } this._initTimeline(this.props); - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(newProps) { if (newProps.timelineSet !== this.props.timelineSet) { // throw new Error("changing timelineSet on a TimelinePanel is not supported"); @@ -260,9 +256,9 @@ const TimelinePanel = createReactClass({ " (was " + this.props.eventId + ")"); return this._initTimeline(newProps); } - }, + } - shouldComponentUpdate: function(nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { if (!ObjectUtils.shallowEqual(this.props, nextProps)) { if (DEBUG) { console.group("Timeline.shouldComponentUpdate: props change"); @@ -284,9 +280,9 @@ const TimelinePanel = createReactClass({ } return false; - }, + } - componentWillUnmount: function() { + componentWillUnmount() { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. // @@ -316,9 +312,9 @@ const TimelinePanel = createReactClass({ client.removeListener("Event.replaced", this.onEventReplaced); client.removeListener("sync", this.onSync); } - }, + } - onMessageListUnfillRequest: function(backwards, scrollToken) { + onMessageListUnfillRequest = (backwards, scrollToken) => { // If backwards, unpaginate from the back (i.e. the start of the timeline) const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; debuglog("TimelinePanel: unpaginating events in direction", dir); @@ -349,18 +345,18 @@ const TimelinePanel = createReactClass({ firstVisibleEventIndex, }); } - }, + }; - onPaginationRequest(timelineWindow, direction, size) { + onPaginationRequest = (timelineWindow, direction, size) => { if (this.props.onPaginationRequest) { return this.props.onPaginationRequest(timelineWindow, direction, size); } else { return timelineWindow.paginate(direction, size); } - }, + }; // set off a pagination request. - onMessageListFillRequest: function(backwards) { + onMessageListFillRequest = backwards => { if (!this._shouldPaginate()) return Promise.resolve(false); const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; @@ -425,9 +421,9 @@ const TimelinePanel = createReactClass({ }); }); }); - }, + }; - onMessageListScroll: function(e) { + onMessageListScroll = e => { if (this.props.onScroll) { this.props.onScroll(e); } @@ -447,9 +443,9 @@ const TimelinePanel = createReactClass({ // NO-OP when timeout already has set to the given value this._readMarkerActivityTimer.changeTimeout(timeout); } - }, + }; - onAction: function(payload) { + onAction = payload => { if (payload.action === 'ignore_state_changed') { this.forceUpdate(); } @@ -463,9 +459,9 @@ const TimelinePanel = createReactClass({ } }); } - }, + }; - onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { + onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { // ignore events for other timeline sets if (data.timeline.getTimelineSet() !== this.props.timelineSet) return; @@ -537,21 +533,19 @@ const TimelinePanel = createReactClass({ } }); }); - }, + }; - onRoomTimelineReset: function(room, timelineSet) { + onRoomTimelineReset = (room, timelineSet) => { if (timelineSet !== this.props.timelineSet) return; if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) { this._loadTimeline(); } - }, + }; - canResetTimeline: function() { - return this._messagePanel.current && this._messagePanel.current.isAtBottom(); - }, + canResetTimeline = () => this._messagePanel.current && this._messagePanel.current.isAtBottom(); - onRoomRedaction: function(ev, room) { + onRoomRedaction = (ev, room) => { if (this.unmounted) return; // ignore events for other rooms @@ -560,9 +554,9 @@ const TimelinePanel = createReactClass({ // we could skip an update if the event isn't in our timeline, // but that's probably an early optimisation. this.forceUpdate(); - }, + }; - onEventReplaced: function(replacedEvent, room) { + onEventReplaced = (replacedEvent, room) => { if (this.unmounted) return; // ignore events for other rooms @@ -571,27 +565,27 @@ const TimelinePanel = createReactClass({ // we could skip an update if the event isn't in our timeline, // but that's probably an early optimisation. this.forceUpdate(); - }, + }; - onRoomReceipt: function(ev, room) { + onRoomReceipt = (ev, room) => { if (this.unmounted) return; // ignore events for other rooms if (room !== this.props.timelineSet.room) return; this.forceUpdate(); - }, + }; - onLocalEchoUpdated: function(ev, room, oldEventId) { + onLocalEchoUpdated = (ev, room, oldEventId) => { if (this.unmounted) return; // ignore events for other rooms if (room !== this.props.timelineSet.room) return; this._reloadEvents(); - }, + }; - onAccountData: function(ev, room) { + onAccountData = (ev, room) => { if (this.unmounted) return; // ignore events for other rooms @@ -605,9 +599,9 @@ const TimelinePanel = createReactClass({ this.setState({ readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); - }, + }; - onEventDecrypted: function(ev) { + onEventDecrypted = ev => { // Can be null for the notification timeline, etc. if (!this.props.timelineSet.room) return; @@ -620,19 +614,19 @@ const TimelinePanel = createReactClass({ if (ev.getRoomId() === this.props.timelineSet.room.roomId) { this.forceUpdate(); } - }, + }; - onSync: function(state, prevState, data) { + onSync = (state, prevState, data) => { this.setState({clientSyncState: state}); - }, + }; _readMarkerTimeout(readMarkerPosition) { return readMarkerPosition === 0 ? this.state.readMarkerInViewThresholdMs : this.state.readMarkerOutOfViewThresholdMs; - }, + } - updateReadMarkerOnUserActivity: async function() { + async updateReadMarkerOnUserActivity() { const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition()); this._readMarkerActivityTimer = new Timer(initialTimeout); @@ -644,9 +638,9 @@ const TimelinePanel = createReactClass({ // outside of try/catch to not swallow errors this.updateReadMarker(); } - }, + } - updateReadReceiptOnUserActivity: async function() { + async updateReadReceiptOnUserActivity() { this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS); while (this._readReceiptActivityTimer) { //unset on unmount UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer); @@ -656,9 +650,9 @@ const TimelinePanel = createReactClass({ // outside of try/catch to not swallow errors this.sendReadReceipt(); } - }, + } - sendReadReceipt: function() { + sendReadReceipt = () => { if (SettingsStore.getValue("lowBandwidth")) return; if (!this._messagePanel.current) return; @@ -766,11 +760,11 @@ const TimelinePanel = createReactClass({ }); } } - }, + }; // if the read marker is on the screen, we can now assume we've caught up to the end // of the screen, so move the marker down to the bottom of the screen. - updateReadMarker: function() { + updateReadMarker = () => { if (!this.props.manageReadMarkers) return; if (this.getReadMarkerPosition() === 1) { // the read marker is at an event below the viewport, @@ -801,11 +795,11 @@ const TimelinePanel = createReactClass({ // Send the updated read marker (along with read receipt) to the server this.sendReadReceipt(); - }, + }; // advance the read marker past any events we sent ourselves. - _advanceReadMarkerPastMyEvents: function() { + _advanceReadMarkerPastMyEvents() { if (!this.props.manageReadMarkers) return; // we call `_timelineWindow.getEvents()` rather than using @@ -837,11 +831,11 @@ const TimelinePanel = createReactClass({ const ev = events[i]; this._setReadMarker(ev.getId(), ev.getTs()); - }, + } /* jump down to the bottom of this room, where new events are arriving */ - jumpToLiveTimeline: function() { + jumpToLiveTimeline = () => { // if we can't forward-paginate the existing timeline, then there // is no point reloading it - just jump straight to the bottom. // @@ -854,12 +848,12 @@ const TimelinePanel = createReactClass({ this._messagePanel.current.scrollToBottom(); } } - }, + }; /* scroll to show the read-up-to marker. We put it 1/3 of the way down * the container. */ - jumpToReadMarker: function() { + jumpToReadMarker = () => { if (!this.props.manageReadMarkers) return; if (!this._messagePanel.current) return; if (!this.state.readMarkerEventId) return; @@ -883,11 +877,11 @@ const TimelinePanel = createReactClass({ // As with jumpToLiveTimeline, we want to reload the timeline around the // read-marker. this._loadTimeline(this.state.readMarkerEventId, 0, 1/3); - }, + }; /* update the read-up-to marker to match the read receipt */ - forgetReadMarker: function() { + forgetReadMarker = () => { if (!this.props.manageReadMarkers) return; const rmId = this._getCurrentReadReceipt(); @@ -903,17 +897,17 @@ const TimelinePanel = createReactClass({ } this._setReadMarker(rmId, rmTs); - }, + }; /* return true if the content is fully scrolled down and we are * at the end of the live timeline. */ - isAtEndOfLiveTimeline: function() { + isAtEndOfLiveTimeline = () => { return this._messagePanel.current && this._messagePanel.current.isAtBottom() && this._timelineWindow && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); - }, + } /* get the current scroll state. See ScrollPanel.getScrollState for @@ -921,10 +915,10 @@ const TimelinePanel = createReactClass({ * * returns null if we are not mounted. */ - getScrollState: function() { + getScrollState = () => { if (!this._messagePanel.current) { return null; } return this._messagePanel.current.getScrollState(); - }, + }; // returns one of: // @@ -932,7 +926,7 @@ const TimelinePanel = createReactClass({ // -1: read marker is above the window // 0: read marker is visible // +1: read marker is below the window - getReadMarkerPosition: function() { + getReadMarkerPosition = () => { if (!this.props.manageReadMarkers) return null; if (!this._messagePanel.current) return null; @@ -953,9 +947,9 @@ const TimelinePanel = createReactClass({ } return null; - }, + }; - canJumpToReadMarker: function() { + canJumpToReadMarker = () => { // 1. Do not show jump bar if neither the RM nor the RR are set. // 3. We want to show the bar if the read-marker is off the top of the screen. // 4. Also, if pos === null, the event might not be paginated - show the unread bar @@ -963,14 +957,14 @@ const TimelinePanel = createReactClass({ const ret = this.state.readMarkerEventId !== null && // 1. (pos < 0 || pos === null); // 3., 4. return ret; - }, + }; /* * called by the parent component when PageUp/Down/etc is pressed. * * We pass it down to the scroll panel. */ - handleScrollKey: function(ev) { + handleScrollKey = ev => { if (!this._messagePanel.current) { return; } // jump to the live timeline on ctrl-end, rather than the end of the @@ -980,9 +974,9 @@ const TimelinePanel = createReactClass({ } else { this._messagePanel.current.handleScrollKey(ev); } - }, + }; - _initTimeline: function(props) { + _initTimeline(props) { const initialEvent = props.eventId; const pixelOffset = props.eventPixelOffset; @@ -994,7 +988,7 @@ const TimelinePanel = createReactClass({ } return this._loadTimeline(initialEvent, pixelOffset, offsetBase); - }, + } /** * (re)-load the event timeline, and initialise the scroll state, centered @@ -1012,7 +1006,7 @@ const TimelinePanel = createReactClass({ * * returns a promise which will resolve when the load completes. */ - _loadTimeline: function(eventId, pixelOffset, offsetBase) { + _loadTimeline(eventId, pixelOffset, offsetBase) { this._timelineWindow = new Matrix.TimelineWindow( MatrixClientPeg.get(), this.props.timelineSet, {windowLimit: this.props.timelineCap}); @@ -1122,21 +1116,21 @@ const TimelinePanel = createReactClass({ }); prom.then(onLoaded, onError); } - }, + } // handle the completion of a timeline load or localEchoUpdate, by // reloading the events from the timelinewindow and pending event list into // the state. - _reloadEvents: function() { + _reloadEvents() { // we might have switched rooms since the load started - just bin // the results if so. if (this.unmounted) return; this.setState(this._getEvents()); - }, + } // get the list of events from the timeline window and the pending event list - _getEvents: function() { + _getEvents() { const events = this._timelineWindow.getEvents(); const firstVisibleEventIndex = this._checkForPreJoinUISI(events); @@ -1154,7 +1148,7 @@ const TimelinePanel = createReactClass({ liveEvents, firstVisibleEventIndex, }; - }, + } /** * Check for undecryptable messages that were sent while the user was not in @@ -1166,7 +1160,7 @@ const TimelinePanel = createReactClass({ * undecryptable event that was sent while the user was not in the room. If no * such events were found, then it returns 0. */ - _checkForPreJoinUISI: function(events) { + _checkForPreJoinUISI(events) { const room = this.props.timelineSet.room; if (events.length === 0 || !room || @@ -1186,7 +1180,7 @@ const TimelinePanel = createReactClass({ if (!timeline) { // Somehow, it seems to be possible for live events to not have // a timeline, even though that should not happen. :( - // https://github.com/vector-im/riot-web/issues/12120 + // https://github.com/vector-im/element-web/issues/12120 console.warn( `Event ${events[i].getId()} in room ${room.roomId} is live, ` + `but it does not have a timeline`, @@ -1228,18 +1222,18 @@ const TimelinePanel = createReactClass({ } } return 0; - }, + } - _indexForEventId: function(evId) { + _indexForEventId(evId) { for (let i = 0; i < this.state.events.length; ++i) { if (evId == this.state.events[i].getId()) { return i; } } return null; - }, + } - _getLastDisplayedEventIndex: function(opts) { + _getLastDisplayedEventIndex(opts) { opts = opts || {}; const ignoreOwn = opts.ignoreOwn || false; const allowPartial = opts.allowPartial || false; @@ -1313,7 +1307,7 @@ const TimelinePanel = createReactClass({ } return null; - }, + } /** * Get the id of the event corresponding to our user's latest read-receipt. @@ -1324,7 +1318,7 @@ const TimelinePanel = createReactClass({ * SDK. * @return {String} the event ID */ - _getCurrentReadReceipt: function(ignoreSynthesized) { + _getCurrentReadReceipt(ignoreSynthesized) { const client = MatrixClientPeg.get(); // the client can be null on logout if (client == null) { @@ -1333,9 +1327,9 @@ const TimelinePanel = createReactClass({ const myUserId = client.credentials.userId; return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized); - }, + } - _setReadMarker: function(eventId, eventTs, inhibitSetState) { + _setReadMarker(eventId, eventTs, inhibitSetState) { const roomId = this.props.timelineSet.room.roomId; // don't update the state (and cause a re-render) if there is @@ -1358,9 +1352,9 @@ const TimelinePanel = createReactClass({ this.setState({ readMarkerEventId: eventId, }, this.props.onReadMarkerUpdated); - }, + } - _shouldPaginate: function() { + _shouldPaginate() { // don't try to paginate while events in the timeline are // still being decrypted. We don't render events while they're // being decrypted, so they don't take up space in the timeline. @@ -1369,13 +1363,11 @@ const TimelinePanel = createReactClass({ return !this.state.events.some((e) => { return e.isBeingDecrypted(); }); - }, + } - getRelationsForEvent(...args) { - return this.props.timelineSet.getRelationsForEvent(...args); - }, + getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args); - render: function() { + render() { const MessagePanel = sdk.getComponent("structures.MessagePanel"); const Loader = sdk.getComponent("elements.Spinner"); @@ -1456,7 +1448,7 @@ const TimelinePanel = createReactClass({ useIRCLayout={this.props.useIRCLayout} /> ); - }, -}); + } +} export default TimelinePanel; diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 421d1d79a7..0865764c5a 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -16,30 +16,28 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import ContentMessages from '../../ContentMessages'; import dis from "../../dispatcher/dispatcher"; import filesize from "filesize"; import { _t } from '../../languageHandler'; -export default createReactClass({ - displayName: 'UploadBar', - propTypes: { +export default class UploadBar extends React.Component { + static propTypes = { room: PropTypes.object, - }, + }; - componentDidMount: function() { + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); this.mounted = true; - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this.mounted = false; dis.unregister(this.dispatcherRef); - }, + } - onAction: function(payload) { + onAction = payload => { switch (payload.action) { case 'upload_progress': case 'upload_finished': @@ -48,9 +46,9 @@ export default createReactClass({ if (this.mounted) this.forceUpdate(); break; } - }, + }; - render: function() { + render() { const uploads = ContentMessages.sharedInstance().getCurrentUploads(); // for testing UI... - also fix up the ContentMessages.getCurrentUploads().length @@ -105,5 +103,5 @@ export default createReactClass({
{ uploadText }
); - }, -}); + } +} diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 3f2e387ccb..b83369d296 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -20,7 +20,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; -import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu"; +import { ContextMenuButton } from "./ContextMenu"; import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; @@ -38,6 +38,18 @@ import BaseAvatar from '../views/avatars/BaseAvatar'; import classNames from "classnames"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { SettingLevel } from "../../settings/SettingLevel"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../views/context_menus/IconizedContextMenu"; +import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; +import * as fbEmitter from "fbemitter"; +import TagOrderStore from "../../stores/TagOrderStore"; +import { showCommunityInviteDialog } from "../../RoomInvite"; +import dis from "../../dispatcher/dispatcher"; +import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import ErrorDialog from "../views/dialogs/ErrorDialog"; +import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog"; interface IProps { isMinimized: boolean; @@ -50,23 +62,11 @@ interface IState { isDarkTheme: boolean; } -interface IMenuButtonProps { - iconClassName: string; - label: string; - onClick(ev: ButtonEvent); -} - -const MenuButton: React.FC = ({iconClassName, label, onClick}) => { - return - - {label} - ; -}; - export default class UserMenu extends React.Component { private dispatcherRef: string; private themeWatcherRef: string; private buttonRef: React.RefObject = createRef(); + private tagStoreRef: fbEmitter.EventSubscription; constructor(props: IProps) { super(props); @@ -86,14 +86,20 @@ export default class UserMenu extends React.Component { public componentDidMount() { this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); + this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate); } public componentWillUnmount() { if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); + this.tagStoreRef.remove(); } + private onTagStoreUpdate = () => { + this.forceUpdate(); // we don't have anything useful in state to update + }; + private isUserOnDarkTheme(): boolean { const theme = SettingsStore.getValue("theme"); if (theme.startsWith("custom-")) { @@ -170,7 +176,7 @@ export default class UserMenu extends React.Component { ev.preventDefault(); ev.stopPropagation(); - // TODO: Archived room view: https://github.com/vector-im/riot-web/issues/14038 + // TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038 // Note: You'll need to uncomment the button too. console.log("TODO: Show archived rooms"); }; @@ -198,9 +204,54 @@ export default class UserMenu extends React.Component { defaultDispatcher.dispatch({action: 'view_home_page'}); }; + private onCommunitySettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, { + communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(), + }); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onCommunityMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + // We'd ideally just pop open a right panel with the member list, but the current + // way the right panel is structured makes this exceedingly difficult. Instead, we'll + // switch to the general room and open the member list there as it should be in sync + // anyways. + const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); + if (chat) { + dis.dispatch({ + action: 'view_room', + room_id: chat.roomId, + }, true); + dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList}); + } else { + // "This should never happen" clauses go here for the prototype. + Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, { + title: _t('Failed to find the general chat for this community'), + description: _t("Failed to find the general chat for this community"), + }); + } + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onCommunityInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId()); + this.setState({contextMenuPosition: null}); // also close the menu + }; + private renderContextMenu = (): React.ReactNode => { if (!this.state.contextMenuPosition) return null; + const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); + let hostingLink; const signupLink = getHostingLink("user-context-menu"); if (signupLink) { @@ -226,7 +277,7 @@ export default class UserMenu extends React.Component { let homeButton = null; if (this.hasHomePage) { homeButton = ( - { ); } - return ( - -
+ let primaryHeader = ( +
+ + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
+ ); + let primaryOptionList = ( + + + {homeButton} + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + /> + this.onSettingsOpen(e, USER_SECURITY_TAB)} + /> + this.onSettingsOpen(e, null)} + /> + {/* */} + + + + + + + ); + let secondarySection = null; + + if (prototypeCommunityName) { + primaryHeader = ( +
+ + {prototypeCommunityName} + +
+ ); + primaryOptionList = ( + + + + + + ); + secondarySection = ( + +
@@ -252,68 +376,98 @@ export default class UserMenu extends React.Component { {MatrixClientPeg.get().getUserId()}
- - {_t("Switch -
- {hostingLink} -
- {homeButton} - this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} - /> - this.onSettingsOpen(e, USER_SECURITY_TAB)} - /> - + this.onSettingsOpen(e, null)} /> - {/* */} - -
-
- + + -
-
-
- ); + + + ) + } + + const classes = classNames({ + "mx_UserMenu_contextMenu": true, + "mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName, + }); + + return +
+ {primaryHeader} + + {_t("Switch + +
+ {hostingLink} + {primaryOptionList} + {secondarySection} +
; }; public render() { const avatarSize = 32; // should match border-radius of the avatar - let name = {OwnProfileStore.instance.displayName}; + const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId(); + const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); + + const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); + + let isPrototype = false; + let menuName = _t("User menu"); + let name = {displayName}; let buttons = ( {/* masked image in CSS */} ); + if (prototypeCommunityName) { + name = ( +
+ {prototypeCommunityName} + {displayName} +
+ ); + menuName = _t("Community and user menu"); + isPrototype = true; + } else if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + name = ( +
+ {_t("Home")} + {displayName} +
+ ); + isPrototype = true; + } if (this.props.isMinimized) { name = null; buttons = null; @@ -322,6 +476,7 @@ export default class UserMenu extends React.Component { const classes = classNames({ 'mx_UserMenu': true, 'mx_UserMenu_minimized': this.props.isMinimized, + 'mx_UserMenu_prototype': isPrototype, }); return ( @@ -330,16 +485,16 @@ export default class UserMenu extends React.Component { className={classes} onClick={this.onOpenMenuClick} inputRef={this.buttonRef} - label={_t("User menu")} + label={menuName} isExpanded={!!this.state.contextMenuPosition} onContextMenu={this.onContextMenu} >
@@ -49,5 +46,5 @@ export default createReactClass({
); - }, -}); + } +} diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 9877c53106..3fa2713a35 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; @@ -40,50 +39,47 @@ const PHASE_EMAIL_SENT = 3; // User has clicked the link in email and completed reset const PHASE_DONE = 4; -export default createReactClass({ - displayName: 'ForgotPassword', - - propTypes: { +export default class ForgotPassword extends React.Component { + static propTypes = { serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, onServerConfigChange: PropTypes.func.isRequired, onLoginClick: PropTypes.func, onComplete: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - phase: PHASE_FORGOT, - email: "", - password: "", - password2: "", - errorText: null, + state = { + phase: PHASE_FORGOT, + email: "", + password: "", + password2: "", + errorText: null, - // We perform liveliness checks later, but for now suppress the errors. - // We also track the server dead errors independently of the regular errors so - // that we can render it differently, and override any other error the user may - // be seeing. - serverIsAlive: true, - serverErrorIsFatal: false, - serverDeadError: "", - serverRequiresIdServer: null, - }; - }, + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverErrorIsFatal: false, + serverDeadError: "", + serverRequiresIdServer: null, + }; - componentDidMount: function() { + componentDidMount() { this.reset = null; this._checkServerLiveliness(this.props.serverConfig); - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; // Do a liveliness check on the new URLs this._checkServerLiveliness(newProps.serverConfig); - }, + } - _checkServerLiveliness: async function(serverConfig) { + async _checkServerLiveliness(serverConfig) { try { await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( serverConfig.hsUrl, @@ -100,9 +96,9 @@ export default createReactClass({ } catch (e) { this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password")); } - }, + } - submitPasswordReset: function(email, password) { + submitPasswordReset(email, password) { this.setState({ phase: PHASE_SENDING_EMAIL, }); @@ -117,9 +113,9 @@ export default createReactClass({ phase: PHASE_FORGOT, }); }); - }, + } - onVerify: async function(ev) { + onVerify = async ev => { ev.preventDefault(); if (!this.reset) { console.error("onVerify called before submitPasswordReset!"); @@ -131,9 +127,9 @@ export default createReactClass({ } catch (err) { this.showErrorDialog(err.message); } - }, + }; - onSubmitForm: async function(ev) { + onSubmitForm = async ev => { ev.preventDefault(); // refresh the server errors, just in case the server came back online @@ -166,41 +162,41 @@ export default createReactClass({ }, }); } - }, + }; - onInputChanged: function(stateKey, ev) { + onInputChanged = (stateKey, ev) => { this.setState({ [stateKey]: ev.target.value, }); - }, + }; - async onServerDetailsNextPhaseClick() { + onServerDetailsNextPhaseClick = async () => { this.setState({ phase: PHASE_FORGOT, }); - }, + }; - onEditServerDetailsClick(ev) { + onEditServerDetailsClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.setState({ phase: PHASE_SERVER_DETAILS, }); - }, + }; - onLoginClick: function(ev) { + onLoginClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.props.onLoginClick(); - }, + }; - showErrorDialog: function(body, title) { + showErrorDialog(body, title) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { title: title, description: body, }); - }, + } renderServerDetails() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); @@ -218,7 +214,7 @@ export default createReactClass({ submitText={_t("Next")} submitClass="mx_Login_submit" />; - }, + } renderForgot() { const Field = sdk.getComponent('elements.Field'); @@ -335,12 +331,12 @@ export default createReactClass({ {_t('Sign in instead')} ; - }, + } renderSendingEmail() { const Spinner = sdk.getComponent("elements.Spinner"); return ; - }, + } renderEmailSent() { return
@@ -350,7 +346,7 @@ export default createReactClass({
; - }, + } renderDone() { return
@@ -363,9 +359,9 @@ export default createReactClass({
; - }, + } - render: function() { + render() { const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); @@ -397,5 +393,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index e65a223bd6..53769fb5a6 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {_t, _td} from '../../../languageHandler'; import * as sdk from '../../../index'; @@ -53,13 +52,11 @@ _td("Invalid base_url for m.identity_server"); _td("Identity server URL does not appear to be a valid identity server"); _td("General failure"); -/** +/* * A wire component which glues together login UI components and Login logic */ -export default createReactClass({ - displayName: 'Login', - - propTypes: { +export default class LoginComponent extends React.Component { + static propTypes = { // Called when the user has logged in. Params: // - The object returned by the login API // - The user's password, if applicable, (may be cached in memory for a @@ -85,10 +82,14 @@ export default createReactClass({ serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, isSyncing: PropTypes.bool, - }, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this._unmounted = false; + + this.state = { busy: false, busyLoggingIn: null, errorText: null, @@ -113,11 +114,6 @@ export default createReactClass({ serverErrorIsFatal: false, serverDeadError: "", }; - }, - - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount: function() { - this._unmounted = false; // map from login step type to a function which will render a control // letting you do that login type @@ -130,33 +126,32 @@ export default createReactClass({ }; this._initLoginLogic(); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._unmounted = true; - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + // eslint-disable-next-line camelcase UNSAFE_componentWillReceiveProps(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; // Ensure that we end up actually logging in to the right place this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); - }, + } - onPasswordLoginError: function(errorText) { + onPasswordLoginError = errorText => { this.setState({ errorText, loginIncorrect: Boolean(errorText), }); - }, + }; - isBusy: function() { - return this.state.busy || this.props.busy; - }, + isBusy = () => this.state.busy || this.props.busy; - onPasswordLogin: async function(username, phoneCountry, phoneNumber, password) { + onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => { if (!this.state.serverIsAlive) { this.setState({busy: true}); // Do a quick liveliness check on the URLs @@ -263,13 +258,13 @@ export default createReactClass({ loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403, }); }); - }, + }; - onUsernameChanged: function(username) { + onUsernameChanged = username => { this.setState({ username: username }); - }, + }; - onUsernameBlur: async function(username) { + onUsernameBlur = async username => { const doWellknownLookup = username[0] === "@"; this.setState({ username: username, @@ -285,7 +280,7 @@ export default createReactClass({ // We'd like to rely on new props coming in via `onServerConfigChange` // so that we know the servers have definitely updated before clearing // the busy state. In the case of a full MXID that resolves to the same - // HS as Riot's default HS though, there may not be any server change. + // HS as Element's default HS though, there may not be any server change. // To avoid this trap, we clear busy here. For cases where the server // actually has changed, `_initLoginLogic` will be called and manages // busy state for its own liveness check. @@ -314,19 +309,19 @@ export default createReactClass({ }); } } - }, + }; - onPhoneCountryChanged: function(phoneCountry) { + onPhoneCountryChanged = phoneCountry => { this.setState({ phoneCountry: phoneCountry }); - }, + }; - onPhoneNumberChanged: function(phoneNumber) { + onPhoneNumberChanged = phoneNumber => { this.setState({ phoneNumber: phoneNumber, }); - }, + }; - onPhoneNumberBlur: function(phoneNumber) { + onPhoneNumberBlur = phoneNumber => { // Validate the phone number entered if (!PHONE_NUMBER_REGEX.test(phoneNumber)) { this.setState({ @@ -339,15 +334,15 @@ export default createReactClass({ canTryLogin: true, }); } - }, + }; - onRegisterClick: function(ev) { + onRegisterClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.props.onRegisterClick(); - }, + }; - onTryRegisterClick: function(ev) { + onTryRegisterClick = ev => { const step = this._getCurrentFlowStep(); if (step === 'm.login.sso' || step === 'm.login.cas') { // If we're showing SSO it means that registration is also probably disabled, @@ -361,23 +356,23 @@ export default createReactClass({ // Don't intercept - just go through to the register page this.onRegisterClick(ev); } - }, + }; - async onServerDetailsNextPhaseClick() { + onServerDetailsNextPhaseClick = () => { this.setState({ phase: PHASE_LOGIN, }); - }, + }; - onEditServerDetailsClick(ev) { + onEditServerDetailsClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.setState({ phase: PHASE_SERVER_DETAILS, }); - }, + }; - _initLoginLogic: async function(hsUrl, isUrl) { + async _initLoginLogic(hsUrl, isUrl) { hsUrl = hsUrl || this.props.serverConfig.hsUrl; isUrl = isUrl || this.props.serverConfig.isUrl; @@ -465,9 +460,9 @@ export default createReactClass({ busy: false, }); }); - }, + } - _isSupportedFlow: function(flow) { + _isSupportedFlow(flow) { // technically the flow can have multiple steps, but no one does this // for login and loginLogic doesn't support it so we can ignore it. if (!this._stepRendererMap[flow.type]) { @@ -475,11 +470,11 @@ export default createReactClass({ return false; } return true; - }, + } - _getCurrentFlowStep: function() { + _getCurrentFlowStep() { return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; - }, + } _errorTextFromError(err) { let errCode = err.errcode; @@ -526,7 +521,7 @@ export default createReactClass({ } return errorText; - }, + } renderServerComponent() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); @@ -552,7 +547,7 @@ export default createReactClass({ delayTimeMs={250} {...serverDetailsProps} />; - }, + } renderLoginComponentForStep() { if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) { @@ -572,9 +567,9 @@ export default createReactClass({ } return null; - }, + } - _renderPasswordStep: function() { + _renderPasswordStep = () => { const PasswordLogin = sdk.getComponent('auth.PasswordLogin'); let onEditServerDetailsClick = null; @@ -603,9 +598,9 @@ export default createReactClass({ busy={this.props.isSyncing || this.state.busyLoggingIn} /> ); - }, + }; - _renderSsoStep: function(loginType) { + _renderSsoStep = loginType => { const SignInToText = sdk.getComponent('views.auth.SignInToText'); let onEditServerDetailsClick = null; @@ -615,8 +610,8 @@ export default createReactClass({ } // XXX: This link does *not* have a target="_blank" because single sign-on relies on // redirecting the user back to a URI once they're logged in. On the web, this means - // we use the same window and redirect back to riot. On electron, this actually - // opens the SSO page in the electron app itself due to + // we use the same window and redirect back to Element. On Electron, this actually + // opens the SSO page in the Electron app itself due to // https://github.com/electron/electron/issues/8841 and so happens to work. // If this bug gets fixed, it will break SSO since it will open the SSO page in the // user's browser, let them log into their SSO provider, then redirect their browser @@ -634,9 +629,9 @@ export default createReactClass({ /> ); - }, + }; - render: function() { + render() { const Loader = sdk.getComponent("elements.Spinner"); const InlineSpinner = sdk.getComponent("elements.InlineSpinner"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); @@ -704,5 +699,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 687ab9a195..aa36de6596 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -15,29 +15,24 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import AuthPage from "../../views/auth/AuthPage"; -export default createReactClass({ - displayName: 'PostRegistration', - - propTypes: { +export default class PostRegistration extends React.Component { + static propTypes = { onComplete: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - avatarUrl: null, - errorString: null, - busy: false, - }; - }, + state = { + avatarUrl: null, + errorString: null, + busy: false, + }; - componentDidMount: function() { + componentDidMount() { // There is some assymetry between ChangeDisplayName and ChangeAvatar, // as ChangeDisplayName will auto-get the name but ChangeAvatar expects // the URL to be passed to you (because it's also used for room avatars). @@ -55,9 +50,9 @@ export default createReactClass({ busy: false, }); }); - }, + } - render: function() { + render() { const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const AuthHeader = sdk.getComponent('auth.AuthHeader'); @@ -78,5 +73,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 3b5f5676dc..630e04da9c 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -19,7 +19,6 @@ limitations under the License. import Matrix from 'matrix-js-sdk'; import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; @@ -43,10 +42,8 @@ const PHASE_REGISTRATION = 1; // Enable phases for registration const PHASES_ENABLED = true; -export default createReactClass({ - displayName: 'Registration', - - propTypes: { +export default class Registration extends React.Component { + static propTypes = { // Called when the user has logged in. Params: // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken // - The user's password, if available and applicable (may be cached in memory @@ -65,12 +62,13 @@ export default createReactClass({ onLoginClick: PropTypes.func.isRequired, onServerConfigChange: PropTypes.func.isRequired, defaultDeviceDisplayName: PropTypes.string, - }, + }; + + constructor(props) { + super(props); - getInitialState: function() { const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig); - - return { + this.state = { busy: false, errorText: null, // We remember the values entered by the user because @@ -118,14 +116,15 @@ export default createReactClass({ // this is the user ID that's logged in. differentLoggedInUserId: null, }; - }, + } - componentDidMount: function() { + componentDidMount() { this._unmounted = false; this._replaceClient(); - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + // eslint-disable-next-line camelcase UNSAFE_componentWillReceiveProps(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; @@ -142,7 +141,7 @@ export default createReactClass({ phase: this.getDefaultPhaseForServerType(serverType), }); } - }, + } getDefaultPhaseForServerType(type) { switch (type) { @@ -155,9 +154,9 @@ export default createReactClass({ case ServerType.ADVANCED: return PHASE_SERVER_DETAILS; } - }, + } - onServerTypeChange(type) { + onServerTypeChange = type => { this.setState({ serverType: type, }); @@ -184,9 +183,9 @@ export default createReactClass({ this.setState({ phase: this.getDefaultPhaseForServerType(type), }); - }, + }; - _replaceClient: async function(serverConfig) { + async _replaceClient(serverConfig) { this.setState({ errorText: null, serverDeadError: null, @@ -262,7 +261,7 @@ export default createReactClass({ // the user off to the login page to figure their account out. try { const loginLogic = new Login(hsUrl, isUrl, null, { - defaultDeviceDisplayName: "riot login check", // We shouldn't ever be used + defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used }); const flows = await loginLogic.getFlows(); const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas'); @@ -286,18 +285,18 @@ export default createReactClass({ showGenericError(e); } } - }, + } - onFormSubmit: function(formVals) { + onFormSubmit = formVals => { this.setState({ errorText: "", busy: true, formVals: formVals, doingUIAuth: true, }); - }, + }; - _requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) { + _requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => { return this.state.matrixClient.requestRegisterEmailToken( emailAddress, clientSecret, @@ -309,9 +308,9 @@ export default createReactClass({ session_id: sessionId, }), ); - }, + } - _onUIAuthFinished: async function(success, response, extra) { + _onUIAuthFinished = async (success, response, extra) => { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? @@ -395,9 +394,9 @@ export default createReactClass({ } this.setState(newState); - }, + }; - _setupPushers: function() { + _setupPushers() { if (!this.props.brand) { return Promise.resolve(); } @@ -418,15 +417,15 @@ export default createReactClass({ }, (error) => { console.error("Couldn't get pushers: " + error); }); - }, + } - onLoginClick: function(ev) { + onLoginClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.props.onLoginClick(); - }, + }; - onGoToFormClicked(ev) { + onGoToFormClicked = ev => { ev.preventDefault(); ev.stopPropagation(); this._replaceClient(); @@ -435,23 +434,23 @@ export default createReactClass({ doingUIAuth: false, phase: PHASE_REGISTRATION, }); - }, + }; - async onServerDetailsNextPhaseClick() { + onServerDetailsNextPhaseClick = async () => { this.setState({ phase: PHASE_REGISTRATION, }); - }, + }; - onEditServerDetailsClick(ev) { + onEditServerDetailsClick = ev => { ev.preventDefault(); ev.stopPropagation(); this.setState({ phase: PHASE_SERVER_DETAILS, }); - }, + }; - _makeRegisterRequest: function(auth) { + _makeRegisterRequest = auth => { // We inhibit login if we're trying to register with an email address: this // avoids a lot of complex race conditions that can occur if we try to log // the user in one one or both of the tabs they might end up with after @@ -471,20 +470,20 @@ export default createReactClass({ if (auth) registerParams.auth = auth; if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin; return this.state.matrixClient.registerRequest(registerParams); - }, + }; - _getUIAuthInputs: function() { + _getUIAuthInputs() { return { emailAddress: this.state.formVals.email, phoneCountry: this.state.formVals.phoneCountry, phoneNumber: this.state.formVals.phoneNumber, }; - }, + } // Links to the login page shown after registration is completed are routed through this // which checks the user hasn't already logged in somewhere else (perhaps we should do // this more generally?) - _onLoginClickWithCheck: async function(ev) { + _onLoginClickWithCheck = async ev => { ev.preventDefault(); const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true}); @@ -492,7 +491,7 @@ export default createReactClass({ // ok fine, there's still no session: really go to the login page this.props.onLoginClick(); } - }, + }; renderServerComponent() { const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); @@ -553,7 +552,7 @@ export default createReactClass({ /> {serverDetails} ; - }, + } renderRegisterComponent() { if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) { @@ -608,9 +607,9 @@ export default createReactClass({ serverRequiresIdServer={this.state.serverRequiresIdServer} />; } - }, + } - render: function() { + render() { const AuthHeader = sdk.getComponent('auth.AuthHeader'); const AuthBody = sdk.getComponent("auth.AuthBody"); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -706,5 +705,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 5b1f025dfb..6d090936e5 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -152,7 +152,7 @@ export default class SetupEncryptionBody extends React.Component {
{_t("%(brand)s iOS", { brand })}
-
{_t("%(brand)s X for Android", { brand })}
+
{_t("%(brand)s Android", { brand })}

{_t("or another cross-signing capable Matrix client")}

diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.js index 1309800772..3de5a19350 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.js @@ -18,16 +18,13 @@ limitations under the License. import { _t } from '../../../languageHandler'; import React from 'react'; -import createReactClass from 'create-react-class'; -export default createReactClass({ - displayName: 'AuthFooter', - - render: function() { +export default class AuthFooter extends React.Component { + render() { return ( ); - }, -}); + } +} diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.js index 6e787ba77c..57499e397c 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.js @@ -17,17 +17,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; -export default createReactClass({ - displayName: 'AuthHeader', - - propTypes: { +export default class AuthHeader extends React.Component { + static propTypes = { disableLanguageSelector: PropTypes.bool, - }, + }; - render: function() { + render() { const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo'); const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector'); @@ -37,5 +34,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js index e162603b01..783d519621 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.js @@ -15,7 +15,6 @@ limitations under the License. */ import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; @@ -24,36 +23,31 @@ const DIV_ID = 'mx_recaptcha'; /** * A pure UI component which displays a captcha form. */ -export default createReactClass({ - displayName: 'CaptchaForm', - - propTypes: { +export default class CaptchaForm extends React.Component { + static propTypes = { sitePublicKey: PropTypes.string, // called with the captcha response onCaptchaResponse: PropTypes.func, - }, + }; - getDefaultProps: function() { - return { - onCaptchaResponse: () => {}, - }; - }, + static defaultProps = { + onCaptchaResponse: () => {}, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this.state = { errorText: null, }; - }, - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { this._captchaWidgetId = null; this._recaptchaContainer = createRef(); - }, + } - componentDidMount: function() { + componentDidMount() { // Just putting a script tag into the returned jsx doesn't work, annoyingly, // so we do this instead. if (global.grecaptcha) { @@ -68,13 +62,13 @@ export default createReactClass({ ); this._recaptchaContainer.current.appendChild(scriptTag); } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._resetRecaptcha(); - }, + } - _renderRecaptcha: function(divId) { + _renderRecaptcha(divId) { if (!global.grecaptcha) { console.error("grecaptcha not loaded!"); throw new Error("Recaptcha did not load successfully"); @@ -93,15 +87,15 @@ export default createReactClass({ sitekey: publicKey, callback: this.props.onCaptchaResponse, }); - }, + } - _resetRecaptcha: function() { + _resetRecaptcha() { if (this._captchaWidgetId !== null) { global.grecaptcha.reset(this._captchaWidgetId); } - }, + } - _onCaptchaLoaded: function() { + _onCaptchaLoaded() { console.log("Loaded recaptcha script."); try { this._renderRecaptcha(DIV_ID); @@ -110,9 +104,9 @@ export default createReactClass({ errorText: e.toString(), }); } - }, + } - render: function() { + render() { let error = null; if (this.state.errorText) { error = ( @@ -131,5 +125,5 @@ export default createReactClass({ { error } ); - }, -}); + } +} diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js index 7b2c8f88aa..138f8c4689 100644 --- a/src/components/views/auth/CustomServerDialog.js +++ b/src/components/views/auth/CustomServerDialog.js @@ -16,14 +16,11 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; -export default createReactClass({ - displayName: 'CustomServerDialog', - - render: function() { +export default class CustomServerDialog extends React.Component { + render() { const brand = SdkConfig.get().brand; return (
@@ -46,5 +43,5 @@ export default createReactClass({
); - }, -}); + } +} diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index f6bc1b8ae7..628c177d94 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -17,7 +17,6 @@ limitations under the License. */ import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import url from 'url'; import classnames from 'classnames'; @@ -75,14 +74,10 @@ import AccessibleButton from "../elements/AccessibleButton"; export const DEFAULT_PHASE = 0; -export const PasswordAuthEntry = createReactClass({ - displayName: 'PasswordAuthEntry', +export class PasswordAuthEntry extends React.Component { + static LOGIN_TYPE = "m.login.password"; - statics: { - LOGIN_TYPE: "m.login.password", - }, - - propTypes: { + static propTypes = { matrixClient: PropTypes.object.isRequired, submitAuthDict: PropTypes.func.isRequired, errorText: PropTypes.string, @@ -90,26 +85,24 @@ export const PasswordAuthEntry = createReactClass({ // happen? busy: PropTypes.bool, onPhaseChange: PropTypes.func.isRequired, - }, + }; - componentDidMount: function() { + componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); - }, + } - getInitialState: function() { - return { - password: "", - }; - }, + state = { + password: "", + }; - _onSubmit: function(e) { + _onSubmit = e => { e.preventDefault(); if (this.props.busy) return; this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, // TODO: Remove `user` once servers support proper UIA - // See https://github.com/vector-im/riot-web/issues/10312 + // See https://github.com/vector-im/element-web/issues/10312 user: this.props.matrixClient.credentials.userId, identifier: { type: "m.id.user", @@ -117,16 +110,16 @@ export const PasswordAuthEntry = createReactClass({ }, password: this.state.password, }); - }, + }; - _onPasswordFieldChange: function(ev) { + _onPasswordFieldChange = ev => { // enable the submit button iff the password is non-empty this.setState({ password: ev.target.value, }); - }, + }; - render: function() { + render() { const passwordBoxClass = classnames({ "error": this.props.errorText, }); @@ -176,36 +169,32 @@ export const PasswordAuthEntry = createReactClass({ { errorSection } ); - }, -}); + } +} -export const RecaptchaAuthEntry = createReactClass({ - displayName: 'RecaptchaAuthEntry', +export class RecaptchaAuthEntry extends React.Component { + static LOGIN_TYPE = "m.login.recaptcha"; - statics: { - LOGIN_TYPE: "m.login.recaptcha", - }, - - propTypes: { + static propTypes = { submitAuthDict: PropTypes.func.isRequired, stageParams: PropTypes.object.isRequired, errorText: PropTypes.string, busy: PropTypes.bool, onPhaseChange: PropTypes.func.isRequired, - }, + }; - componentDidMount: function() { + componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); - }, + } - _onCaptchaResponse: function(response) { + _onCaptchaResponse = response => { this.props.submitAuthDict({ type: RecaptchaAuthEntry.LOGIN_TYPE, response: response, }); - }, + }; - render: function() { + render() { if (this.props.busy) { const Loader = sdk.getComponent("elements.Spinner"); return ; @@ -241,31 +230,24 @@ export const RecaptchaAuthEntry = createReactClass({ { errorSection } ); - }, -}); + } +} -export const TermsAuthEntry = createReactClass({ - displayName: 'TermsAuthEntry', +export class TermsAuthEntry extends React.Component { + static LOGIN_TYPE = "m.login.terms"; - statics: { - LOGIN_TYPE: "m.login.terms", - }, - - propTypes: { + static propTypes = { submitAuthDict: PropTypes.func.isRequired, stageParams: PropTypes.object.isRequired, errorText: PropTypes.string, busy: PropTypes.bool, showContinue: PropTypes.bool, onPhaseChange: PropTypes.func.isRequired, - }, + }; - componentDidMount: function() { - this.props.onPhaseChange(DEFAULT_PHASE); - }, + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Move this to constructor - componentWillMount: function() { // example stageParams: // // { @@ -310,17 +292,22 @@ export const TermsAuthEntry = createReactClass({ pickedPolicies.push(langPolicy); } - this.setState({ - "toggledPolicies": initToggles, - "policies": pickedPolicies, - }); - }, + this.state = { + toggledPolicies: initToggles, + policies: pickedPolicies, + }; + } - tryContinue: function() { + + componentDidMount() { + this.props.onPhaseChange(DEFAULT_PHASE); + } + + tryContinue = () => { this._trySubmit(); - }, + }; - _togglePolicy: function(policyId) { + _togglePolicy(policyId) { const newToggles = {}; for (const policy of this.state.policies) { let checked = this.state.toggledPolicies[policy.id]; @@ -329,9 +316,9 @@ export const TermsAuthEntry = createReactClass({ newToggles[policy.id] = checked; } this.setState({"toggledPolicies": newToggles}); - }, + } - _trySubmit: function() { + _trySubmit = () => { let allChecked = true; for (const policy of this.state.policies) { const checked = this.state.toggledPolicies[policy.id]; @@ -340,9 +327,9 @@ export const TermsAuthEntry = createReactClass({ if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")}); - }, + }; - render: function() { + render() { if (this.props.busy) { const Loader = sdk.getComponent("elements.Spinner"); return ; @@ -387,17 +374,13 @@ export const TermsAuthEntry = createReactClass({ { submitButton } ); - }, -}); + } +} -export const EmailIdentityAuthEntry = createReactClass({ - displayName: 'EmailIdentityAuthEntry', +export class EmailIdentityAuthEntry extends React.Component { + static LOGIN_TYPE = "m.login.email.identity"; - statics: { - LOGIN_TYPE: "m.login.email.identity", - }, - - propTypes: { + static propTypes = { matrixClient: PropTypes.object.isRequired, submitAuthDict: PropTypes.func.isRequired, authSessionId: PropTypes.string.isRequired, @@ -407,13 +390,13 @@ export const EmailIdentityAuthEntry = createReactClass({ fail: PropTypes.func.isRequired, setEmailSid: PropTypes.func.isRequired, onPhaseChange: PropTypes.func.isRequired, - }, + }; - componentDidMount: function() { + componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); - }, + } - render: function() { + render() { // This component is now only displayed once the token has been requested, // so we know the email has been sent. It can also get loaded after the user // has clicked the validation link if the server takes a while to propagate @@ -434,17 +417,13 @@ export const EmailIdentityAuthEntry = createReactClass({ ); } - }, -}); + } +} -export const MsisdnAuthEntry = createReactClass({ - displayName: 'MsisdnAuthEntry', +export class MsisdnAuthEntry extends React.Component { + static LOGIN_TYPE = "m.login.msisdn"; - statics: { - LOGIN_TYPE: "m.login.msisdn", - }, - - propTypes: { + static propTypes = { inputs: PropTypes.shape({ phoneCountry: PropTypes.string, phoneNumber: PropTypes.string, @@ -454,16 +433,14 @@ export const MsisdnAuthEntry = createReactClass({ submitAuthDict: PropTypes.func.isRequired, matrixClient: PropTypes.object, onPhaseChange: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - token: '', - requestingToken: false, - }; - }, + state = { + token: '', + requestingToken: false, + }; - componentDidMount: function() { + componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); this._submitUrl = null; @@ -477,12 +454,12 @@ export const MsisdnAuthEntry = createReactClass({ }).finally(() => { this.setState({requestingToken: false}); }); - }, + } /* * Requests a verification token by SMS. */ - _requestMsisdnToken: function() { + _requestMsisdnToken() { return this.props.matrixClient.requestRegisterMsisdnToken( this.props.inputs.phoneCountry, this.props.inputs.phoneNumber, @@ -493,15 +470,15 @@ export const MsisdnAuthEntry = createReactClass({ this._sid = result.sid; this._msisdn = result.msisdn; }); - }, + } - _onTokenChange: function(e) { + _onTokenChange = e => { this.setState({ token: e.target.value, }); - }, + }; - _onFormSubmit: async function(e) { + _onFormSubmit = async e => { e.preventDefault(); if (this.state.token == '') return; @@ -538,7 +515,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.submitAuthDict({ type: MsisdnAuthEntry.LOGIN_TYPE, // TODO: Remove `threepid_creds` once servers support proper UIA - // See https://github.com/vector-im/riot-web/issues/10312 + // See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/matrix-org/matrix-doc/issues/2220 threepid_creds: creds, threepidCreds: creds, @@ -552,9 +529,9 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); console.log("Failed to submit msisdn token"); } - }, + }; - render: function() { + render() { if (this.state.requestingToken) { const Loader = sdk.getComponent("elements.Spinner"); return ; @@ -598,8 +575,8 @@ export const MsisdnAuthEntry = createReactClass({ ); } - }, -}); + } +} export class SSOAuthEntry extends React.Component { static propTypes = { @@ -686,46 +663,46 @@ export class SSOAuthEntry extends React.Component { } } -export const FallbackAuthEntry = createReactClass({ - displayName: 'FallbackAuthEntry', - - propTypes: { +export class FallbackAuthEntry extends React.Component { + static propTypes = { matrixClient: PropTypes.object.isRequired, authSessionId: PropTypes.string.isRequired, loginType: PropTypes.string.isRequired, submitAuthDict: PropTypes.func.isRequired, errorText: PropTypes.string, onPhaseChange: PropTypes.func.isRequired, - }, + }; - componentDidMount: function() { - this.props.onPhaseChange(DEFAULT_PHASE); - }, + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { // we have to make the user click a button, as browsers will block // the popup if we open it immediately. this._popupWindow = null; window.addEventListener("message", this._onReceiveMessage); this._fallbackButton = createRef(); - }, + } - componentWillUnmount: function() { + + componentDidMount() { + this.props.onPhaseChange(DEFAULT_PHASE); + } + + componentWillUnmount() { window.removeEventListener("message", this._onReceiveMessage); if (this._popupWindow) { this._popupWindow.close(); } - }, + } - focus: function() { + focus = () => { if (this._fallbackButton.current) { this._fallbackButton.current.focus(); } - }, + }; - _onShowFallbackClick: function(e) { + _onShowFallbackClick = e => { e.preventDefault(); e.stopPropagation(); @@ -735,18 +712,18 @@ export const FallbackAuthEntry = createReactClass({ ); this._popupWindow = window.open(url); this._popupWindow.opener = null; - }, + }; - _onReceiveMessage: function(event) { + _onReceiveMessage = event => { if ( event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl() ) { this.props.submitAuthDict({}); } - }, + }; - render: function() { + render() { let errorSection; if (this.props.errorText) { errorSection = ( @@ -761,8 +738,8 @@ export const FallbackAuthEntry = createReactClass({ {errorSection} ); - }, -}); + } +} const AuthEntryComponents = [ PasswordAuthEntry, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 17c65fa94e..c07486d3bd 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -18,7 +18,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import * as Email from '../../../email'; @@ -39,13 +38,11 @@ const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. -/** +/* * A pure UI component which displays a registration form. */ -export default createReactClass({ - displayName: 'RegistrationForm', - - propTypes: { +export default class RegistrationForm extends React.Component { + static propTypes = { // Values pre-filled in the input boxes when the component loads defaultEmail: PropTypes.string, defaultPhoneCountry: PropTypes.string, @@ -58,17 +55,17 @@ export default createReactClass({ serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, canSubmit: PropTypes.bool, serverRequiresIdServer: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - onValidationChange: console.error, - canSubmit: true, - }; - }, + static defaultProps = { + onValidationChange: console.error, + canSubmit: true, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this.state = { // Field error codes by field ID fieldValid: {}, // The ISO2 country code selected in the phone number entry @@ -80,9 +77,9 @@ export default createReactClass({ passwordConfirm: this.props.defaultPassword || "", passwordComplexity: null, }; - }, + } - onSubmit: async function(ev) { + onSubmit = async ev => { ev.preventDefault(); if (!this.props.canSubmit) return; @@ -118,7 +115,7 @@ export default createReactClass({ title: _t("Warning!"), description: desc, button: _t("Continue"), - onFinished: function(confirmed) { + onFinished(confirmed) { if (confirmed) { self._doSubmit(ev); } @@ -127,9 +124,9 @@ export default createReactClass({ } else { self._doSubmit(ev); } - }, + }; - _doSubmit: function(ev) { + _doSubmit(ev) { const email = this.state.email.trim(); const promise = this.props.onRegisterClick({ username: this.state.username.trim(), @@ -145,7 +142,7 @@ export default createReactClass({ ev.target.disabled = false; }); } - }, + } async verifyFieldsBeforeSubmit() { // Blur the active element if any, so we first run its blur validation, @@ -196,12 +193,12 @@ export default createReactClass({ invalidField.focus(); invalidField.validate({ allowEmpty: false, focused: true }); return false; - }, + } /** * @returns {boolean} true if all fields were valid last time they were validated. */ - allFieldsValid: function() { + allFieldsValid() { const keys = Object.keys(this.state.fieldValid); for (let i = 0; i < keys.length; ++i) { if (!this.state.fieldValid[keys[i]]) { @@ -209,7 +206,7 @@ export default createReactClass({ } } return true; - }, + } findFirstInvalidField(fieldIDs) { for (const fieldID of fieldIDs) { @@ -218,34 +215,34 @@ export default createReactClass({ } } return null; - }, + } - markFieldValid: function(fieldID, valid) { + markFieldValid(fieldID, valid) { const { fieldValid } = this.state; fieldValid[fieldID] = valid; this.setState({ fieldValid, }); - }, + } - onEmailChange(ev) { + onEmailChange = ev => { this.setState({ email: ev.target.value, }); - }, + }; - async onEmailValidate(fieldState) { + onEmailValidate = async fieldState => { const result = await this.validateEmailRules(fieldState); this.markFieldValid(FIELD_EMAIL, result.valid); return result; - }, + }; - validateEmailRules: withValidation({ + validateEmailRules = withValidation({ description: () => _t("Use an email address to recover your account"), rules: [ { key: "required", - test: function({ value, allowEmpty }) { + test({ value, allowEmpty }) { return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value; }, invalid: () => _t("Enter email address (required on this homeserver)"), @@ -256,31 +253,31 @@ export default createReactClass({ invalid: () => _t("Doesn't look like a valid email address"), }, ], - }), + }); - onPasswordChange(ev) { + onPasswordChange = ev => { this.setState({ password: ev.target.value, }); - }, + }; - onPasswordValidate(result) { + onPasswordValidate = result => { this.markFieldValid(FIELD_PASSWORD, result.valid); - }, + }; - onPasswordConfirmChange(ev) { + onPasswordConfirmChange = ev => { this.setState({ passwordConfirm: ev.target.value, }); - }, + }; - async onPasswordConfirmValidate(fieldState) { + onPasswordConfirmValidate = async fieldState => { const result = await this.validatePasswordConfirmRules(fieldState); this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid); return result; - }, + }; - validatePasswordConfirmRules: withValidation({ + validatePasswordConfirmRules = withValidation({ rules: [ { key: "required", @@ -289,39 +286,39 @@ export default createReactClass({ }, { key: "match", - test: function({ value }) { + test({ value }) { return !value || value === this.state.password; }, invalid: () => _t("Passwords don't match"), }, ], - }), + }); - onPhoneCountryChange(newVal) { + onPhoneCountryChange = newVal => { this.setState({ phoneCountry: newVal.iso2, phonePrefix: newVal.prefix, }); - }, + }; - onPhoneNumberChange(ev) { + onPhoneNumberChange = ev => { this.setState({ phoneNumber: ev.target.value, }); - }, + }; - async onPhoneNumberValidate(fieldState) { + onPhoneNumberValidate = async fieldState => { const result = await this.validatePhoneNumberRules(fieldState); this.markFieldValid(FIELD_PHONE_NUMBER, result.valid); return result; - }, + }; - validatePhoneNumberRules: withValidation({ + validatePhoneNumberRules = withValidation({ description: () => _t("Other users can invite you to rooms using your contact details"), rules: [ { key: "required", - test: function({ value, allowEmpty }) { + test({ value, allowEmpty }) { return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value; }, invalid: () => _t("Enter phone number (required on this homeserver)"), @@ -332,21 +329,21 @@ export default createReactClass({ invalid: () => _t("Doesn't look like a valid phone number"), }, ], - }), + }); - onUsernameChange(ev) { + onUsernameChange = ev => { this.setState({ username: ev.target.value, }); - }, + }; - async onUsernameValidate(fieldState) { + onUsernameValidate = async fieldState => { const result = await this.validateUsernameRules(fieldState); this.markFieldValid(FIELD_USERNAME, result.valid); return result; - }, + }; - validateUsernameRules: withValidation({ + validateUsernameRules = withValidation({ description: () => _t("Use lowercase letters, numbers, dashes and underscores only"), rules: [ { @@ -360,7 +357,7 @@ export default createReactClass({ invalid: () => _t("Some characters not allowed"), }, ], - }), + }); /** * A step is required if all flows include that step. @@ -372,7 +369,7 @@ export default createReactClass({ return this.props.flows.every((flow) => { return flow.stages.includes(step); }); - }, + } /** * A step is used if any flows include that step. @@ -384,7 +381,7 @@ export default createReactClass({ return this.props.flows.some((flow) => { return flow.stages.includes(step); }); - }, + } _showEmail() { const haveIs = Boolean(this.props.serverConfig.isUrl); @@ -395,7 +392,7 @@ export default createReactClass({ return false; } return true; - }, + } _showPhoneNumber() { const threePidLogin = !SdkConfig.get().disable_3pid_login; @@ -408,7 +405,7 @@ export default createReactClass({ return false; } return true; - }, + } renderEmail() { if (!this._showEmail()) { @@ -426,7 +423,7 @@ export default createReactClass({ onChange={this.onEmailChange} onValidate={this.onEmailValidate} />; - }, + } renderPassword() { return ; - }, + } renderPasswordConfirm() { const Field = sdk.getComponent('elements.Field'); @@ -451,7 +448,7 @@ export default createReactClass({ onChange={this.onPasswordConfirmChange} onValidate={this.onPasswordConfirmValidate} />; - }, + } renderPhoneNumber() { if (!this._showPhoneNumber()) { @@ -477,7 +474,7 @@ export default createReactClass({ onChange={this.onPhoneNumberChange} onValidate={this.onPhoneNumberValidate} />; - }, + } renderUsername() { const Field = sdk.getComponent('elements.Field'); @@ -491,9 +488,9 @@ export default createReactClass({ onChange={this.onUsernameChange} onValidate={this.onUsernameValidate} />; - }, + } - render: function() { + render() { let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { serverName: this.props.serverConfig.hsName, }); @@ -578,5 +575,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 4c6fde19eb..245c50576a 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useState} from 'react'; import classNames from 'classnames'; import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; @@ -42,34 +42,35 @@ interface IProps { className?: string; } +const calculateUrls = (url, urls) => { + // work out the full set of urls to try to load. This is formed like so: + // imageUrls: [ props.url, ...props.urls ] + + let _urls = []; + if (!SettingsStore.getValue("lowBandwidth")) { + _urls = urls || []; + + if (url) { + _urls.unshift(url); // put in urls[0] + } + } + + // deduplicate URLs + return Array.from(new Set(_urls)); +}; + const useImageUrl = ({url, urls}): [string, () => void] => { - const [imageUrls, setUrls] = useState([]); - const [urlsIndex, setIndex] = useState(); + const [imageUrls, setUrls] = useState(calculateUrls(url, urls)); + const [urlsIndex, setIndex] = useState(0); const onError = useCallback(() => { setIndex(i => i + 1); // try the next one }, []); - const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { - // work out the full set of urls to try to load. This is formed like so: - // imageUrls: [ props.url, ...props.urls ] - - let _urls = []; - if (!SettingsStore.getValue("lowBandwidth")) { - _urls = memoizedUrls || []; - - if (url) { - _urls.unshift(url); // put in urls[0] - } - } - - // deduplicate URLs - _urls = Array.from(new Set(_urls)); - + setUrls(calculateUrls(url, urls)); setIndex(0); - setUrls(_urls); - }, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps + }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps const cli = useContext(MatrixClientContext); const onClientSync = useCallback((syncState, prevState) => { @@ -95,7 +96,7 @@ const BaseAvatar = (props: IProps) => { urls, width = 40, height = 40, - resizeMethod = "crop", // eslint-disable-line no-unused-vars + resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars defaultToInitialLetter = true, onClick, inputRef, diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index daf28400f2..d7e012467b 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -14,15 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; +import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; +import { User } from "matrix-js-sdk/src/models/user"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { TagID } from '../../../stores/room-list/models'; import RoomAvatar from "./RoomAvatar"; -import RoomTileIcon from "../rooms/RoomTileIcon"; import NotificationBadge from '../rooms/NotificationBadge'; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; +import {isPresenceEnabled} from "../../../utils/presence"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import {_t} from "../../../languageHandler"; +import TextWithTooltip from "../elements/TextWithTooltip"; +import DMRoomMap from "../../../utils/DMRoomMap"; interface IProps { room: Room; @@ -36,18 +43,134 @@ interface IProps { interface IState { notificationState?: NotificationState; + icon: Icon; +} + +enum Icon { + // Note: the names here are used in CSS class names + None = "NONE", // ... except this one + Globe = "GLOBE", + PresenceOnline = "ONLINE", + PresenceAway = "AWAY", + PresenceOffline = "OFFLINE", +} + +function tooltipText(variant: Icon) { + switch (variant) { + case Icon.Globe: + return _t("This room is public"); + case Icon.PresenceOnline: + return _t("Online"); + case Icon.PresenceAway: + return _t("Away"); + case Icon.PresenceOffline: + return _t("Offline"); + } } export default class DecoratedRoomAvatar extends React.PureComponent { + private _dmUser: User; + private isUnmounted = false; + private isWatchingTimeline = false; constructor(props: IProps) { super(props); this.state = { notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room), + icon: this.calculateIcon(), }; } + public componentWillUnmount() { + this.isUnmounted = true; + if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline); + this.dmUser = null; // clear listeners, if any + } + + private get isPublicRoom(): boolean { + const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", ""); + const joinRule = joinRules && joinRules.getContent().join_rule; + return joinRule === 'public'; + } + + private get dmUser(): User { + return this._dmUser; + } + + private set dmUser(val: User) { + const oldUser = this._dmUser; + this._dmUser = val; + if (oldUser && oldUser !== this._dmUser) { + oldUser.off('User.currentlyActive', this.onPresenceUpdate); + oldUser.off('User.presence', this.onPresenceUpdate); + } + if (this._dmUser && oldUser !== this._dmUser) { + this._dmUser.on('User.currentlyActive', this.onPresenceUpdate); + this._dmUser.on('User.presence', this.onPresenceUpdate); + } + } + + private onRoomTimeline = (ev: MatrixEvent, room: Room) => { + if (this.isUnmounted) return; + + // apparently these can happen? + if (!room) return; + if (this.props.room.roomId !== room.roomId) return; + + if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') { + this.setState({icon: this.calculateIcon()}); + } + }; + + private onPresenceUpdate = () => { + if (this.isUnmounted) return; + + const newIcon = this.getPresenceIcon(); + if (newIcon !== this.state.icon) this.setState({icon: newIcon}); + }; + + private getPresenceIcon(): Icon { + if (!this.dmUser) return Icon.None; + + let icon = Icon.None; + + const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online'; + if (isOnline) { + icon = Icon.PresenceOnline; + } else if (this.dmUser.presence === 'offline') { + icon = Icon.PresenceOffline; + } else if (this.dmUser.presence === 'unavailable') { + icon = Icon.PresenceAway; + } + + return icon; + } + + private calculateIcon(): Icon { + let icon = Icon.None; + + // We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); + if (otherUserId && this.props.room.getJoinedMemberCount() === 2) { + // Track presence, if available + if (isPresenceEnabled()) { + if (otherUserId) { + this.dmUser = MatrixClientPeg.get().getUser(otherUserId); + icon = this.getPresenceIcon(); + } + } + } else { + // Track publicity + icon = this.isPublicRoom ? Icon.Globe : Icon.None; + if (!this.isWatchingTimeline) { + this.props.room.on('Room.timeline', this.onRoomTimeline); + this.isWatchingTimeline = true; + } + } + return icon; + } + public render(): React.ReactNode { let badge: React.ReactNode; if (this.props.displayBadge) { @@ -58,7 +181,19 @@ export default class DecoratedRoomAvatar extends React.PureComponent; } - return
+ let icon; + if (this.state.icon !== Icon.None) { + icon = ; + } + + const classes = classNames("mx_DecoratedRoomAvatar", { + mx_DecoratedRoomAvatar_cutout: icon, + }); + + return
- + {icon} {badge}
; } diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index e55e2e6fac..51327605c0 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -47,7 +47,7 @@ export default class GroupAvatar extends React.Component { render() { // extract the props we use from props so we can pass any others through // should consider adding this as a global rule in js-sdk? - /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ + /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props; return ( diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index eef3f86d9a..d5d927106c 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -53,7 +53,7 @@ export default class MemberStatusMessageAvatar extends React.Component { if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); } - if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { + if (!SettingsStore.getValue("feature_custom_status")) { return; } const { user } = this.props.member; @@ -105,7 +105,7 @@ export default class MemberStatusMessageAvatar extends React.Component { resizeMethod={this.props.resizeMethod} />; - if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { + if (!SettingsStore.getValue("feature_custom_status")) { return avatar; } diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx index 94a6c87687..b4e876b9f6 100644 --- a/src/components/views/avatars/PulsedAvatar.tsx +++ b/src/components/views/avatars/PulsedAvatar.tsx @@ -25,4 +25,4 @@ const PulsedAvatar: React.FC = (props) => {
; }; -export default PulsedAvatar; \ No newline at end of file +export default PulsedAvatar; diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 3317ed3a60..e37dff4bfe 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -114,9 +114,12 @@ export default class RoomAvatar extends React.Component { } private onRoomAvatarClick = () => { - const avatarUrl = this.props.room.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - null, null, null, false); + const avatarUrl = Avatar.avatarUrlForRoom( + this.props.room, + null, + null, + null, + ); const params = { src: avatarUrl, name: this.props.room.name, diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx new file mode 100644 index 0000000000..b3ca9fde6f --- /dev/null +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -0,0 +1,124 @@ +/* +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 classNames from "classnames"; + +import { + ChevronFace, + ContextMenu, + IProps as IContextMenuProps, + MenuItem, + MenuItemCheckbox, MenuItemRadio, +} from "../../structures/ContextMenu"; + +interface IProps extends IContextMenuProps { + className?: string; + compact?: boolean; +} + +interface IOptionListProps { + first?: boolean; + red?: boolean; + className?: string; +} + +interface IOptionProps extends React.ComponentProps { + iconClassName: string; +} + +interface ICheckboxProps extends React.ComponentProps { + iconClassName: string; +} + +interface IRadioProps extends React.ComponentProps { + iconClassName: string; +} + +export const IconizedContextMenuRadio: React.FC = ({ + label, + iconClassName, + active, + className, + ...props +}) => { + return + + {label} + {active && } + ; +}; + +export const IconizedContextMenuCheckbox: React.FC = ({ + label, + iconClassName, + active, + className, + ...props +}) => { + return + + {label} + {active && } + ; +}; + +export const IconizedContextMenuOption: React.FC = ({label, iconClassName, ...props}) => { + return + + {label} + ; +}; + +export const IconizedContextMenuOptionList: React.FC = ({first, red, className, children}) => { + const classes = classNames("mx_IconizedContextMenu_optionList", className, { + mx_IconizedContextMenu_optionList_notFirst: !first, + mx_IconizedContextMenu_optionList_red: red, + }); + + return
+ {children} +
; +}; + +const IconizedContextMenu: React.FC = ({className, children, compact, ...props}) => { + const classes = classNames("mx_IconizedContextMenu", className, { + mx_IconizedContextMenu_compact: compact, + }); + + return +
+ { children } +
+
; +}; + +export default IconizedContextMenu; + diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 59e3d4c230..d760c8defa 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import {EventStatus} from 'matrix-js-sdk'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; @@ -37,10 +36,8 @@ function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -export default createReactClass({ - displayName: 'MessageContextMenu', - - propTypes: { +export default class MessageContextMenu extends React.Component { + static propTypes = { /* the MatrixEvent associated with the context menu */ mxEvent: PropTypes.object.isRequired, @@ -52,28 +49,26 @@ export default createReactClass({ /* callback called when the menu is dismissed */ onFinished: PropTypes.func, - }, + }; - getInitialState: function() { - return { - canRedact: false, - canPin: false, - }; - }, + state = { + canRedact: false, + canPin: false, + }; - componentDidMount: function() { + componentDidMount() { MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); this._checkPermissions(); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener('RoomMember.powerLevel', this._checkPermissions); } - }, + } - _checkPermissions: function() { + _checkPermissions = () => { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); @@ -81,50 +76,50 @@ export default createReactClass({ let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality - if (!SettingsStore.isFeatureEnabled("feature_pinning")) canPin = false; + if (!SettingsStore.getValue("feature_pinning")) canPin = false; this.setState({canRedact, canPin}); - }, + }; - _isPinned: function() { + _isPinned() { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', ''); if (!pinnedEvent) return false; const content = pinnedEvent.getContent(); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); - }, + } - onResendClick: function() { + onResendClick = () => { Resend.resend(this.props.mxEvent); this.closeMenu(); - }, + }; - onResendEditClick: function() { + onResendEditClick = () => { Resend.resend(this.props.mxEvent.replacingEvent()); this.closeMenu(); - }, + }; - onResendRedactionClick: function() { + onResendRedactionClick = () => { Resend.resend(this.props.mxEvent.localRedactionEvent()); this.closeMenu(); - }, + }; - onResendReactionsClick: function() { + onResendReactionsClick = () => { for (const reaction of this._getUnsentReactions()) { Resend.resend(reaction); } this.closeMenu(); - }, + }; - onReportEventClick: function() { + onReportEventClick = () => { const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog"); Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { mxEvent: this.props.mxEvent, }, 'mx_Dialog_reportEvent'); this.closeMenu(); - }, + }; - onViewSourceClick: function() { + onViewSourceClick = () => { const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', '', ViewSource, { @@ -133,9 +128,9 @@ export default createReactClass({ content: ev.event, }, 'mx_Dialog_viewsource'); this.closeMenu(); - }, + }; - onViewClearSourceClick: function() { + onViewClearSourceClick = () => { const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, { @@ -145,9 +140,9 @@ export default createReactClass({ content: ev._clearEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); - }, + }; - onRedactClick: function() { + onRedactClick = () => { const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog"); Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { onFinished: async (proceed) => { @@ -176,9 +171,9 @@ export default createReactClass({ }, }, 'mx_Dialog_confirmredact'); this.closeMenu(); - }, + }; - onCancelSendClick: function() { + onCancelSendClick = () => { const mxEvent = this.props.mxEvent; const editEvent = mxEvent.replacingEvent(); const redactEvent = mxEvent.localRedactionEvent(); @@ -199,17 +194,17 @@ export default createReactClass({ Resend.removeFromQueue(this.props.mxEvent); } this.closeMenu(); - }, + }; - onForwardClick: function() { + onForwardClick = () => { dis.dispatch({ action: 'forward_event', event: this.props.mxEvent, }); this.closeMenu(); - }, + }; - onPinClick: function() { + onPinClick = () => { MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '') .catch((e) => { // Intercept the Event Not Found error and fall through the promise chain with no event. @@ -230,28 +225,28 @@ export default createReactClass({ cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, ''); }); this.closeMenu(); - }, + }; - closeMenu: function() { + closeMenu = () => { if (this.props.onFinished) this.props.onFinished(); - }, + }; - onUnhidePreviewClick: function() { + onUnhidePreviewClick = () => { if (this.props.eventTileOps) { this.props.eventTileOps.unhideWidget(); } this.closeMenu(); - }, + }; - onQuoteClick: function() { + onQuoteClick = () => { dis.dispatch({ action: 'quote', event: this.props.mxEvent, }); this.closeMenu(); - }, + }; - onPermalinkClick: function(e: Event) { + onPermalinkClick = (e: Event) => { e.preventDefault(); const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { @@ -259,12 +254,12 @@ export default createReactClass({ permalinkCreator: this.props.permalinkCreator, }); this.closeMenu(); - }, + }; - onCollapseReplyThreadClick: function() { + onCollapseReplyThreadClick = () => { this.props.collapseReplyThread(); this.closeMenu(); - }, + }; _getReactions(filter) { const cli = MatrixClientPeg.get(); @@ -277,17 +272,17 @@ export default createReactClass({ relation.event_id === eventId && filter(e); }); - }, + } _getPendingReactions() { return this._getReactions(e => canCancel(e.status)); - }, + } _getUnsentReactions() { return this._getReactions(e => e.status === EventStatus.NOT_SENT); - }, + } - render: function() { + render() { const cli = MatrixClientPeg.get(); const me = cli.getUserId(); const mxEvent = this.props.mxEvent; @@ -489,5 +484,5 @@ export default createReactClass({ { reportEventButton } ); - }, -}); + } +} diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js deleted file mode 100644 index b08cf3be60..0000000000 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ /dev/null @@ -1,404 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations 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. -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 createReactClass from 'create-react-class'; -import classNames from 'classnames'; -import * as sdk from '../../../index'; -import { _t, _td } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher/dispatcher'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import * as Rooms from '../../../Rooms'; -import * as RoomNotifs from '../../../RoomNotifs'; -import Modal from '../../../Modal'; -import RoomListActions from '../../../actions/RoomListActions'; -import RoomViewStore from '../../../stores/RoomViewStore'; -import {sleep} from "../../../utils/promise"; -import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextMenu"; - -const RoomTagOption = ({active, onClick, src, srcSet, label}) => { - const classes = classNames('mx_RoomTileContextMenu_tag_field', { - 'mx_RoomTileContextMenu_tag_fieldSet': active, - 'mx_RoomTileContextMenu_tag_fieldDisabled': false, - }); - - return ( - - - - { label } - - ); -}; - -const NotifOption = ({active, onClick, src, label}) => { - const classes = classNames('mx_RoomTileContextMenu_notif_field', { - 'mx_RoomTileContextMenu_notif_fieldSet': active, - }); - - return ( - - - - { label } - - ); -}; - -export default createReactClass({ - displayName: 'RoomTileContextMenu', - - propTypes: { - room: PropTypes.object.isRequired, - /* callback called when the menu is dismissed */ - onFinished: PropTypes.func, - }, - - getInitialState() { - const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); - return { - roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), - isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"), - isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"), - isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)), - }; - }, - - componentDidMount: function() { - this._unmounted = false; - }, - - componentWillUnmount: function() { - this._unmounted = true; - }, - - _toggleTag: function(tagNameOn, tagNameOff) { - if (!MatrixClientPeg.get().isGuest()) { - sleep(500).then(() => { - dis.dispatch(RoomListActions.tagRoom( - MatrixClientPeg.get(), - this.props.room, - tagNameOff, tagNameOn, - undefined, 0, - ), true); - - this.props.onFinished(); - }); - } - }, - - _onClickFavourite: function() { - // Tag room as 'Favourite' - if (!this.state.isFavourite && this.state.isLowPriority) { - this.setState({ - isFavourite: true, - isLowPriority: false, - }); - this._toggleTag("m.favourite", "m.lowpriority"); - } else if (this.state.isFavourite) { - this.setState({isFavourite: false}); - this._toggleTag(null, "m.favourite"); - } else if (!this.state.isFavourite) { - this.setState({isFavourite: true}); - this._toggleTag("m.favourite"); - } - }, - - _onClickLowPriority: function() { - // Tag room as 'Low Priority' - if (!this.state.isLowPriority && this.state.isFavourite) { - this.setState({ - isFavourite: false, - isLowPriority: true, - }); - this._toggleTag("m.lowpriority", "m.favourite"); - } else if (this.state.isLowPriority) { - this.setState({isLowPriority: false}); - this._toggleTag(null, "m.lowpriority"); - } else if (!this.state.isLowPriority) { - this.setState({isLowPriority: true}); - this._toggleTag("m.lowpriority"); - } - }, - - _onClickDM: function() { - if (MatrixClientPeg.get().isGuest()) return; - - const newIsDirectMessage = !this.state.isDirectMessage; - this.setState({ - isDirectMessage: newIsDirectMessage, - }); - - Rooms.guessAndSetDMRoom( - this.props.room, newIsDirectMessage, - ).then(sleep(500)).finally(() => { - // Close the context menu - if (this.props.onFinished) { - this.props.onFinished(); - } - }, (err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to set Direct Message status of room', '', ErrorDialog, { - title: _t('Failed to set Direct Message status of room'), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - }, - - _onClickLeave: function() { - // Leave room - dis.dispatch({ - action: 'leave_room', - room_id: this.props.room.roomId, - }); - - // Close the context menu - if (this.props.onFinished) { - this.props.onFinished(); - } - }, - - _onClickReject: function() { - dis.dispatch({ - action: 'reject_invite', - room_id: this.props.room.roomId, - }); - - // Close the context menu - if (this.props.onFinished) { - this.props.onFinished(); - } - }, - - _onClickForget: function() { - // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { - // Switch to another room view if we're currently viewing the - // historical room - if (RoomViewStore.getRoomId() === this.props.room.roomId) { - dis.dispatch({ action: 'view_next_room' }); - } - }, function(err) { - const errCode = err.errcode || _td("unknown error code"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, { - title: _t('Failed to forget room %(errCode)s', {errCode: errCode}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - - // Close the context menu - if (this.props.onFinished) { - this.props.onFinished(); - } - }, - - _saveNotifState: function(newState) { - if (MatrixClientPeg.get().isGuest()) return; - - const oldState = this.state.roomNotifState; - const roomId = this.props.room.roomId; - - this.setState({ - roomNotifState: newState, - }); - RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { - // delay slightly so that the user can see their state change - // before closing the menu - return sleep(500).then(() => { - if (this._unmounted) return; - // Close the context menu - if (this.props.onFinished) { - this.props.onFinished(); - } - }); - }, (error) => { - // TODO: some form of error notification to the user - // to inform them that their state change failed. - // For now we at least set the state back - if (this._unmounted) return; - this.setState({ - roomNotifState: oldState, - }); - }); - }, - - _onClickAlertMe: function() { - this._saveNotifState(RoomNotifs.ALL_MESSAGES_LOUD); - }, - - _onClickAllNotifs: function() { - this._saveNotifState(RoomNotifs.ALL_MESSAGES); - }, - - _onClickMentions: function() { - this._saveNotifState(RoomNotifs.MENTIONS_ONLY); - }, - - _onClickMute: function() { - this._saveNotifState(RoomNotifs.MUTE); - }, - - _renderNotifMenu: function() { - return ( -
-
- -
- - - - - -
- ); - }, - - _onClickSettings: function() { - dis.dispatch({ - action: 'open_room_settings', - room_id: this.props.room.roomId, - }); - if (this.props.onFinished) { - this.props.onFinished(); - } - }, - - _renderSettingsMenu: function() { - return ( -
- - - { _t('Settings') } - -
- ); - }, - - _renderLeaveMenu: function(membership) { - if (!membership) { - return null; - } - - let leaveClickHandler = null; - let leaveText = null; - - switch (membership) { - case "join": - leaveClickHandler = this._onClickLeave; - leaveText = _t('Leave'); - break; - case "leave": - case "ban": - leaveClickHandler = this._onClickForget; - leaveText = _t('Forget'); - break; - case "invite": - leaveClickHandler = this._onClickReject; - leaveText = _t('Reject'); - break; - } - - return ( -
- - - { leaveText } - -
- ); - }, - - _renderRoomTagMenu: function() { - return ( -
- - - -
- ); - }, - - render: function() { - const myMembership = this.props.room.getMyMembership(); - - switch (myMembership) { - case 'join': - return
- { this._renderNotifMenu() } -
- { this._renderLeaveMenu(myMembership) } -
- { this._renderRoomTagMenu() } -
- { this._renderSettingsMenu() } -
; - case 'invite': - return
- { this._renderLeaveMenu(myMembership) } -
; - default: - return
- { this._renderLeaveMenu(myMembership) } -
- { this._renderSettingsMenu() } -
; - } - }, -}); diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js deleted file mode 100644 index ec99c63724..0000000000 --- a/src/components/views/context_menus/TopLeftMenu.js +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2018, 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. -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 dis from '../../../dispatcher/dispatcher'; -import { _t } from '../../../languageHandler'; -import LogoutDialog from "../dialogs/LogoutDialog"; -import Modal from "../../../Modal"; -import SdkConfig from '../../../SdkConfig'; -import { getHostingLink } from '../../../utils/HostingLink'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {MenuItem} from "../../structures/ContextMenu"; -import * as sdk from "../../../index"; -import {getHomePageUrl} from "../../../utils/pages"; -import {Action} from "../../../dispatcher/actions"; - -export default class TopLeftMenu extends React.Component { - static propTypes = { - displayName: PropTypes.string.isRequired, - userId: PropTypes.string.isRequired, - onFinished: PropTypes.func, - - // Optional function to collect a reference to the container - // of this component directly. - containerRef: PropTypes.func, - }; - - constructor() { - super(); - this.viewHomePage = this.viewHomePage.bind(this); - this.openSettings = this.openSettings.bind(this); - this.signIn = this.signIn.bind(this); - this.signOut = this.signOut.bind(this); - } - - hasHomePage() { - return !!getHomePageUrl(SdkConfig.get()); - } - - render() { - const isGuest = MatrixClientPeg.get().isGuest(); - - const hostingSignupLink = getHostingLink('user-context-menu'); - let hostingSignup = null; - if (hostingSignupLink) { - hostingSignup =
- {_t( - "Upgrade to your own domain", {}, - { - a: sub => - {sub}, - }, - )} - - - -
; - } - - let homePageItem = null; - if (this.hasHomePage()) { - homePageItem = ( - - {_t("Home")} - - ); - } - - let signInOutItem; - if (isGuest) { - signInOutItem = ( - - {_t("Sign in")} - - ); - } else { - signInOutItem = ( - - {_t("Sign out")} - - ); - } - - const helpItem = ( - - {_t("Help")} - - ); - - const settingsItem = ( - - {_t("Settings")} - - ); - - return
-
-
{this.props.displayName}
-
{this.props.userId}
- {hostingSignup} -
-
    - {homePageItem} - {settingsItem} - {helpItem} - {signInOutItem} -
-
; - } - - openHelp = () => { - this.closeMenu(); - const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); - Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); - }; - - viewHomePage() { - dis.dispatch({action: 'view_home_page'}); - this.closeMenu(); - } - - openSettings() { - dis.fire(Action.ViewUserSettings); - this.closeMenu(); - } - - signIn() { - dis.dispatch({action: 'start_login'}); - this.closeMenu(); - } - - signOut() { - Modal.createTrackedDialog('Logout E2E Export', '', LogoutDialog); - this.closeMenu(); - } - - closeMenu() { - if (this.props.onFinished) this.props.onFinished(); - } -} diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js deleted file mode 100644 index 0f18d11511..0000000000 --- a/src/components/views/create_room/Presets.js +++ /dev/null @@ -1,57 +0,0 @@ -/* -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. -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 createReactClass from 'create-react-class'; -import { _t } from '../../../languageHandler'; - -const Presets = { - PrivateChat: "private_chat", - PublicChat: "public_chat", - Custom: "custom", -}; - -export default createReactClass({ - displayName: 'CreateRoomPresets', - propTypes: { - onChange: PropTypes.func, - preset: PropTypes.string, - }, - - Presets: Presets, - - getDefaultProps: function() { - return { - onChange: function() {}, - }; - }, - - onValueChanged: function(ev) { - this.props.onChange(ev.target.value); - }, - - render: function() { - return ( - - ); - }, -}); diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js deleted file mode 100644 index 5bdfdde08d..0000000000 --- a/src/components/views/create_room/RoomAlias.js +++ /dev/null @@ -1,106 +0,0 @@ -/* -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. -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 createReactClass from 'create-react-class'; -import { _t } from '../../../languageHandler'; - -export default createReactClass({ - displayName: 'RoomAlias', - propTypes: { - // Specifying a homeserver will make magical things happen when you, - // e.g. start typing in the room alias box. - homeserver: PropTypes.string, - alias: PropTypes.string, - onChange: PropTypes.func, - }, - - getDefaultProps: function() { - return { - onChange: function() {}, - alias: '', - }; - }, - - getAliasLocalpart: function() { - let room_alias = this.props.alias; - - if (room_alias && this.props.homeserver) { - const suffix = ":" + this.props.homeserver; - if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) { - room_alias = room_alias.slice(1, -suffix.length); - } - } - - return room_alias; - }, - - onValueChanged: function(ev) { - this.props.onChange(ev.target.value); - }, - - onFocus: function(ev) { - const target = ev.target; - const curr_val = ev.target.value; - - if (this.props.homeserver) { - if (curr_val == "") { - const self = this; - setTimeout(function() { - target.value = "#:" + self.props.homeserver; - target.setSelectionRange(1, 1); - }, 0); - } else { - const suffix = ":" + this.props.homeserver; - setTimeout(function() { - target.setSelectionRange( - curr_val.startsWith("#") ? 1 : 0, - curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length, - ); - }, 0); - } - } - }, - - onBlur: function(ev) { - const curr_val = ev.target.value; - - if (this.props.homeserver) { - if (curr_val == "#:" + this.props.homeserver) { - ev.target.value = ""; - return; - } - - if (curr_val != "") { - let new_val = ev.target.value; - const suffix = ":" + this.props.homeserver; - if (!curr_val.startsWith("#")) new_val = "#" + new_val; - if (!curr_val.endsWith(suffix)) new_val = new_val + suffix; - ev.target.value = new_val; - } - } - }, - - render: function() { - return ( - - ); - }, -}); diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 8ddd89dc65..2cd09874b2 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -19,7 +19,6 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; import * as sdk from '../../../index'; @@ -45,10 +44,8 @@ const addressTypeName = { }; -export default createReactClass({ - displayName: "AddressPickerDialog", - - propTypes: { +export default class AddressPickerDialog extends React.Component { + static propTypes = { title: PropTypes.string.isRequired, description: PropTypes.node, // Extra node inserted after picker input, dropdown and errors @@ -66,26 +63,28 @@ export default createReactClass({ // Whether the current user should be included in the addresses returned. Only // applicable when pickerType is `user`. Default: false. includeSelf: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - value: "", - focus: true, - validAddressTypes: addressTypes, - pickerType: 'user', - includeSelf: false, - }; - }, + static defaultProps = { + value: "", + focus: true, + validAddressTypes: addressTypes, + pickerType: 'user', + includeSelf: false, + }; + + constructor(props) { + super(props); + + this._textinput = createRef(); - getInitialState: function() { let validAddressTypes = this.props.validAddressTypes; // Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) { validAddressTypes = validAddressTypes.filter(type => type !== "email"); } - return { + this.state = { // Whether to show an error message because of an invalid address invalidAddressError: false, // List of UserAddressType objects representing @@ -106,19 +105,14 @@ export default createReactClass({ // dialog is open and represents the supported list of address types at this time. validAddressTypes, }; - }, + } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { - this._textinput = createRef(); - }, - - componentDidMount: function() { + componentDidMount() { if (this.props.focus) { // Set the cursor at the end of the text input this._textinput.current.value = this.props.value; } - }, + } getPlaceholder() { const { placeholder } = this.props; @@ -127,9 +121,9 @@ export default createReactClass({ } // Otherwise it's a function, as checked by prop types. return placeholder(this.state.validAddressTypes); - }, + } - onButtonClick: function() { + onButtonClick = () => { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local selectedList @@ -138,13 +132,13 @@ export default createReactClass({ if (selectedList === null) return; } this.props.onFinished(true, selectedList); - }, + }; - onCancel: function() { + onCancel = () => { this.props.onFinished(false); - }, + }; - onKeyDown: function(e) { + onKeyDown = e => { const textInput = this._textinput.current ? this._textinput.current.value : undefined; if (e.key === Key.ESCAPE) { @@ -181,9 +175,9 @@ export default createReactClass({ e.preventDefault(); this._addAddressesToList([textInput]); } - }, + }; - onQueryChanged: function(ev) { + onQueryChanged = ev => { const query = ev.target.value; if (this.queryChangedDebouncer) { clearTimeout(this.queryChangedDebouncer); @@ -216,28 +210,24 @@ export default createReactClass({ searchError: null, }); } - }, + }; - onDismissed: function(index) { - return () => { - const selectedList = this.state.selectedList.slice(); - selectedList.splice(index, 1); - this.setState({ - selectedList, - suggestedList: [], - query: "", - }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - }; - }, + onDismissed = index => () => { + const selectedList = this.state.selectedList.slice(); + selectedList.splice(index, 1); + this.setState({ + selectedList, + suggestedList: [], + query: "", + }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + }; - onClick: function(index) { - return () => { - this.onSelected(index); - }; - }, + onClick = index => () => { + this.onSelected(index); + }; - onSelected: function(index) { + onSelected = index => { const selectedList = this.state.selectedList.slice(); selectedList.push(this._getFilteredSuggestions()[index]); this.setState({ @@ -246,9 +236,9 @@ export default createReactClass({ query: "", }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - }, + }; - _doNaiveGroupSearch: function(query) { + _doNaiveGroupSearch(query) { const lowerCaseQuery = query.toLowerCase(); this.setState({ busy: true, @@ -280,9 +270,9 @@ export default createReactClass({ busy: false, }); }); - }, + } - _doNaiveGroupRoomSearch: function(query) { + _doNaiveGroupRoomSearch(query) { const lowerCaseQuery = query.toLowerCase(); const results = []; GroupStore.getGroupRooms(this.props.groupId).forEach((r) => { @@ -302,9 +292,9 @@ export default createReactClass({ this.setState({ busy: false, }); - }, + } - _doRoomSearch: function(query) { + _doRoomSearch(query) { const lowerCaseQuery = query.toLowerCase(); const rooms = MatrixClientPeg.get().getRooms(); const results = []; @@ -359,9 +349,9 @@ export default createReactClass({ this.setState({ busy: false, }); - }, + } - _doUserDirectorySearch: function(query) { + _doUserDirectorySearch(query) { this.setState({ busy: true, query, @@ -393,9 +383,9 @@ export default createReactClass({ busy: false, }); }); - }, + } - _doLocalSearch: function(query) { + _doLocalSearch(query) { this.setState({ query, searchError: null, @@ -417,9 +407,9 @@ export default createReactClass({ }); }); this._processResults(results, query); - }, + } - _processResults: function(results, query) { + _processResults(results, query) { const suggestedList = []; results.forEach((result) => { if (result.room_id) { @@ -485,9 +475,9 @@ export default createReactClass({ }, () => { if (this.addressSelector) this.addressSelector.moveSelectionTop(); }); - }, + } - _addAddressesToList: function(addressTexts) { + _addAddressesToList(addressTexts) { const selectedList = this.state.selectedList.slice(); let hasError = false; @@ -529,9 +519,9 @@ export default createReactClass({ }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); return hasError ? null : selectedList; - }, + } - _lookupThreepid: async function(medium, address) { + async _lookupThreepid(medium, address) { let cancelled = false; // Note that we can't safely remove this after we're done // because we don't know that it's the same one, so we just @@ -577,9 +567,9 @@ export default createReactClass({ searchError: _t('Something went wrong!'), }); } - }, + } - _getFilteredSuggestions: function() { + _getFilteredSuggestions() { // map addressType => set of addresses to avoid O(n*m) operation const selectedAddresses = {}; this.state.selectedList.forEach(({address, addressType}) => { @@ -591,17 +581,17 @@ export default createReactClass({ return this.state.suggestedList.filter(({address, addressType}) => { return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address)); }); - }, + } - _onPaste: function(e) { + _onPaste = e => { // Prevent the text being pasted into the textarea e.preventDefault(); const text = e.clipboardData.getData("text"); // Process it as a list of addresses to add instead this._addAddressesToList(text.split(/[\s,]+/)); - }, + }; - onUseDefaultIdentityServerClick(e) { + onUseDefaultIdentityServerClick = e => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -612,15 +602,15 @@ export default createReactClass({ const { validAddressTypes } = this.state; validAddressTypes.push('email'); this.setState({ validAddressTypes }); - }, + }; - onManageSettingsClick(e) { + onManageSettingsClick = e => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.onCancel(); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AddressSelector = sdk.getComponent("elements.AddressSelector"); @@ -738,5 +728,5 @@ export default createReactClass({ onCancel={this.onCancel} /> ); - }, -}); + } +} diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js index 7a12d2bd20..c69400977a 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -16,37 +16,36 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import {SettingLevel} from "../../../settings/SettingLevel"; -export default createReactClass({ - propTypes: { +export default class AskInviteAnywayDialog extends React.Component { + static propTypes = { unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] onInviteAnyways: PropTypes.func.isRequired, onGiveUp: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired, - }, + }; - _onInviteClicked: function() { + _onInviteClicked = () => { this.props.onInviteAnyways(); this.props.onFinished(true); - }, + }; - _onInviteNeverWarnClicked: function() { + _onInviteNeverWarnClicked = () => { SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); this.props.onInviteAnyways(); this.props.onFinished(true); - }, + }; - _onGiveUpClicked: function() { + _onGiveUpClicked = () => { this.props.onGiveUp(); this.props.onFinished(false); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const errorList = this.props.unknownProfileUsers @@ -78,5 +77,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 353298032c..9ba5368ee5 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import FocusLock from 'react-focus-lock'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -28,16 +27,14 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -/** +/* * Basic container for modal dialogs. * * Includes a div for the title, and a keypress handler which cancels the * dialog on escape. */ -export default createReactClass({ - displayName: 'BaseDialog', - - propTypes: { +export default class BaseDialog extends React.Component { + static propTypes = { // onFinished callback to call when Escape is pressed // Take a boolean which is true if the dialog was dismissed // with a positive / confirm action or false if it was @@ -81,21 +78,20 @@ export default createReactClass({ PropTypes.object, PropTypes.arrayOf(PropTypes.string), ]), - }, + }; - getDefaultProps: function() { - return { - hasCancel: true, - fixedWidth: true, - }; - }, + static defaultProps = { + hasCancel: true, + fixedWidth: true, + }; + + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount() { this._matrixClient = MatrixClientPeg.get(); - }, + } - _onKeyDown: function(e) { + _onKeyDown = (e) => { if (this.props.onKeyDown) { this.props.onKeyDown(e); } @@ -104,13 +100,13 @@ export default createReactClass({ e.preventDefault(); this.props.onFinished(false); } - }, + }; - _onCancelClick: function(e) { + _onCancelClick = (e) => { this.props.onFinished(false); - }, + }; - render: function() { + render() { let cancelButton; if (this.props.hasCancel) { cancelButton = ( @@ -161,5 +157,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index 9bb716fe3f..d001d3993d 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -23,7 +23,8 @@ import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -import sendBugReport from '../../../rageshake/submit-rageshake'; +import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake'; +import AccessibleButton from "../elements/AccessibleButton"; export default class BugReportDialog extends React.Component { constructor(props) { @@ -35,6 +36,8 @@ export default class BugReportDialog extends React.Component { issueUrl: "", text: "", progress: null, + downloadBusy: false, + downloadProgress: null, }; this._unmounted = false; this._onSubmit = this._onSubmit.bind(this); @@ -43,6 +46,7 @@ export default class BugReportDialog extends React.Component { this._onIssueUrlChange = this._onIssueUrlChange.bind(this); this._onSendLogsChange = this._onSendLogsChange.bind(this); this._sendProgressCallback = this._sendProgressCallback.bind(this); + this._downloadProgressCallback = this._downloadProgressCallback.bind(this); } componentWillUnmount() { @@ -95,6 +99,31 @@ export default class BugReportDialog extends React.Component { }); } + _onDownload = async (ev) => { + this.setState({ downloadBusy: true }); + this._downloadProgressCallback(_t("Preparing to download logs")); + + try { + await downloadBugReport({ + sendLogs: true, + progressCallback: this._downloadProgressCallback, + label: this.props.label, + }); + + this.setState({ + downloadBusy: false, + downloadProgress: null, + }); + } catch (err) { + if (!this._unmounted) { + this.setState({ + downloadBusy: false, + downloadProgress: _t("Failed to send logs: ") + `${err.message}`, + }); + } + } + }; + _onTextChange(ev) { this.setState({ text: ev.target.value }); } @@ -114,6 +143,13 @@ export default class BugReportDialog extends React.Component { this.setState({progress: progress}); } + _downloadProgressCallback(downloadProgress) { + if (this._unmounted) { + return; + } + this.setState({ downloadProgress }); + } + render() { const Loader = sdk.getComponent("elements.Spinner"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); @@ -166,20 +202,28 @@ export default class BugReportDialog extends React.Component { { a: (sub) => { sub } , }, ) }

+ +
+ + { _t("Download logs") } + + {this.state.downloadProgress && {this.state.downloadProgress} ...} +
+ { + constructor(props: IProps) { + super(props); + + this.state = { + emailTargets: [], + userTargets: [], + showPeople: false, + people: this.buildSuggestions(), + numPeople: 5, // arbitrary default + busy: false, + }; + } + + private buildSuggestions(): IPerson[] { + const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]); + if (this.props.roomId) { + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (!room) throw new Error("Room ID given to InviteDialog does not look like a room"); + room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId)); + room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId)); + // add banned users, so we don't try to invite them + room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId)); + } + + return InviteDialog.buildRecents(alreadyInvited); + } + + private onSubmit = async (ev: FormEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true}); + try { + const targets = [...this.state.emailTargets, ...this.state.userTargets]; + const result = await inviteMultipleToRoom(this.props.roomId, targets); + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + const success = showAnyInviteErrors(result.states, room, result.inviter); + if (success) { + this.props.onFinished(true); + } else { + this.setState({busy: false}); + } + } catch (e) { + this.setState({busy: false}); + console.error(e); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((e && e.message) ? e.message : _t("Operation failed")), + }); + } + }; + + private onAddressChange = (ev: ChangeEvent, index: number) => { + const targets = arrayFastClone(this.state.emailTargets); + if (index >= targets.length) { + targets.push(ev.target.value); + } else { + targets[index] = ev.target.value; + } + this.setState({emailTargets: targets}); + }; + + private onAddressBlur = (index: number) => { + const targets = arrayFastClone(this.state.emailTargets); + if (index >= targets.length) return; // not important + if (targets[index].trim() === "") { + targets.splice(index, 1); + this.setState({emailTargets: targets}); + } + }; + + private onShowPeopleClick = () => { + this.setState({showPeople: !this.state.showPeople}); + }; + + private setPersonToggle = (person: IPerson, selected: boolean) => { + const targets = arrayFastClone(this.state.userTargets); + if (selected && !targets.includes(person.userId)) { + targets.push(person.userId); + } else if (!selected && targets.includes(person.userId)) { + targets.splice(targets.indexOf(person.userId), 1); + } + this.setState({userTargets: targets}); + }; + + private renderPerson(person: IPerson, key: any) { + const avatarSize = 36; + return ( +
+ +
+ {person.user.name} + {person.userId} +
+ this.setPersonToggle(person, e.target.checked)} /> +
+ ); + } + + private onShowMorePeople = () => { + this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase + }; + + public render() { + const emailAddresses = []; + this.state.emailTargets.forEach((address, i) => { + emailAddresses.push(( + this.onAddressChange(e, i)} + label={_t("Email address")} + placeholder={_t("Email address")} + onBlur={() => this.onAddressBlur(i)} + /> + )); + }); + + // Push a clean input + emailAddresses.push(( + this.onAddressChange(e, emailAddresses.length)} + label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} + placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} + /> + )); + + let peopleIntro = null; + const people = []; + if (this.state.showPeople) { + const humansToPresent = this.state.people.slice(0, this.state.numPeople); + humansToPresent.forEach((person, i) => { + people.push(this.renderPerson(person, i)); + }); + if (humansToPresent.length < this.state.people.length) { + people.push(( + {_t("Show more")} + )); + } + } + if (this.state.people.length > 0) { + peopleIntro = ( +
+ {_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})} + + {this.state.showPeople ? _t("Hide") : _t("Show")} + +
+ ); + } + + let buttonText = _t("Skip"); + const targetCount = this.state.userTargets.length + this.state.emailTargets.length; + if (targetCount > 0) { + buttonText = _t("Send %(count)s invites", {count: targetCount}); + } + + return ( + +
+
+ {emailAddresses} + {peopleIntro} + {people} + {buttonText} +
+
+
+ ); + } +} diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index 71139155ec..3106df1d5b 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -15,17 +15,14 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; /* * A dialog for confirming a redaction. */ -export default createReactClass({ - displayName: 'ConfirmRedactDialog', - - render: function() { +export default class ConfirmRedactDialog extends React.Component { + render() { const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); return ( ); - }, -}); + } +} diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 2495c46327..44f57f047e 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import * as sdk from '../../../index'; @@ -30,9 +29,8 @@ import { GroupMemberType } from '../../../groups'; * to make it obvious what is going to happen. * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ -export default createReactClass({ - displayName: 'ConfirmUserActionDialog', - propTypes: { +export default class ConfirmUserActionDialog extends React.Component { + static propTypes = { // matrix-js-sdk (room) member object. Supply either this or 'groupMember' member: PropTypes.object, // group member object. Supply either this or 'member' @@ -48,35 +46,36 @@ export default createReactClass({ askReason: PropTypes.bool, danger: PropTypes.bool, onFinished: PropTypes.func.isRequired, - }, + }; - getDefaultProps: () => ({ + static defaultProps = { danger: false, askReason: false, - }), + }; + + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount: function() { this._reasonField = null; - }, + } - onOk: function() { + onOk = () => { let reason; if (this._reasonField) { reason = this._reasonField.value; } this.props.onFinished(true, reason); - }, + }; - onCancel: function() { + onCancel = () => { this.props.onFinished(false); - }, + }; - _collectReasonField: function(e) { + _collectReasonField = e => { this._reasonField = e; - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); @@ -134,5 +133,5 @@ export default createReactClass({ onCancel={this.onCancel} /> ); - }, -}); + } +} diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx new file mode 100644 index 0000000000..1d9d92b9c9 --- /dev/null +++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx @@ -0,0 +1,227 @@ +/* +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, { ChangeEvent } from 'react'; +import BaseDialog from "./BaseDialog"; +import { _t } from "../../../languageHandler"; +import { IDialogProps } from "./IDialogProps"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import InfoTooltip from "../elements/InfoTooltip"; +import dis from "../../../dispatcher/dispatcher"; +import {showCommunityRoomInviteDialog} from "../../../RoomInvite"; +import GroupStore from "../../../stores/GroupStore"; + +interface IProps extends IDialogProps { +} + +interface IState { + name: string; + localpart: string; + error: string; + busy: boolean; + avatarFile: File; + avatarPreview: string; +} + +export default class CreateCommunityPrototypeDialog extends React.PureComponent { + private avatarUploadRef: React.RefObject = React.createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + name: "", + localpart: "", + error: null, + busy: false, + avatarFile: null, + avatarPreview: null, + }; + } + + private onNameChange = (ev: ChangeEvent) => { + const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-'); + this.setState({name: ev.target.value, localpart}); + }; + + private onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.state.busy) return; + + // We'll create the community now to see if it's taken, leaving it active in + // the background for the user to look at while they invite people. + this.setState({busy: true}); + try { + let avatarUrl = ''; // must be a string for synapse to accept it + if (this.state.avatarFile) { + avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile); + } + + const result = await MatrixClientPeg.get().createGroup({ + localpart: this.state.localpart, + profile: { + name: this.state.name, + avatar_url: avatarUrl, + }, + }); + + // Ensure the tag gets selected now that we've created it + dis.dispatch({action: 'deselect_tags'}, true); + dis.dispatch({ + action: 'select_tag', + tag: result.group_id, + }); + + // Close our own dialog before moving much further + this.props.onFinished(true); + + if (result.room_id) { + // Force the group store to update as it might have missed the general chat + await GroupStore.refreshGroupRooms(result.group_id); + dis.dispatch({ + action: 'view_room', + room_id: result.room_id, + }); + showCommunityRoomInviteDialog(result.room_id, this.state.name); + } else { + dis.dispatch({ + action: 'view_group', + group_id: result.group_id, + group_is_new: true, + }); + } + } catch (e) { + console.error(e); + this.setState({ + busy: false, + error: _t( + "There was an error creating your community. The name may be taken or the " + + "server is unable to process your request.", + ), + }); + } + }; + + private onAvatarChanged = (e: ChangeEvent) => { + if (!e.target.files || !e.target.files.length) { + this.setState({avatarFile: null}); + } else { + this.setState({busy: true}); + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev: ProgressEvent) => { + this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string}); + }; + reader.readAsDataURL(file); + } + }; + + private onChangeAvatar = () => { + if (this.avatarUploadRef.current) this.avatarUploadRef.current.click(); + }; + + public render() { + let communityId = null; + if (this.state.localpart) { + communityId = ( + + {_t("Community ID: +:%(domain)s", { + domain: MatrixClientPeg.getHomeserverName(), + }, { + localpart: () => {this.state.localpart}, + })} + + + ); + } + + let helpText = ( + + {_t("You can change this later if needed.")} + + ); + if (this.state.error) { + const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error"; + helpText = ( + + {this.state.error} + + ); + } + + let preview = ; + if (!this.state.avatarPreview) { + preview =
+ } + + return ( + +
+
+
+ + {helpText} + + {/*nbsp is to reserve the height of this element when there's nothing*/} +  {communityId} + + + {_t("Create")} + +
+
+ + + {preview} + +
+ {_t("Add image (optional)")} + + {_t("An image will help people identify your community.")} + +
+
+
+
+
+ ); + } +} diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 10285ccee0..6636153c98 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -15,46 +15,42 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -export default createReactClass({ - displayName: 'CreateGroupDialog', - propTypes: { +export default class CreateGroupDialog extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - groupName: '', - groupId: '', - groupError: null, - creating: false, - createError: null, - }; - }, + state = { + groupName: '', + groupId: '', + groupError: null, + creating: false, + createError: null, + }; - _onGroupNameChange: function(e) { + _onGroupNameChange = e => { this.setState({ groupName: e.target.value, }); - }, + }; - _onGroupIdChange: function(e) { + _onGroupIdChange = e => { this.setState({ groupId: e.target.value, }); - }, + }; - _onGroupIdBlur: function(e) { + _onGroupIdBlur = e => { this._checkGroupId(); - }, + }; - _checkGroupId: function(e) { + _checkGroupId(e) { let error = null; if (!this.state.groupId) { error = _t("Community IDs cannot be empty."); @@ -67,9 +63,9 @@ export default createReactClass({ createError: null, }); return error; - }, + } - _onFormSubmit: function(e) { + _onFormSubmit = e => { e.preventDefault(); if (this._checkGroupId()) return; @@ -94,13 +90,13 @@ export default createReactClass({ }).finally(() => { this.setState({creating: false}); }); - }, + }; - _onCancel: function() { + _onCancel = () => { this.props.onFinished(false); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const Spinner = sdk.getComponent('elements.Spinner'); @@ -171,5 +167,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index ce7ac6e59c..21d48409e8 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; @@ -25,17 +24,19 @@ import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; import {privateShouldBeEncrypted} from "../../../createRoom"; +import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; -export default createReactClass({ - displayName: 'CreateRoomDialog', - propTypes: { +export default class CreateRoomDialog extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, defaultPublic: PropTypes.bool, - }, + }; + + constructor(props) { + super(props); - getInitialState() { const config = SdkConfig.get(); - return { + this.state = { isPublic: this.props.defaultPublic || false, isEncrypted: privateShouldBeEncrypted(), name: "", @@ -45,7 +46,7 @@ export default createReactClass({ noFederate: config.default_federate === false, nameIsValid: false, }; - }, + } _roomCreateOptions() { const opts = {}; @@ -70,28 +71,32 @@ export default createReactClass({ opts.encryption = this.state.isEncrypted; } + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { + opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId(); + } + return opts; - }, + } componentDidMount() { this._detailsRef.addEventListener("toggle", this.onDetailsToggled); // move focus to first field when showing dialog this._nameFieldRef.focus(); - }, + } componentWillUnmount() { this._detailsRef.removeEventListener("toggle", this.onDetailsToggled); - }, + } - _onKeyDown: function(event) { + _onKeyDown = event => { if (event.key === Key.ENTER) { this.onOk(); event.preventDefault(); event.stopPropagation(); } - }, + }; - onOk: async function() { + onOk = async () => { const activeElement = document.activeElement; if (activeElement) { activeElement.blur(); @@ -117,51 +122,51 @@ export default createReactClass({ field.validate({ allowEmpty: false, focused: true }); } } - }, + }; - onCancel: function() { + onCancel = () => { this.props.onFinished(false); - }, + }; - onNameChange(ev) { + onNameChange = ev => { this.setState({name: ev.target.value}); - }, + }; - onTopicChange(ev) { + onTopicChange = ev => { this.setState({topic: ev.target.value}); - }, + }; - onPublicChange(isPublic) { + onPublicChange = isPublic => { this.setState({isPublic}); - }, + }; - onEncryptedChange(isEncrypted) { + onEncryptedChange = isEncrypted => { this.setState({isEncrypted}); - }, + }; - onAliasChange(alias) { + onAliasChange = alias => { this.setState({alias}); - }, + }; - onDetailsToggled(ev) { + onDetailsToggled = ev => { this.setState({detailsOpen: ev.target.open}); - }, + }; - onNoFederateChange(noFederate) { + onNoFederateChange = noFederate => { this.setState({noFederate}); - }, + }; - collectDetailsRef(ref) { + collectDetailsRef = ref => { this._detailsRef = ref; - }, + }; - async onNameValidate(fieldState) { - const result = await this._validateRoomName(fieldState); + onNameValidate = async fieldState => { + const result = await CreateRoomDialog._validateRoomName(fieldState); this.setState({nameIsValid: result.valid}); return result; - }, + }; - _validateRoomName: withValidation({ + static _validateRoomName = withValidation({ rules: [ { key: "required", @@ -169,27 +174,34 @@ export default createReactClass({ invalid: () => _t("Please enter a name for the room"), }, ], - }), + }); - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - let publicPrivateLabel; let aliasField; if (this.state.isPublic) { - publicPrivateLabel = (

{_t("Set a room address to easily share your room with other people.")}

); const domain = MatrixClientPeg.get().getDomain(); aliasField = (
this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
); - } else { - publicPrivateLabel = (

{_t("This room is private, and can only be joined by invitation.")}

); + } + + let publicPrivateLabel =

{_t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone.", + )}

; + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { + publicPrivateLabel =

{_t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone in this community.", + )}

; } let e2eeSection; @@ -212,7 +224,24 @@ export default createReactClass({ ; } - const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + let federateLabel = _t( + "You might enable this if the room will only be used for collaborating with internal " + + "teams on your homeserver. This cannot be changed later.", + ); + if (SdkConfig.get().default_federate === false) { + // We only change the label if the default setting is different to avoid jarring text changes to the + // user. They will have read the implications of turning this off/on, so no need to rephrase for them. + federateLabel = _t( + "You might disable this if the room will be used for collaborating with external " + + "teams who have their own homeserver. This cannot be changed later.", + ); + } + + let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { + const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); + title = _t("Create a room in %(communityName)s", {communityName: name}); + } return ( { this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } - + +

{federateLabel}

@@ -236,5 +273,5 @@ export default createReactClass({ onCancel={this.onCancel} /> ); - }, -}); + } +} diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx new file mode 100644 index 0000000000..3071854b3e --- /dev/null +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -0,0 +1,167 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ChangeEvent } from 'react'; +import BaseDialog from "./BaseDialog"; +import { _t } from "../../../languageHandler"; +import { IDialogProps } from "./IDialogProps"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; +import FlairStore from "../../../stores/FlairStore"; + +interface IProps extends IDialogProps { + communityId: string; +} + +interface IState { + name: string; + error: string; + busy: boolean; + currentAvatarUrl: string; + avatarFile: File; + avatarPreview: string; +} + +// XXX: This is a lot of duplication from the create dialog, just in a different shape +export default class EditCommunityPrototypeDialog extends React.PureComponent { + private avatarUploadRef: React.RefObject = React.createRef(); + + constructor(props: IProps) { + super(props); + + const profile = CommunityPrototypeStore.instance.getCommunityProfile(props.communityId); + + this.state = { + name: profile?.name || "", + error: null, + busy: false, + avatarFile: null, + avatarPreview: null, + currentAvatarUrl: profile?.avatarUrl, + }; + } + + private onNameChange = (ev: ChangeEvent) => { + this.setState({name: ev.target.value}); + }; + + private onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.state.busy) return; + + // We'll create the community now to see if it's taken, leaving it active in + // the background for the user to look at while they invite people. + this.setState({busy: true}); + try { + let avatarUrl = this.state.currentAvatarUrl || ""; // must be a string for synapse to accept it + if (this.state.avatarFile) { + avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile); + } + + await MatrixClientPeg.get().setGroupProfile(this.props.communityId, { + name: this.state.name, + avatar_url: avatarUrl, + }); + + // ask the flair store to update the profile too + await FlairStore.refreshGroupProfile(MatrixClientPeg.get(), this.props.communityId); + + // we did it, so close the dialog + this.props.onFinished(true); + } catch (e) { + console.error(e); + this.setState({ + busy: false, + error: _t("There was an error updating your community. The server is unable to process your request."), + }); + } + }; + + private onAvatarChanged = (e: ChangeEvent) => { + if (!e.target.files || !e.target.files.length) { + this.setState({avatarFile: null}); + } else { + this.setState({busy: true}); + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev: ProgressEvent) => { + this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string}); + }; + reader.readAsDataURL(file); + } + }; + + private onChangeAvatar = () => { + if (this.avatarUploadRef.current) this.avatarUploadRef.current.click(); + }; + + public render() { + let preview = ; + if (!this.state.avatarPreview) { + if (this.state.currentAvatarUrl) { + const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl); + preview = ; + } else { + preview =
+ } + } + + return ( + +
+
+
+ +
+
+ + {preview} +
+ {_t("Add image (optional)")} + + {_t("An image will help people identify your community.")} + +
+
+ + {_t("Save")} + +
+
+
+ ); + } +} diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index fbc5509457..acebdcd854 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -26,14 +26,12 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -export default createReactClass({ - displayName: 'ErrorDialog', - propTypes: { +export default class ErrorDialog extends React.Component { + static propTypes = { title: PropTypes.string, description: PropTypes.oneOfType([ PropTypes.element, @@ -43,18 +41,16 @@ export default createReactClass({ focus: PropTypes.bool, onFinished: PropTypes.func.isRequired, headerImage: PropTypes.string, - }, + }; - getDefaultProps: function() { - return { - focus: true, - title: null, - description: null, - button: null, - }; - }, + static defaultProps = { + focus: true, + title: null, + description: null, + button: null, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( ); - }, -}); + } +} diff --git a/src/components/views/dialogs/IDialogProps.ts b/src/components/views/dialogs/IDialogProps.ts new file mode 100644 index 0000000000..1027ca7607 --- /dev/null +++ b/src/components/views/dialogs/IDialogProps.ts @@ -0,0 +1,19 @@ +/* +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. +*/ + +export interface IDialogProps { + onFinished: (bool) => void; +} diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index b63f6ba9c6..8125bc3edd 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -17,15 +17,13 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import classNames from "classnames"; -export default createReactClass({ - displayName: 'InfoDialog', - propTypes: { +export default class InfoDialog extends React.Component { + static propTypes = { className: PropTypes.string, title: PropTypes.string, description: PropTypes.node, @@ -33,21 +31,19 @@ export default createReactClass({ onFinished: PropTypes.func, hasCloseButton: PropTypes.bool, onKeyDown: PropTypes.func, - }, + }; - getDefaultProps: function() { - return { - title: '', - description: '', - hasCloseButton: false, - }; - }, + static defaultProps = { + title: '', + description: '', + hasCloseButton: false, + }; - onFinished: function() { + onFinished = () => { this.props.onFinished(); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( @@ -69,5 +65,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index b06ce63ecd..22291225ad 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; @@ -27,10 +26,8 @@ import AccessibleButton from '../elements/AccessibleButton'; import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents"; -export default createReactClass({ - displayName: 'InteractiveAuthDialog', - - propTypes: { +export default class InteractiveAuthDialog extends React.Component { + static propTypes = { // matrix client to use for UI auth requests matrixClient: PropTypes.object.isRequired, @@ -70,19 +67,17 @@ export default createReactClass({ // // Default is defined in _getDefaultDialogAesthetics() aestheticsForStagePhases: PropTypes.object, - }, + }; - getInitialState: function() { - return { - authError: null, + state = { + authError: null, - // See _onUpdateStagePhase() - uiaStage: null, - uiaStagePhase: null, - }; - }, + // See _onUpdateStagePhase() + uiaStage: null, + uiaStagePhase: null, + }; - _getDefaultDialogAesthetics: function() { + _getDefaultDialogAesthetics() { const ssoAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), @@ -102,9 +97,9 @@ export default createReactClass({ [SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics, }; - }, + } - _onAuthFinished: function(success, result) { + _onAuthFinished = (success, result) => { if (success) { this.props.onFinished(true, result); } else { @@ -116,18 +111,18 @@ export default createReactClass({ }); } } - }, + }; - _onUpdateStagePhase: function(newStage, newPhase) { + _onUpdateStagePhase = (newStage, newPhase) => { // We copy the stage and stage phase params into state for title selection in render() this.setState({uiaStage: newStage, uiaStagePhase: newPhase}); - }, + }; - _onDismissClick: function() { + _onDismissClick = () => { this.props.onFinished(false); - }, + }; - render: function() { + render() { const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); @@ -190,5 +185,5 @@ export default createReactClass({ { content } ); - }, -}); + } +} diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 4d7a66e957..80d8f1fc2c 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -32,11 +32,12 @@ import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom"; -import {inviteMultipleToRoom} from "../../../RoomInvite"; +import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; import RoomListStore from "../../../stores/room-list/RoomListStore"; +import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -327,7 +328,7 @@ export default class InviteDialog extends React.PureComponent { this.state = { targets: [], // array of Member objects (see interface above) filterText: "", - recents: this._buildRecents(alreadyInvited), + recents: InviteDialog.buildRecents(alreadyInvited), numRecentsShown: INITIAL_ROOMS_SHOWN, suggestions: this._buildSuggestions(alreadyInvited), numSuggestionsShown: INITIAL_ROOMS_SHOWN, @@ -344,12 +345,12 @@ export default class InviteDialog extends React.PureComponent { this._editorRef = createRef(); } - _buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { + static buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM]; + const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM] || []; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); @@ -909,12 +910,23 @@ export default class InviteDialog extends React.PureComponent { this.props.onFinished(); }; + _onCommunityInviteClick = (e) => { + this.props.onFinished(); + showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId()); + }; + _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; let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); + let sectionSubname = null; + + if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) { + const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); + sectionSubname = _t("May include members not in %(communityName)s", {communityName}); + } if (this.props.kind === KIND_INVITE) { sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions"); @@ -993,6 +1005,7 @@ export default class InviteDialog extends React.PureComponent { return (

{sectionName}

+ {sectionSubname ?

{sectionSubname}

: null} {tiles} {showMore}
@@ -1083,6 +1096,33 @@ export default class InviteDialog extends React.PureComponent { return {userId}; }}, ); + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { + const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); + helpText = _t( + "Start a conversation with someone using their name, username (like ) or email address. " + + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " + + "here.", + {communityName}, { + userId: () => { + return ( + {userId} + ); + }, + a: (sub) => { + return ( + {sub} + ); + }, + }, + ); + } buttonText = _t("Go"); goButtonFn = this._startDm; } else { // KIND_INVITE diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 07a1eae5d5..d6de60195f 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -16,14 +16,12 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -export default createReactClass({ - displayName: 'QuestionDialog', - propTypes: { +export default class QuestionDialog extends React.Component { + static propTypes = { title: PropTypes.string, description: PropTypes.node, extraButtons: PropTypes.node, @@ -34,29 +32,27 @@ export default createReactClass({ headerImage: PropTypes.string, quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x]. fixedWidth: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - title: "", - description: "", - extraButtons: null, - focus: true, - hasCancelButton: true, - danger: false, - quitOnly: false, - }; - }, + static defaultProps = { + title: "", + description: "", + extraButtons: null, + focus: true, + hasCancelButton: true, + danger: false, + quitOnly: false, + }; - onOk: function() { + onOk = () => { this.props.onFinished(true); - }, + }; - onCancel: function() { + onCancel = () => { this.props.onFinished(false); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let primaryButtonClass = ""; @@ -88,5 +84,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/RebrandDialog.tsx b/src/components/views/dialogs/RebrandDialog.tsx deleted file mode 100644 index 79b4b69a4a..0000000000 --- a/src/components/views/dialogs/RebrandDialog.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import BaseDialog from './BaseDialog'; -import { _t } from '../../../languageHandler'; -import DialogButtons from '../elements/DialogButtons'; - -export enum RebrandDialogKind { - NAG, - ONE_TIME, -} - -interface IProps { - onFinished: (bool) => void; - kind: RebrandDialogKind; - targetUrl?: string; -} - -export default class RebrandDialog extends React.PureComponent { - private onDoneClick = () => { - this.props.onFinished(true); - }; - - private onGoToElementClick = () => { - this.props.onFinished(true); - }; - - private onRemindMeLaterClick = () => { - this.props.onFinished(false); - }; - - private getPrettyTargetUrl() { - const u = new URL(this.props.targetUrl); - let ret = u.host; - if (u.pathname !== '/') ret += u.pathname; - return ret; - } - - getBodyText() { - if (this.props.kind === RebrandDialogKind.NAG) { - return _t( - "Use your account to sign in to the latest version of the app at ", {}, - { - a: sub => {this.getPrettyTargetUrl()}, - }, - ); - } else { - return _t( - "You’re already signed in and good to go here, but you can also grab the latest " + - "versions of the app on all platforms at element.io/get-started.", {}, - { - a: sub => {sub}, - }, - ); - } - } - - getDialogButtons() { - if (this.props.kind === RebrandDialogKind.NAG) { - return ; - } else { - return ; - } - } - - render() { - return -
{this.getBodyText()}
-
- Riot Logo - - Element Logo -
-
- {_t( - "Learn more at element.io/previously-riot", {}, { - a: sub => {sub}, - } - )} -
- {this.getDialogButtons()} -
; - } -} diff --git a/src/components/views/dialogs/RedesignFeedbackDialog.js b/src/components/views/dialogs/RedesignFeedbackDialog.js index 8f345bdd92..51b71ac6cb 100644 --- a/src/components/views/dialogs/RedesignFeedbackDialog.js +++ b/src/components/views/dialogs/RedesignFeedbackDialog.js @@ -19,9 +19,9 @@ import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; export default (props) => { - const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" + + const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" + "?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc"; - const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new"; + const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; const description1 = _t("If you run into any bugs or have feedback you'd like to share, " + diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 7ad1001f75..613708e436 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -87,7 +87,7 @@ export default class RoomSettingsDialog extends React.Component { , )); - if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { + if (SettingsStore.getValue("feature_bridge_state")) { tabs.push(new Tab( ROOM_BRIDGES_TAB, _td("Bridges"), diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index c45d82303b..85e97444ed 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -15,38 +15,33 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default createReactClass({ - displayName: 'RoomUpgradeDialog', - - propTypes: { +export default class RoomUpgradeDialog extends React.Component { + static propTypes = { room: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, - }, + }; - componentDidMount: async function() { + state = { + busy: true, + }; + + async componentDidMount() { const recommended = await this.props.room.getRecommendedVersion(); this._targetVersion = recommended.version; this.setState({busy: false}); - }, + } - getInitialState: function() { - return { - busy: true, - }; - }, - - _onCancelClick: function() { + _onCancelClick = () => { this.props.onFinished(false); - }, + }; - _onUpgradeClick: function() { + _onUpgradeClick = () => { this.setState({busy: true}); MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => { this.props.onFinished(true); @@ -59,9 +54,9 @@ export default createReactClass({ }).finally(() => { this.setState({busy: false}); }); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Spinner = sdk.getComponent('views.elements.Spinner'); @@ -106,5 +101,5 @@ export default createReactClass({ {buttons} ); - }, -}); + } +} diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx index f6767dcb8d..81f628343b 100644 --- a/src/components/views/dialogs/ServerOfflineDialog.tsx +++ b/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IDialogProps } from "./IDialogProps"; -interface IProps { - onFinished: (bool) => void; +interface IProps extends IDialogProps { } export default class ServerOfflineDialog extends React.PureComponent { diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index 3706172085..bae6b19fbe 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; @@ -25,20 +24,18 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default createReactClass({ - displayName: 'SessionRestoreErrorDialog', - - propTypes: { +export default class SessionRestoreErrorDialog extends React.Component { + static propTypes = { error: PropTypes.string.isRequired, onFinished: PropTypes.func.isRequired, - }, + }; - _sendBugReport: function() { + _sendBugReport = () => { const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {}); - }, + }; - _onClearStorageClick: function() { + _onClearStorageClick = () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, { title: _t("Sign out"), @@ -48,15 +45,15 @@ export default createReactClass({ danger: true, onFinished: this.props.onFinished, }); - }, + }; - _onRefreshClick: function() { + _onRefreshClick = () => { // Is this likely to help? Probably not, but giving only one button // that clears your storage seems awful. window.location.reload(true); - }, + }; - render: function() { + render() { const brand = SdkConfig.get().brand; const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -110,5 +107,5 @@ export default createReactClass({ { dialogButtons } ); - }, -}); + } +} diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index 2e38d6a7c4..6514d94dc9 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import * as Email from '../../../email'; @@ -25,31 +24,28 @@ import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; -/** +/* * Prompt the user to set an email address. * * On success, `onFinished(true)` is called. */ -export default createReactClass({ - displayName: 'SetEmailDialog', - propTypes: { +export default class SetEmailDialog extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - emailAddress: '', - emailBusy: false, - }; - }, + state = { + emailAddress: '', + emailBusy: false, + }; - onEmailAddressChanged: function(value) { + onEmailAddressChanged = value => { this.setState({ emailAddress: value, }); - }, + }; - onSubmit: function() { + onSubmit = () => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -81,21 +77,21 @@ export default createReactClass({ }); }); this.setState({emailBusy: true}); - }, + }; - onCancelled: function() { + onCancelled = () => { this.props.onFinished(false); - }, + }; - onEmailDialogFinished: function(ok) { + onEmailDialogFinished = ok => { if (ok) { this.verifyEmailAddress(); } else { this.setState({emailBusy: false}); } - }, + }; - verifyEmailAddress: function() { + verifyEmailAddress() { this._addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { @@ -119,9 +115,9 @@ export default createReactClass({ }); } }); - }, + } - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const Spinner = sdk.getComponent('elements.Spinner'); const EditableText = sdk.getComponent('elements.EditableText'); @@ -161,5 +157,5 @@ export default createReactClass({
); - }, -}); + } +} diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index f99d065e7e..090def5e54 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -16,7 +16,6 @@ limitations under the License. */ import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; @@ -29,23 +28,27 @@ import { SAFE_LOCALPART_REGEX } from '../../../Registration'; // sending a request to the server const USERNAME_CHECK_DEBOUNCE_MS = 250; -/** +/* * Prompt the user to set a display name. * * On success, `onFinished(true, newDisplayName)` is called. */ -export default createReactClass({ - displayName: 'SetMxIdDialog', - propTypes: { +export default class SetMxIdDialog extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, // Called when the user requests to register with a different homeserver onDifferentServerClicked: PropTypes.func.isRequired, // Called if the user wants to switch to login instead onLoginClick: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this._input_value = createRef(); + this._uiAuth = createRef(); + + this.state = { // The entered username username: '', // Indicate ongoing work on the username @@ -60,21 +63,15 @@ export default createReactClass({ // Indicate error with auth authError: '', }; - }, + } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { - this._input_value = createRef(); - this._uiAuth = createRef(); - }, - - componentDidMount: function() { + componentDidMount() { this._input_value.current.select(); this._matrixClient = MatrixClientPeg.get(); - }, + } - onValueChange: function(ev) { + onValueChange = ev => { this.setState({ username: ev.target.value, usernameBusy: true, @@ -99,24 +96,24 @@ export default createReactClass({ }); }, USERNAME_CHECK_DEBOUNCE_MS); }); - }, + }; - onKeyUp: function(ev) { + onKeyUp = ev => { if (ev.key === Key.ENTER) { this.onSubmit(); } - }, + }; - onSubmit: function(ev) { + onSubmit = ev => { if (this._uiAuth.current) { this._uiAuth.current.tryContinue(); } this.setState({ doingUIAuth: true, }); - }, + }; - _doUsernameCheck: function() { + _doUsernameCheck() { // We do a quick check ahead of the username availability API to ensure the // user ID roughly looks okay from a Matrix perspective. if (!SAFE_LOCALPART_REGEX.test(this.state.username)) { @@ -167,13 +164,13 @@ export default createReactClass({ this.setState(newState); }, ); - }, + } - _generatePassword: function() { + _generatePassword() { return Math.random().toString(36).slice(2); - }, + } - _makeRegisterRequest: function(auth) { + _makeRegisterRequest = auth => { // Not upgrading - changing mxids const guestAccessToken = null; if (!this._generatedPassword) { @@ -187,9 +184,9 @@ export default createReactClass({ {}, guestAccessToken, ); - }, + }; - _onUIAuthFinished: function(success, response) { + _onUIAuthFinished = (success, response) => { this.setState({ doingUIAuth: false, }); @@ -207,9 +204,9 @@ export default createReactClass({ accessToken: response.access_token, password: this._generatedPassword, }); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); @@ -303,5 +300,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index fcc6e67656..3649190ac9 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -63,32 +62,25 @@ const WarmFuzzy = function(props) { * * On success, `onFinished()` when finished */ -export default createReactClass({ - displayName: 'SetPasswordDialog', - propTypes: { +export default class SetPasswordDialog extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, - }, + }; - getInitialState: function() { - return { - error: null, - }; - }, + state = { + error: null, + }; - componentDidMount: function() { - console.info('SetPasswordDialog component did mount'); - }, - - _onPasswordChanged: function(res) { + _onPasswordChanged = res => { Modal.createDialog(WarmFuzzy, { didSetEmail: res.didSetEmail, onFinished: () => { this.props.onFinished(); }, }); - }, + }; - _onPasswordChangeError: function(err) { + _onPasswordChangeError = err => { let errMsg = err.error || ""; if (err.httpStatus === 403) { errMsg = _t('Failed to change password. Is your password correct?'); @@ -101,9 +93,9 @@ export default createReactClass({ this.setState({ error: errMsg, }); - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const ChangePassword = sdk.getComponent('views.settings.ChangePassword'); @@ -132,5 +124,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index 2e1529cbf1..e849f7efe3 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -30,6 +30,8 @@ import * as ContextMenu from "../../structures/ContextMenu"; import {toRightOf} from "../../structures/ContextMenu"; import {copyPlaintext, selectText} from "../../../utils/strings"; import StyledCheckbox from '../elements/StyledCheckbox'; +import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import { IDialogProps } from "./IDialogProps"; const socials = [ { @@ -59,8 +61,7 @@ const socials = [ }, ]; -interface IProps { - onFinished: () => void; +interface IProps extends IDialogProps { target: Room | User | Group | RoomMember | MatrixEvent; permalinkCreator: RoomPermalinkCreator; } @@ -185,8 +186,8 @@ export default class ShareDialog extends React.PureComponent { title = _t('Share Room Message'); checkbox =
{ _t('Link to selected message') } @@ -197,23 +198,26 @@ export default class ShareDialog extends React.PureComponent { const encodedUrl = encodeURIComponent(matrixToUrl); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - return
{ checkbox }
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index d7ca3f144d..571ed7e413 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -15,14 +15,12 @@ limitations under the License. */ import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import Field from "../elements/Field"; -export default createReactClass({ - displayName: 'TextInputDialog', - propTypes: { +export default class TextInputDialog extends React.Component { + static propTypes = { title: PropTypes.string, description: PropTypes.oneOfType([ PropTypes.element, @@ -36,39 +34,36 @@ export default createReactClass({ hasCancel: PropTypes.bool, validator: PropTypes.func, // result of withValidation fixedWidth: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - title: "", - value: "", - description: "", - focus: true, - hasCancel: true, - }; - }, + static defaultProps = { + title: "", + value: "", + description: "", + focus: true, + hasCancel: true, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this._field = createRef(); + + this.state = { value: this.props.value, valid: false, }; - }, + } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { - this._field = createRef(); - }, - - componentDidMount: function() { + componentDidMount() { if (this.props.focus) { // Set the cursor at the end of the text input // this._field.current.value = this.props.value; this._field.current.focus(); } - }, + } - onOk: async function(ev) { + onOk = async ev => { ev.preventDefault(); if (this.props.validator) { await this._field.current.validate({ allowEmpty: false }); @@ -80,27 +75,27 @@ export default createReactClass({ } } this.props.onFinished(true, this.state.value); - }, + }; - onCancel: function() { + onCancel = () => { this.props.onFinished(false); - }, + }; - onChange: function(ev) { + onChange = ev => { this.setState({ value: ev.target.value, }); - }, + }; - onValidate: async function(fieldState) { + onValidate = async fieldState => { const result = await this.props.validator(fieldState); this.setState({ valid: result.valid, }); return result; - }, + }; - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( @@ -137,5 +132,5 @@ export default createReactClass({ /> ); - }, -}); + } +} diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 1f1a8d1523..ffde03fe31 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -54,7 +54,7 @@ export default class UserSettingsDialog extends React.Component { super(); this.state = { - mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), + mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), }; } @@ -116,7 +116,7 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_securityIcon", , )); - if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { + if (SdkConfig.get()['showLabsSettings']) { tabs.push(new Tab( USER_LABS_TAB, _td("Labs"), diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 5c01a6907f..85ace249a3 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { debounce } from 'lodash'; +import {debounce} from "lodash"; import classNames from 'classnames'; import React from 'react'; import PropTypes from "prop-types"; diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 3546f62359..0388c565ad 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -25,6 +25,7 @@ interface ITooltipProps extends React.ComponentProps { title: string; tooltip?: React.ReactNode; tooltipClassName?: string; + forceHide?: boolean; } interface IState { @@ -39,7 +40,16 @@ export default class AccessibleTooltipButton extends React.PureComponent) { + if (!prevProps.forceHide && this.props.forceHide && this.state.hover) { + this.setState({ + hover: false, + }); + } + } + onMouseOver = () => { + if (this.props.forceHide) return; this.setState({ hover: true, }); diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 7536d66653..bec016bce0 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -16,16 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import AccessibleButton from './AccessibleButton'; import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; -export default createReactClass({ - displayName: 'RoleButton', - - propTypes: { +export default class ActionButton extends React.Component { + static propTypes = { size: PropTypes.string, tooltip: PropTypes.bool, action: PropTypes.string.isRequired, @@ -33,39 +30,35 @@ export default createReactClass({ label: PropTypes.string.isRequired, iconPath: PropTypes.string, className: PropTypes.string, - }, + }; - getDefaultProps: function() { - return { - size: "25", - tooltip: false, - }; - }, + static defaultProps = { + size: "25", + tooltip: false, + }; - getInitialState: function() { - return { - showTooltip: false, - }; - }, + state = { + showTooltip: false, + }; - _onClick: function(ev) { + _onClick = (ev) => { ev.stopPropagation(); Analytics.trackEvent('Action Button', 'click', this.props.action); dis.dispatch({action: this.props.action}); - }, + }; - _onMouseEnter: function() { + _onMouseEnter = () => { if (this.props.tooltip) this.setState({showTooltip: true}); if (this.props.mouseOverAction) { dis.dispatch({action: this.props.mouseOverAction}); } - }, + }; - _onMouseLeave: function() { + _onMouseLeave = () => { this.setState({showTooltip: false}); - }, + }; - render: function() { + render() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); let tooltip; @@ -94,5 +87,5 @@ export default createReactClass({ { tooltip } ); - }, -}); + } +} diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index ab29723a45..45cdbeced8 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -17,15 +17,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; import classNames from 'classnames'; import { UserAddressType } from '../../../UserAddress'; -export default createReactClass({ - displayName: 'AddressSelector', - - propTypes: { +export default class AddressSelector extends React.Component { + static propTypes = { onSelected: PropTypes.func.isRequired, // List of the addresses to display @@ -37,90 +34,91 @@ export default createReactClass({ // Element to put as a header on top of the list header: PropTypes.node, - }, + }; - getInitialState: function() { - return { + constructor(props) { + super(props); + + this.state = { selected: this.props.selected === undefined ? 0 : this.props.selected, hover: false, }; - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(props) { + UNSAFE_componentWillReceiveProps(props) { // Make sure the selected item isn't outside the list bounds const selected = this.state.selected; const maxSelected = this._maxSelected(props.addressList); if (selected > maxSelected) { this.setState({ selected: maxSelected }); } - }, + } - componentDidUpdate: function() { + componentDidUpdate() { // As the user scrolls with the arrow keys keep the selected item // at the top of the window. if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) { const elementHeight = this.addressListElement.getBoundingClientRect().height; this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight; } - }, + } - moveSelectionTop: function() { + moveSelectionTop = () => { if (this.state.selected > 0) { this.setState({ selected: 0, hover: false, }); } - }, + }; - moveSelectionUp: function() { + moveSelectionUp = () => { if (this.state.selected > 0) { this.setState({ selected: this.state.selected - 1, hover: false, }); } - }, + }; - moveSelectionDown: function() { + moveSelectionDown = () => { if (this.state.selected < this._maxSelected(this.props.addressList)) { this.setState({ selected: this.state.selected + 1, hover: false, }); } - }, + }; - chooseSelection: function() { + chooseSelection = () => { this.selectAddress(this.state.selected); - }, + }; - onClick: function(index) { + onClick = index => { this.selectAddress(index); - }, + }; - onMouseEnter: function(index) { + onMouseEnter = index => { this.setState({ selected: index, hover: true, }); - }, + }; - onMouseLeave: function() { + onMouseLeave = () => { this.setState({ hover: false }); - }, + }; - selectAddress: function(index) { + selectAddress = index => { // Only try to select an address if one exists if (this.props.addressList.length !== 0) { this.props.onSelected(index); this.setState({ hover: false }); } - }, + }; - createAddressListTiles: function() { - const self = this; + createAddressListTiles() { const AddressTile = sdk.getComponent("elements.AddressTile"); const maxSelected = this._maxSelected(this.props.addressList); const addressList = []; @@ -157,15 +155,15 @@ export default createReactClass({ } } return addressList; - }, + } - _maxSelected: function(list) { + _maxSelected(list) { const listSize = list.length === 0 ? 0 : list.length - 1; const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize; return maxSelected; - }, + } - render: function() { + render() { const classes = classNames({ "mx_AddressSelector": true, "mx_AddressSelector_empty": this.props.addressList.length === 0, @@ -177,5 +175,5 @@ export default createReactClass({ { this.createAddressListTiles() }
); - }, -}); + } +} diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index e5ea2e5d20..dc6c6b2914 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import classNames from 'classnames'; import * as sdk from "../../../index"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -25,25 +24,21 @@ import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; -export default createReactClass({ - displayName: 'AddressTile', - - propTypes: { +export default class AddressTile extends React.Component { + static propTypes = { address: UserAddressType.isRequired, canDismiss: PropTypes.bool, onDismissed: PropTypes.func, justified: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - canDismiss: false, - onDismissed: function() {}, // NOP - justified: false, - }; - }, + static defaultProps = { + canDismiss: false, + onDismissed: function() {}, // NOP + justified: false, + }; - render: function() { + render() { const address = this.props.address; const name = address.displayName || address.address; @@ -144,5 +139,5 @@ export default createReactClass({ { dismiss }
); - }, -}); + } +} diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index d0fc56743f..299025f949 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -311,16 +311,16 @@ export default class AppTile extends React.Component { this.props.onEditClick(); } else { // TODO: Open the right manager for the widget - if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll( this.props.room, - 'type_' + this.props.type, + 'type_' + this.props.app.type, this.props.app.id, ); } else { IntegrationManagers.sharedInstance().getPrimaryManager().open( this.props.room, - 'type_' + this.props.type, + 'type_' + this.props.app.type, this.props.app.id, ); } @@ -361,14 +361,14 @@ export default class AppTile extends React.Component { return terminationPromise.finally(() => { // HACK: This is a really dirty way to ensure that Jitsi cleans up // its hold on the webcam. Without this, the widget holds a media - // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351 + // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351 if (this._appFrame.current) { // In practice we could just do `+= ''` to trick the browser // into thinking the URL changed, however I can foresee this // being optimized out by a browser. Instead, we'll just point // the iframe at a page that is reasonably safe to use in the // event the iframe doesn't wink away. - // This is relative to where the Riot instance is located. + // This is relative to where the Element instance is located. this._appFrame.current.src = 'about:blank'; } @@ -727,7 +727,7 @@ export default class AppTile extends React.Component { // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // because that would allow the iframe to programmatically remove the sandbox attribute, but - // this would only be for content hosted on the same origin as the riot client: anything + // this would only be for content hosted on the same origin as the element client: anything // hosted on the same origin as the client will get the same access as if you clicked // a link to it. const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ @@ -735,7 +735,7 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media; autoplay;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); @@ -924,7 +924,7 @@ AppTile.propTypes = { // Optionally show the reload widget icon // This is not currently intended for use with production widgets. However // it can be useful when developing persistent widgets in order to avoid - // having to reload all of riot to get new widget content. + // having to reload all of Element to get new widget content. showReload: PropTypes.bool, // Widget capabilities to allow by default (without user confirmation) // NOTE -- Use with caution. This is intended to aid better integration / UX diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js index 9223b5ade8..001292b6b7 100644 --- a/src/components/views/elements/DialogButtons.js +++ b/src/components/views/elements/DialogButtons.js @@ -18,16 +18,13 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; -import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; /** * Basic container for buttons in modal dialogs. */ -export default createReactClass({ - displayName: "DialogButtons", - - propTypes: { +export default class DialogButtons extends React.Component { + static propTypes = { // The primary button which is styled differently and has default focus. primaryButton: PropTypes.node.isRequired, @@ -57,20 +54,18 @@ export default createReactClass({ // disables only the primary button primaryDisabled: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - hasCancel: true, - disabled: false, - }; - }, + static defaultProps = { + hasCancel: true, + disabled: false, + }; - _onCancelClick: function() { + _onCancelClick = () => { this.props.onCancel(); - }, + }; - render: function() { + render() { let primaryButtonClassName = "mx_Dialog_primary"; if (this.props.primaryButtonClass) { primaryButtonClassName += " " + this.props.primaryButtonClass; @@ -104,5 +99,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx index 3397fd901c..a6eb8323f3 100644 --- a/src/components/views/elements/Draggable.tsx +++ b/src/components/views/elements/Draggable.tsx @@ -34,7 +34,6 @@ export interface ILocationState { } export default class Draggable extends React.Component { - constructor(props: IProps) { super(props); @@ -77,5 +76,4 @@ export default class Draggable extends React.Component { render() { return
; } - -} \ No newline at end of file +} diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 82f5eef125..49eb331aef 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -17,13 +17,10 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import {Key} from "../../../Keyboard"; -export default createReactClass({ - displayName: 'EditableText', - - propTypes: { +export default class EditableText extends React.Component { + static propTypes = { onValueChanged: PropTypes.func, initialValue: PropTypes.string, label: PropTypes.string, @@ -36,60 +33,58 @@ export default createReactClass({ // Will cause onValueChanged(value, true) to fire on blur blurToSubmit: PropTypes.bool, editable: PropTypes.bool, - }, + }; - Phases: { + static Phases = { Display: "display", Edit: "edit", - }, + }; - getDefaultProps: function() { - return { - onValueChanged: function() {}, - initialValue: '', - label: '', - placeholder: '', - editable: true, - className: "mx_EditableText", - placeholderClassName: "mx_EditableText_placeholder", - blurToSubmit: false, - }; - }, + static defaultProps = { + onValueChanged() {}, + initialValue: '', + label: '', + placeholder: '', + editable: true, + className: "mx_EditableText", + placeholderClassName: "mx_EditableText_placeholder", + blurToSubmit: false, + }; - getInitialState: function() { - return { - phase: this.Phases.Display, - }; - }, + constructor(props) { + super(props); - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(nextProps) { - if (nextProps.initialValue !== this.props.initialValue) { - this.value = nextProps.initialValue; - if (this._editable_div.current) { - this.showPlaceholder(!this.value); - } - } - }, - - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount: function() { // we track value as an JS object field rather than in React state // as React doesn't play nice with contentEditable. this.value = ''; this.placeholder = false; this._editable_div = createRef(); - }, + } - componentDidMount: function() { + state = { + phase: EditableText.Phases.Display, + }; + + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { + if (nextProps.initialValue !== this.props.initialValue) { + this.value = nextProps.initialValue; + if (this._editable_div.current) { + this.showPlaceholder(!this.value); + } + } + } + + componentDidMount() { this.value = this.props.initialValue; if (this._editable_div.current) { this.showPlaceholder(!this.value); } - }, + } - showPlaceholder: function(show) { + showPlaceholder = show => { if (show) { this._editable_div.current.textContent = this.props.placeholder; this._editable_div.current.setAttribute("class", this.props.className @@ -101,38 +96,36 @@ export default createReactClass({ this._editable_div.current.setAttribute("class", this.props.className); this.placeholder = false; } - }, + }; - getValue: function() { - return this.value; - }, + getValue = () => this.value; - setValue: function(value) { + setValue = value => { this.value = value; this.showPlaceholder(!this.value); - }, + }; - edit: function() { + edit = () => { this.setState({ - phase: this.Phases.Edit, + phase: EditableText.Phases.Edit, }); - }, + }; - cancelEdit: function() { + cancelEdit = () => { this.setState({ - phase: this.Phases.Display, + phase: EditableText.Phases.Display, }); this.value = this.props.initialValue; this.showPlaceholder(!this.value); this.onValueChanged(false); this._editable_div.current.blur(); - }, + }; - onValueChanged: function(shouldSubmit) { + onValueChanged = shouldSubmit => { this.props.onValueChanged(this.value, shouldSubmit); - }, + }; - onKeyDown: function(ev) { + onKeyDown = ev => { // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); if (this.placeholder) { @@ -145,9 +138,9 @@ export default createReactClass({ } // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); - }, + }; - onKeyUp: function(ev) { + onKeyUp = ev => { // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); if (!ev.target.textContent) { @@ -163,17 +156,17 @@ export default createReactClass({ } // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); - }, + }; - onClickDiv: function(ev) { + onClickDiv = ev => { if (!this.props.editable) return; this.setState({ - phase: this.Phases.Edit, + phase: EditableText.Phases.Edit, }); - }, + }; - onFocus: function(ev) { + onFocus = ev => { //ev.target.setSelectionRange(0, ev.target.textContent.length); const node = ev.target.childNodes[0]; @@ -186,21 +179,21 @@ export default createReactClass({ sel.removeAllRanges(); sel.addRange(range); } - }, + }; - onFinish: function(ev, shouldSubmit) { + onFinish = (ev, shouldSubmit) => { const self = this; const submit = (ev.key === Key.ENTER) || shouldSubmit; this.setState({ - phase: this.Phases.Display, + phase: EditableText.Phases.Display, }, () => { if (this.value !== this.props.initialValue) { self.onValueChanged(submit); } }); - }, + }; - onBlur: function(ev) { + onBlur = ev => { const sel = window.getSelection(); sel.removeAllRanges(); @@ -211,13 +204,15 @@ export default createReactClass({ } this.showPlaceholder(!this.value); - }, + }; - render: function() { + render() { const {className, editable, initialValue, label, labelClassName} = this.props; let editableEl; - if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) { + if (!editable || (this.state.phase === EditableText.Phases.Display && + (label || labelClassName) && !this.value) + ) { // show the label editableEl =
{ label || initialValue } @@ -234,5 +229,5 @@ export default createReactClass({ } return editableEl; - }, -}); + } +} diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index a043b350ab..68bec667d8 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -72,7 +72,7 @@ export default class ErrorBoundary extends React.PureComponent { render() { if (this.state.error) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new"; + const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; return

{_t("Something went wrong!")}

diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 7d8b774955..61e5f5381d 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -39,11 +39,13 @@ interface IProps { className: string; } +/* eslint-disable camelcase */ interface IState { userId: string; displayname: string; avatar_url: string; } +/* eslint-enable camelcase */ const AVATAR_SIZE = 32; @@ -63,19 +65,18 @@ export default class EventTilePreview extends React.Component { const client = MatrixClientPeg.get(); const userId = client.getUserId(); const profileInfo = await client.getProfileInfo(userId); - const avatar_url = Avatar.avatarUrlForUser( + const avatarUrl = Avatar.avatarUrlForUser( {avatarUrl: profileInfo.avatar_url}, AVATAR_SIZE, AVATAR_SIZE, "crop"); this.setState({ userId, displayname: profileInfo.displayname, - avatar_url, + avatar_url: avatarUrl, }); - } - private fakeEvent({userId, displayname, avatar_url}: IState) { + private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) { // Fake it till we make it const event = new MatrixEvent(JSON.parse(`{ "type": "m.room.message", @@ -85,12 +86,12 @@ export default class EventTilePreview extends React.Component { "msgtype": "m.text", "body": "${this.props.message}", "displayname": "${displayname}", - "avatar_url": "${avatar_url}" + "avatar_url": "${avatarUrl}" }, "msgtype": "m.text", "body": "${this.props.message}", "displayname": "${displayname}", - "avatar_url": "${avatar_url}" + "avatar_url": "${avatarUrl}" }, "unsigned": { "age": 97 @@ -104,7 +105,7 @@ export default class EventTilePreview extends React.Component { name: displayname, userId: userId, getAvatarUrl: (..._) => { - return avatar_url; + return avatarUrl; }, }; @@ -114,13 +115,10 @@ export default class EventTilePreview extends React.Component { public render() { const event = this.fakeEvent(this.state); - let className = classnames( - this.props.className, - { - "mx_IRCLayout": this.props.useIRCLayout, - "mx_GroupLayout": !this.props.useIRCLayout, - } - ); + const className = classnames(this.props.className, { + "mx_IRCLayout": this.props.useIRCLayout, + "mx_GroupLayout": !this.props.useIRCLayout, + }); return
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index d9fd59dc11..7fd154047d 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react'; import classNames from 'classnames'; import * as sdk from '../../../index'; -import { debounce } from 'lodash'; +import {debounce} from "lodash"; import {IFieldState, IValidationResult} from "./Validation"; // Invoke validation from user input (when typing, etc.) at most once every N ms. @@ -198,11 +198,9 @@ export default class Field extends React.PureComponent { } } - - public render() { - const { - element, prefixComponent, postfixComponent, className, onValidate, children, + /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ + const { element, prefixComponent, postfixComponent, className, onValidate, children, tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props; // Set some defaults for the element diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 1098d0293e..ecd63816de 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -78,7 +78,12 @@ export default class IRCTimelineProfileResizer extends React.Component +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. +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 classNames from 'classnames'; + +import Tooltip from './Tooltip'; +import { _t } from "../../../languageHandler"; + +interface ITooltipProps { + tooltip?: React.ReactNode; + tooltipClassName?: string; +} + +interface IState { + hover: boolean; +} + +export default class InfoTooltip extends React.PureComponent { + constructor(props: ITooltipProps) { + super(props); + this.state = { + hover: false, + }; + } + + onMouseOver = () => { + this.setState({ + hover: true, + }); + }; + + onMouseLeave = () => { + this.setState({ + hover: false, + }); + }; + + render() { + const {tooltip, children, tooltipClassName} = this.props; + const title = _t("Information"); + + // Tooltip are forced on the right for a more natural feel to them on info icons + const tip = this.state.hover ? :
; + return ( +
+ + {children} + {tip} +
+ ); + } +} diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index 89b5e6f19d..73316157f4 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -15,20 +15,17 @@ limitations under the License. */ import React from "react"; -import createReactClass from 'create-react-class'; import {_t} from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; -export default createReactClass({ - displayName: 'InlineSpinner', - - render: function() { +export default class InlineSpinner extends React.Component { + render() { const w = this.props.w || 16; const h = this.props.h || 16; const imgClass = this.props.imgClassName || ""; let imageSource; - if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { + if (SettingsStore.getValue('feature_new_spinner')) { imageSource = require("../../../../res/img/spinner.svg"); } else { imageSource = require("../../../../res/img/spinner.gif"); @@ -45,5 +42,5 @@ export default createReactClass({ />
); - }, -}); + } +} diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index ac8a98a94a..0990218c65 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -34,7 +34,7 @@ export default class ManageIntegsButton extends React.Component { if (!managers.hasManager()) { managers.openNoManagerDialog(); } else { - if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + if (SettingsStore.getValue("feature_many_integration_managers")) { managers.openAll(this.props.room); } else { managers.getPrimaryManager().open(this.props.room); diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 956b69ca7b..e16b52c8a2 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -18,17 +18,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import * as sdk from "../../../index"; import {MatrixEvent} from "matrix-js-sdk"; import {isValid3pidInvite} from "../../../RoomInvite"; -export default createReactClass({ - displayName: 'MemberEventListSummary', - - propTypes: { +export default class MemberEventListSummary extends React.Component { + static propTypes = { // An array of member events to summarise events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired, // An array of EventTiles to render when expanded @@ -43,17 +40,15 @@ export default createReactClass({ onToggle: PropTypes.func, // Whether or not to begin with state.expanded=true startExpanded: PropTypes.bool, - }, + }; - getDefaultProps: function() { - return { - summaryLength: 1, - threshold: 3, - avatarsMaxLength: 5, - }; - }, + static defaultProps = { + summaryLength: 1, + threshold: 3, + avatarsMaxLength: 5, + }; - shouldComponentUpdate: function(nextProps) { + shouldComponentUpdate(nextProps) { // Update if // - The number of summarised events has changed // - or if the summary is about to toggle to become collapsed @@ -62,7 +57,7 @@ export default createReactClass({ nextProps.events.length !== this.props.events.length || nextProps.events.length < this.props.threshold ); - }, + } /** * Generate the text for users aggregated by their transition sequences (`eventAggregates`) where @@ -73,7 +68,7 @@ export default createReactClass({ * `Object.keys(eventAggregates)`. * @returns {string} the textual summary of the aggregated events that occurred. */ - _generateSummary: function(eventAggregates, orderedTransitionSequences) { + _generateSummary(eventAggregates, orderedTransitionSequences) { const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this._renderNameList(userNames); @@ -105,7 +100,7 @@ export default createReactClass({ } return summaries.join(", "); - }, + } /** * @param {string[]} users an array of user display names or user IDs. @@ -113,9 +108,9 @@ export default createReactClass({ * more items in `users` than `this.props.summaryLength`, which is the number of names * included before "and [n] others". */ - _renderNameList: function(users) { + _renderNameList(users) { return formatCommaSeparatedList(users, this.props.summaryLength); - }, + } /** * Canonicalise an array of transitions such that some pairs of transitions become @@ -124,7 +119,7 @@ export default createReactClass({ * @param {string[]} transitions an array of transitions. * @returns {string[]} an array of transitions. */ - _getCanonicalTransitions: function(transitions) { + _getCanonicalTransitions(transitions) { const modMap = { 'joined': { 'after': 'left', @@ -155,7 +150,7 @@ export default createReactClass({ res.push(transition); } return res; - }, + } /** * Transform an array of transitions into an array of transitions and how many times @@ -171,7 +166,7 @@ export default createReactClass({ * @param {string[]} transitions the array of transitions to transform. * @returns {object[]} an array of coalesced transitions. */ - _coalesceRepeatedTransitions: function(transitions) { + _coalesceRepeatedTransitions(transitions) { const res = []; for (let i = 0; i < transitions.length; i++) { if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) { @@ -184,7 +179,7 @@ export default createReactClass({ } } return res; - }, + } /** * For a certain transition, t, describe what happened to the users that @@ -268,11 +263,11 @@ export default createReactClass({ } return res; - }, + } - _getTransitionSequence: function(events) { + _getTransitionSequence(events) { return events.map(this._getTransition); - }, + } /** * Label a given membership event, `e`, where `getContent().membership` has @@ -282,7 +277,7 @@ export default createReactClass({ * @returns {string?} the transition type given to this event. This defaults to `null` * if a transition is not recognised. */ - _getTransition: function(e) { + _getTransition(e) { if (e.mxEvent.getType() === 'm.room.third_party_invite') { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { @@ -323,9 +318,9 @@ export default createReactClass({ } default: return null; } - }, + } - _getAggregate: function(userEvents) { + _getAggregate(userEvents) { // A map of aggregate type to arrays of display names. Each aggregate type // is a comma-delimited string of transitions, e.g. "joined,left,kicked". // The array of display names is the array of users who went through that @@ -364,9 +359,9 @@ export default createReactClass({ names: aggregate, indices: aggregateIndices, }; - }, + } - render: function() { + render() { const eventsToRender = this.props.events; // Map user IDs to an array of objects: @@ -420,5 +415,5 @@ export default createReactClass({ children={this.props.children} summaryMembers={avatarMembers} summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />; - }, -}); + } +} diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index a146debc45..bdf5f60234 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -16,49 +16,44 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import RoomViewStore from '../../../stores/RoomViewStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import WidgetUtils from '../../../utils/WidgetUtils'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -export default createReactClass({ - displayName: 'PersistentApp', +export default class PersistentApp extends React.Component { + state = { + roomId: RoomViewStore.getRoomId(), + persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), + }; - getInitialState: function() { - return { - roomId: RoomViewStore.getRoomId(), - persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), - }; - }, - - componentDidMount: function() { + componentDidMount() { this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { if (this._roomStoreToken) { this._roomStoreToken.remove(); } ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate); - }, + } - _onRoomViewStoreUpdate: function(payload) { + _onRoomViewStoreUpdate = payload => { if (RoomViewStore.getRoomId() === this.state.roomId) return; this.setState({ roomId: RoomViewStore.getRoomId(), }); - }, + }; - _onActiveWidgetStoreUpdate: function() { + _onActiveWidgetStoreUpdate = () => { this.setState({ persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), }); - }, + }; - render: function() { + render() { if (this.state.persistentWidgetId) { const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); if (this.state.roomId !== persistentWidgetInRoomId) { @@ -91,6 +86,6 @@ export default createReactClass({ } } return null; - }, -}); + } +} diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 03a1aeed85..8247225a2b 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -16,7 +16,6 @@ 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/dispatcher'; import classNames from 'classnames'; @@ -32,27 +31,29 @@ import {Action} from "../../../dispatcher/actions"; // HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/; -const Pill = createReactClass({ - statics: { - isPillUrl: (url) => { - return !!getPrimaryPermalinkEntity(url); - }, - isMessagePillUrl: (url) => { - return !!REGEX_LOCAL_PERMALINK.exec(url); - }, - roomNotifPos: (text) => { - return text.indexOf("@room"); - }, - roomNotifLen: () => { - return "@room".length; - }, - TYPE_USER_MENTION: 'TYPE_USER_MENTION', - TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION', - TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION', - TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention - }, +class Pill extends React.Component { + static isPillUrl(url) { + return !!getPrimaryPermalinkEntity(url); + } - props: { + static isMessagePillUrl(url) { + return !!REGEX_LOCAL_PERMALINK.exec(url); + } + + static roomNotifPos(text) { + return text.indexOf("@room"); + } + + static roomNotifLen() { + return "@room".length; + } + + static TYPE_USER_MENTION = 'TYPE_USER_MENTION'; + static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION'; + static TYPE_GROUP_MENTION = 'TYPE_GROUP_MENTION'; + static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention + + static propTypes = { // The Type of this Pill. If url is given, this is auto-detected. type: PropTypes.string, // The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl) @@ -65,25 +66,24 @@ const Pill = createReactClass({ shouldShowPillAvatar: PropTypes.bool, // Whether to render this pill as if it were highlit by a selection isSelected: PropTypes.bool, - }, + }; - getInitialState() { - return { - // ID/alias of the room/user - resourceId: null, - // Type of pill - pillType: null, + state = { + // ID/alias of the room/user + resourceId: null, + // Type of pill + pillType: null, - // The member related to the user pill - member: null, - // The group related to the group pill - group: null, - // The room related to the room pill - room: null, - }; - }, + // The member related to the user pill + member: null, + // The group related to the group pill + group: null, + // The room related to the room pill + room: null, + }; // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + // eslint-disable-next-line camelcase async UNSAFE_componentWillReceiveProps(nextProps) { let resourceId; let prefix; @@ -155,7 +155,7 @@ const Pill = createReactClass({ } } this.setState({resourceId, pillType, member, group, room}); - }, + } componentDidMount() { this._unmounted = false; @@ -163,13 +163,13 @@ const Pill = createReactClass({ // eslint-disable-next-line new-cap this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves. - }, + } componentWillUnmount() { this._unmounted = true; - }, + } - doProfileLookup: function(userId, member) { + doProfileLookup(userId, member) { MatrixClientPeg.get().getProfileInfo(userId).then((resp) => { if (this._unmounted) { return; @@ -188,15 +188,16 @@ const Pill = createReactClass({ }).catch((err) => { console.error('Could not retrieve profile data for ' + userId + ':', err); }); - }, + } - onUserPillClicked: function() { + onUserPillClicked = () => { dis.dispatch({ action: Action.ViewUser, member: this.state.member, }); - }, - render: function() { + }; + + render() { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); @@ -285,7 +286,7 @@ const Pill = createReactClass({ // Deliberately render nothing if the URL isn't recognised return null; } - }, -}); + } +} export default Pill; diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 948b4835d5..e5f217dd90 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -16,16 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import * as Roles from '../../../Roles'; import { _t } from '../../../languageHandler'; import Field from "./Field"; import {Key} from "../../../Keyboard"; -export default createReactClass({ - displayName: 'PowerSelector', - - propTypes: { +export default class PowerSelector extends React.Component { + static propTypes = { value: PropTypes.number.isRequired, // The maximum value that can be set with the power selector maxValue: PropTypes.number.isRequired, @@ -42,10 +39,17 @@ export default createReactClass({ // The name to annotate the selector with label: PropTypes.string, - }, + } - getInitialState: function() { - return { + static defaultProps = { + maxValue: Infinity, + usersDefault: 0, + }; + + constructor(props) { + super(props); + + this.state = { levelRoleMap: {}, // List of power levels to show in the drop-down options: [], @@ -53,26 +57,17 @@ export default createReactClass({ customValue: this.props.value, selectValue: 0, }; - }, - getDefaultProps: function() { - return { - maxValue: Infinity, - usersDefault: 0, - }; - }, - - componentDidMount: function() { - // TODO: [REACT-WARNING] Move this to class constructor this._initStateFromProps(this.props); - }, + } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(newProps) { this._initStateFromProps(newProps); - }, + } - _initStateFromProps: function(newProps) { + _initStateFromProps(newProps) { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const options = Object.keys(levelRoleMap).filter(level => { @@ -92,9 +87,9 @@ export default createReactClass({ customLevel: newProps.value, selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value, }); - }, + } - onSelectChange: function(event) { + onSelectChange = event => { const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; if (isCustom) { this.setState({custom: true}); @@ -102,20 +97,20 @@ export default createReactClass({ this.props.onChange(event.target.value, this.props.powerLevelKey); this.setState({selectValue: event.target.value}); } - }, + }; - onCustomChange: function(event) { + onCustomChange = event => { this.setState({customValue: event.target.value}); - }, + }; - onCustomBlur: function(event) { + onCustomBlur = event => { event.preventDefault(); event.stopPropagation(); this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey); - }, + }; - onCustomKeyDown: function(event) { + onCustomKeyDown = event => { if (event.key === Key.ENTER) { event.preventDefault(); event.stopPropagation(); @@ -127,9 +122,9 @@ export default createReactClass({ // handle the onBlur safely. event.target.blur(); } - }, + }; - render: function() { + render() { let picker; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { @@ -166,5 +161,5 @@ export default createReactClass({ { picker }
); - }, -}); + } +} diff --git a/src/components/views/elements/QRCode.tsx b/src/components/views/elements/QRCode.tsx index f70ab48fa3..9ce3dc7202 100644 --- a/src/components/views/elements/QRCode.tsx +++ b/src/components/views/elements/QRCode.tsx @@ -41,7 +41,7 @@ const QRCode: React.FC = ({data, className, ...options}) => { return () => { cancelled = true; }; - }, [JSON.stringify(data), options]); + }, [JSON.stringify(data), options]); // eslint-disable-line react-hooks/exhaustive-deps return
{ dataUri ? {_t("QR : } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 6c038a0ddb..e9407156d8 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -45,8 +45,8 @@ export default class ReplyThread extends React.Component { static contextType = MatrixClientContext; - constructor(props) { - super(props); + constructor(props, context) { + super(props, context); this.state = { // The loaded events to be rendered as linear-replies @@ -335,8 +335,14 @@ export default class ReplyThread extends React.Component { { _t('In reply to ', {}, { 'a': (sub) => { sub }, - 'pill': , + 'pill': ( + + ), }) } ; diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index a88c581d07..b7c8e1b533 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -45,7 +45,7 @@ export default class Slider extends React.Component { // non linear slider. private offset(values: number[], value: number): number { // the index of the first number greater than value. - let closest = values.reduce((prev, curr) => { + const closest = values.reduce((prev, curr) => { return (value > curr ? prev + 1 : prev); }, 0); @@ -68,17 +68,16 @@ export default class Slider extends React.Component { const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue); return 100 * (closest - 1 + linearInterpolation) * intervalWidth; - } render(): React.ReactNode { - const dots = this.props.values.map(v => - {} : () => this.props.onSelectionChange(v)} - key={v} - disabled={this.props.disabled} - />); + const dots = this.props.values.map(v => {} : () => this.props.onSelectionChange(v)} + key={v} + disabled={this.props.disabled} + />); let selection = null; @@ -93,7 +92,7 @@ export default class Slider extends React.Component { return
-
{} : this.onClick.bind(this)}/> +
{} : this.onClick.bind(this)} /> { selection }
diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index 033d4d13f4..4d2dcea90a 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore"; const Spinner = ({w = 32, h = 32, imgClassName, message}) => { let imageSource; - if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { + if (SettingsStore.getValue('feature_new_spinner')) { imageSource = require("../../../../res/img/spinner.svg"); } else { imageSource = require("../../../../res/img/spinner.gif"); diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index be983828ff..f8d2665d07 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -17,8 +17,6 @@ limitations under the License. import React from "react"; import { randomString } from "matrix-js-sdk/src/randomstring"; -const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg"); - interface IProps extends React.InputHTMLAttributes { } @@ -39,13 +37,14 @@ export default class StyledCheckbox extends React.PureComponent } public render() { + /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { children, className, ...otherProps } = this.props; return