Merge branch 'develop' into hs/custom-notif-sounds

pull/21833/head
Will Hunt 2019-05-07 20:04:29 +01:00 committed by GitHub
commit efc93abb50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 2034 additions and 1165 deletions

View File

@ -79,3 +79,13 @@ steps:
- docker#v3.0.1: - docker#v3.0.1:
image: "node:10" image: "node:10"
propagate-environment: true propagate-environment: true
- wait
- label: "🐴 Trigger riot-web"
trigger: "riot-web"
branches: "develop"
build:
branch: "develop"
message: "[react-sdk] ${BUILDKITE_MESSAGE}"
async: true

View File

@ -10,7 +10,6 @@ src/components/structures/RoomStatusBar.js
src/components/structures/RoomView.js src/components/structures/RoomView.js
src/components/structures/ScrollPanel.js src/components/structures/ScrollPanel.js
src/components/structures/SearchBox.js src/components/structures/SearchBox.js
src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js src/components/structures/UploadBar.js
src/components/views/avatars/BaseAvatar.js src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js src/components/views/avatars/MemberAvatar.js

3
.gitignore vendored
View File

@ -9,9 +9,6 @@ package-lock.json
/git-revision.txt /git-revision.txt
/matrix-react-sdk-*.tgz /matrix-react-sdk-*.tgz
# test reports created by karma
/karma-reports
/.idea /.idea
/src/component-index.js /src/component-index.js

View File

@ -1,3 +1,196 @@
Changes in [1.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.0) (2019-05-07)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.0-rc.1...v1.1.0)
* Relax password requirements to score of 3 out of 4
[\#2949](https://github.com/matrix-org/matrix-react-sdk/pull/2949)
* Restore access to message quote option on first click
[\#2948](https://github.com/matrix-org/matrix-react-sdk/pull/2948)
* Check for `room` in all `Room.timeline*` handlers
[\#2946](https://github.com/matrix-org/matrix-react-sdk/pull/2946)
Changes in [1.1.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.0-rc.1) (2019-04-30)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.7...v1.1.0-rc.1)
* Add important info to new preview bar
[\#2936](https://github.com/matrix-org/matrix-react-sdk/pull/2936)
* Add a message action bar
[\#2935](https://github.com/matrix-org/matrix-react-sdk/pull/2935)
* Trigger riot-web build
[\#2934](https://github.com/matrix-org/matrix-react-sdk/pull/2934)
* Input validation tooltips for registration
[\#2933](https://github.com/matrix-org/matrix-react-sdk/pull/2933)
* Also say "Connect ..." on remaining key backup buttons
[\#2931](https://github.com/matrix-org/matrix-react-sdk/pull/2931)
* Mark a few CSS classes as not selectable
[\#2929](https://github.com/matrix-org/matrix-react-sdk/pull/2929)
* Cleanup message composer render() method
[\#2883](https://github.com/matrix-org/matrix-react-sdk/pull/2883)
* Redesigned room preview bar
[\#2925](https://github.com/matrix-org/matrix-react-sdk/pull/2925)
* Prevent user pills containing only emoji from embiggening
[\#2907](https://github.com/matrix-org/matrix-react-sdk/pull/2907)
* Make alt-enter insert new line on macOS
[\#2923](https://github.com/matrix-org/matrix-react-sdk/pull/2923)
* Test `defaultServerName` before showing it on forgot password
[\#2924](https://github.com/matrix-org/matrix-react-sdk/pull/2924)
* Add a function to append/overwrite objects in the config on the fly
[\#2922](https://github.com/matrix-org/matrix-react-sdk/pull/2922)
* use SdkConfig brand name instead of static "Riot"
[\#2921](https://github.com/matrix-org/matrix-react-sdk/pull/2921)
* Use dedicated permalink creators in search results with multiple rooms
[\#2898](https://github.com/matrix-org/matrix-react-sdk/pull/2898)
* Clarify that use backup means restore
[\#2917](https://github.com/matrix-org/matrix-react-sdk/pull/2917)
* Fix key backup status when missing device
[\#2919](https://github.com/matrix-org/matrix-react-sdk/pull/2919)
* Ensure `<b>` tags appear bold for all browsers
[\#2918](https://github.com/matrix-org/matrix-react-sdk/pull/2918)
* Add a link in room settings to get at the tombstoned room if it exists
[\#2908](https://github.com/matrix-org/matrix-react-sdk/pull/2908)
* Add a generic error page element for startup errors
[\#2915](https://github.com/matrix-org/matrix-react-sdk/pull/2915)
* Add strings for js-sdk autodiscovery errors
[\#2916](https://github.com/matrix-org/matrix-react-sdk/pull/2916)
* Focus the composer view on file upload
[\#2914](https://github.com/matrix-org/matrix-react-sdk/pull/2914)
* use medium agent for e2e tests
[\#2911](https://github.com/matrix-org/matrix-react-sdk/pull/2911)
* adjust prop in HeaderButton
[\#2912](https://github.com/matrix-org/matrix-react-sdk/pull/2912)
* Remove breadcrumb scroll tolerances and use sensible defaults
[\#2913](https://github.com/matrix-org/matrix-react-sdk/pull/2913)
* Fix having to click the member list button twice to show it after having
changed room.
[\#2906](https://github.com/matrix-org/matrix-react-sdk/pull/2906)
* Add period to the end of upgrade notice
[\#2909](https://github.com/matrix-org/matrix-react-sdk/pull/2909)
* Remove duplicate space in credits
[\#2889](https://github.com/matrix-org/matrix-react-sdk/pull/2889)
* Handle M_UNSUPPORTED_ROOM_VERSION in invites and room creation
[\#2905](https://github.com/matrix-org/matrix-react-sdk/pull/2905)
* Re-enable E2E tests
[\#2867](https://github.com/matrix-org/matrix-react-sdk/pull/2867)
* Remove BottomLeftMenu and supporting bits
[\#2903](https://github.com/matrix-org/matrix-react-sdk/pull/2903)
* Fix for retina thumbnails being massive
[\#2439](https://github.com/matrix-org/matrix-react-sdk/pull/2439)
* Send breadcrumb updates only when they change
[\#2894](https://github.com/matrix-org/matrix-react-sdk/pull/2894)
* Add some tolerances to breadcrumb scrolling
[\#2892](https://github.com/matrix-org/matrix-react-sdk/pull/2892)
* Fix validation to avoid `undefined` class on fields
[\#2902](https://github.com/matrix-org/matrix-react-sdk/pull/2902)
* Always return a client from onRegistered
[\#2895](https://github.com/matrix-org/matrix-react-sdk/pull/2895)
* Fix room upgrade warnings popping up in upgraded rooms
[\#2897](https://github.com/matrix-org/matrix-react-sdk/pull/2897)
* Fix style lint errors & enable on CI
[\#2901](https://github.com/matrix-org/matrix-react-sdk/pull/2901)
* Add stylelint
[\#2900](https://github.com/matrix-org/matrix-react-sdk/pull/2900)
* Key backup: Handle case where your onw sig is invalid
[\#2899](https://github.com/matrix-org/matrix-react-sdk/pull/2899)
* Simplify settings dialog CSS
[\#2891](https://github.com/matrix-org/matrix-react-sdk/pull/2891)
* Fix upload cancel in e2e rooms
[\#2893](https://github.com/matrix-org/matrix-react-sdk/pull/2893)
* Set E2E room status to warning when crypto is disabled
[\#2890](https://github.com/matrix-org/matrix-react-sdk/pull/2890)
* Move SettingsDialog width override to fixedWidth
[\#2888](https://github.com/matrix-org/matrix-react-sdk/pull/2888)
* Prevent the permalink creator from causing cascading failure
[\#2882](https://github.com/matrix-org/matrix-react-sdk/pull/2882)
* Don't include all networks by default in the room directory
[\#2881](https://github.com/matrix-org/matrix-react-sdk/pull/2881)
* Fix fixed width dialogs
[\#2886](https://github.com/matrix-org/matrix-react-sdk/pull/2886)
* Fix settings dialog layout
[\#2885](https://github.com/matrix-org/matrix-react-sdk/pull/2885)
* Update from Weblate
[\#2884](https://github.com/matrix-org/matrix-react-sdk/pull/2884)
* Design tweaks to dialogs
[\#2868](https://github.com/matrix-org/matrix-react-sdk/pull/2868)
* Remove 'try the app' link from login
[\#2880](https://github.com/matrix-org/matrix-react-sdk/pull/2880)
* Track store failures after startup
[\#2870](https://github.com/matrix-org/matrix-react-sdk/pull/2870)
* Translate vertical scrolling to horizontal movement in breadcrumbs
[\#2877](https://github.com/matrix-org/matrix-react-sdk/pull/2877)
* Add telemetry for breadcrumbs and have the setting apply without refresh
[\#2873](https://github.com/matrix-org/matrix-react-sdk/pull/2873)
* Fix a few bugs introduced in file upload rework
[\#2879](https://github.com/matrix-org/matrix-react-sdk/pull/2879)
* Sync breadcrumb rooms through account data
[\#2875](https://github.com/matrix-org/matrix-react-sdk/pull/2875)
* Scroll breadcrumbs to the left when they change
[\#2878](https://github.com/matrix-org/matrix-react-sdk/pull/2878)
* Add an indicator to show a room is a direct chat in breadcrumbs
[\#2874](https://github.com/matrix-org/matrix-react-sdk/pull/2874)
* Use the most recent version of the room in breadcrumbs
[\#2872](https://github.com/matrix-org/matrix-react-sdk/pull/2872)
* Autohide the scrollbar on breadcrumbs
[\#2876](https://github.com/matrix-org/matrix-react-sdk/pull/2876)
* Ensure the page URL is redacted before tracking analytics events
[\#2871](https://github.com/matrix-org/matrix-react-sdk/pull/2871)
* fix NPE for rooms with redacted tombstones
[\#2869](https://github.com/matrix-org/matrix-react-sdk/pull/2869)
* Don't re-init the stickerpicker unless something actually changes
[\#2862](https://github.com/matrix-org/matrix-react-sdk/pull/2862)
* Add option to rotate images
[\#2855](https://github.com/matrix-org/matrix-react-sdk/pull/2855)
* Add badges to breadcrumb rooms
[\#2861](https://github.com/matrix-org/matrix-react-sdk/pull/2861)
* Include the current power level in the selector
[\#2866](https://github.com/matrix-org/matrix-react-sdk/pull/2866)
* Apply 50% opacity to left breadcrumbs
[\#2860](https://github.com/matrix-org/matrix-react-sdk/pull/2860)
* Small scroll fixes
[\#2865](https://github.com/matrix-org/matrix-react-sdk/pull/2865)
* Put the stickerpicker below dialogs
[\#2863](https://github.com/matrix-org/matrix-react-sdk/pull/2863)
* Logging tweaks
[\#2864](https://github.com/matrix-org/matrix-react-sdk/pull/2864)
* Implement redesigned upload confirmation screens
[\#2858](https://github.com/matrix-org/matrix-react-sdk/pull/2858)
* Use Field component in bug report dialog
[\#2859](https://github.com/matrix-org/matrix-react-sdk/pull/2859)
* Notify user when crypto data is missing
[\#2841](https://github.com/matrix-org/matrix-react-sdk/pull/2841)
* Update from Weblate
[\#2857](https://github.com/matrix-org/matrix-react-sdk/pull/2857)
* Download PDFs as blobs to avoid empty grey screens
[\#2847](https://github.com/matrix-org/matrix-react-sdk/pull/2847)
* Set title attribute on images in lightbox
[\#2848](https://github.com/matrix-org/matrix-react-sdk/pull/2848)
* Add MemberInfo for 3pid invites and support revoking those invites
[\#2843](https://github.com/matrix-org/matrix-react-sdk/pull/2843)
* round scrollTop upwards to prevent never detecting bottom
[\#2846](https://github.com/matrix-org/matrix-react-sdk/pull/2846)
* Notifier is how singleton is known outside of this module
[\#2845](https://github.com/matrix-org/matrix-react-sdk/pull/2845)
* Delay `Notifier` check until we have push rules
[\#2844](https://github.com/matrix-org/matrix-react-sdk/pull/2844)
* BACAT Scrolling
[\#2842](https://github.com/matrix-org/matrix-react-sdk/pull/2842)
* Handle storage fallback cases in consistency check
[\#2840](https://github.com/matrix-org/matrix-react-sdk/pull/2840)
* Handle all the segments of a v3 event ID
[\#2827](https://github.com/matrix-org/matrix-react-sdk/pull/2827)
* Add custom tooltips and scrolling to breadcrumbs
[\#2839](https://github.com/matrix-org/matrix-react-sdk/pull/2839)
* Check if the message panel is at the end of the timeline on init
[\#2829](https://github.com/matrix-org/matrix-react-sdk/pull/2829)
* Persist breadcrumb state between sessions
[\#2837](https://github.com/matrix-org/matrix-react-sdk/pull/2837)
* Always append the current room to the breadcrumbs
[\#2838](https://github.com/matrix-org/matrix-react-sdk/pull/2838)
* Alert the user to unread notifications in prior versions of rooms
[\#2831](https://github.com/matrix-org/matrix-react-sdk/pull/2831)
* Filter out upgraded rooms from autocomplete results
[\#2830](https://github.com/matrix-org/matrix-react-sdk/pull/2830)
Changes in [1.0.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.7) (2019-04-08) Changes in [1.0.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.7) (2019-04-08)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.6...v1.0.7) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.6...v1.0.7)

View File

@ -229,7 +229,7 @@ Controllers are notified of changes by the `SettingsStore`, and are given the op
### Features ### Features
Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is 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 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 the option of being forced-on or forced-off for the application. This is done by the `features` section and looks
something like this: something like this:
@ -260,4 +260,4 @@ In practice, handlers which rely on remote changes (account data, room events, e
generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers
which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the
setting themselves as there's nothing to really 'watch'. setting themselves as there's nothing to really 'watch'.

View File

@ -94,7 +94,7 @@ module.exports = function (config) {
// test results reporter to use // test results reporter to use
// possible values: 'dots', 'progress' // possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter // available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['logcapture', 'spec', 'junit', 'summary'], reporters: ['logcapture', 'spec', 'summary'],
specReporter: { specReporter: {
suppressErrorSummary: false, // do print error summary suppressErrorSummary: false, // do print error summary
@ -156,10 +156,6 @@ module.exports = function (config) {
// how many browser should be started simultaneous // how many browser should be started simultaneous
concurrency: Infinity, concurrency: Infinity,
junitReporter: {
outputDir: 'karma-reports',
},
webpack: { webpack: {
module: { module: {
rules: [ rules: [

View File

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "1.0.7", "version": "1.1.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -80,7 +80,7 @@
"linkifyjs": "^2.1.6", "linkifyjs": "^2.1.6",
"lodash": "^4.13.1", "lodash": "^4.13.1",
"lolex": "2.3.2", "lolex": "2.3.2",
"matrix-js-sdk": "1.0.4", "matrix-js-sdk": "1.1.0",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"pako": "^1.0.5", "pako": "^1.0.5",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
@ -136,7 +136,6 @@
"karma": "^4.0.1", "karma": "^4.0.1",
"karma-chrome-launcher": "^2.2.0", "karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1", "karma-cli": "^1.0.1",
"karma-junit-reporter": "^2.0.0",
"karma-logcapture-reporter": "0.0.1", "karma-logcapture-reporter": "0.0.1",
"karma-mocha": "^1.3.0", "karma-mocha": "^1.3.0",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",

View File

@ -100,6 +100,7 @@
@import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToggleSwitch.scss";
@import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_ToolTipButton.scss";
@import "./views/elements/_Tooltip.scss"; @import "./views/elements/_Tooltip.scss";
@import "./views/elements/_Validation.scss";
@import "./views/globals/_MatrixToolbar.scss"; @import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupRoomList.scss";
@ -112,7 +113,10 @@
@import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MNoticeBody.scss";
@import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_ReactionsRow.scss";
@import "./views/messages/_ReactionsRowButton.scss";
@import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_RoomAvatarEvent.scss";
@import "./views/messages/_SenderProfile.scss"; @import "./views/messages/_SenderProfile.scss";
@import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_TextualEvent.scss";

View File

@ -130,3 +130,27 @@ limitations under the License.
.mx_AuthBody_spinner { .mx_AuthBody_spinner {
margin: 1em 0; margin: 1em 0;
} }
.mx_AuthBody_passwordScore {
width: 100%;
appearance: none;
height: 4px;
border: 0;
border-radius: 2px;
position: absolute;
top: -12px;
&::-moz-progress-bar {
border-radius: 2px;
background-color: $accent-color;
}
&::-webkit-progress-bar,
&::-webkit-progress-value {
border-radius: 2px;
}
&::-webkit-progress-value {
background-color: $accent-color;
}
}

View File

@ -26,6 +26,7 @@ limitations under the License.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 // https://bugzilla.mozilla.org/show_bug.cgi?id=1535053
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139 // https://bugzilla.mozilla.org/show_bug.cgi?id=255139
display: inline-block; display: inline-block;
user-select: none;
} }
.mx_BaseAvatar_initial { .mx_BaseAvatar_initial {

View File

@ -168,6 +168,7 @@ limitations under the License.
.mx_Field_tooltip { .mx_Field_tooltip {
margin-top: -12px; margin-top: -12px;
margin-left: 4px; margin-left: 4px;
width: 200px;
} }
.mx_Field_tooltip.mx_Field_valid { .mx_Field_tooltip.mx_Field_valid {

View File

@ -50,7 +50,6 @@ limitations under the License.
.mx_Tooltip { .mx_Tooltip {
display: none; display: none;
animation: mx_fadein 0.2s;
position: fixed; position: fixed;
border: 1px solid $menu-border-color; border: 1px solid $menu-border-color;
border-radius: 4px; border-radius: 4px;
@ -66,4 +65,12 @@ limitations under the License.
max-width: 200px; max-width: 200px;
word-break: break-word; word-break: break-word;
margin-right: 50px; margin-right: 50px;
&.mx_Tooltip_visible {
animation: mx_fadein 0.2s forwards;
}
&.mx_Tooltip_invisible {
animation: mx_fadeout 0.1s forwards;
}
} }

View File

@ -0,0 +1,69 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_Validation {
position: relative;
}
.mx_Validation_details {
padding-left: 20px;
margin: 0;
}
.mx_Validation_description + .mx_Validation_details {
margin: 1em 0 0;
}
.mx_Validation_detail {
position: relative;
font-weight: normal;
list-style: none;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
&::before {
content: "";
position: absolute;
width: 14px;
height: 14px;
top: 0;
left: -18px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
&.mx_Validation_valid {
color: $input-valid-border-color;
&::before {
mask-image: url('$(res)/img/feather-customised/check.svg');
background-color: $input-valid-border-color;
}
}
&.mx_Validation_invalid {
color: $input-invalid-border-color;
&::before {
mask-image: url('$(res)/img/feather-customised/x.svg');
background-color: $input-invalid-border-color;
}
}
}

View File

@ -0,0 +1,84 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MessageActionBar {
position: absolute;
visibility: hidden;
cursor: pointer;
display: flex;
height: 24px;
line-height: 24px;
border-radius: 4px;
background: $message-action-bar-bg-color;
top: -13px;
right: 8px;
user-select: none;
> * {
display: inline-block;
position: relative;
width: 27px;
border: 1px solid $message-action-bar-border-color;
margin-left: -1px;
&:hover {
border-color: $message-action-bar-hover-border-color;
z-index: 1;
}
&:first-child {
border-radius: 3px 0 0 3px;
}
&:last-child {
border-radius: 0 3px 3px 0;
}
&:only-child {
border-radius: 3px;
}
}
}
.mx_MessageActionBar_maskButton::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
mask-repeat: no-repeat;
mask-position: center;
background-color: $message-action-bar-fg-color;
}
.mx_MessageActionBar_replyButton::after {
mask-image: url('$(res)/img/reply.svg');
}
.mx_MessageActionBar_optionsButton::after {
mask-image: url('$(res)/img/icon_context.svg');
}
.mx_MessageActionBar_reactionDimension {
width: 42px;
display: flex;
justify-content: space-evenly;
}
.mx_MessageActionBar_reactionDisabled {
opacity: 0.4;
}

View File

@ -0,0 +1,19 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ReactionsRow {
margin: 6px 0;
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ReactionsRowButton {
display: inline-block;
height: 20px;
line-height: 21px;
margin-right: 6px;
padding: 0 6px;
border: 1px solid $reaction-row-button-border-color;
border-radius: 10px;
background-color: $reaction-row-button-bg-color;
cursor: pointer;
&:hover {
border-color: $reaction-row-button-hover-border-color;
}
&.mx_ReactionsRowButton_selected {
background-color: $reaction-row-button-selected-bg-color;
border-color: $reaction-row-button-selected-border-color;
}
}

View File

@ -31,6 +31,7 @@ limitations under the License.
top: 14px; top: 14px;
left: 8px; left: 8px;
cursor: pointer; cursor: pointer;
user-select: none;
} }
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
@ -62,6 +63,7 @@ limitations under the License.
vertical-align: top; vertical-align: top;
height: 16px; height: 16px;
overflow: hidden; overflow: hidden;
user-select: none;
img { img {
vertical-align: -2px; vertical-align: -2px;
@ -80,6 +82,7 @@ limitations under the License.
width: 46px; /* 8 + 30 (avatar) + 8 */ width: 46px; /* 8 + 30 (avatar) + 8 */
text-align: center; text-align: center;
position: absolute; position: absolute;
user-select: none;
} }
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
@ -118,7 +121,7 @@ limitations under the License.
} }
.mx_EventTile:hover .mx_EventTile_line, .mx_EventTile:hover .mx_EventTile_line,
.mx_EventTile.menu .mx_EventTile_line .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line
{ {
background-color: $event-selected-color; background-color: $event-selected-color;
} }
@ -203,7 +206,7 @@ limitations under the License.
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile_last > div > a > .mx_MessageTimestamp,
.mx_EventTile:hover > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp,
.mx_EventTile.menu > div > a > .mx_MessageTimestamp { .mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp {
visibility: visible; visibility: visible;
} }
@ -216,24 +219,8 @@ limitations under the License.
width: auto; width: auto;
} }
.mx_EventTile_editButton { .mx_EventTile:hover .mx_MessageActionBar,
position: absolute; .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar {
display: inline-block;
visibility: hidden;
cursor: pointer;
top: 6px;
right: 6px;
width: 19px;
height: 19px;
background-image: url($edit-button-url);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mx_EventTile:hover .mx_EventTile_editButton,
.mx_EventTile.menu .mx_EventTile_editButton {
visibility: visible; visibility: visible;
} }
@ -243,6 +230,7 @@ limitations under the License.
width: 14px; width: 14px;
height: 14px; height: 14px;
top: 29px; top: 29px;
user-select: none;
} }
.mx_EventTile_continuation .mx_EventTile_readAvatars, .mx_EventTile_continuation .mx_EventTile_readAvatars,
@ -550,10 +538,6 @@ limitations under the License.
top: 3px; top: 3px;
} }
.mx_EventTile_editButton {
top: 3px;
}
.mx_EventTile_readAvatars { .mx_EventTile_readAvatars {
top: 27px; top: 27px;
} }

View File

@ -0,0 +1,3 @@
<svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m20 6-11 11-5-5"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@ -0,0 +1,4 @@
<svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m18 6-12 12"/>
<path d="m6 6 12 12"/>
</svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>ED5D3E59-2561-4AC1-9B43-82FBC51767FC</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon_context">
<g>
<path d="M9.5,19 C14.7467051,19 19,14.7467051 19,9.5 C19,4.25329488 14.7467051,0 9.5,0 C4.25329488,0 0,4.25329488 0,9.5 C0,14.7467051 4.25329488,19 9.5,19 Z" id="Oval-69" fill="#ECECEC"></path>
<path d="M4.5,9.50063771 C4.5,9.13148623 4.59887838,8.85242947 4.7966381,8.66345907 C4.99439782,8.47448867 5.28224377,8.38000488 5.66018457,8.38000488 C6.0249414,8.38000488 6.3072941,8.47668596 6.50725115,8.67005103 C6.70720821,8.86341609 6.80718523,9.14027555 6.80718523,9.50063771 C6.80718523,9.84781589 6.70610956,10.1213794 6.50395517,10.3213365 C6.30180079,10.5212935 6.02054674,10.6212705 5.66018457,10.6212705 C5.29103309,10.6212705 5.00538444,10.5234908 4.80323006,10.3279284 C4.60107568,10.132366 4.5,9.85660521 4.5,9.50063771 L4.5,9.50063771 Z M8.3431114,9.50063771 C8.3431114,9.13148623 8.44198978,8.85242947 8.63974951,8.66345907 C8.83750923,8.47448867 9.12755247,8.38000488 9.50988794,8.38000488 C9.87464476,8.38000488 10.1569975,8.47668596 10.3569545,8.67005103 C10.5569116,8.86341609 10.6568886,9.14027555 10.6568886,9.50063771 C10.6568886,9.84781589 10.5558129,10.1213794 10.3536585,10.3213365 C10.1515042,10.5212935 9.8702501,10.6212705 9.50988794,10.6212705 C9.13634179,10.6212705 8.84849585,10.5234908 8.64634146,10.3279284 C8.44418708,10.132366 8.3431114,9.85660521 8.3431114,9.50063771 L8.3431114,9.50063771 Z M12.1928148,9.50063771 C12.1928148,9.13148623 12.2916931,8.85242947 12.4894529,8.66345907 C12.6872126,8.47448867 12.9750585,8.38000488 13.3529993,8.38000488 C13.7177562,8.38000488 14.0001089,8.47668596 14.2000659,8.67005103 C14.400023,8.86341609 14.5,9.14027555 14.5,9.50063771 C14.5,9.84781589 14.3989243,10.1213794 14.1967699,10.3213365 C13.9946156,10.5212935 13.7133615,10.6212705 13.3529993,10.6212705 C12.9838479,10.6212705 12.6981992,10.5234908 12.4960448,10.3279284 C12.2938904,10.132366 12.1928148,9.85660521 12.1928148,9.50063771 L12.1928148,9.50063771 Z" id="…" fill="#9B9B9B"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

6
res/img/reply.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg">
<g stroke="#2E2F32" stroke-width=".75" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.75 4.75L12.5 8.5l-3.75 3.75"/>
<path d="M.5.25V5.5a3 3 0 0 0 3 3h9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@ -40,7 +40,7 @@ $tagpanel-bg-color: $base-color;
$selected-color: $room-highlight-color; $selected-color: $room-highlight-color;
// selected for hoverover & selected event tiles // selected for hoverover & selected event tiles
$event-selected-color: #111316; $event-selected-color: $header-panel-bg-color;
// used for the hairline dividers in RoomView // used for the hairline dividers in RoomView
$primary-hairline-color: $header-panel-border-color; $primary-hairline-color: $header-panel-border-color;
@ -146,6 +146,17 @@ $room-warning-bg-color: $header-panel-bg-color;
$dark-panel-bg-color: $header-panel-bg-color; $dark-panel-bg-color: $header-panel-bg-color;
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1); $panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
$message-action-bar-bg-color: $header-panel-bg-color;
$message-action-bar-fg-color: $header-panel-text-primary-color;
$message-action-bar-border-color: #616b7f;
$message-action-bar-hover-border-color: $header-panel-text-primary-color;
$reaction-row-button-bg-color: $header-panel-bg-color;
$reaction-row-button-border-color: #616b7f;
$reaction-row-button-hover-border-color: $header-panel-text-primary-color;
$reaction-row-button-selected-bg-color: #1f6954;
$reaction-row-button-selected-border-color: $accent-color;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View File

@ -11,7 +11,7 @@ $font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
$accent-color: #03b381; $accent-color: #03b381;
$notice-primary-color: #ff4b55; $notice-primary-color: #ff4b55;
$notice-secondary-color: #61708b; $notice-secondary-color: #61708b;
$header-panel-bg-color: #f2f5f8; $header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin) // typical text (dark-on-white in light skin)
$primary-fg-color: #2e2f32; $primary-fg-color: #2e2f32;
@ -66,14 +66,14 @@ $droptarget-bg-color: rgba(255,255,255,0.5);
$selected-color: $secondary-accent-color; $selected-color: $secondary-accent-color;
// selected for hoverover & selected event tiles // selected for hoverover & selected event tiles
$event-selected-color: #f7f7f7; $event-selected-color: $header-panel-bg-color;
// used for the hairline dividers in RoomView // used for the hairline dividers in RoomView
$primary-hairline-color: #e5e5e5; $primary-hairline-color: #e5e5e5;
// used for the border of input text fields // used for the border of input text fields
$input-border-color: #e7e7e7; $input-border-color: #e7e7e7;
$input-darker-bg-color: rgba(193, 201, 214, 0.29); $input-darker-bg-color: #e3e8f0;
$input-darker-fg-color: #9fa9ba; $input-darker-fg-color: #9fa9ba;
$input-lighter-bg-color: #f2f5f8; $input-lighter-bg-color: #f2f5f8;
$input-lighter-fg-color: $input-darker-fg-color; $input-lighter-fg-color: $input-darker-fg-color;
@ -153,7 +153,7 @@ $roomheader-button-color: #91A1C0;
$groupheader-button-color: #91A1C0; $groupheader-button-color: #91A1C0;
$rightpanel-button-color: #91A1C0; $rightpanel-button-color: #91A1C0;
$composer-button-color: #91A1C0; $composer-button-color: #91A1C0;
$roomtopic-color: #9fa9ba; $roomtopic-color: #9e9e9e;
$eventtile-meta-color: $roomtopic-color; $eventtile-meta-color: $roomtopic-color;
$composer-e2e-icon-color: #c9ced6; $composer-e2e-icon-color: #c9ced6;
@ -203,7 +203,6 @@ $event-redacted-border-color: #cccccc;
// event timestamp // event timestamp
$event-timestamp-color: #acacac; $event-timestamp-color: #acacac;
$edit-button-url: "$(res)/img/icon_context_message.svg";
$copy-button-url: "$(res)/img/icon_copy_message.svg"; $copy-button-url: "$(res)/img/icon_copy_message.svg";
// e2e // e2e
@ -255,6 +254,17 @@ $authpage-secondary-color: #61708b;
$dark-panel-bg-color: $secondary-accent-color; $dark-panel-bg-color: $secondary-accent-color;
$panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1); $panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1);
$message-action-bar-bg-color: $primary-bg-color;
$message-action-bar-fg-color: $primary-fg-color;
$message-action-bar-border-color: #e9edf1;
$message-action-bar-hover-border-color: #b8c1d2;
$reaction-row-button-bg-color: $header-panel-bg-color;
$reaction-row-button-border-color: #e9edf1;
$reaction-row-button-hover-border-color: #bebebe;
$reaction-row-button-selected-bg-color: #e9fff9;
$reaction-row-button-selected-border-color: $accent-color;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View File

@ -50,7 +50,6 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js"; import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils'; import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import TimelineExplosionDialog from "../views/dialogs/TimelineExplosionDialog";
const AutoDiscovery = Matrix.AutoDiscovery; const AutoDiscovery = Matrix.AutoDiscovery;
@ -250,17 +249,6 @@ export default React.createClass({
return this.state.defaultIsUrl || "https://vector.im"; return this.state.defaultIsUrl || "https://vector.im";
}, },
/**
* Whether to skip the server details phase of registration and start at the
* actual form.
* @return {boolean}
* If there was a configured default HS or default server name, skip the
* the server details.
*/
skipServerDetailsForRegistration() {
return !!this.state.defaultHsUrl;
},
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
@ -1307,17 +1295,6 @@ export default React.createClass({
return self._loggedInView.child.canResetTimelineInRoom(roomId); return self._loggedInView.child.canResetTimelineInRoom(roomId);
}); });
cli.on('sync.unexpectedError', function(err) {
if (err.message && err.message.includes("live timeline ") && err.message.includes(" is no longer live ")) {
console.error("Caught timeline explosion - trying to ask user for more information");
if (Modal.hasDialogs()) {
console.warn("User has another dialog open - skipping prompt");
return;
}
Modal.createTrackedDialog('Timeline exploded', '', TimelineExplosionDialog, {});
}
});
cli.on('sync', function(state, prevState, data) { cli.on('sync', function(state, prevState, data) {
// LifecycleStore and others cannot directly subscribe to matrix client for // LifecycleStore and others cannot directly subscribe to matrix client for
// events because flux only allows store state changes during flux dispatches. // events because flux only allows store state changes during flux dispatches.
@ -1985,7 +1962,6 @@ export default React.createClass({
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()} defaultIsUrl={this.getDefaultIsUrl()}
skipServerDetails={this.skipServerDetailsForRegistration()}
brand={this.props.config.brand} brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}

View File

@ -44,11 +44,10 @@ const READ_RECEIPT_INTERVAL_MS = 500;
const DEBUG = false; const DEBUG = false;
let debuglog = function() {};
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console); debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
} }
/* /*
@ -56,7 +55,7 @@ if (DEBUG) {
* *
* Also responsible for handling and sending read receipts. * Also responsible for handling and sending read receipts.
*/ */
var TimelinePanel = React.createClass({ const TimelinePanel = React.createClass({
displayName: 'TimelinePanel', displayName: 'TimelinePanel',
propTypes: { propTypes: {
@ -445,6 +444,7 @@ var TimelinePanel = React.createClass({
const updatedState = {events: events}; const updatedState = {events: events};
let callRMUpdated;
if (this.props.manageReadMarkers) { if (this.props.manageReadMarkers) {
// when a new event arrives when the user is not watching the // when a new event arrives when the user is not watching the
// window, but the window is in its auto-scroll mode, make sure the // window, but the window is in its auto-scroll mode, make sure the
@ -456,7 +456,7 @@ var TimelinePanel = React.createClass({
// //
const myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
const sender = ev.sender ? ev.sender.userId : null; const sender = ev.sender ? ev.sender.userId : null;
var callRMUpdated = false; callRMUpdated = false;
if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) { if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
updatedState.readMarkerVisible = true; updatedState.readMarkerVisible = true;
} else if (lastEv && this.getReadMarkerPosition() === 0) { } else if (lastEv && this.getReadMarkerPosition() === 0) {
@ -566,7 +566,7 @@ var TimelinePanel = React.createClass({
UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer); UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer);
try { try {
await this._readMarkerActivityTimer.finished(); await this._readMarkerActivityTimer.finished();
} catch(e) { continue; /* aborted */ } } catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors // outside of try/catch to not swallow errors
this.updateReadMarker(); this.updateReadMarker();
} }
@ -578,7 +578,7 @@ var TimelinePanel = React.createClass({
UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer); UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer);
try { try {
await this._readReceiptActivityTimer.finished(); await this._readReceiptActivityTimer.finished();
} catch(e) { continue; /* aborted */ } } catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors // outside of try/catch to not swallow errors
this.sendReadReceipt(); this.sendReadReceipt();
} }
@ -732,7 +732,8 @@ var TimelinePanel = React.createClass({
const events = this._timelineWindow.getEvents(); const events = this._timelineWindow.getEvents();
// first find where the current RM is // first find where the current RM is
for (var i = 0; i < events.length; i++) { let i;
for (i = 0; i < events.length; i++) {
if (events[i].getId() == this.state.readMarkerEventId) { if (events[i].getId() == this.state.readMarkerEventId) {
break; break;
} }
@ -744,7 +745,7 @@ var TimelinePanel = React.createClass({
// now think about advancing it // now think about advancing it
const myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
for (i++; i < events.length; i++) { for (i++; i < events.length; i++) {
var ev = events[i]; const ev = events[i];
if (!ev.sender || ev.sender.userId != myUserId) { if (!ev.sender || ev.sender.userId != myUserId) {
break; break;
} }
@ -752,7 +753,7 @@ var TimelinePanel = React.createClass({
// i is now the first unread message which we didn't send ourselves. // i is now the first unread message which we didn't send ourselves.
i--; i--;
var ev = events[i]; const ev = events[i];
this._setReadMarker(ev.getId(), ev.getTs()); this._setReadMarker(ev.getId(), ev.getTs());
}, },
@ -882,7 +883,7 @@ var TimelinePanel = React.createClass({
return ret; return ret;
}, },
/** /*
* called by the parent component when PageUp/Down/etc is pressed. * called by the parent component when PageUp/Down/etc is pressed.
* *
* We pass it down to the scroll panel. * We pass it down to the scroll panel.
@ -975,11 +976,10 @@ var TimelinePanel = React.createClass({
}; };
const onError = (error) => { const onError = (error) => {
this.setState({timelineLoading: false}); this.setState({ timelineLoading: false });
console.error( console.error(
`Error loading timeline panel at ${eventId}: ${error}`, `Error loading timeline panel at ${eventId}: ${error}`,
); );
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
let onFinished; let onFinished;
@ -997,9 +997,18 @@ var TimelinePanel = React.createClass({
}); });
}; };
} }
const message = (error.errcode == 'M_FORBIDDEN') let message;
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.") if (error.errcode == 'M_FORBIDDEN') {
: _t("Tried to load a specific point in this room's timeline, but was unable to find it."); message = _t(
"Tried to load a specific point in this room's timeline, but you " +
"do not have permission to view the message in question.",
);
} else {
message = _t(
"Tried to load a specific point in this room's timeline, but was " +
"unable to find it.",
);
}
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, { Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
title: _t("Failed to load timeline position"), title: _t("Failed to load timeline position"),
description: message, description: message,
@ -1104,12 +1113,13 @@ var TimelinePanel = React.createClass({
}, },
/** /**
* get the id of the event corresponding to our user's latest read-receipt. * Get the id of the event corresponding to our user's latest read-receipt.
* *
* @param {Boolean} ignoreSynthesized If true, return only receipts that * @param {Boolean} ignoreSynthesized If true, return only receipts that
* have been sent by the server, not * have been sent by the server, not
* implicit ones generated by the JS * implicit ones generated by the JS
* SDK. * SDK.
* @return {String} the event ID
*/ */
_getCurrentReadReceipt: function(ignoreSynthesized) { _getCurrentReadReceipt: function(ignoreSynthesized) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();

View File

@ -28,8 +28,6 @@ import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import * as ServerType from '../../views/auth/ServerTypeSelector'; import * as ServerType from '../../views/auth/ServerTypeSelector';
const MIN_PASSWORD_LENGTH = 6;
// Phases // Phases
// Show controls to configure server details // Show controls to configure server details
const PHASE_SERVER_DETAILS = 0; const PHASE_SERVER_DETAILS = 0;
@ -60,7 +58,6 @@ module.exports = React.createClass({
customIsUrl: PropTypes.string, customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string, defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string, defaultIsUrl: PropTypes.string,
skipServerDetails: PropTypes.bool,
brand: PropTypes.string, brand: PropTypes.string,
email: PropTypes.string, email: PropTypes.string,
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
@ -71,26 +68,6 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl); const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
const customURLsAllowed = !SdkConfig.get()['disable_custom_urls'];
let initialPhase = this.getDefaultPhaseForServerType(serverType);
if (
// if we have these two, skip to the good bit
// (they could come in from the URL params in a
// registration email link)
(this.props.clientSecret && this.props.sessionId) ||
// if custom URLs aren't allowed, skip to form
!customURLsAllowed ||
// if other logic says to, skip to form
this.props.skipServerDetails
) {
// TODO: It would seem we've now added enough conditions here that the initial
// phase will _always_ be the form. It's tempting to remove the complexity and
// just do that, but we keep tweaking and changing auth, so let's wait until
// things settle a bit.
// Filed https://github.com/vector-im/riot-web/issues/8886 to track this.
initialPhase = PHASE_REGISTRATION;
}
return { return {
busy: false, busy: false,
errorText: null, errorText: null,
@ -113,7 +90,7 @@ module.exports = React.createClass({
hsUrl: this.props.customHsUrl, hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl, isUrl: this.props.customIsUrl,
// Phase of the overall registration dialog. // Phase of the overall registration dialog.
phase: initialPhase, phase: PHASE_REGISTRATION,
flows: null, flows: null,
}; };
}, },
@ -308,58 +285,6 @@ module.exports = React.createClass({
}); });
}, },
onFormValidationChange: function(fieldErrors) {
// `fieldErrors` is an object mapping field IDs to error codes when there is an
// error or `null` for no error, so the values array will be something like:
// `[ null, "RegistrationForm.ERR_PASSWORD_MISSING", null]`
// Find the first non-null error code and show that.
const errCode = Object.values(fieldErrors).find(value => !!value);
if (!errCode) {
this.setState({
errorText: null,
});
return;
}
let errMsg;
switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = _t('Missing password.');
break;
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
errMsg = _t('Passwords don\'t match.');
break;
case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH});
break;
case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = _t('This doesn\'t look like a valid email address.');
break;
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
errMsg = _t('This doesn\'t look like a valid phone number.');
break;
case "RegistrationForm.ERR_MISSING_EMAIL":
errMsg = _t('An email address is required to register on this homeserver.');
break;
case "RegistrationForm.ERR_MISSING_PHONE_NUMBER":
errMsg = _t('A phone number is required to register on this homeserver.');
break;
case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = _t("A username can only contain lower case letters, numbers and '=_-./'");
break;
case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = _t('You need to enter a username.');
break;
default:
console.error("Unknown error code: %s", errCode);
errMsg = _t('An unknown error occurred.');
break;
}
this.setState({
errorText: errMsg,
});
},
onLoginClick: function(ev) { onLoginClick: function(ev) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -534,8 +459,6 @@ module.exports = React.createClass({
defaultPhoneCountry={this.state.formVals.phoneCountry} defaultPhoneCountry={this.state.formVals.phoneCountry}
defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password} defaultPassword={this.state.formVals.password}
minPasswordLength={MIN_PASSWORD_LENGTH}
onValidationChange={this.onFormValidationChange}
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
onEditServerDetailsClick={onEditServerDetailsClick} onEditServerDetailsClick={onEditServerDetailsClick}
flows={this.state.flows} flows={this.state.flows}

View File

@ -25,6 +25,7 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
const FIELD_EMAIL = 'field_email'; const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_PHONE_NUMBER = 'field_phone_number';
@ -32,6 +33,8 @@ const FIELD_USERNAME = 'field_username';
const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD = 'field_password';
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; 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. * A pure UI component which displays a registration form.
*/ */
@ -45,8 +48,6 @@ module.exports = React.createClass({
defaultPhoneNumber: PropTypes.string, defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string, defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string, defaultPassword: PropTypes.string,
minPasswordLength: PropTypes.number,
onValidationChange: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
onEditServerDetailsClick: PropTypes.func, onEditServerDetailsClick: PropTypes.func,
flows: PropTypes.arrayOf(PropTypes.object).isRequired, flows: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -59,7 +60,6 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
minPasswordLength: 6,
onValidationChange: console.error, onValidationChange: console.error,
}; };
}, },
@ -67,7 +67,7 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
// Field error codes by field ID // Field error codes by field ID
fieldErrors: {}, fieldValid: {},
// The ISO2 country code selected in the phone number entry // The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry, phoneCountry: this.props.defaultPhoneCountry,
username: "", username: "",
@ -75,44 +75,37 @@ module.exports = React.createClass({
phoneNumber: "", phoneNumber: "",
password: "", password: "",
passwordConfirm: "", passwordConfirm: "",
passwordComplexity: null,
}; };
}, },
onSubmit: function(ev) { onSubmit: async function(ev) {
ev.preventDefault(); ev.preventDefault();
// validate everything, in reverse order so const allFieldsValid = await this.verifyFieldsBeforeSubmit();
// the error that ends up being displayed if (!allFieldsValid) {
// is the one from the first invalid field. return;
// It's not super ideal that this just calls }
// onValidationChange once for each invalid field.
this.validateField(FIELD_PHONE_NUMBER, ev.type);
this.validateField(FIELD_EMAIL, ev.type);
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
this.validateField(FIELD_PASSWORD, ev.type);
this.validateField(FIELD_USERNAME, ev.type);
const self = this; const self = this;
if (this.allFieldsValid()) { if (this.state.email == '') {
if (this.state.email == '') { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, { title: _t("Warning!"),
title: _t("Warning!"), description:
description: <div>
<div> { _t("If you don't specify an email address, you won't be able to reset your password. " +
{ _t("If you don't specify an email address, you won't be able to reset your password. " + "Are you sure?") }
"Are you sure?") } </div>,
</div>, button: _t("Continue"),
button: _t("Continue"), onFinished: function(confirmed) {
onFinished: function(confirmed) { if (confirmed) {
if (confirmed) { self._doSubmit(ev);
self._doSubmit(ev); }
} },
}, });
}); } else {
} else { self._doSubmit(ev);
self._doSubmit(ev);
}
} }
}, },
@ -134,118 +127,81 @@ module.exports = React.createClass({
} }
}, },
async verifyFieldsBeforeSubmit() {
// Blur the active element if any, so we first run its blur validation,
// which is less strict than the pass we're about to do below for all fields.
const activeElement = document.activeElement;
if (activeElement) {
activeElement.blur();
}
const fieldIDsInDisplayOrder = [
FIELD_USERNAME,
FIELD_PASSWORD,
FIELD_PASSWORD_CONFIRM,
FIELD_EMAIL,
FIELD_PHONE_NUMBER,
];
// Run all fields with stricter validation that no longer allows empty
// values for required fields.
for (const fieldID of fieldIDsInDisplayOrder) {
const field = this[fieldID];
if (!field) {
continue;
}
field.validate({ allowEmpty: false });
}
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve));
if (this.allFieldsValid()) {
return true;
}
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
if (!invalidField) {
return true;
}
// Focus the first invalid field and show feedback in the stricter mode
// that no longer allows empty values for required fields.
invalidField.focus();
invalidField.validate({ allowEmpty: false, focused: true });
return false;
},
/** /**
* @returns {boolean} true if all fields were valid last time they were validated. * @returns {boolean} true if all fields were valid last time they were validated.
*/ */
allFieldsValid: function() { allFieldsValid: function() {
const keys = Object.keys(this.state.fieldErrors); const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (this.state.fieldErrors[keys[i]]) { if (!this.state.fieldValid[keys[i]]) {
return false; return false;
} }
} }
return true; return true;
}, },
validateField: function(fieldID, eventType) { findFirstInvalidField(fieldIDs) {
const pwd1 = this.state.password.trim(); for (const fieldID of fieldIDs) {
const pwd2 = this.state.passwordConfirm.trim(); if (!this.state.fieldValid[fieldID] && this[fieldID]) {
const allowEmpty = eventType === "blur"; return this[fieldID];
switch (fieldID) {
case FIELD_EMAIL: {
const email = this.state.email;
const emailValid = email === '' || Email.looksValid(email);
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL");
} else this.markFieldValid(fieldID, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
} }
case FIELD_PHONE_NUMBER: {
const phoneNumber = this.state.phoneNumber;
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) {
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
} else this.markFieldValid(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break;
}
case FIELD_USERNAME: {
const username = this.state.username;
if (allowEmpty && username === '') {
this.markFieldValid(fieldID, true);
} else if (!SAFE_LOCALPART_REGEX.test(username)) {
this.markFieldValid(
fieldID,
false,
"RegistrationForm.ERR_USERNAME_INVALID",
);
} else if (username == '') {
this.markFieldValid(
fieldID,
false,
"RegistrationForm.ERR_USERNAME_BLANK",
);
} else {
this.markFieldValid(fieldID, true);
}
break;
}
case FIELD_PASSWORD:
if (allowEmpty && pwd1 === "") {
this.markFieldValid(fieldID, true);
} else if (pwd1 == '') {
this.markFieldValid(
fieldID,
false,
"RegistrationForm.ERR_PASSWORD_MISSING",
);
} else if (pwd1.length < this.props.minPasswordLength) {
this.markFieldValid(
fieldID,
false,
"RegistrationForm.ERR_PASSWORD_LENGTH",
);
} else {
this.markFieldValid(fieldID, true);
}
break;
case FIELD_PASSWORD_CONFIRM:
if (allowEmpty && pwd2 === "") {
this.markFieldValid(fieldID, true);
} else {
this.markFieldValid(
fieldID, pwd1 == pwd2,
"RegistrationForm.ERR_PASSWORD_MISMATCH",
);
}
break;
} }
return null;
}, },
markFieldValid: function(fieldID, valid, errorCode) { markFieldValid: function(fieldID, valid) {
const { fieldErrors } = this.state; const { fieldValid } = this.state;
if (valid) { fieldValid[fieldID] = valid;
fieldErrors[fieldID] = null;
} else {
fieldErrors[fieldID] = errorCode;
}
this.setState({ this.setState({
fieldErrors, fieldValid,
}); });
this.props.onValidationChange(fieldErrors);
},
_classForField: function(fieldID, ...baseClasses) {
let cls = baseClasses.join(' ');
if (this.state.fieldErrors[fieldID]) {
if (cls) cls += ' ';
cls += 'error';
}
return cls;
},
onEmailBlur(ev) {
this.validateField(FIELD_EMAIL, ev.type);
}, },
onEmailChange(ev) { onEmailChange(ev) {
@ -254,26 +210,113 @@ module.exports = React.createClass({
}); });
}, },
onPasswordBlur(ev) { async onEmailValidate(fieldState) {
this.validateField(FIELD_PASSWORD, ev.type); const result = await this.validateEmailRules(fieldState);
this.markFieldValid(FIELD_EMAIL, result.valid);
return result;
}, },
validateEmailRules: withValidation({
description: () => _t("Use an email address to recover your account"),
rules: [
{
key: "required",
test: function({ value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value;
},
invalid: () => _t("Enter email address (required on this homeserver)"),
},
{
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t("Doesn't look like a valid email address"),
},
],
}),
onPasswordChange(ev) { onPasswordChange(ev) {
this.setState({ this.setState({
password: ev.target.value, password: ev.target.value,
}); });
}, },
onPasswordConfirmBlur(ev) { async onPasswordValidate(fieldState) {
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type); const result = await this.validatePasswordRules(fieldState);
this.markFieldValid(FIELD_PASSWORD, result.valid);
return result;
}, },
validatePasswordRules: withValidation({
description: function() {
const complexity = this.state.passwordComplexity;
const score = complexity ? complexity.score : 0;
return <progress
className="mx_AuthBody_passwordScore"
max={PASSWORD_MIN_SCORE}
value={score}
/>;
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Enter password"),
},
{
key: "complexity",
test: async function({ value }) {
if (!value) {
return false;
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
this.setState({
passwordComplexity: complexity,
});
return complexity.score >= PASSWORD_MIN_SCORE;
},
valid: () => _t("Nice, strong password!"),
invalid: function() {
const complexity = this.state.passwordComplexity;
if (!complexity) {
return null;
}
const { feedback } = complexity;
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
},
},
],
}),
onPasswordConfirmChange(ev) { onPasswordConfirmChange(ev) {
this.setState({ this.setState({
passwordConfirm: ev.target.value, passwordConfirm: ev.target.value,
}); });
}, },
async onPasswordConfirmValidate(fieldState) {
const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid);
return result;
},
validatePasswordConfirmRules: withValidation({
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Confirm password"),
},
{
key: "match",
test: function({ value }) {
return !value || value === this.state.password;
},
invalid: () => _t("Passwords don't match"),
},
],
}),
onPhoneCountryChange(newVal) { onPhoneCountryChange(newVal) {
this.setState({ this.setState({
phoneCountry: newVal.iso2, phoneCountry: newVal.iso2,
@ -281,26 +324,64 @@ module.exports = React.createClass({
}); });
}, },
onPhoneNumberBlur(ev) {
this.validateField(FIELD_PHONE_NUMBER, ev.type);
},
onPhoneNumberChange(ev) { onPhoneNumberChange(ev) {
this.setState({ this.setState({
phoneNumber: ev.target.value, phoneNumber: ev.target.value,
}); });
}, },
onUsernameBlur(ev) { async onPhoneNumberValidate(fieldState) {
this.validateField(FIELD_USERNAME, ev.type); const result = await this.validatePhoneNumberRules(fieldState);
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid);
return result;
}, },
validatePhoneNumberRules: withValidation({
description: () => _t("Other users can invite you to rooms using your contact details"),
rules: [
{
key: "required",
test: function({ value, allowEmpty }) {
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value;
},
invalid: () => _t("Enter phone number (required on this homeserver)"),
},
{
key: "email",
test: ({ value }) => !value || phoneNumberLooksValid(value),
invalid: () => _t("Doesn't look like a valid phone number"),
},
],
}),
onUsernameChange(ev) { onUsernameChange(ev) {
this.setState({ this.setState({
username: ev.target.value, username: ev.target.value,
}); });
}, },
async onUsernameValidate(fieldState) {
const result = await this.validateUsernameRules(fieldState);
this.markFieldValid(FIELD_USERNAME, result.valid);
return result;
},
validateUsernameRules: withValidation({
description: () => _t("Use letters, numbers, dashes and underscores only"),
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Enter username"),
},
{
key: "safeLocalpart",
test: ({ value }) => !value || SAFE_LOCALPART_REGEX.test(value),
invalid: () => _t("Some characters not allowed"),
},
],
}),
/** /**
* A step is required if all flows include that step. * A step is required if all flows include that step.
* *
@ -325,9 +406,99 @@ module.exports = React.createClass({
}); });
}, },
render: function() { renderEmail() {
if (!this._authStepIsUsed('m.login.email.identity')) {
return null;
}
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
_t("Email") :
_t("Email (optional)");
return <Field
id="mx_RegistrationForm_email"
ref={field => this[FIELD_EMAIL] = field}
type="text"
label={emailPlaceholder}
defaultValue={this.props.defaultEmail}
value={this.state.email}
onChange={this.onEmailChange}
onValidate={this.onEmailValidate}
/>;
},
renderPassword() {
const Field = sdk.getComponent('elements.Field');
return <Field
id="mx_RegistrationForm_password"
ref={field => this[FIELD_PASSWORD] = field}
type="password"
label={_t("Password")}
defaultValue={this.props.defaultPassword}
value={this.state.password}
onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate}
/>;
},
renderPasswordConfirm() {
const Field = sdk.getComponent('elements.Field');
return <Field
id="mx_RegistrationForm_passwordConfirm"
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password"
label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange}
onValidate={this.onPasswordConfirmValidate}
/>;
},
renderPhoneNumber() {
const threePidLogin = !SdkConfig.get().disable_3pid_login;
if (!threePidLogin || !this._authStepIsUsed('m.login.msisdn')) {
return null;
}
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
const Field = sdk.getComponent('elements.Field');
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
_t("Phone") :
_t("Phone (optional)");
const phoneCountry = <CountryDropdown
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
onOptionChange={this.onPhoneCountryChange}
/>;
return <Field
id="mx_RegistrationForm_phoneNumber"
ref={field => this[FIELD_PHONE_NUMBER] = field}
type="text"
label={phoneLabel}
defaultValue={this.props.defaultPhoneNumber}
value={this.state.phoneNumber}
prefix={phoneCountry}
onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate}
/>;
},
renderUsername() {
const Field = sdk.getComponent('elements.Field');
return <Field
id="mx_RegistrationForm_username"
ref={field => this[FIELD_USERNAME] = field}
type="text"
autoFocus={true}
label={_t("Username")}
defaultValue={this.props.defaultUsername}
value={this.state.username}
onChange={this.onUsernameChange}
onValidate={this.onUsernameValidate}
/>;
},
render: function() {
let yourMatrixAccountText = _t('Create your Matrix account'); let yourMatrixAccountText = _t('Create your Matrix account');
if (this.props.hsName) { if (this.props.hsName) {
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
@ -353,53 +524,6 @@ module.exports = React.createClass({
</a>; </a>;
} }
let emailSection;
if (this._authStepIsUsed('m.login.email.identity')) {
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
_t("Email") :
_t("Email (optional)");
emailSection = (
<Field
className={this._classForField(FIELD_EMAIL)}
id="mx_RegistrationForm_email"
type="text"
label={emailPlaceholder}
defaultValue={this.props.defaultEmail}
value={this.state.email}
onBlur={this.onEmailBlur}
onChange={this.onEmailChange}
/>
);
}
const threePidLogin = !SdkConfig.get().disable_3pid_login;
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
let phoneSection;
if (threePidLogin && this._authStepIsUsed('m.login.msisdn')) {
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
_t("Phone") :
_t("Phone (optional)");
const phoneCountry = <CountryDropdown
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
onOptionChange={this.onPhoneCountryChange}
/>;
phoneSection = <Field
className={this._classForField(FIELD_PHONE_NUMBER)}
id="mx_RegistrationForm_phoneNumber"
type="text"
label={phoneLabel}
defaultValue={this.props.defaultPhoneNumber}
value={this.state.phoneNumber}
prefix={phoneCountry}
onBlur={this.onPhoneNumberBlur}
onChange={this.onPhoneNumberChange}
/>;
}
const registerButton = ( const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} /> <input className="mx_Login_submit" type="submit" value={_t("Register")} />
); );
@ -412,48 +536,18 @@ module.exports = React.createClass({
</h3> </h3>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<Field {this.renderUsername()}
className={this._classForField(FIELD_USERNAME)}
id="mx_RegistrationForm_username"
type="text"
autoFocus={true}
label={_t("Username")}
defaultValue={this.props.defaultUsername}
value={this.state.username}
onBlur={this.onUsernameBlur}
onChange={this.onUsernameChange}
/>
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<Field {this.renderPassword()}
className={this._classForField(FIELD_PASSWORD)} {this.renderPasswordConfirm()}
id="mx_RegistrationForm_password"
type="password"
label={_t("Password")}
defaultValue={this.props.defaultPassword}
value={this.state.password}
onBlur={this.onPasswordBlur}
onChange={this.onPasswordChange}
/>
<Field
className={this._classForField(FIELD_PASSWORD_CONFIRM)}
id="mx_RegistrationForm_passwordConfirm"
type="password"
label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
value={this.state.passwordConfirm}
onBlur={this.onPasswordConfirmBlur}
onChange={this.onPasswordConfirmChange}
/>
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
{ emailSection } {this.renderEmail()}
{ phoneSection } {this.renderPhoneNumber()}
</div> </div>
{_t( {_t("Use an email address to recover your account.") + " "}
"Use an email address to recover your account. Other users " + {_t("Other users can invite you to rooms using your contact details.")}
"can invite you to rooms using your contact details.",
)}
{ registerButton } { registerButton }
</form> </form>
</div> </div>

View File

@ -27,6 +27,7 @@ import Modal from '../../../Modal';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils'; import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MessageContextMenu', displayName: 'MessageContextMenu',
@ -201,14 +202,6 @@ module.exports = React.createClass({
this.closeMenu(); this.closeMenu();
}, },
onReplyClick: function() {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
});
this.closeMenu();
},
onCollapseReplyThreadClick: function() { onCollapseReplyThreadClick: function() {
this.props.collapseReplyThread(); this.props.collapseReplyThread();
this.closeMenu(); this.closeMenu();
@ -226,7 +219,6 @@ module.exports = React.createClass({
let unhidePreviewButton; let unhidePreviewButton;
let externalURLButton; let externalURLButton;
let quoteButton; let quoteButton;
let replyButton;
let collapseReplyThread; let collapseReplyThread;
// status is SENT before remote-echo, null after // status is SENT before remote-echo, null after
@ -256,28 +248,19 @@ module.exports = React.createClass({
); );
} }
if (isSent && mxEvent.getType() === 'm.room.message') { if (isContentActionable(mxEvent)) {
const content = mxEvent.getContent(); forwardButton = (
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { <div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
forwardButton = ( { _t('Forward Message') }
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}> </div>
{ _t('Forward Message') } );
if (this.state.canPin) {
pinButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
</div> </div>
); );
replyButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
{ _t('Reply') }
</div>
);
if (this.state.canPin) {
pinButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
</div>
);
}
} }
} }
@ -368,7 +351,6 @@ module.exports = React.createClass({
{ unhidePreviewButton } { unhidePreviewButton }
{ permalinkButton } { permalinkButton }
{ quoteButton } { quoteButton }
{ replyButton }
{ externalURLButton } { externalURLButton }
{ collapseReplyThread } { collapseReplyThread }
{ e2eInfo } { e2eInfo }

View File

@ -130,7 +130,7 @@ export default class LogoutDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let setupButtonCaption; let setupButtonCaption;
if (this.state.backupInfo) { if (this.state.backupInfo) {
setupButtonCaption = _t("Use Key Backup"); setupButtonCaption = _t("Connect this device to Key Backup");
} else { } else {
// if there's an error fetching the backup info, we'll just assume there's // if there's an error fetching the backup info, we'll just assume there's
// no backup for the purpose of the button caption // no backup for the purpose of the button caption

View File

@ -1,130 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler';
// Dev note: this should be a temporary dialog while we work out what is
// actually going on. See https://github.com/vector-im/riot-web/issues/8593
// for more details. This dialog is almost entirely a copy/paste job of
// BugReportDialog.
export default class TimelineExplosionDialog extends React.Component {
static propTypes = {
onFinished: React.PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.state = {
busy: false,
progress: null,
};
}
_onCancel() {
console.log("Reloading without sending logs for timeline explosion");
window.location.reload();
}
_onSubmit = () => {
const userText = "Caught timeline explosion\n\nhttps://github.com/vector-im/riot-web/issues/8593";
this.setState({busy: true, progress: null});
this._sendProgressCallback(_t("Preparing to send logs"));
require(['../../../rageshake/submit-rageshake'], (s) => {
s(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
}).then(() => {
console.log("Logs sent for timeline explosion - reloading Riot");
window.location.reload();
}, (err) => {
console.error("Error sending logs for timeline explosion - reloading anyways.", err);
window.location.reload();
});
});
};
_sendProgressCallback = (progress) => {
this.setState({progress: progress});
};
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
{this.state.progress} ...
<Loader />
</div>
);
}
return (
<BaseDialog className="mx_TimelineExplosionDialog" onFinished={this._onCancel}
title={_t('Error showing you your room')} contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{_t(
"Riot has run into a problem which makes it difficult to show you " +
"your messages right now. Nothing has been lost and reloading the app " +
"should fix this for you. In order to assist us in troubleshooting the " +
"problem, we'd like to take a look at your debug logs. You do not need " +
"to send your logs unless you want to, but we would really appreciate " +
"it if you did. We'd also like to apologize for having to show this " +
"message to you - we hope your debug logs are the key to solving the " +
"issue once and for all. If you'd like more information on the bug you've " +
"accidentally run into, please visit <a>the issue</a>.",
{},
{
'a': (sub) => {
return <a href="https://github.com/vector-im/riot-web/issues/8593"
target="_blank" rel="noopener">{sub}</a>;
},
},
)}
</p>
<p>
{_t(
"Debug logs contain application usage data including your " +
"username, the IDs or aliases of the rooms or groups you " +
"have visited and the usernames of other users. They do " +
"not contain messages.",
)}
</p>
{progress}
</div>
<DialogButtons primaryButton={_t("Send debug logs and reload Riot")}
onPrimaryButtonClick={this._onSubmit}
cancelButton={_t("Reload Riot without sending logs")}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View File

@ -18,6 +18,10 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { throttle } from 'lodash';
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
export default class Field extends React.PureComponent { export default class Field extends React.PureComponent {
static propTypes = { static propTypes = {
@ -53,20 +57,73 @@ export default class Field extends React.PureComponent {
}; };
} }
onChange = (ev) => { onFocus = (ev) => {
if (this.props.onValidate) { this.validate({
const result = this.props.onValidate(ev.target.value); focused: true,
this.setState({ });
valid: result.valid, // Parent component may have supplied its own `onFocus` as well
feedback: result.feedback, if (this.props.onFocus) {
}); this.props.onFocus(ev);
} }
};
onChange = (ev) => {
this.validateOnChange();
// Parent component may have supplied its own `onChange` as well // Parent component may have supplied its own `onChange` as well
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(ev); this.props.onChange(ev);
} }
}; };
onBlur = (ev) => {
this.validate({
focused: false,
});
// Parent component may have supplied its own `onBlur` as well
if (this.props.onBlur) {
this.props.onBlur(ev);
}
};
focus() {
this.input.focus();
}
async validate({ focused, allowEmpty = true }) {
if (!this.props.onValidate) {
return;
}
const value = this.input ? this.input.value : null;
const { valid, feedback } = await this.props.onValidate({
value,
focused,
allowEmpty,
});
if (feedback) {
this.setState({
valid,
feedback,
feedbackVisible: true,
});
} else {
// When we receive null `feedback`, we want to hide the tooltip.
// We leave the previous `feedback` content in state without updating it,
// so that we can hide the tooltip containing the most recent feedback
// via CSS animation.
this.setState({
valid,
feedbackVisible: false,
});
}
}
validateOnChange = throttle(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
render() { render() {
const { element, prefix, onValidate, children, ...inputProps } = this.props; const { element, prefix, onValidate, children, ...inputProps } = this.props;
@ -74,10 +131,12 @@ export default class Field extends React.PureComponent {
// Set some defaults for the <input> element // Set some defaults for the <input> element
inputProps.type = inputProps.type || "text"; inputProps.type = inputProps.type || "text";
inputProps.ref = "fieldInput"; inputProps.ref = input => this.input = input;
inputProps.placeholder = inputProps.placeholder || inputProps.label; inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange; inputProps.onChange = this.onChange;
inputProps.onBlur = this.onBlur;
const fieldInput = React.createElement(inputElement, inputProps, children); const fieldInput = React.createElement(inputElement, inputProps, children);
@ -95,12 +154,13 @@ export default class Field extends React.PureComponent {
mx_Field_invalid: onValidate && this.state.valid === false, mx_Field_invalid: onValidate && this.state.valid === false,
}); });
// handle displaying feedback on validity // Handle displaying feedback on validity
const Tooltip = sdk.getComponent("elements.Tooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
let feedback; let tooltip;
if (this.state.feedback) { if (this.state.feedback) {
feedback = <Tooltip tooltip = <Tooltip
tooltipClassName="mx_Field_tooltip" tooltipClassName="mx_Field_tooltip"
visible={this.state.feedbackVisible}
label={this.state.feedback} label={this.state.feedback}
/>; />;
} }
@ -109,7 +169,7 @@ export default class Field extends React.PureComponent {
{prefixContainer} {prefixContainer}
{fieldInput} {fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label> <label htmlFor={this.props.id}>{this.props.label}</label>
{feedback} {tooltip}
</div>; </div>;
} }
} }

View File

@ -31,10 +31,20 @@ module.exports = React.createClass({
className: React.PropTypes.string, className: React.PropTypes.string,
// Class applied to the tooltip itself // Class applied to the tooltip itself
tooltipClassName: React.PropTypes.string, tooltipClassName: React.PropTypes.string,
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible: React.PropTypes.bool,
// the react element to put into the tooltip // the react element to put into the tooltip
label: React.PropTypes.node, label: React.PropTypes.node,
}, },
getDefaultProps() {
return {
visible: true,
};
},
// Create a wrapper for the tooltip outside the parent and attach it to the body element // Create a wrapper for the tooltip outside the parent and attach it to the body element
componentDidMount: function() { componentDidMount: function() {
this.tooltipContainer = document.createElement("div"); this.tooltipContainer = document.createElement("div");
@ -85,7 +95,10 @@ module.exports = React.createClass({
style = this._updatePosition(style); style = this._updatePosition(style);
style.display = "block"; style.display = "block";
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName); const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
"mx_Tooltip_visible": this.props.visible,
"mx_Tooltip_invisible": !this.props.visible,
});
const tooltip = ( const tooltip = (
<div className={tooltipClasses} style={style}> <div className={tooltipClasses} style={style}>

View File

@ -0,0 +1,131 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable babel/no-invalid-this */
import classNames from 'classnames';
/**
* Creates a validation function from a set of rules describing what to validate.
*
* @param {Function} description
* Function that returns a string summary of the kind of value that will
* meet the validation rules. Shown at the top of the validation feedback.
* @param {Object} rules
* An array of rules describing how to check to input value. Each rule in an object
* and may have the following properties:
* - `key`: A unique ID for the rule. Required.
* - `test`: A function used to determine the rule's current validity. Required.
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
* @returns {Function}
* A validation function that takes in the current input value and returns
* the overall validity and a feedback UI that can be rendered for more detail.
*/
export default function withValidation({ description, rules }) {
return async function onValidate({ value, focused, allowEmpty = true }) {
if (!value && allowEmpty) {
return {
valid: null,
feedback: null,
};
}
const results = [];
let valid = true;
if (rules && rules.length) {
for (const rule of rules) {
if (!rule.key || !rule.test) {
continue;
}
// We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component.
const ruleValid = await rule.test.call(this, { value, allowEmpty });
valid = valid && ruleValid;
if (ruleValid && rule.valid) {
// If the rule's result is valid and has text to show for
// the valid state, show it.
const text = rule.valid.call(this);
if (!text) {
continue;
}
results.push({
key: rule.key,
valid: true,
text,
});
} else if (!ruleValid && rule.invalid) {
// If the rule's result is invalid and has text to show for
// the invalid state, show it.
const text = rule.invalid.call(this);
if (!text) {
continue;
}
results.push({
key: rule.key,
valid: false,
text,
});
}
}
}
// Hide feedback when not focused
if (!focused) {
return {
valid,
feedback: null,
};
}
let details;
if (results && results.length) {
details = <ul className="mx_Validation_details">
{results.map(result => {
const classes = classNames({
"mx_Validation_detail": true,
"mx_Validation_valid": result.valid,
"mx_Validation_invalid": !result.valid,
});
return <li key={result.key} className={classes}>
{result.text}
</li>;
})}
</ul>;
}
let summary;
if (description) {
// We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component.
const content = description.call(this);
summary = <div className="mx_Validation_description">{content}</div>;
}
let feedback;
if (summary || details) {
feedback = <div className="mx_Validation">
{summary}
{details}
</div>;
}
return {
valid,
feedback,
};
};
}

View File

@ -0,0 +1,223 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import { createMenu } from '../../structures/ContextualMenu';
import SettingsStore from '../../../settings/SettingsStore';
import { isContentActionable } from '../../../utils/EventUtils';
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
permalinkCreator: PropTypes.object,
getTile: PropTypes.func,
getReplyThread: PropTypes.func,
onFocusChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
agreeDimension: null,
likeDimension: null,
};
}
onFocusChange = (focused) => {
if (!this.props.onFocusChange) {
return;
}
this.props.onFocusChange(focused);
}
onCryptoClicked = () => {
const event = this.props.mxEvent;
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
{event},
);
}
onAgreeClick = (ev) => {
this.toggleDimensionValue("agreeDimension", "agree");
}
onDisagreeClick = (ev) => {
this.toggleDimensionValue("agreeDimension", "disagree");
}
onLikeClick = (ev) => {
this.toggleDimensionValue("likeDimension", "like");
}
onDislikeClick = (ev) => {
this.toggleDimensionValue("likeDimension", "dislike");
}
toggleDimensionValue(dimension, value) {
const state = this.state[dimension];
const newState = state !== value ? value : null;
this.setState({
[dimension]: newState,
});
// TODO: Send the reaction event
}
onReplyClick = (ev) => {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
});
}
onOptionsClick = (ev) => {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const buttonRect = ev.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const { getTile, getReplyThread } = this.props;
const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread();
let e2eInfoCallback = null;
if (this.props.mxEvent.isEncrypted()) {
e2eInfoCallback = () => this.onCryptoClicked();
}
createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent,
left: x,
top: y,
permalinkCreator: this.props.permalinkCreator,
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
e2eInfoCallback: e2eInfoCallback,
onFinished: () => {
this.onFocusChange(false);
},
});
this.onFocusChange(true);
}
isReactionsEnabled() {
return SettingsStore.isFeatureEnabled("feature_reactions");
}
renderAgreeDimension() {
if (!this.isReactionsEnabled()) {
return null;
}
const state = this.state.agreeDimension;
const options = [
{
key: "agree",
content: "👍",
onClick: this.onAgreeClick,
},
{
key: "disagree",
content: "👎",
onClick: this.onDisagreeClick,
},
];
return <span className="mx_MessageActionBar_reactionDimension"
title={_t("Agree or Disagree")}
>
{this.renderReactionDimensionItems(state, options)}
</span>;
}
renderLikeDimension() {
if (!this.isReactionsEnabled()) {
return null;
}
const state = this.state.likeDimension;
const options = [
{
key: "like",
content: "🙂",
onClick: this.onLikeClick,
},
{
key: "dislike",
content: "😔",
onClick: this.onDislikeClick,
},
];
return <span className="mx_MessageActionBar_reactionDimension"
title={_t("Like or Dislike")}
>
{this.renderReactionDimensionItems(state, options)}
</span>;
}
renderReactionDimensionItems(state, options) {
return options.map(option => {
const disabled = state && state !== option.key;
const classes = classNames({
mx_MessageActionBar_reactionDisabled: disabled,
});
return <span key={option.key}
className={classes}
onClick={option.onClick}
>
{option.content}
</span>;
});
}
render() {
let agreeDimensionReactionButtons;
let likeDimensionReactionButtons;
let replyButton;
if (isContentActionable(this.props.mxEvent)) {
agreeDimensionReactionButtons = this.renderAgreeDimension();
likeDimensionReactionButtons = this.renderLikeDimension();
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
/>;
}
return <div className="mx_MessageActionBar">
{agreeDimensionReactionButtons}
{likeDimensionReactionButtons}
{replyButton}
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={this.onOptionsClick}
/>
</div>;
}
}

View File

@ -0,0 +1,65 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { isContentActionable } from '../../../utils/EventUtils';
// TODO: Actually load reactions from the timeline
// Since we don't yet load reactions, let's inject some dummy data for testing the UI
// only. The UI assumes these are already sorted into the order we want to present,
// presumably highest vote first.
const SAMPLE_REACTIONS = {
"👍": 4,
"👎": 2,
"🙂": 1,
};
export default class ReactionsRow extends React.PureComponent {
static propTypes = {
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
}
render() {
const { mxEvent } = this.props;
if (!isContentActionable(mxEvent)) {
return null;
}
const content = mxEvent.getContent();
// TODO: Remove this once we load real reactions
if (!content.body || content.body !== "reactions test") {
return null;
}
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
const items = Object.entries(SAMPLE_REACTIONS).map(([content, count]) => {
return <ReactionsRowButton
key={content}
content={content}
count={count}
/>;
});
return <div className="mx_ReactionsRow">
{items}
</div>;
}
}

View File

@ -0,0 +1,65 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
content: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
}
constructor(props) {
super(props);
// TODO: This should be derived from actual reactions you may have sent
// once we have some API to read them.
this.state = {
selected: false,
};
}
onClick = (ev) => {
const state = this.state.selected;
this.setState({
selected: !state,
});
// TODO: Send the reaction event
};
render() {
const { content, count } = this.props;
const { selected } = this.state;
const classes = classNames({
mx_ReactionsRowButton: true,
mx_ReactionsRowButton_selected: selected,
});
let adjustedCount = count;
if (selected) {
adjustedCount++;
}
return <span className={classes}
onClick={this.onClick}
>
{content} {adjustedCount}
</span>;
}
}

View File

@ -17,7 +17,6 @@ limitations under the License.
'use strict'; 'use strict';
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
const React = require('react'); const React = require('react');
@ -30,7 +29,6 @@ const sdk = require('../../../index');
const TextForEvent = require('../../../TextForEvent'); const TextForEvent = require('../../../TextForEvent');
import withMatrixClient from '../../../wrappers/withMatrixClient'; import withMatrixClient from '../../../wrappers/withMatrixClient';
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk';
@ -172,8 +170,8 @@ module.exports = withMatrixClient(React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
// Whether the context menu is being displayed. // Whether the action bar is focused.
menu: false, actionBarFocused: false,
// Whether all read receipts are being displayed. If not, only display // Whether all read receipts are being displayed. If not, only display
// a truncation of them. // a truncation of them.
allReadAvatars: false, allReadAvatars: false,
@ -309,36 +307,6 @@ module.exports = withMatrixClient(React.createClass({
return actions.tweaks.highlight; return actions.tweaks.highlight;
}, },
onEditClicked: function(e) {
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const self = this;
const {tile, replyThread} = this.refs;
let e2eInfoCallback = null;
if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked();
ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent,
left: x,
top: y,
permalinkCreator: this.props.permalinkCreator,
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
e2eInfoCallback: e2eInfoCallback,
onFinished: function() {
self.setState({menu: false});
},
});
this.setState({menu: true});
},
toggleAllReadAvatars: function() { toggleAllReadAvatars: function() {
this.setState({ this.setState({
allReadAvatars: !this.state.allReadAvatars, allReadAvatars: !this.state.allReadAvatars,
@ -490,6 +458,20 @@ module.exports = withMatrixClient(React.createClass({
return null; return null;
}, },
onActionBarFocusChange(focused) {
this.setState({
actionBarFocused: focused,
});
},
getTile() {
return this.refs.tile;
},
getReplyThread() {
return this.refs.replyThread;
},
render: function() { render: function() {
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
const SenderProfile = sdk.getComponent('messages.SenderProfile'); const SenderProfile = sdk.getComponent('messages.SenderProfile');
@ -536,7 +518,7 @@ module.exports = withMatrixClient(React.createClass({
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation, mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last, mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual, mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu, mx_EventTile_actionBarFocused: this.state.actionBarFocused,
mx_EventTile_verified: this.state.verified === true, mx_EventTile_verified: this.state.verified === true,
mx_EventTile_unverified: this.state.verified === false, mx_EventTile_unverified: this.state.verified === false,
mx_EventTile_bad: isEncryptionFailure, mx_EventTile_bad: isEncryptionFailure,
@ -602,9 +584,14 @@ module.exports = withMatrixClient(React.createClass({
} }
} }
const editButton = ( const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
<span className="mx_EventTile_editButton" title={_t("Options")} onClick={this.onEditClicked} /> const actionBar = <MessageActionBar
); mxEvent={this.props.mxEvent}
permalinkCreator={this.props.permalinkCreator}
getTile={this.getTile}
getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange}
/>;
const timestamp = this.props.mxEvent.getTs() ? const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null; <MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -643,6 +630,14 @@ module.exports = withMatrixClient(React.createClass({
<ToolTipButton helpText={keyRequestHelpText} /> <ToolTipButton helpText={keyRequestHelpText} />
</div> : null; </div> : null;
let reactions;
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
reactions = <ReactionsRow
mxEvent={this.props.mxEvent}
/>;
}
switch (this.props.tileShape) { switch (this.props.tileShape) {
case 'notif': { case 'notif': {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
@ -755,7 +750,8 @@ module.exports = withMatrixClient(React.createClass({
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged} /> onHeightChanged={this.props.onHeightChanged} />
{ keyRequestInfo } { keyRequestInfo }
{ editButton } { reactions }
{ actionBar }
</div> </div>
{ {
// The avatar goes after the event tile as it's absolutly positioned to be over the // The avatar goes after the event tile as it's absolutly positioned to be over the

View File

@ -117,6 +117,7 @@ export default class RoomBreadcrumbs extends React.Component {
}; };
onRoomTimeline = (event, room) => { onRoomTimeline = (event, room) => {
if (!room) return; // Can be null for the notification timeline, etc.
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) { if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room); this._calculateRoomBadges(room);
} }

View File

@ -24,7 +24,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
const MessageCase = Object.freeze({ const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn", NotLoggedIn: "NotLoggedIn",
@ -105,12 +104,6 @@ module.exports = React.createClass({
} }
}, },
_onInviterClick(evt) {
evt.preventDefault();
const member = this._getInviteMember();
dis.dispatch({action: 'view_user_info', userId: member.userId});
},
_getMessageCase() { _getMessageCase() {
const isGuest = MatrixClientPeg.get().isGuest(); const isGuest = MatrixClientPeg.get().isGuest();
@ -118,8 +111,7 @@ module.exports = React.createClass({
return MessageCase.NotLoggedIn; return MessageCase.NotLoggedIn;
} }
const myMember = this.props.room && const myMember = this._getMyMember();
this.props.room.getMember(MatrixClientPeg.get().getUserId());
if (myMember) { if (myMember) {
if (myMember.isKicked()) { if (myMember.isKicked()) {
@ -158,9 +150,7 @@ module.exports = React.createClass({
}, },
_getKickOrBanInfo() { _getKickOrBanInfo() {
const myMember = this.props.room ? const myMember = this._getMyMember();
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
null;
if (!myMember) { if (!myMember) {
return {}; return {};
} }
@ -194,6 +184,13 @@ module.exports = React.createClass({
} }
}, },
_getMyMember() {
return (
this.props.room &&
this.props.room.getMember(MatrixClientPeg.get().getUserId())
);
},
_getInviteMember: function() { _getInviteMember: function() {
const {room} = this.props; const {room} = this.props;
if (!room) { if (!room) {
@ -208,6 +205,16 @@ module.exports = React.createClass({
return room.currentState.getMember(inviterUserId); return room.currentState.getMember(inviterUserId);
}, },
_isDMInvite() {
const myMember = this._getMyMember();
if (!myMember) {
return false;
}
const memberEvent = myMember.events.member;
const memberContent = memberEvent.getContent();
return memberContent.membership === "invite" && memberContent.is_direct;
},
onLoginClick: function() { onLoginClick: function() {
dis.dispatch({ action: 'start_login' }); dis.dispatch({ action: 'start_login' });
}, },
@ -279,7 +286,8 @@ module.exports = React.createClass({
break; break;
} }
case MessageCase.OtherThreePIDError: { case MessageCase.OtherThreePIDError: {
title = _t("Something went wrong with your invite to this room"); title = _t("Something went wrong with your invite to %(roomName)s",
{roomName: this._roomName()});
const joinRule = this._joinRule(); const joinRule = this._joinRule();
const errCodeMessage = _t("%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.", const errCodeMessage = _t("%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.",
{errcode: this.state.threePidFetchError.errcode}, {errcode: this.state.threePidFetchError.errcode},
@ -305,14 +313,19 @@ module.exports = React.createClass({
break; break;
} }
case MessageCase.InvitedEmailMismatch: { case MessageCase.InvitedEmailMismatch: {
title = _t("The room invite wasn't sent to your account"); title = _t("This invite to %(roomName)s wasn't sent to your account",
{roomName: this._roomName()});
const joinRule = this._joinRule(); const joinRule = this._joinRule();
if (joinRule === "public") { if (joinRule === "public") {
subTitle = _t("You can still join it because this is a public room."); subTitle = _t("You can still join it because this is a public room.");
primaryActionLabel = _t("Join the discussion"); primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick; primaryActionHandler = this.props.onJoinClick;
} else { } else {
subTitle = _t("Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.", {email: this.props.invitedEmail}); subTitle = _t(
"Sign in with a different account, ask for another invite, or " +
"add the e-mail address %(email)s to this account.",
{email: this.props.invitedEmail},
);
if (joinRule !== "invite") { if (joinRule !== "invite") {
primaryActionLabel = _t("Try to join anyway"); primaryActionLabel = _t("Try to join anyway");
primaryActionHandler = this.props.onJoinClick; primaryActionHandler = this.props.onJoinClick;
@ -321,26 +334,29 @@ module.exports = React.createClass({
break; break;
} }
case MessageCase.Invite: { case MessageCase.Invite: {
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
const avatar = <RoomAvatar room={this.props.room} />;
const inviteMember = this._getInviteMember(); const inviteMember = this._getInviteMember();
let avatar;
let inviterElement; let inviterElement;
if (inviteMember) { if (inviteMember) {
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); inviterElement = <span>
avatar = (<MemberAvatar member={inviteMember} onClick={this._onInviterClick} />); <span className="mx_RoomPreviewBar_inviter">
const inviterClasses = [ {inviteMember.rawDisplayName}
"mx_RoomPreviewBar_inviter", </span> ({inviteMember.userId})
getUserNameColorClass(inviteMember.userId), </span>;
].join(" ");
inviterElement = (
<a onClick={this._onInviterClick} className={inviterClasses}>
{inviteMember.name}
</a>
);
} else { } else {
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>); inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>);
} }
title = _t("Do you want to join this room?"); const isDM = this._isDMInvite();
if (isDM) {
title = _t("Do you want to chat with %(user)s?",
{ user: inviteMember.name });
} else {
title = _t("Do you want to join %(roomName)s?",
{ roomName: this._roomName() });
}
subTitle = [ subTitle = [
avatar, avatar,
_t("<userName/> invited you", {}, {userName: () => inviterElement}), _t("<userName/> invited you", {}, {userName: () => inviterElement}),
@ -354,7 +370,8 @@ module.exports = React.createClass({
} }
case MessageCase.ViewingRoom: { case MessageCase.ViewingRoom: {
if (this.props.canPreview) { if (this.props.canPreview) {
title = _t("You're previewing this room. Want to join it?"); title = _t("You're previewing %(roomName)s. Want to join it?",
{roomName: this._roomName()});
} else { } else {
title = _t("%(roomName)s can't be previewed. Do you want to join it?", title = _t("%(roomName)s can't be previewed. Do you want to join it?",
{roomName: this._roomName(true)}); {roomName: this._roomName(true)});
@ -372,7 +389,10 @@ module.exports = React.createClass({
title = _t("%(roomName)s is not accessible at this time.", {roomName: this._roomName(true)}); title = _t("%(roomName)s is not accessible at this time.", {roomName: this._roomName(true)});
subTitle = [ subTitle = [
_t("Try again later, or ask a room admin to check if you have access."), _t("Try again later, or ask a room admin to check if you have access."),
_t("%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.", _t(
"%(errcode)s was returned while trying to access the room. " +
"If you think you're seeing this message in error, please " +
"<issueLink>submit a bug report</issueLink>.",
{ errcode: this.props.error.errcode }, { errcode: this.props.error.errcode },
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose" { issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
target="_blank" rel="noopener">{ label }</a> }, target="_blank" rel="noopener">{ label }</a> },

View File

@ -119,7 +119,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
let setupCaption; let setupCaption;
if (this.state.backupInfo) { if (this.state.backupInfo) {
setupCaption = _t("Use Key Backup"); setupCaption = _t("Connect this device to Key Backup");
} else { } else {
setupCaption = _t("Start using Key Backup"); setupCaption = _t("Start using Key Backup");
} }

View File

@ -507,6 +507,7 @@ module.exports = React.createClass({
//'.m.rule.member_event': 'vector', //'.m.rule.member_event': 'vector',
'.m.rule.call': 'vector', '.m.rule.call': 'vector',
'.m.rule.suppress_notices': 'vector', '.m.rule.suppress_notices': 'vector',
'.m.rule.tombstone': 'vector',
// Others go to others // Others go to others
}; };
@ -562,6 +563,7 @@ module.exports = React.createClass({
//'im.vector.rule.member_event', //'im.vector.rule.member_event',
'.m.rule.call', '.m.rule.call',
'.m.rule.suppress_notices', '.m.rule.suppress_notices',
'.m.rule.tombstone',
]; ];
for (const i in vectorRuleIds) { for (const i in vectorRuleIds) {
const vectorRuleId = vectorRuleIds[i]; const vectorRuleId = vectorRuleIds[i];
@ -702,6 +704,10 @@ module.exports = React.createClass({
const rows = []; const rows = [];
for (const i in this.state.vectorPushRules) { for (const i in this.state.vectorPushRules) {
const rule = this.state.vectorPushRules[i]; const rule = this.state.vectorPushRules[i];
if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) {
console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`);
continue;
}
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState); //console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState)); rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
} }

View File

@ -297,6 +297,7 @@
"Show recent room avatars above the room list": "Show recent room avatars above the room list", "Show recent room avatars above the room list": "Show recent room avatars above the room list",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header", "Render simple counters in room header": "Render simple counters in room header",
"React to messages with emoji": "React to messages with emoji",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
"Use compact timeline layout": "Use compact timeline layout", "Use compact timeline layout": "Use compact timeline layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show a placeholder for removed messages": "Show a placeholder for removed messages",
@ -342,6 +343,7 @@
"When I'm invited to a room": "When I'm invited to a room", "When I'm invited to a room": "When I'm invited to a room",
"Call invitation": "Call invitation", "Call invitation": "Call invitation",
"Messages sent by bot": "Messages sent by bot", "Messages sent by bot": "Messages sent by bot",
"When rooms are upgraded": "When rooms are upgraded",
"Active call (%(roomName)s)": "Active call (%(roomName)s)", "Active call (%(roomName)s)": "Active call (%(roomName)s)",
"unknown caller": "unknown caller", "unknown caller": "unknown caller",
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s", "Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
@ -671,7 +673,6 @@
"%(senderName)s sent an image": "%(senderName)s sent an image", "%(senderName)s sent an image": "%(senderName)s sent an image",
"%(senderName)s sent a video": "%(senderName)s sent a video", "%(senderName)s sent a video": "%(senderName)s sent a video",
"%(senderName)s uploaded a file": "%(senderName)s uploaded a file", "%(senderName)s uploaded a file": "%(senderName)s uploaded a file",
"Options": "Options",
"Your key share request has been sent - please check your other devices for key share requests.": "Your key share request has been sent - please check your other devices for key share requests.", "Your key share request has been sent - please check your other devices for key share requests.": "Your key share request has been sent - please check your other devices for key share requests.",
"Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.", "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.",
"If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.", "If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.",
@ -727,20 +728,20 @@
"block-quote": "block-quote", "block-quote": "block-quote",
"bulleted-list": "bulleted-list", "bulleted-list": "bulleted-list",
"numbered-list": "numbered-list", "numbered-list": "numbered-list",
"Hangup": "Hangup",
"Voice call": "Voice call", "Voice call": "Voice call",
"Video call": "Video call", "Video call": "Video call",
"Upload file": "Upload file", "Hangup": "Hangup",
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
"Upload file": "Upload file",
"Send an encrypted reply…": "Send an encrypted reply…", "Send an encrypted reply…": "Send an encrypted reply…",
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…", "Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
"Send an encrypted message…": "Send an encrypted message…", "Send an encrypted message…": "Send an encrypted message…",
"Send a message (unencrypted)…": "Send a message (unencrypted)…", "Send a message (unencrypted)…": "Send a message (unencrypted)…",
"Markdown is disabled": "Markdown is disabled",
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
"The conversation continues here.": "The conversation continues here.", "The conversation continues here.": "The conversation continues here.",
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
"You do not have permission to post to this room": "You do not have permission to post to this room", "You do not have permission to post to this room": "You do not have permission to post to this room",
"Markdown is disabled": "Markdown is disabled",
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
"Server error": "Server error", "Server error": "Server error",
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
"Command error": "Command error", "Command error": "Command error",
@ -804,25 +805,25 @@
"Forget this room": "Forget this room", "Forget this room": "Forget this room",
"Re-join": "Re-join", "Re-join": "Re-join",
"You were banned from %(roomName)s by %(memberName)s": "You were banned from %(roomName)s by %(memberName)s", "You were banned from %(roomName)s by %(memberName)s": "You were banned from %(roomName)s by %(memberName)s",
"Something went wrong with your invite to this room": "Something went wrong with your invite to this room", "Something went wrong with your invite to %(roomName)s": "Something went wrong with your invite to %(roomName)s",
"%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.", "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.",
"You can only join it with a working invite.": "You can only join it with a working invite.", "You can only join it with a working invite.": "You can only join it with a working invite.",
"You can still join it because this is a public room.": "You can still join it because this is a public room.", "You can still join it because this is a public room.": "You can still join it because this is a public room.",
"Join the discussion": "Join the discussion", "Join the discussion": "Join the discussion",
"Try to join anyway": "Try to join anyway", "Try to join anyway": "Try to join anyway",
"The room invite wasn't sent to your account": "The room invite wasn't sent to your account", "This invite to %(roomName)s wasn't sent to your account": "This invite to %(roomName)s wasn't sent to your account",
"Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.", "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.",
"Do you want to join this room?": "Do you want to join this room?", "Do you want to chat with %(user)s?": "Do you want to chat with %(user)s?",
"Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
"<userName/> invited you": "<userName/> invited you", "<userName/> invited you": "<userName/> invited you",
"Reject": "Reject", "Reject": "Reject",
"You're previewing this room. Want to join it?": "You're previewing this room. Want to join it?", "You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s can't be previewed. Do you want to join it?", "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s can't be previewed. Do you want to join it?",
"%(roomName)s does not exist.": "%(roomName)s does not exist.", "%(roomName)s does not exist.": "%(roomName)s does not exist.",
"This room doesn't exist. Are you sure you're at the right place?": "This room doesn't exist. Are you sure you're at the right place?", "This room doesn't exist. Are you sure you're at the right place?": "This room doesn't exist. Are you sure you're at the right place?",
"%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.",
"Try again later, or ask a room admin to check if you have access.": "Try again later, or ask a room admin to check if you have access.", "Try again later, or ask a room admin to check if you have access.": "Try again later, or ask a room admin to check if you have access.",
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.", "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
"Use Key Backup": "Use Key Backup",
"Never lose encrypted messages": "Never lose encrypted messages", "Never lose encrypted messages": "Never lose encrypted messages",
"Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>", "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
@ -890,6 +891,10 @@
"Today": "Today", "Today": "Today",
"Yesterday": "Yesterday", "Yesterday": "Yesterday",
"Error decrypting audio": "Error decrypting audio", "Error decrypting audio": "Error decrypting audio",
"Agree or Disagree": "Agree or Disagree",
"Like or Dislike": "Like or Dislike",
"Reply": "Reply",
"Options": "Options",
"Attachment": "Attachment", "Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment", "Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s", "Decrypt %(text)s": "Decrypt %(text)s",
@ -1206,10 +1211,6 @@
"Missing session data": "Missing session data", "Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
"Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
"Error showing you your room": "Error showing you your room",
"Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit <a>the issue</a>.": "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit <a>the issue</a>.",
"Send debug logs and reload Riot": "Send debug logs and reload Riot",
"Reload Riot without sending logs": "Reload Riot without sending logs",
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.", "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.",
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.", "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.",
"Room contains unknown devices": "Room contains unknown devices", "Room contains unknown devices": "Room contains unknown devices",
@ -1260,7 +1261,6 @@
"Resend": "Resend", "Resend": "Resend",
"Cancel Sending": "Cancel Sending", "Cancel Sending": "Cancel Sending",
"Forward Message": "Forward Message", "Forward Message": "Forward Message",
"Reply": "Reply",
"Pin Message": "Pin Message", "Pin Message": "Pin Message",
"View Source": "View Source", "View Source": "View Source",
"View Decrypted Source": "View Decrypted Source", "View Decrypted Source": "View Decrypted Source",
@ -1323,12 +1323,26 @@
"Change": "Change", "Change": "Change",
"Sign in with": "Sign in with", "Sign in with": "Sign in with",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Use an email address to recover your account": "Use an email address to recover your account",
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
"Enter password": "Enter password",
"Nice, strong password!": "Nice, strong password!",
"Keep going...": "Keep going...",
"Passwords don't match": "Passwords don't match",
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
"Use letters, numbers, dashes and underscores only": "Use letters, numbers, dashes and underscores only",
"Enter username": "Enter username",
"Some characters not allowed": "Some characters not allowed",
"Email (optional)": "Email (optional)",
"Confirm": "Confirm",
"Phone (optional)": "Phone (optional)",
"Create your Matrix account": "Create your Matrix account", "Create your Matrix account": "Create your Matrix account",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Email (optional)": "Email (optional)", "Use an email address to recover your account.": "Use an email address to recover your account.",
"Phone (optional)": "Phone (optional)", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.",
"Confirm": "Confirm",
"Use an email address to recover your account. Other users can invite you to rooms using your contact details.": "Use an email address to recover your account. Other users can invite you to rooms using your contact details.",
"Other servers": "Other servers", "Other servers": "Other servers",
"Enter custom server URLs <a>What does this mean?</a>": "Enter custom server URLs <a>What does this mean?</a>", "Enter custom server URLs <a>What does this mean?</a>": "Enter custom server URLs <a>What does this mean?</a>",
"Homeserver URL": "Homeserver URL", "Homeserver URL": "Homeserver URL",
@ -1516,15 +1530,6 @@
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
"Missing password.": "Missing password.",
"Passwords don't match.": "Passwords don't match.",
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Password too short (min %(MIN_PASSWORD_LENGTH)s).",
"This doesn't look like a valid email address.": "This doesn't look like a valid email address.",
"This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.",
"An email address is required to register on this homeserver.": "An email address is required to register on this homeserver.",
"A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.",
"You need to enter a username.": "You need to enter a username.",
"An unknown error occurred.": "An unknown error occurred.",
"Create your account": "Create your account", "Create your account": "Create your account",
"Commands": "Commands", "Commands": "Commands",
"Results from DuckDuckGo": "Results from DuckDuckGo", "Results from DuckDuckGo": "Results from DuckDuckGo",
@ -1563,7 +1568,6 @@
"File to import": "File to import", "File to import": "File to import",
"Import": "Import", "Import": "Import",
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
"Keep going...": "Keep going...",
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.", "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Enter a passphrase...": "Enter a passphrase...", "Enter a passphrase...": "Enter a passphrase...",

View File

@ -183,4 +183,15 @@ module.exports = {
off: StandardActions.ACTION_DONT_NOTIFY, off: StandardActions.ACTION_DONT_NOTIFY,
}, },
}), }),
// Room upgrades (tombstones)
".m.rule.tombstone": new VectorPushRuleDefinition({
kind: "override",
description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
on: StandardActions.ACTION_NOTIFY,
loud: StandardActions.ACTION_HIGHLIGHT,
off: StandardActions.ACTION_DISABLED,
},
}),
}; };

View File

@ -122,6 +122,9 @@ export const SETTINGS = {
"feature_notification_sounds": { "feature_notification_sounds": {
isFeature: true, isFeature: true,
displayName: _td("Custom Notification Sounds"), displayName: _td("Custom Notification Sounds"),
"feature_reactions": {
isFeature: true,
displayName: _td("React to messages with emoji"),
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },

45
src/utils/EventUtils.js Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventStatus } from 'matrix-js-sdk';
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
* which effectively checks whether it's a regular message that has been sent and that we
* can display.
*
* @param {MatrixEvent} mxEvent The event to check
* @returns {boolean} true if actionable
*/
export function isContentActionable(mxEvent) {
const { status: eventStatus } = mxEvent;
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
if (isSent && mxEvent.getType() === 'm.room.message') {
const content = mxEvent.getContent();
if (
content.msgtype &&
content.msgtype !== 'm.bad.encrypted' &&
content.hasOwnProperty('body')
) {
return true;
}
}
return false;
}

View File

@ -147,7 +147,7 @@ export async function encryptMegolmKeyFile(data, password, options) {
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss // (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay. // of a single bit of iv is a price we have to pay.
iv[9] &= 0x7f; iv[8] &= 0x7f;
const [aesKey, hmacKey] = await deriveKeys(salt, kdfRounds, password); const [aesKey, hmacKey] = await deriveKeys(salt, kdfRounds, password);
const encodedData = new TextEncoder().encode(data); const encodedData = new TextEncoder().encode(data);

View File

@ -67,7 +67,9 @@ export function scorePassword(password) {
if (password.length === 0) return null; if (password.length === 0) return null;
const userInputs = ZXCVBN_USER_INPUTS.slice(); const userInputs = ZXCVBN_USER_INPUTS.slice();
userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); if (MatrixClientPeg.get()) {
userInputs.push(MatrixClientPeg.get().getUserIdLocalpart());
}
let zxcvbnResult = zxcvbn(password, userInputs); let zxcvbnResult = zxcvbn(password, userInputs);
// Work around https://github.com/dropbox/zxcvbn/issues/216 // Work around https://github.com/dropbox/zxcvbn/issues/216

899
yarn.lock

File diff suppressed because it is too large Load Diff