Merge remote-tracking branch 'upstream/develop' into hs/custom-notif-sounds
This commit is contained in:
commit
9369e964fa
143 changed files with 3498 additions and 1773 deletions
|
@ -4,7 +4,6 @@ src/component-index.js
|
|||
src/components/structures/BottomLeftMenu.js
|
||||
src/components/structures/CreateRoom.js
|
||||
src/components/structures/MessagePanel.js
|
||||
src/components/structures/NotificationPanel.js
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/structures/RoomStatusBar.js
|
||||
src/components/structures/RoomView.js
|
||||
|
|
158
CHANGELOG.md
158
CHANGELOG.md
|
@ -1,3 +1,161 @@
|
|||
Changes in [1.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.0) (2019-05-29)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.0-rc.1...v1.2.0)
|
||||
|
||||
* COLR font check fixes for release
|
||||
[\#3041](https://github.com/matrix-org/matrix-react-sdk/pull/3041)
|
||||
* Revert "Make the timeline less noisy for screen readers (mk II) #3019" for
|
||||
release
|
||||
[\#3036](https://github.com/matrix-org/matrix-react-sdk/pull/3036)
|
||||
* Override font for usercontent download link for release
|
||||
[\#3037](https://github.com/matrix-org/matrix-react-sdk/pull/3037)
|
||||
|
||||
Changes in [1.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.0-rc.1) (2019-05-23)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.2...v1.2.0-rc.1)
|
||||
|
||||
* Update from Weblate
|
||||
[\#3023](https://github.com/matrix-org/matrix-react-sdk/pull/3023)
|
||||
* Use the correct line-height for bold emoji
|
||||
[\#3022](https://github.com/matrix-org/matrix-react-sdk/pull/3022)
|
||||
* Make the timeline less noisy for screen readers (mk II)
|
||||
[\#3019](https://github.com/matrix-org/matrix-react-sdk/pull/3019)
|
||||
* Label message edit field as such for screen readers
|
||||
[\#3020](https://github.com/matrix-org/matrix-react-sdk/pull/3020)
|
||||
* Move checkmark to the front of key backup message
|
||||
[\#3014](https://github.com/matrix-org/matrix-react-sdk/pull/3014)
|
||||
* Revert "Make the timeline less noisy for screen readers"
|
||||
[\#3017](https://github.com/matrix-org/matrix-react-sdk/pull/3017)
|
||||
* Translate scroll movement if the deltaX is the same as the threshold
|
||||
[\#3016](https://github.com/matrix-org/matrix-react-sdk/pull/3016)
|
||||
* Make the timeline less noisy for screen readers
|
||||
[\#3007](https://github.com/matrix-org/matrix-react-sdk/pull/3007)
|
||||
* Windows emoji tweaks
|
||||
[\#3015](https://github.com/matrix-org/matrix-react-sdk/pull/3015)
|
||||
* Message editing: update link previews after editing
|
||||
[\#3004](https://github.com/matrix-org/matrix-react-sdk/pull/3004)
|
||||
* js-sdk interactive auth now sends email token
|
||||
[\#3010](https://github.com/matrix-org/matrix-react-sdk/pull/3010)
|
||||
* remove SBIX font and fallback to native emoji
|
||||
[\#3011](https://github.com/matrix-org/matrix-react-sdk/pull/3011)
|
||||
* Update from Weblate
|
||||
[\#3012](https://github.com/matrix-org/matrix-react-sdk/pull/3012)
|
||||
* load twemoji dynamically as colr or sbix; fix monospace
|
||||
[\#3008](https://github.com/matrix-org/matrix-react-sdk/pull/3008)
|
||||
* Guard against null rooms in `onEventDecrypted`
|
||||
[\#3009](https://github.com/matrix-org/matrix-react-sdk/pull/3009)
|
||||
* Only show reactions in main message timeline
|
||||
[\#3005](https://github.com/matrix-org/matrix-react-sdk/pull/3005)
|
||||
* Add voice labels for quick add room buttons
|
||||
[\#3006](https://github.com/matrix-org/matrix-react-sdk/pull/3006)
|
||||
* Update TopLeftMenu for accessibility: Keyboard shortcut, reduced screen
|
||||
reader noise
|
||||
[\#2994](https://github.com/matrix-org/matrix-react-sdk/pull/2994)
|
||||
* Remove reacted with text when shortcode missing
|
||||
[\#3003](https://github.com/matrix-org/matrix-react-sdk/pull/3003)
|
||||
* Fixup: also change editor margin when last event and buttons are not
|
||||
overlaying
|
||||
[\#3002](https://github.com/matrix-org/matrix-react-sdk/pull/3002)
|
||||
* Message editing: render avatars for pills in the editor
|
||||
[\#2997](https://github.com/matrix-org/matrix-react-sdk/pull/2997)
|
||||
* Replace emojione with twemoji + emojibase
|
||||
[\#2995](https://github.com/matrix-org/matrix-react-sdk/pull/2995)
|
||||
* Hide WhoIsTyping component if the MessagePanel is shaped e.g file grid
|
||||
[\#3000](https://github.com/matrix-org/matrix-react-sdk/pull/3000)
|
||||
* Close copy tooltip in edge cases correctly
|
||||
[\#2999](https://github.com/matrix-org/matrix-react-sdk/pull/2999)
|
||||
* Limit reaction sender tooltip to 6 people
|
||||
[\#2998](https://github.com/matrix-org/matrix-react-sdk/pull/2998)
|
||||
* Message editing: apply design
|
||||
[\#2996](https://github.com/matrix-org/matrix-react-sdk/pull/2996)
|
||||
* Add debug feature to show hidden events in timeline
|
||||
[\#2993](https://github.com/matrix-org/matrix-react-sdk/pull/2993)
|
||||
* Mute screen readers over reactions
|
||||
[\#2986](https://github.com/matrix-org/matrix-react-sdk/pull/2986)
|
||||
* Fix not being able to edit already edited messages
|
||||
[\#2992](https://github.com/matrix-org/matrix-react-sdk/pull/2992)
|
||||
* Add a basic tooltip showing who reacted
|
||||
[\#2991](https://github.com/matrix-org/matrix-react-sdk/pull/2991)
|
||||
* Message editing: show (edited) marker on edited messages, with tooltip
|
||||
[\#2990](https://github.com/matrix-org/matrix-react-sdk/pull/2990)
|
||||
* Update from Weblate
|
||||
[\#2989](https://github.com/matrix-org/matrix-react-sdk/pull/2989)
|
||||
* Message editing: only allow editing of text messages
|
||||
[\#2988](https://github.com/matrix-org/matrix-react-sdk/pull/2988)
|
||||
* Message editing: shift+enter for newline, enter to send
|
||||
[\#2987](https://github.com/matrix-org/matrix-react-sdk/pull/2987)
|
||||
* Apply Flex voodoo for devtools send event dialog
|
||||
[\#2985](https://github.com/matrix-org/matrix-react-sdk/pull/2985)
|
||||
* Fix some source strings noticed as incorrect by translators
|
||||
[\#2984](https://github.com/matrix-org/matrix-react-sdk/pull/2984)
|
||||
* Message editing: fix some bugs in cursor behaviour
|
||||
[\#2983](https://github.com/matrix-org/matrix-react-sdk/pull/2983)
|
||||
* Message editing: local echo & back-pagination
|
||||
[\#2982](https://github.com/matrix-org/matrix-react-sdk/pull/2982)
|
||||
* Listen for removed relations
|
||||
[\#2981](https://github.com/matrix-org/matrix-react-sdk/pull/2981)
|
||||
* Update from Weblate
|
||||
[\#2980](https://github.com/matrix-org/matrix-react-sdk/pull/2980)
|
||||
* Use `getRelation` helper
|
||||
[\#2977](https://github.com/matrix-org/matrix-react-sdk/pull/2977)
|
||||
* Add tooltips to rotate and close buttons in ImageView (#9686)
|
||||
[\#2979](https://github.com/matrix-org/matrix-react-sdk/pull/2979)
|
||||
* Message editing: smaller fixes
|
||||
[\#2978](https://github.com/matrix-org/matrix-react-sdk/pull/2978)
|
||||
* Message editing: adjust to js-sdk changes of marking original event as
|
||||
replaced
|
||||
[\#2973](https://github.com/matrix-org/matrix-react-sdk/pull/2973)
|
||||
* Fix Single Sign-on
|
||||
[\#2974](https://github.com/matrix-org/matrix-react-sdk/pull/2974)
|
||||
* Initial support for editing messages
|
||||
[\#2952](https://github.com/matrix-org/matrix-react-sdk/pull/2952)
|
||||
* Check permission to invite before showing invite buttons/disable them
|
||||
[\#2957](https://github.com/matrix-org/matrix-react-sdk/pull/2957)
|
||||
* Support a backup room ID in PermalinkCreator
|
||||
[\#2963](https://github.com/matrix-org/matrix-react-sdk/pull/2963)
|
||||
* Always thumbnail for GIFs
|
||||
[\#2962](https://github.com/matrix-org/matrix-react-sdk/pull/2962)
|
||||
* Fix registration with email
|
||||
[\#2967](https://github.com/matrix-org/matrix-react-sdk/pull/2967)
|
||||
* Add configuration flag to disable minimum password requirements
|
||||
[\#2947](https://github.com/matrix-org/matrix-react-sdk/pull/2947)
|
||||
* Send and undo reaction events
|
||||
[\#2954](https://github.com/matrix-org/matrix-react-sdk/pull/2954)
|
||||
* Fix bug where email was not required where it shouldn't have been
|
||||
[\#2961](https://github.com/matrix-org/matrix-react-sdk/pull/2961)
|
||||
* add /rainbow and /rainbowme Slash Commands
|
||||
[\#2958](https://github.com/matrix-org/matrix-react-sdk/pull/2958)
|
||||
* Fix invite via MemberInfo
|
||||
[\#2956](https://github.com/matrix-org/matrix-react-sdk/pull/2956)
|
||||
* Close Room Settings upon Leave Room
|
||||
[\#2955](https://github.com/matrix-org/matrix-react-sdk/pull/2955)
|
||||
* Command to change avatar for a single room, including upload of mxc res
|
||||
[\#2953](https://github.com/matrix-org/matrix-react-sdk/pull/2953)
|
||||
* Add View Servers in Room to Devtools
|
||||
[\#2804](https://github.com/matrix-org/matrix-react-sdk/pull/2804)
|
||||
* Update 'Rooms' import RoomView.js file
|
||||
[\#2951](https://github.com/matrix-org/matrix-react-sdk/pull/2951)
|
||||
* Extract `ReactionDimension` out of `MessageActionBar`
|
||||
[\#2950](https://github.com/matrix-org/matrix-react-sdk/pull/2950)
|
||||
* Always default to the registration form
|
||||
[\#2942](https://github.com/matrix-org/matrix-react-sdk/pull/2942)
|
||||
* Check for `room` in all `Room.timeline*` handlers
|
||||
[\#2945](https://github.com/matrix-org/matrix-react-sdk/pull/2945)
|
||||
* Remove the karma junit reporter
|
||||
[\#2944](https://github.com/matrix-org/matrix-react-sdk/pull/2944)
|
||||
* yarn upgrade
|
||||
[\#2943](https://github.com/matrix-org/matrix-react-sdk/pull/2943)
|
||||
* Support changing options for .m.rule.tombstone push rule
|
||||
[\#2798](https://github.com/matrix-org/matrix-react-sdk/pull/2798)
|
||||
* Remove timeline explosion rageshake prompt
|
||||
[\#2939](https://github.com/matrix-org/matrix-react-sdk/pull/2939)
|
||||
* Add existing reactions below message
|
||||
[\#2940](https://github.com/matrix-org/matrix-react-sdk/pull/2940)
|
||||
* Fix lint errors in TimelinePanel
|
||||
[\#2938](https://github.com/matrix-org/matrix-react-sdk/pull/2938)
|
||||
* Add primary reactions to action bar
|
||||
[\#2937](https://github.com/matrix-org/matrix-react-sdk/pull/2937)
|
||||
|
||||
Changes in [1.1.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.2) (2019-05-15)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.1...v1.1.2)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -65,7 +65,8 @@
|
|||
"classnames": "^2.1.2",
|
||||
"commonmark": "^0.28.1",
|
||||
"counterpart": "^0.18.0",
|
||||
"emojione": "2.2.7",
|
||||
"emojibase-data": "^4.0.0",
|
||||
"emojibase-regex": "^3.0.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"filesize": "3.5.6",
|
||||
"flux": "2.1.1",
|
||||
|
@ -80,7 +81,7 @@
|
|||
"linkifyjs": "^2.1.6",
|
||||
"lodash": "^4.13.1",
|
||||
"lolex": "2.3.2",
|
||||
"matrix-js-sdk": "1.1.0",
|
||||
"matrix-js-sdk": "1.2.0",
|
||||
"optimist": "^0.6.1",
|
||||
"pako": "^1.0.5",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
|
|
|
@ -32,6 +32,11 @@ body {
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: $monospace-font-family;
|
||||
font-size: 100% !important;
|
||||
}
|
||||
|
||||
.error, .warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
@ -110,6 +115,14 @@ textarea {
|
|||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
// This is used to hide the standard outline added by browsers for
|
||||
// accessible (focusable) components. Not intended for buttons, but
|
||||
// should be used on things like focusable containers where the outline
|
||||
// is usually not helping anyone.
|
||||
.mx_HiddenFocusable {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// .mx_textinput is a container for a text input
|
||||
// + some other controls like buttons, ...
|
||||
// it has the appearance of a text box so the controls
|
||||
|
@ -445,15 +458,6 @@ textarea {
|
|||
background-color: $primary-bg-color;
|
||||
}
|
||||
|
||||
.mx_emojione {
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_emojione_selected {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background-color: $accent-color;
|
||||
color: $selection-fg-color;
|
||||
|
@ -557,4 +561,3 @@ textarea {
|
|||
.mx_Username_color8 {
|
||||
color: $username-variant8-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
@import "./views/elements/_RoleButton.scss";
|
||||
@import "./views/elements/_Spinner.scss";
|
||||
@import "./views/elements/_SyntaxHighlight.scss";
|
||||
@import "./views/elements/_TextWithTooltip.scss";
|
||||
@import "./views/elements/_ToggleSwitch.scss";
|
||||
@import "./views/elements/_ToolTipButton.scss";
|
||||
@import "./views/elements/_Tooltip.scss";
|
||||
|
@ -119,10 +120,12 @@
|
|||
@import "./views/messages/_ReactionDimension.scss";
|
||||
@import "./views/messages/_ReactionsRow.scss";
|
||||
@import "./views/messages/_ReactionsRowButton.scss";
|
||||
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
|
||||
@import "./views/messages/_RoomAvatarEvent.scss";
|
||||
@import "./views/messages/_SenderProfile.scss";
|
||||
@import "./views/messages/_TextualEvent.scss";
|
||||
@import "./views/messages/_UnknownBody.scss";
|
||||
@import "./views/messages/_ViewSourceEvent.scss";
|
||||
@import "./views/room_settings/_AliasSettings.scss";
|
||||
@import "./views/room_settings/_ColorSettings.scss";
|
||||
@import "./views/rooms/_AppsDrawer.scss";
|
||||
|
|
|
@ -79,3 +79,7 @@ limitations under the License.
|
|||
.mx_Login_type_dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.mx_Login_underlinedServerName {
|
||||
border-bottom: 1px dashed $accent-color;
|
||||
}
|
||||
|
|
|
@ -35,3 +35,8 @@ limitations under the License.
|
|||
.mx_ServerConfig_help:link {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mx_ServerConfig_error {
|
||||
display: block;
|
||||
color: $warning-color;
|
||||
}
|
||||
|
|
|
@ -82,8 +82,13 @@ limitations under the License.
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_DevTools_content .mx_Field_input + .mx_Field_input {
|
||||
margin-left: 42px;
|
||||
.mx_DevTools_eventTypeStateKeyGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mx_DevTools_content .mx_Field_input:first-of-type {
|
||||
margin-right: 42px;
|
||||
}
|
||||
|
||||
.mx_DevTools_tgl {
|
||||
|
|
|
@ -16,40 +16,61 @@ limitations under the License.
|
|||
|
||||
.mx_MessageEditor {
|
||||
border-radius: 4px;
|
||||
background-color: $header-panel-bg-color;
|
||||
padding: 11px 13px 7px 56px;
|
||||
padding: 3px;
|
||||
// this is to try not make the text move but still have some
|
||||
// padding around and in the editor.
|
||||
// Actual values from fiddling around in inspector
|
||||
margin: -7px -10px -5px -10px;
|
||||
overflow: visible !important; // override mx_EventTile_content
|
||||
|
||||
.mx_MessageEditor_editor {
|
||||
border-radius: 4px;
|
||||
border: solid 1px #e9edf1;
|
||||
background-color: #ffffff;
|
||||
padding: 10px;
|
||||
border: solid 1px $primary-hairline-color;
|
||||
background-color: $primary-bg-color;
|
||||
padding: 3px 6px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
outline: none;
|
||||
max-height: 200px;
|
||||
overflow-x: auto;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
span.mx_UserPill, span.mx_RoomPill {
|
||||
padding-left: 21px;
|
||||
position: relative;
|
||||
|
||||
span.user-pill, span.room-pill {
|
||||
border-radius: 16px;
|
||||
display: inline-block;
|
||||
color: $primary-fg-color;
|
||||
background-color: $other-user-pill-bg-color;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
// avatar psuedo element
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
content: var(--avatar-letter);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--avatar-background), $avatar-bg-color;
|
||||
color: $avatar-initial-color;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
font-size: 10.4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageEditor_buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
padding: 5px 0;
|
||||
justify-content: flex-end;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: $header-panel-bg-color;
|
||||
z-index: 100;
|
||||
right: 0;
|
||||
margin: 0 -110px 0 0;
|
||||
padding-right: 147px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
margin-left: 5px;
|
||||
|
@ -62,3 +83,8 @@ limitations under the License.
|
|||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_last .mx_MessageEditor_buttons {
|
||||
position: static;
|
||||
margin-right: -147px;
|
||||
}
|
||||
|
|
19
res/css/views/elements/_TextWithTooltip.scss
Normal file
19
res/css/views/elements/_TextWithTooltip.scss
Normal 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_TextWithTooltip_tooltip {
|
||||
display: none;
|
||||
}
|
|
@ -74,3 +74,19 @@ limitations under the License.
|
|||
animation: mx_fadeout 0.1s forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Tooltip_timeline {
|
||||
box-shadow: none;
|
||||
background-color: $tooltip-timeline-bg-color;
|
||||
color: $tooltip-timeline-fg-color;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
padding: 6px 8px;
|
||||
|
||||
.mx_Tooltip_chevron::after {
|
||||
border-right-color: $tooltip-timeline-bg-color;
|
||||
}
|
||||
}
|
||||
|
|
19
res/css/views/messages/_ReactionsRowButtonTooltip.scss
Normal file
19
res/css/views/messages/_ReactionsRowButtonTooltip.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ReactionsRowButtonTooltip_reactedWith {
|
||||
opacity: 0.7;
|
||||
}
|
50
res/css/views/messages/_ViewSourceEvent.scss
Normal file
50
res/css/views/messages/_ViewSourceEvent.scss
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_EventTile_content.mx_ViewSourceEvent {
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
|
||||
pre, code {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
pre {
|
||||
line-height: 1.2;
|
||||
margin: 3.5px 0;
|
||||
}
|
||||
|
||||
.mx_ViewSourceEvent_toggle {
|
||||
width: 12px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0 center;
|
||||
mask-size: auto 12px;
|
||||
visibility: hidden;
|
||||
background-color: $accent-color;
|
||||
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
|
||||
}
|
||||
|
||||
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
|
||||
mask-position: 0 bottom;
|
||||
margin-bottom: 7px;
|
||||
mask-image: url('$(res)/img/feather-customised/widget/minimise.svg');
|
||||
}
|
||||
|
||||
&:hover .mx_ViewSourceEvent_toggle {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
|
@ -40,7 +40,16 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile_continuation {
|
||||
padding-top: 0px ! important;
|
||||
padding-top: 0px !important;
|
||||
|
||||
&.mx_EventTile_isEditing {
|
||||
padding-top: 5px !important;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_isEditing {
|
||||
background-color: $header-panel-bg-color;
|
||||
}
|
||||
|
||||
.mx_EventTile .mx_SenderProfile {
|
||||
|
@ -72,6 +81,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.mx_EventTile .mx_MessageTimestamp {
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
|
@ -108,7 +121,7 @@ limitations under the License.
|
|||
/* HACK to override line-height which is already marked important elsewhere */
|
||||
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
||||
font-size: 48px ! important;
|
||||
line-height: 48px ! important;
|
||||
line-height: 57px ! important;
|
||||
}
|
||||
|
||||
/* this is used for the tile for the event which is selected via the URL.
|
||||
|
@ -149,8 +162,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile_sending .mx_UserPill,
|
||||
.mx_EventTile_sending .mx_RoomPill,
|
||||
.mx_EventTile_sending .mx_emojione {
|
||||
.mx_EventTile_sending .mx_RoomPill {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
@ -377,6 +389,14 @@ limitations under the License.
|
|||
left: 41px;
|
||||
}
|
||||
|
||||
.mx_EventTile_content .mx_EventTile_edited {
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
color: $roomtopic-color;
|
||||
display: inline-block;
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
/* Various markdown overrides */
|
||||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
|
@ -404,6 +424,7 @@ limitations under the License.
|
|||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
pre, code {
|
||||
font-family: $monospace-font-family ! important;
|
||||
// deliberate constants as we're behind an invert filter
|
||||
color: #333;
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,92 +0,0 @@
|
|||
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2
Normal file
BIN
res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2
Normal file
Binary file not shown.
BIN
res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2
Normal file
BIN
res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2
Normal file
BIN
res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2
Normal file
Binary file not shown.
BIN
res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2
Normal file
BIN
res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2
Normal file
Binary file not shown.
|
@ -1 +1 @@
|
|||
<svg height="14.865319" viewBox="0 0 15.093 14.865319" width="15.093" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="a" height="1.158" width="1.118" x="-.059" y="-.079"><feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="16"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><g style="fill:none;fill-rule:evenodd;stroke:#2e2f32;stroke-width:.75;stroke-linecap:round;stroke-linejoin:round;filter:url(#a)" transform="translate(-1048.2035 -582.14881)"><path d="m1055.75 596h6.75m-3.375-12.375a1.591 1.591 0 0 1 2.25 2.25l-9.375 9.375-3 .75.75-3z"/></g></svg>
|
||||
<svg height="14.865319" viewBox="0 0 15.093 14.865319" width="15.093" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g style="fill:none;fill-rule:evenodd;stroke:#2e2f32;stroke-width:.75;stroke-linecap:round;stroke-linejoin:round;" transform="translate(-1048.2035 -582.14881)"><path d="m1055.75 596h6.75m-3.375-12.375a1.591 1.591 0 0 1 2.25 2.25l-9.375 9.375-3 .75.75-3z"/></g></svg>
|
||||
|
|
Before Width: | Height: | Size: 874 B After Width: | Height: | Size: 415 B |
|
@ -157,6 +157,9 @@ $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;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -15,39 +15,70 @@
|
|||
/* the 'src' links are relative to the bundle.css, which is in a subdirectory.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* Fira Mono
|
||||
* Used for monospace copy, i.e. code
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
src: url('$(res)/fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype');
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
src: url('$(res)/fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji
|
||||
* taken from https://github.com/mozilla/twemoji-colr
|
||||
* using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to
|
||||
* work on macOS
|
||||
*/
|
||||
/*
|
||||
// except we now load it dynamically via FontManager to handle browsers
|
||||
// which can't render COLR/CPAL still
|
||||
@font-face {
|
||||
font-family: "Twemoji Mozilla";
|
||||
src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2');
|
||||
}
|
||||
*/
|
|
@ -1,10 +1,13 @@
|
|||
// XXX: check this?
|
||||
/* Nunito lacks combining diacritics, so these will fall through
|
||||
to the next font. Helevetica's diacritics however do not combine
|
||||
nicely with Open Sans (on OSX, at least) and result in a huge
|
||||
horizontal mess. Arial empirically gets it right, hence prioritising
|
||||
Arial here. */
|
||||
$font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
|
||||
nicely (on OSX, at least) and result in a huge horizontal mess.
|
||||
Arial empirically gets it right, hence prioritising Arial here. */
|
||||
/* We fall through to Twemoji for emoji rather than falling through
|
||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||
$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif;
|
||||
|
||||
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace;
|
||||
|
||||
// unified palette
|
||||
// try to use these colors when possible
|
||||
|
@ -265,6 +268,9 @@ $reaction-row-button-hover-border-color: $focus-bg-color;
|
|||
$reaction-row-button-selected-bg-color: #e9fff9;
|
||||
$reaction-row-button-selected-border-color: $accent-color;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
#!/usr/bin/env node
|
||||
const EMOJI_DATA = require('emojione/emoji.json');
|
||||
const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList);
|
||||
|
||||
// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete
|
||||
// provider.
|
||||
|
||||
const EMOJIBASE = require('emojibase-data/en/compact.json');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const output = Object.keys(EMOJI_DATA).map(
|
||||
(key) => {
|
||||
const datum = EMOJI_DATA[key];
|
||||
const output = EMOJIBASE.map(
|
||||
(datum) => {
|
||||
const newDatum = {
|
||||
name: datum.name,
|
||||
shortname: datum.shortname,
|
||||
category: datum.category,
|
||||
emoji_order: datum.emoji_order,
|
||||
name: datum.annotation,
|
||||
shortname: `:${datum.shortcodes[0]}:`,
|
||||
category: datum.group,
|
||||
emoji_order: datum.order,
|
||||
};
|
||||
if (datum.aliases.length > 0) {
|
||||
newDatum.aliases = datum.aliases;
|
||||
if (datum.shortcodes.length > 1) {
|
||||
newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`);
|
||||
}
|
||||
if (datum.aliases_ascii.length > 0) {
|
||||
newDatum.aliases_ascii = datum.aliases_ascii;
|
||||
if (datum.emoticon) {
|
||||
newDatum.aliases_ascii = [ datum.emoticon ];
|
||||
}
|
||||
return newDatum;
|
||||
}
|
||||
).filter((datum) => {
|
||||
return EMOJI_SUPPORTED.includes(datum.shortname);
|
||||
});
|
||||
);
|
||||
|
||||
// Write to a file in src. Changes should be checked into git. This file is copied by
|
||||
// babel using --copy-files
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
import {ContentRepo} from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
|
||||
module.exports = {
|
||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||
|
@ -58,4 +59,71 @@ module.exports = {
|
|||
}
|
||||
return require('../res/img/' + images[total % images.length] + '.png');
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
* @param {string} name
|
||||
* @return {string} the first letter
|
||||
*/
|
||||
getInitialLetter(name) {
|
||||
if (name.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
);
|
||||
if (explicitRoomAvatar) {
|
||||
return explicitRoomAvatar;
|
||||
}
|
||||
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -361,7 +361,7 @@ async function _startCallApp(roomId, type) {
|
|||
|
||||
Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, {
|
||||
title: _t('Could not connect to the integration server'),
|
||||
description: _t('A conference call could not be started because the intgrations server is not available'),
|
||||
description: _t('A conference call could not be started because the integrations server is not available'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Value } from 'slate';
|
||||
|
||||
import _clamp from 'lodash/clamp';
|
||||
|
||||
type MessageFormat = 'rich' | 'markdown';
|
||||
|
||||
class HistoryItem {
|
||||
// We store history items in their native format to ensure history is accurate
|
||||
// and then convert them if our RTE has subsequently changed format.
|
||||
value: Value;
|
||||
format: MessageFormat = 'rich';
|
||||
|
||||
constructor(value: ?Value, format: ?MessageFormat) {
|
||||
this.value = value;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Object): HistoryItem {
|
||||
return new HistoryItem(
|
||||
Value.fromJSON(obj.value),
|
||||
obj.format,
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): Object {
|
||||
return {
|
||||
value: this.value.toJSON(),
|
||||
format: this.format,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default class ComposerHistoryManager {
|
||||
history: Array<HistoryItem> = [];
|
||||
prefix: string;
|
||||
lastIndex: number = 0; // used for indexing the storage
|
||||
currentIndex: number = 0; // used for indexing the loaded validated history Array
|
||||
|
||||
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||
this.prefix = prefix + roomId;
|
||||
|
||||
// TODO: Performance issues?
|
||||
let item;
|
||||
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||
try {
|
||||
this.history.push(
|
||||
HistoryItem.fromJSON(JSON.parse(item)),
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Throwing away unserialisable history", e);
|
||||
}
|
||||
}
|
||||
this.lastIndex = this.currentIndex;
|
||||
// reset currentIndex to account for any unserialisable history
|
||||
this.currentIndex = this.history.length;
|
||||
}
|
||||
|
||||
save(value: Value, format: MessageFormat) {
|
||||
const item = new HistoryItem(value, format);
|
||||
this.history.push(item);
|
||||
this.currentIndex = this.history.length;
|
||||
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
|
||||
}
|
||||
|
||||
getItem(offset: number): ?HistoryItem {
|
||||
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
||||
return this.history[this.currentIndex];
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ export function showGroupInviteDialog(groupId) {
|
|||
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
||||
title: _t("Invite new community members"),
|
||||
description: description,
|
||||
placeholder: _t("Name or matrix ID"),
|
||||
placeholder: _t("Name or Matrix ID"),
|
||||
button: _t("Invite to Community"),
|
||||
validAddressTypes: ['mx-user-id'],
|
||||
onFinished: (success, addrs) => {
|
||||
|
|
127
src/HtmlUtils.js
127
src/HtmlUtils.js
|
@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix';
|
|||
import _linkifyElement from 'linkifyjs/element';
|
||||
import _linkifyString from 'linkifyjs/string';
|
||||
import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import url from 'url';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
|
||||
emojione.imagePathSVG = 'emojione/svg/';
|
||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||
emojione.imagePathPNG = 'emojione/png/';
|
||||
// Use SVGs for emojis
|
||||
emojione.imageType = 'svg';
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||
// And there a bunch more symbol characters that emojione has within the
|
||||
// And there a bunch more symbol characters that emojibase has within the
|
||||
// BMP, so this includes the ranges from 'letterlike symbols' to
|
||||
// 'miscellaneous symbols and arrows' which should catch all of them
|
||||
// (with plenty of false positives, but that's OK)
|
||||
|
@ -54,15 +50,15 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
|
|||
// Regex pattern for whitespace characters
|
||||
const WHITESPACE_REGEX = new RegExp("\\s", "g");
|
||||
|
||||
// And this is emojione's complete regex
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
||||
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojione's so will give false
|
||||
* Uses a much, much simpler regex than emojibase's so will give false
|
||||
* positives, but useful for fast-path testing strings to see if they
|
||||
* need emojification.
|
||||
* unicodeToImage uses this function.
|
||||
|
@ -71,62 +67,27 @@ export function containsEmoji(str) {
|
|||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||
}
|
||||
|
||||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||
* because we want to include emoji shortnames in title text
|
||||
/**
|
||||
* Returns the shortcode for an emoji character.
|
||||
*
|
||||
* @param {String} char The emoji character
|
||||
* @return {String} The shortcode (such as :thumbup:)
|
||||
*/
|
||||
function unicodeToImage(str, addAlt) {
|
||||
if (addAlt === undefined) addAlt = true;
|
||||
|
||||
let replaceWith; let unicode; let short; let fname;
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
||||
// if the unicodeChar doesnt exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
short = mappedUnicode[unicode];
|
||||
fname = emojione.emojioneList[short].fname;
|
||||
|
||||
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
|
||||
const title = mappedUnicode[unicode];
|
||||
|
||||
if (addAlt) {
|
||||
const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
} else {
|
||||
replaceWith = `<img class="mx_emojione" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
}
|
||||
return replaceWith;
|
||||
}
|
||||
});
|
||||
|
||||
return str;
|
||||
export function unicodeToShortcode(char) {
|
||||
const data = EMOJIBASE.find(e => e.unicode === char);
|
||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one or more unicode characters (represented by unicode
|
||||
* character number), return an image node with the corresponding
|
||||
* emoji.
|
||||
* Returns the unicode character for an emoji shortcode
|
||||
*
|
||||
* @param alt {string} String to use for the image alt text
|
||||
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
|
||||
* @param unicode {integer} One or more integers representing unicode characters
|
||||
* @returns A img node with the corresponding emoji
|
||||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
||||
* @return {String} The emoji character; null if none exists
|
||||
*/
|
||||
export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||
const fileName = unicode.map((u) => {
|
||||
return u.toString(16);
|
||||
}).join('-');
|
||||
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
|
||||
const fileType = useSvg ? 'svg' : 'png';
|
||||
return <img
|
||||
alt={alt}
|
||||
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
|
||||
/>;
|
||||
export function shortcodeToUnicode(shortcode) {
|
||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
|
||||
return data ? data.unicode : null;
|
||||
}
|
||||
|
||||
export function processHtmlForSending(html: string): string {
|
||||
|
@ -433,13 +394,10 @@ class TextHighlighter extends BaseHighlighter {
|
|||
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
||||
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
|
||||
* opts.returnString: return an HTML string rather than JSX elements
|
||||
* opts.emojiOne: optional param to do emojiOne (default true)
|
||||
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||
*/
|
||||
export function bodyToHtml(content, highlights, opts={}) {
|
||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||
|
||||
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
|
||||
let bodyHasEmoji = false;
|
||||
|
||||
let sanitizeParams = sanitizeHtmlParams;
|
||||
|
@ -470,28 +428,12 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
|
||||
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
|
||||
|
||||
if (doEmojiOne) {
|
||||
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
|
||||
}
|
||||
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
|
||||
|
||||
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||
if (isHtmlMessage) {
|
||||
isDisplayedWithHtml = true;
|
||||
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
||||
} else {
|
||||
// ... or if there are emoji, which we insert as HTML alongside the
|
||||
// escaped plaintext body.
|
||||
if (bodyHasEmoji) {
|
||||
isDisplayedWithHtml = true;
|
||||
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
|
||||
}
|
||||
}
|
||||
|
||||
// An HTML message with emoji
|
||||
// or a plaintext message with emoji that was escaped and sanitized into
|
||||
// HTML.
|
||||
if (bodyHasEmoji) {
|
||||
safeBody = unicodeToImage(safeBody);
|
||||
}
|
||||
} finally {
|
||||
delete sanitizeParams.textFilter;
|
||||
|
@ -503,7 +445,6 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
|
||||
let emojiBody = false;
|
||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
||||
|
||||
// Ignore spaces in body text. Emojis with spaces in between should
|
||||
|
@ -515,29 +456,25 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
// presented as large.
|
||||
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
|
||||
|
||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length
|
||||
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
|
||||
// Prevent user pills expanding for users with only emoji in
|
||||
// their username
|
||||
&& (content.formatted_body == undefined
|
||||
|| !content.formatted_body.includes("https://matrix.to/"));
|
||||
(
|
||||
content.formatted_body == undefined ||
|
||||
!content.formatted_body.includes("https://matrix.to/")
|
||||
);
|
||||
}
|
||||
|
||||
const className = classNames({
|
||||
'mx_EventTile_body': true,
|
||||
'mx_EventTile_bigEmoji': emojiBody,
|
||||
'markdown-body': isHtmlMessage,
|
||||
'markdown-body': isHtmlMessage && !emojiBody,
|
||||
});
|
||||
|
||||
return isDisplayedWithHtml ?
|
||||
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||
<span className={className} dir="auto">{ strippedBody }</span>;
|
||||
}
|
||||
|
||||
export function emojifyText(text, addAlt) {
|
||||
return {
|
||||
__html: unicodeToImage(escape(text), addAlt),
|
||||
};
|
||||
<span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -33,6 +33,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
|||
import PlatformPeg from "./PlatformPeg";
|
||||
import { sendLoginRequest } from "./Login";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -499,7 +500,9 @@ async function startMatrixClient() {
|
|||
|
||||
Notifier.start();
|
||||
UserActivity.sharedInstance().start();
|
||||
Presence.start();
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
Presence.start();
|
||||
}
|
||||
DMRoomMap.makeShared().start();
|
||||
ActiveWidgetStore.start();
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ class MatrixClientPeg {
|
|||
// try to initialise e2e on the new client
|
||||
try {
|
||||
// check that we have a version of the js-sdk which includes initCrypto
|
||||
if (this.matrixClient.initCrypto) {
|
||||
if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) {
|
||||
await this.matrixClient.initCrypto();
|
||||
StorageManager.setCryptoInitialised(true);
|
||||
}
|
||||
|
@ -188,8 +188,7 @@ class MatrixClientPeg {
|
|||
timelineSupport: true,
|
||||
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
unstableClientRelationAggregation: aggregateRelations,
|
||||
unstableClientRelationReplacements: enableEdits,
|
||||
unstableClientRelationAggregation: aggregateRelations || enableEdits,
|
||||
};
|
||||
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
|
|
|
@ -85,7 +85,11 @@ const Notifier = {
|
|||
msg = '';
|
||||
}
|
||||
|
||||
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
|
||||
let avatarUrl = null;
|
||||
if (ev.sender && !SettingsStore.getValue("lowBandwidth")) {
|
||||
avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop');
|
||||
}
|
||||
|
||||
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
|
||||
|
||||
// if displayNotification returns non-null, the platform supports
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 - 2017 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as emojione from 'emojione';
|
||||
|
||||
|
||||
export function unicodeToEmojiUri(str) {
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
// remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them
|
||||
return str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) {
|
||||
// if the unicodeChar doesn't exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
const unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
const short = mappedUnicode[unicode];
|
||||
const fname = emojione.emojioneList[short].fname;
|
||||
|
||||
return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -45,7 +45,7 @@ export function showStartChatInviteDialog() {
|
|||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||
title: _t('Start a chat'),
|
||||
description: _t("Who would you like to communicate with?"),
|
||||
placeholder: _t("Email, name or matrix ID"),
|
||||
placeholder: _t("Email, name or Matrix ID"),
|
||||
validAddressTypes: ['mx-user-id', 'email'],
|
||||
button: _t("Start Chat"),
|
||||
onFinished: _onStartChatFinished,
|
||||
|
@ -58,7 +58,7 @@ export function showRoomInviteDialog(roomId) {
|
|||
title: _t('Invite new room members'),
|
||||
description: _t('Who would you like to add to this room?'),
|
||||
button: _t('Send Invites'),
|
||||
placeholder: _t("Email, name or matrix ID"),
|
||||
placeholder: _t("Email, name or Matrix ID"),
|
||||
onFinished: (shouldInvite, addrs) => {
|
||||
_onRoomInviteFinished(roomId, shouldInvite, addrs);
|
||||
},
|
||||
|
|
|
@ -518,7 +518,7 @@ export const CommandMap = {
|
|||
unban: new Command({
|
||||
name: 'unban',
|
||||
args: '<user-id>',
|
||||
description: _td('Unbans user with given id'),
|
||||
description: _td('Unbans user with given ID'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
|
|
|
@ -19,47 +19,31 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import sdk from '../index';
|
||||
import {PillCompletion} from './Components';
|
||||
import type {Completion, SelectionRange} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
|
||||
const LIMIT = 20;
|
||||
const CATEGORY_ORDER = [
|
||||
'people',
|
||||
'food',
|
||||
'objects',
|
||||
'activity',
|
||||
'nature',
|
||||
'travel',
|
||||
'flags',
|
||||
'regional',
|
||||
'symbols',
|
||||
'modifier',
|
||||
];
|
||||
|
||||
// Match for ":wink:" or ascii-style ";-)" provided by emojione
|
||||
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
|
||||
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
|
||||
// that we need to support inputting multiple emoji with no space between them.
|
||||
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g');
|
||||
|
||||
// We also need to match the non-zero-length prefixes to remove them from the final match,
|
||||
// and update the range so that we don't replace the whitespace or the previous emoji.
|
||||
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')');
|
||||
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
|
||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
|
||||
|
||||
// XXX: it's very unclear why we bother with this generated emojidata file.
|
||||
// all it means is that we end up bloating the bundle with precomputed stuff
|
||||
// which would be trivial to calculate and cache on demand.
|
||||
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
||||
(a, b) => {
|
||||
if (a.category === b.category) {
|
||||
return a.emoji_order - b.emoji_order;
|
||||
}
|
||||
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
|
||||
return a.category - b.category;
|
||||
},
|
||||
).map((a, index) => {
|
||||
return {
|
||||
|
@ -101,26 +85,20 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
return []; // don't give any suggestions if the user doesn't want them
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let completions = [];
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
let matchedString = command[0];
|
||||
|
||||
// Remove prefix of any length (single whitespace or unicode emoji)
|
||||
const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString);
|
||||
if (prefixMatch) {
|
||||
matchedString = matchedString.slice(prefixMatch[0].length);
|
||||
range.start += prefixMatch[0].length;
|
||||
}
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
|
||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||
|
||||
const sorters = [];
|
||||
// First, sort by score (Infinity if matchedString not in shortname)
|
||||
// make sure that emoticons come first
|
||||
sorters.push((c) => score(matchedString, c.aliases_ascii));
|
||||
|
||||
// then sort by score (Infinity if matchedString not in shortname)
|
||||
sorters.push((c) => score(matchedString, c.shortname));
|
||||
// If the matchedString is not empty, sort by length of shortname. Example:
|
||||
// matchedString = ":bookmark"
|
||||
|
@ -133,12 +111,12 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
completions = _sortBy(_uniq(completions), sorters);
|
||||
|
||||
completions = completions.map((result) => {
|
||||
const {shortname} = result;
|
||||
const unicode = shortnameToUnicode(shortname);
|
||||
const { shortname } = result;
|
||||
const unicode = shortcodeToUnicode(shortname);
|
||||
return {
|
||||
completion: unicode,
|
||||
component: (
|
||||
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
|
||||
<PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
|
|
@ -265,7 +265,7 @@ const RoleUserList = React.createClass({
|
|||
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add users to the community summary'),
|
||||
description: _t("Who would you like to add to this summary?"),
|
||||
placeholder: _t("Name or matrix ID"),
|
||||
placeholder: _t("Name or Matrix ID"),
|
||||
button: _t("Add to summary"),
|
||||
validAddressTypes: ['mx-user-id'],
|
||||
groupId: this.props.groupId,
|
||||
|
|
|
@ -129,7 +129,7 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||
const yRetention = 1.0;
|
||||
|
||||
if (Math.abs(e.deltaX) < xyThreshold) {
|
||||
if (Math.abs(e.deltaX) <= xyThreshold) {
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
this._scrollElement.scrollLeft += e.deltaY * yRetention;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -60,7 +61,7 @@ export default React.createClass({
|
|||
inputs: PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: PropTypes.func,
|
||||
requestEmailToken: PropTypes.func,
|
||||
sessionId: PropTypes.string,
|
||||
clientSecret: PropTypes.string,
|
||||
emailSid: PropTypes.string,
|
||||
|
@ -96,6 +97,7 @@ export default React.createClass({
|
|||
sessionId: this.props.sessionId,
|
||||
clientSecret: this.props.clientSecret,
|
||||
emailSid: this.props.emailSid,
|
||||
requestEmailToken: this.props.requestEmailToken,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
|
@ -202,7 +204,6 @@ export default React.createClass({
|
|||
stageState={this.state.stageState}
|
||||
fail={this._onAuthStageFailed}
|
||||
setEmailSid={this._setEmailSid}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
showContinue={!this.props.continueIsManaged}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -24,6 +24,7 @@ import { DragDropContext } from 'react-beautiful-dnd';
|
|||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
|
@ -118,6 +119,8 @@ const LoggedInView = React.createClass({
|
|||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
fixupColorFonts();
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -322,6 +325,18 @@ const LoggedInView = React.createClass({
|
|||
handled = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.KEY_I:
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
// will have to do.
|
||||
|
||||
if (ctrlCmdOnly) {
|
||||
dis.dispatch({
|
||||
action: 'toggle_top_left_menu',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
|
|
|
@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
|||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
|
||||
const AutoDiscovery = Matrix.AutoDiscovery;
|
||||
import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils";
|
||||
|
||||
// Disable warnings for now: we use deprecated bluebird functions
|
||||
// and need to migrate, but they spam the console with warnings.
|
||||
|
@ -109,6 +108,7 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
config: PropTypes.object,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig),
|
||||
ConferenceHandler: PropTypes.any,
|
||||
onNewScreen: PropTypes.func,
|
||||
registrationUrl: PropTypes.string,
|
||||
|
@ -181,16 +181,8 @@ export default React.createClass({
|
|||
// Parameters used in the registration dance with the IS
|
||||
register_client_secret: null,
|
||||
register_session_id: null,
|
||||
register_hs_url: null,
|
||||
register_is_url: null,
|
||||
register_id_sid: null,
|
||||
|
||||
// Parameters used for setting up the authentication views
|
||||
defaultServerName: this.props.config.default_server_name,
|
||||
defaultHsUrl: this.props.config.default_hs_url,
|
||||
defaultIsUrl: this.props.config.default_is_url,
|
||||
defaultServerDiscoveryError: null,
|
||||
|
||||
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||
// and disable it when there are no dialogs
|
||||
hideToSRUsers: false,
|
||||
|
@ -211,42 +203,19 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
getDefaultServerName: function() {
|
||||
return this.state.defaultServerName;
|
||||
},
|
||||
|
||||
getCurrentHsUrl: function() {
|
||||
if (this.state.register_hs_url) {
|
||||
return this.state.register_hs_url;
|
||||
} else if (MatrixClientPeg.get()) {
|
||||
return MatrixClientPeg.get().getHomeserverUrl();
|
||||
} else {
|
||||
return this.getDefaultHsUrl();
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultHsUrl(defaultToMatrixDotOrg) {
|
||||
defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
|
||||
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
|
||||
return this.state.defaultHsUrl;
|
||||
},
|
||||
|
||||
getFallbackHsUrl: function() {
|
||||
return this.props.config.fallback_hs_url;
|
||||
},
|
||||
|
||||
getCurrentIsUrl: function() {
|
||||
if (this.state.register_is_url) {
|
||||
return this.state.register_is_url;
|
||||
} else if (MatrixClientPeg.get()) {
|
||||
return MatrixClientPeg.get().getIdentityServerUrl();
|
||||
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
|
||||
return this.props.config.fallback_hs_url;
|
||||
} else {
|
||||
return this.getDefaultIsUrl();
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultIsUrl() {
|
||||
return this.state.defaultIsUrl || "https://vector.im";
|
||||
getServerProperties() {
|
||||
let props = this.state.serverConfig;
|
||||
if (!props) props = this.props.serverConfig; // for unit tests
|
||||
if (!props) props = SdkConfig.get()["validated_server_config"];
|
||||
return {serverConfig: props};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -260,40 +229,6 @@ export default React.createClass({
|
|||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||
}
|
||||
|
||||
// Set up the default URLs (async)
|
||||
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
|
||||
this.setState({loadingDefaultHomeserver: true});
|
||||
this._tryDiscoverDefaultHomeserver(this.getDefaultServerName());
|
||||
} else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) {
|
||||
// Ideally we would somehow only communicate this to the server admins, but
|
||||
// given this is at login time we can't really do much besides hope that people
|
||||
// will check their settings.
|
||||
this.setState({
|
||||
defaultServerName: null, // To un-hide any secrets people might be keeping
|
||||
defaultServerDiscoveryError: _t(
|
||||
"Invalid configuration: Cannot supply a default homeserver URL and " +
|
||||
"a default server name",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Set a default HS with query param `hs_url`
|
||||
const paramHs = this.props.startingFragmentQueryParams.hs_url;
|
||||
if (paramHs) {
|
||||
console.log('Setting register_hs_url ', paramHs);
|
||||
this.setState({
|
||||
register_hs_url: paramHs,
|
||||
});
|
||||
}
|
||||
// Set a default IS with query param `is_url`
|
||||
const paramIs = this.props.startingFragmentQueryParams.is_url;
|
||||
if (paramIs) {
|
||||
console.log('Setting register_is_url ', paramIs);
|
||||
this.setState({
|
||||
register_is_url: paramIs,
|
||||
});
|
||||
}
|
||||
|
||||
// a thing to call showScreen with once login completes. this is kept
|
||||
// outside this.state because updating it should never trigger a
|
||||
// rerender.
|
||||
|
@ -374,8 +309,8 @@ export default React.createClass({
|
|||
return Lifecycle.loadSession({
|
||||
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
||||
enableGuest: this.props.enableGuest,
|
||||
guestHsUrl: this.getCurrentHsUrl(),
|
||||
guestIsUrl: this.getCurrentIsUrl(),
|
||||
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
|
||||
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
}).then((loadedSession) => {
|
||||
|
@ -1827,44 +1762,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
onServerConfigChange(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl) {
|
||||
newState.register_hs_url = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl) {
|
||||
newState.register_is_url = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
_tryDiscoverDefaultHomeserver: async function(serverName) {
|
||||
try {
|
||||
const discovery = await AutoDiscovery.findClientConfig(serverName);
|
||||
const state = discovery["m.homeserver"].state;
|
||||
if (state !== AutoDiscovery.SUCCESS) {
|
||||
console.error("Failed to discover homeserver on startup:", discovery);
|
||||
this.setState({
|
||||
defaultServerDiscoveryError: discovery["m.homeserver"].error,
|
||||
loadingDefaultHomeserver: false,
|
||||
});
|
||||
} else {
|
||||
const hsUrl = discovery["m.homeserver"].base_url;
|
||||
const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "https://vector.im";
|
||||
this.setState({
|
||||
defaultHsUrl: hsUrl,
|
||||
defaultIsUrl: isUrl,
|
||||
loadingDefaultHomeserver: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
defaultServerDiscoveryError: _t("Unknown error discovering homeserver"),
|
||||
loadingDefaultHomeserver: false,
|
||||
});
|
||||
}
|
||||
this.setState({serverConfig: config});
|
||||
},
|
||||
|
||||
_makeRegistrationUrl: function(params) {
|
||||
|
@ -1883,8 +1781,7 @@ export default React.createClass({
|
|||
|
||||
if (
|
||||
this.state.view === VIEWS.LOADING ||
|
||||
this.state.view === VIEWS.LOGGING_IN ||
|
||||
this.state.loadingDefaultHomeserver
|
||||
this.state.view === VIEWS.LOGGING_IN
|
||||
) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
|
@ -1962,18 +1859,13 @@ export default React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
brand={this.props.config.brand}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1982,14 +1874,11 @@ export default React.createClass({
|
|||
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
|
||||
return (
|
||||
<ForgotPassword
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
onComplete={this.onLoginClick}
|
||||
onLoginClick={this.onLoginClick} />
|
||||
onLoginClick={this.onLoginClick}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1999,16 +1888,11 @@ export default React.createClass({
|
|||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
fallbackHsUrl={this.getFallbackHsUrl()}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../DateUtils';
|
|||
import sdk from '../../index';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
@ -95,6 +96,9 @@ module.exports = React.createClass({
|
|||
|
||||
// helper function to access relations for an event
|
||||
getRelationsForEvent: PropTypes.func,
|
||||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -230,6 +234,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
scrollToEventIfNeeded: function(eventId) {
|
||||
const node = this.eventNodes[eventId];
|
||||
if (node) {
|
||||
node.scrollIntoView({block: "nearest", behavior: "instant"});
|
||||
}
|
||||
},
|
||||
|
||||
/* check the scroll state and send out pagination requests if necessary.
|
||||
*/
|
||||
checkFillState: function() {
|
||||
|
@ -248,6 +259,10 @@ module.exports = React.createClass({
|
|||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
return false; // no tile = no show
|
||||
|
@ -450,14 +465,10 @@ module.exports = React.createClass({
|
|||
|
||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const MessageEditor = sdk.getComponent('elements.MessageEditor');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const ret = [];
|
||||
|
||||
if (this.props.editEvent && this.props.editEvent.getId() === mxEv.getId()) {
|
||||
return [<MessageEditor key={mxEv.getId()} event={mxEv} />];
|
||||
}
|
||||
|
||||
const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId();
|
||||
// is this a continuation of the previous message?
|
||||
let continuation = false;
|
||||
|
||||
|
@ -527,18 +538,20 @@ module.exports = React.createClass({
|
|||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
isEditing={isEditing}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.status}
|
||||
eventSendStatus={mxEv.replacementOrOwnStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
</li>,
|
||||
);
|
||||
|
@ -714,7 +727,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
let whoIsTyping;
|
||||
if (this.props.room) {
|
||||
if (this.props.room && !this.props.tileShape) {
|
||||
whoIsTyping = (<WhoIsTypingTile
|
||||
room={this.props.room}
|
||||
onShown={this._onTypingShown}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -15,12 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import { _t } from '../../languageHandler';
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
|
@ -44,7 +42,7 @@ const NotificationPanel = React.createClass({
|
|||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview = {false}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={_t('You have no visible notifications')}
|
||||
/>
|
||||
|
|
|
@ -304,8 +304,6 @@ module.exports = React.createClass({
|
|||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
|
|
|
@ -29,6 +29,7 @@ import { Group } from 'matrix-js-sdk';
|
|||
import PropTypes from 'prop-types';
|
||||
import RoomTile from "../views/rooms/RoomTile";
|
||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||
import {_t} from "../../languageHandler";
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
@ -42,6 +43,7 @@ const RoomSubList = React.createClass({
|
|||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
addRoomLabel: PropTypes.string,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
|
@ -232,7 +234,11 @@ const RoomSubList = React.createClass({
|
|||
let addRoomButton;
|
||||
if (this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
|
||||
<AccessibleButton
|
||||
onClick={ this.props.onAddRoom }
|
||||
className="mx_RoomSubList_addRoom"
|
||||
title={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1832,6 +1832,7 @@ module.exports = React.createClass({
|
|||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -106,6 +106,9 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: PropTypes.string,
|
||||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -204,11 +207,11 @@ const TimelinePanel = React.createClass({
|
|||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
|
||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||
MatrixClientPeg.get().on("Room.replaceEvent", this.onRoomReplaceEvent);
|
||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
|
||||
MatrixClientPeg.get().on("sync", this.onSync);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
|
@ -283,11 +286,11 @@ const TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
client.removeListener("Room.replaceEvent", this.onRoomReplaceEvent);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
client.removeListener("Room.accountData", this.onAccountData);
|
||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
client.removeListener("Event.replaced", this.onEventReplaced);
|
||||
client.removeListener("sync", this.onSync);
|
||||
}
|
||||
},
|
||||
|
@ -405,7 +408,13 @@ const TimelinePanel = React.createClass({
|
|||
this.forceUpdate();
|
||||
}
|
||||
if (payload.action === "edit_event") {
|
||||
this.setState({editEvent: payload.event});
|
||||
this.setState({editEvent: payload.event}, () => {
|
||||
if (payload.event && this.refs.messagePanel) {
|
||||
this.refs.messagePanel.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -507,7 +516,7 @@ const TimelinePanel = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onRoomReplaceEvent: function(replacedEvent, room) {
|
||||
onEventReplaced: function(replacedEvent, room) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// ignore events for other rooms
|
||||
|
@ -553,6 +562,9 @@ const TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
onEventDecrypted: function(ev) {
|
||||
// Can be null for the notification timeline, etc.
|
||||
if (!this.props.timelineSet.room) return;
|
||||
|
||||
// Need to update as we don't display event tiles for events that
|
||||
// haven't yet been decrypted. The event will have just been updated
|
||||
// in place so we just need to re-render.
|
||||
|
@ -601,6 +613,8 @@ const TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
sendReadReceipt: function() {
|
||||
if (SettingsStore.getValue("lowBandwidth")) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this.props.manageReadReceipts) return;
|
||||
// This happens on user_activity_end which is delayed, and it's
|
||||
|
@ -1261,6 +1275,7 @@ const TimelinePanel = React.createClass({
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editEvent={this.state.editEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,6 +24,8 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import Avatar from '../../Avatar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from "../../dispatcher";
|
||||
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
||||
|
@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
super();
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
menuFunctions: null, // should be { close: fn }
|
||||
profileInfo: null,
|
||||
};
|
||||
|
||||
|
@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
try {
|
||||
const profileInfo = await this._getProfileInfo();
|
||||
this.setState({profileInfo});
|
||||
|
@ -68,6 +74,17 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
// For accessibility
|
||||
if (payload.action === "toggle_top_left_menu") {
|
||||
if (this._buttonRef) this._buttonRef.click();
|
||||
}
|
||||
};
|
||||
|
||||
_getDisplayName() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return _t("Guest");
|
||||
|
@ -88,7 +105,13 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
|
||||
<AccessibleButton
|
||||
className="mx_TopLeftMenuButton"
|
||||
role="button"
|
||||
onClick={this.onToggleMenu}
|
||||
inputRef={(r) => this._buttonRef = r}
|
||||
aria-label={_t("Your profile")}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={name}
|
||||
|
@ -98,7 +121,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
resizeMethod="crop"
|
||||
/>
|
||||
{ nameElement }
|
||||
<span className="mx_TopLeftMenuButton_chevron"></span>
|
||||
<span className="mx_TopLeftMenuButton_chevron" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -107,20 +130,26 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.state.menuDisplayed && this.state.menuFunctions) {
|
||||
this.state.menuFunctions.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top + elementRect.height;
|
||||
|
||||
ContextualMenu.createMenu(TopLeftMenu, {
|
||||
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
|
||||
chevronFace: "none",
|
||||
left: x,
|
||||
top: y,
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
displayName: this._getDisplayName(),
|
||||
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.setState({ menuDisplayed: false, menuFunctions: null });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
this.setState({ menuDisplayed: true, menuFunctions });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler';
|
|||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -40,28 +40,14 @@ module.exports = React.createClass({
|
|||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about "your account".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
onLoginClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
phase: PHASE_FORGOT,
|
||||
email: "",
|
||||
password: "",
|
||||
|
@ -70,11 +56,11 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||
submitPasswordReset: function(email, password) {
|
||||
this.setState({
|
||||
phase: PHASE_SENDING_EMAIL,
|
||||
});
|
||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
|
||||
this.reset.resetPassword(email, password).done(() => {
|
||||
this.setState({
|
||||
phase: PHASE_EMAIL_SENT,
|
||||
|
@ -103,13 +89,6 @@ module.exports = React.createClass({
|
|||
onSubmitForm: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Don't allow the user to register if there's a discovery error
|
||||
// Without this, the user could end up registering on the wrong homeserver.
|
||||
if (this.props.defaultServerDiscoveryError) {
|
||||
this.setState({errorText: this.props.defaultServerDiscoveryError});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
} else if (!this.state.password || !this.state.password2) {
|
||||
|
@ -132,10 +111,7 @@ module.exports = React.createClass({
|
|||
button: _t('Continue'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHsUrl, this.state.enteredIsUrl,
|
||||
this.state.email, this.state.password,
|
||||
);
|
||||
this.submitPasswordReset(this.state.email, this.state.password);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -148,19 +124,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIsUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_FORGOT,
|
||||
});
|
||||
|
@ -190,26 +154,19 @@ module.exports = React.createClass({
|
|||
|
||||
renderServerDetails() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerConfig
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.state.enteredHsUrl}
|
||||
customIsUrl={this.state.enteredIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0} />
|
||||
<AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
return <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={0}
|
||||
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||
submitText={_t("Next")}
|
||||
submitClass="mx_Login_submit"
|
||||
/>;
|
||||
},
|
||||
|
||||
renderForgot() {
|
||||
|
@ -221,25 +178,22 @@ module.exports = React.createClass({
|
|||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your Matrix account');
|
||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) {
|
||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.defaultServerName,
|
||||
let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.state.enteredHsUrl);
|
||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
errorText = <div className="mx_Login_error">{_t(
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
|
||||
"enter a valid URL including the protocol prefix.",
|
||||
{
|
||||
hsUrl: this.state.enteredHsUrl,
|
||||
})}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
|
|
|
@ -20,12 +20,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import { AutoDiscovery } from "matrix-js-sdk";
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -59,19 +59,14 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about where to "sign in to".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
// went wrong. May be replaced with a different error within the
|
||||
// Login component.
|
||||
errorText: PropTypes.string,
|
||||
|
||||
// If true, the component will consider itself busy.
|
||||
busy: PropTypes.bool,
|
||||
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different homeserver without confusing users.
|
||||
|
@ -79,12 +74,13 @@ module.exports = React.createClass({
|
|||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
// login shouldn't know or care how registration, password recovery,
|
||||
// etc is done.
|
||||
onRegisterClick: PropTypes.func.isRequired,
|
||||
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -93,9 +89,6 @@ module.exports = React.createClass({
|
|||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
|
||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
|
||||
// used for preserving form values when changing homeserver
|
||||
username: "",
|
||||
phoneCountry: null,
|
||||
|
@ -105,10 +98,6 @@ module.exports = React.createClass({
|
|||
phase: PHASE_LOGIN,
|
||||
// The current login flow, such as password, SSO, etc.
|
||||
currentFlow: "m.login.password",
|
||||
|
||||
// .well-known discovery
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -132,6 +121,14 @@ module.exports = React.createClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
// Ensure that we end up actually logging in to the right place
|
||||
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
},
|
||||
|
||||
onPasswordLoginError: function(errorText) {
|
||||
this.setState({
|
||||
errorText,
|
||||
|
@ -139,10 +136,17 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
isBusy: function() {
|
||||
return this.state.busy || this.props.busy;
|
||||
},
|
||||
|
||||
hasError: function() {
|
||||
return this.state.errorText || this.props.errorText;
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
// Prevent people from submitting their password when homeserver
|
||||
// discovery went wrong
|
||||
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
|
||||
// Prevent people from submitting their password when something isn't right.
|
||||
if (this.isBusy() || this.hasError()) return;
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -164,7 +168,7 @@ module.exports = React.createClass({
|
|||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This homeserver does not support login using email address.');
|
||||
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
|
@ -194,11 +198,10 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||
<div className="mx_Login_smallError">
|
||||
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{
|
||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
|
||||
})
|
||||
}
|
||||
{_t(
|
||||
'Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{hs: this.props.serverConfig.hsName},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -232,21 +235,26 @@ module.exports = React.createClass({
|
|||
this.setState({ username: username });
|
||||
},
|
||||
|
||||
onUsernameBlur: function(username) {
|
||||
onUsernameBlur: async function(username) {
|
||||
const doWellknownLookup = username[0] === "@";
|
||||
this.setState({
|
||||
username: username,
|
||||
discoveryError: null,
|
||||
busy: doWellknownLookup, // unset later by the result of onServerConfigChange
|
||||
errorText: null,
|
||||
});
|
||||
if (username[0] === "@") {
|
||||
if (doWellknownLookup) {
|
||||
const serverName = username.split(':').slice(1).join(':');
|
||||
try {
|
||||
// we have to append 'https://' to make the URL constructor happy
|
||||
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
|
||||
const url = new URL("https://" + serverName);
|
||||
this._tryWellKnownDiscovery(url.hostname);
|
||||
const result = await AutoDiscoveryUtils.validateServerName(serverName);
|
||||
this.props.onServerConfigChange(result);
|
||||
} catch (e) {
|
||||
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
|
||||
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
|
||||
|
||||
let message = _t("Failed to perform homeserver discovery");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({errorText: message, busy: false});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -274,32 +282,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
const self = this;
|
||||
const newState = {
|
||||
errorText: null, // reset err messages
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIsUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
},
|
||||
|
||||
onRegisterClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
});
|
||||
|
@ -313,64 +302,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_tryWellKnownDiscovery: async function(serverName) {
|
||||
if (!serverName.trim()) {
|
||||
// Nothing to discover
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({findingHomeserver: true});
|
||||
try {
|
||||
const discovery = await AutoDiscovery.findClientConfig(serverName);
|
||||
|
||||
const state = discovery["m.homeserver"].state;
|
||||
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveryError: discovery["m.homeserver"].error,
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.SUCCESS) {
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
this.onServerConfigChange({
|
||||
hsUrl: discovery["m.homeserver"].base_url,
|
||||
isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "",
|
||||
});
|
||||
} else {
|
||||
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
|
||||
this.setState({
|
||||
discoveryError: _t("Unknown failure discovering homeserver"),
|
||||
findingHomeserver: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
findingHomeserver: false,
|
||||
discoveryError: _t("Unknown error discovering homeserver"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHsUrl;
|
||||
isUrl = isUrl || this.state.enteredIsUrl;
|
||||
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
|
||||
isUrl = isUrl || this.props.serverConfig.isUrl;
|
||||
|
||||
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
// TODO: TravisR - Only use this if the homeserver is the default homeserver
|
||||
const fallbackHsUrl = this.props.fallbackHsUrl;
|
||||
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
|
@ -378,8 +316,6 @@ module.exports = React.createClass({
|
|||
this._loginLogic = loginLogic;
|
||||
|
||||
this.setState({
|
||||
enteredHsUrl: hsUrl,
|
||||
enteredIsUrl: isUrl,
|
||||
busy: true,
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
@ -445,8 +381,8 @@ module.exports = React.createClass({
|
|||
|
||||
if (err.cors === 'rejected') {
|
||||
if (window.location.protocol === 'https:' &&
|
||||
(this.state.enteredHsUrl.startsWith("http:") ||
|
||||
!this.state.enteredHsUrl.startsWith("http"))
|
||||
(this.props.serverConfig.hsUrl.startsWith("http:") ||
|
||||
!this.props.serverConfig.hsUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
|
@ -469,9 +405,9 @@ module.exports = React.createClass({
|
|||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener"
|
||||
href={this.state.enteredHsUrl}
|
||||
>{ sub }</a>;
|
||||
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
|
@ -484,7 +420,6 @@ module.exports = React.createClass({
|
|||
|
||||
renderServerComponent() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
|
@ -494,28 +429,19 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.enteredHsUrl}
|
||||
customIsUrl={this.state.enteredIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
/>;
|
||||
|
||||
let nextButton = null;
|
||||
const serverDetailsProps = {};
|
||||
if (PHASES_ENABLED) {
|
||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>;
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||
}
|
||||
|
||||
return <div>
|
||||
{serverDetails}
|
||||
{nextButton}
|
||||
</div>;
|
||||
return <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
},
|
||||
|
||||
renderLoginComponentForStep() {
|
||||
|
@ -547,13 +473,6 @@ module.exports = React.createClass({
|
|||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
// If the current HS URL is the default HS URL, then we can label it
|
||||
// with the default HS name (if it exists).
|
||||
let hsName;
|
||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
|
||||
hsName = this.props.defaultServerName;
|
||||
}
|
||||
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
|
@ -569,10 +488,9 @@ module.exports = React.createClass({
|
|||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsName={hsName}
|
||||
hsUrl={this.state.enteredHsUrl}
|
||||
disableSubmit={this.state.findingHomeserver}
|
||||
/>
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -595,9 +513,9 @@ module.exports = React.createClass({
|
|||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
|
||||
const errorText = this.state.errorText || this.props.errorText;
|
||||
|
||||
let errorTextSection;
|
||||
if (errorText) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,16 +18,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -46,18 +46,7 @@ module.exports = React.createClass({
|
|||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about "your account".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
|
@ -66,7 +55,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
|
||||
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
|
||||
|
||||
return {
|
||||
busy: false,
|
||||
|
@ -87,8 +76,6 @@ module.exports = React.createClass({
|
|||
// straight back into UI auth
|
||||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
serverType,
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
// Phase of the overall registration dialog.
|
||||
phase: PHASE_REGISTRATION,
|
||||
flows: null,
|
||||
|
@ -100,18 +87,22 @@ module.exports = React.createClass({
|
|||
this._replaceClient();
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.hsUrl = config.hsUrl;
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
this._replaceClient(newProps.serverConfig);
|
||||
|
||||
// Handle cases where the user enters "https://matrix.org" for their server
|
||||
// from the advanced option - we should default to FREE at that point.
|
||||
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
|
||||
if (serverType !== this.state.serverType) {
|
||||
// Reset the phase to default phase for the server type.
|
||||
this.setState({
|
||||
serverType,
|
||||
phase: this.getDefaultPhaseForServerType(serverType),
|
||||
});
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, () => {
|
||||
this._replaceClient();
|
||||
});
|
||||
},
|
||||
|
||||
getDefaultPhaseForServerType(type) {
|
||||
|
@ -136,19 +127,17 @@ module.exports = React.createClass({
|
|||
// the new type.
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
|
||||
this.onServerConfigChange({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
});
|
||||
const { serverConfig } = ServerType.TYPES.FREE;
|
||||
this.props.onServerConfigChange(serverConfig);
|
||||
break;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
// We can accept whatever server config was the default here as this essentially
|
||||
// acts as a slightly different "custom server"/ADVANCED option.
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
this.onServerConfigChange({
|
||||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
// Use the default config from the config
|
||||
this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -158,13 +147,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_replaceClient: async function() {
|
||||
_replaceClient: async function(serverConfig) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
if (!serverConfig) serverConfig = this.props.serverConfig;
|
||||
const {hsUrl, isUrl} = serverConfig;
|
||||
this._matrixClient = Matrix.createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
idBaseUrl: this.state.isUrl,
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
|
@ -189,12 +180,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
// Don't allow the user to register if there's a discovery error
|
||||
// Without this, the user could end up registering on the wrong homeserver.
|
||||
if (this.props.defaultServerDiscoveryError) {
|
||||
this.setState({errorText: this.props.defaultServerDiscoveryError});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true,
|
||||
|
@ -203,11 +188,25 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) {
|
||||
return this._matrixClient.requestRegisterEmailToken(
|
||||
emailAddress,
|
||||
clientSecret,
|
||||
sendAttempt,
|
||||
this.props.makeRegistrationUrl({
|
||||
client_secret: clientSecret,
|
||||
hs_url: this._matrixClient.getHomeserverUrl(),
|
||||
is_url: this._matrixClient.getIdentityServerUrl(),
|
||||
session_id: sessionId,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
_onUIAuthFinished: async function(success, response, extra) {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
|
@ -302,8 +301,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_REGISTRATION,
|
||||
});
|
||||
|
@ -348,7 +346,6 @@ module.exports = React.createClass({
|
|||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
|
@ -365,47 +362,41 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
const serverDetailsProps = {};
|
||||
if (PHASES_ENABLED) {
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||
}
|
||||
|
||||
let serverDetails = null;
|
||||
switch (this.state.serverType) {
|
||||
case ServerType.FREE:
|
||||
break;
|
||||
case ServerType.PREMIUM:
|
||||
serverDetails = <ModularServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
||||
let nextButton = null;
|
||||
if (PHASES_ENABLED) {
|
||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
{serverDetails}
|
||||
{nextButton}
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
@ -424,7 +415,7 @@ module.exports = React.createClass({
|
|||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={this._getUIAuthInputs()}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
requestEmailToken={this._requestEmailToken}
|
||||
sessionId={this.props.sessionId}
|
||||
clientSecret={this.props.clientSecret}
|
||||
emailSid={this.props.idSid}
|
||||
|
@ -446,13 +437,6 @@ module.exports = React.createClass({
|
|||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
// If the current HS URL is the default HS URL, then we can label it
|
||||
// with the default HS name (if it exists).
|
||||
let hsName;
|
||||
if (this.state.hsUrl === this.props.defaultHsUrl) {
|
||||
hsName = this.props.defaultServerName;
|
||||
}
|
||||
|
||||
return <RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
|
@ -462,8 +446,7 @@ module.exports = React.createClass({
|
|||
onRegisterClick={this.onFormSubmit}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
flows={this.state.flows}
|
||||
hsName={hsName}
|
||||
hsUrl={this.state.hsUrl}
|
||||
serverConfig={this.props.serverConfig}
|
||||
/>;
|
||||
}
|
||||
},
|
||||
|
@ -474,7 +457,7 @@ module.exports = React.createClass({
|
|||
const AuthPage = sdk.getComponent('auth.AuthPage');
|
||||
|
||||
let errorText;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
const err = this.state.errorText;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -57,7 +58,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
* session to be failed and the process to go back to the start.
|
||||
* setEmailSid: m.login.email.identity only: a function to be called with the
|
||||
* email sid after a token is requested.
|
||||
* makeRegistrationUrl A function that makes a registration URL
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* focus: set the input focus appropriately in the form.
|
||||
|
@ -365,7 +365,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -374,38 +373,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (this.props.stageState.emailSid === null) {
|
||||
this.setState({requestingToken: true});
|
||||
this._requestEmailToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Requests a verification token by email.
|
||||
*/
|
||||
_requestEmailToken: function() {
|
||||
const nextLink = this.props.makeRegistrationUrl({
|
||||
client_secret: this.props.clientSecret,
|
||||
hs_url: this.props.matrixClient.getHomeserverUrl(),
|
||||
is_url: this.props.matrixClient.getIdentityServerUrl(),
|
||||
session_id: this.props.authSessionId,
|
||||
});
|
||||
|
||||
return this.props.matrixClient.requestRegisterEmailToken(
|
||||
this.props.inputs.emailAddress,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
nextLink,
|
||||
).then((result) => {
|
||||
this.props.setEmailSid(result.sid);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.requestingToken) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
|
|
@ -18,9 +18,15 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
/*
|
||||
* Configure the Modular server name.
|
||||
*
|
||||
|
@ -31,65 +37,107 @@ export default class ModularServerConfig extends React.PureComponent {
|
|||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
// In other words, if the custom URL is blank, the default is used.
|
||||
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
|
||||
|
||||
// This component always uses the default IS URL and doesn't allow it
|
||||
// to be changed. We still receive it as a prop here to simplify
|
||||
// consumers by still passing the IS URL via onServerConfigChange.
|
||||
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
|
||||
|
||||
// custom URLs are explicitly provided by the user and override the
|
||||
// default URLs. The user enters them via the component's input fields,
|
||||
// which is reflected on these properties whenever on..UrlChanged fires.
|
||||
// They are persisted in localStorage by MatrixClientPeg, and so can
|
||||
// override the default URLs when the component initially loads.
|
||||
customHsUrl: PropTypes.string,
|
||||
// The current configuration that the user is expecting to change.
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
}
|
||||
|
||||
// Called after the component calls onServerConfigChange
|
||||
onAfterSubmit: PropTypes.func,
|
||||
|
||||
// Optional text for the submit button. If falsey, no button will be shown.
|
||||
submitText: PropTypes.string,
|
||||
|
||||
// Optional class for the submit button. Only applies if the submit button
|
||||
// is to be rendered.
|
||||
submitClass: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
delayTimeMs: 0,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hsUrl: props.customHsUrl,
|
||||
busy: false,
|
||||
errorText: "",
|
||||
hsUrl: props.serverConfig.hsUrl,
|
||||
isUrl: props.serverConfig.isUrl,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hsUrl) return;
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
}
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
|
||||
}
|
||||
|
||||
onHomeserverBlur = (ev) => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
this.validateServer();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onHomeserverChange = (ev) => {
|
||||
const hsUrl = ev.target.value;
|
||||
this.setState({ hsUrl });
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
await this.validateServer();
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
|
@ -100,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
|
@ -113,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent {
|
|||
</a>,
|
||||
},
|
||||
)}
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
</div>
|
||||
{submitButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -21,11 +22,29 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
*/
|
||||
class PasswordLogin extends React.Component {
|
||||
export default class PasswordLogin extends React.Component {
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
initialPhoneNumber: PropTypes.string,
|
||||
initialPassword: PropTypes.string,
|
||||
onUsernameChanged: PropTypes.func,
|
||||
onPhoneCountryChanged: PropTypes.func,
|
||||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
disableSubmit: PropTypes.bool,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onEditServerDetailsClick: null,
|
||||
|
@ -40,13 +59,12 @@ class PasswordLogin extends React.Component {
|
|||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
// This is optional and only set if we used a server name to determine
|
||||
// the HS URL via `.well-known` discovery. The server name is used
|
||||
// instead of the HS URL when talking about where to "sign in to".
|
||||
hsName: null,
|
||||
hsUrl: "",
|
||||
disableSubmit: false,
|
||||
}
|
||||
};
|
||||
|
||||
static LOGIN_FIELD_EMAIL = "login_field_email";
|
||||
static LOGIN_FIELD_MXID = "login_field_mxid";
|
||||
static LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -193,10 +211,7 @@ class PasswordLogin extends React.Component {
|
|||
name="username" // make it a little easier for browser's remember-password
|
||||
key="username_input"
|
||||
type="text"
|
||||
label={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
}) : _t("Username")}
|
||||
label={_t("Username")}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
|
@ -258,20 +273,22 @@ class PasswordLogin extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
let signInToText = _t('Sign in to your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.hsName,
|
||||
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
|
@ -353,27 +370,3 @@ class PasswordLogin extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
|
||||
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
|
||||
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
initialPhoneNumber: PropTypes.string,
|
||||
initialPassword: PropTypes.string,
|
||||
onUsernameChanged: PropTypes.func,
|
||||
onPhoneCountryChanged: PropTypes.func,
|
||||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
hsName: PropTypes.string,
|
||||
hsUrl: PropTypes.string,
|
||||
disableSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
module.exports = PasswordLogin;
|
||||
|
|
|
@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
|
@ -51,11 +52,7 @@ module.exports = React.createClass({
|
|||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
// This is optional and only set if we used a server name to determine
|
||||
// the HS URL via `.well-known` discovery. The server name is used
|
||||
// instead of the HS URL when talking about "your account".
|
||||
hsName: PropTypes.string,
|
||||
hsUrl: PropTypes.string,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -515,20 +512,22 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let yourMatrixAccountText = _t('Create your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.hsName,
|
||||
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
|
|
|
@ -20,6 +20,9 @@ import PropTypes from 'prop-types';
|
|||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
/*
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
|
@ -27,82 +30,119 @@ import { _t } from '../../../languageHandler';
|
|||
|
||||
export default class ServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
// In other words, if the custom URL is blank, the default is used.
|
||||
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
|
||||
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
|
||||
|
||||
// custom URLs are explicitly provided by the user and override the
|
||||
// default URLs. The user enters them via the component's input fields,
|
||||
// which is reflected on these properties whenever on..UrlChanged fires.
|
||||
// They are persisted in localStorage by MatrixClientPeg, and so can
|
||||
// override the default URLs when the component initially loads.
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
// The current configuration that the user is expecting to change.
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
}
|
||||
|
||||
// Called after the component calls onServerConfigChange
|
||||
onAfterSubmit: PropTypes.func,
|
||||
|
||||
// Optional text for the submit button. If falsey, no button will be shown.
|
||||
submitText: PropTypes.string,
|
||||
|
||||
// Optional class for the submit button. Only applies if the submit button
|
||||
// is to be rendered.
|
||||
submitClass: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
delayTimeMs: 0,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hsUrl: props.customHsUrl,
|
||||
isUrl: props.customIsUrl,
|
||||
busy: false,
|
||||
errorText: "",
|
||||
hsUrl: props.serverConfig.hsUrl,
|
||||
isUrl: props.serverConfig.isUrl,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hsUrl &&
|
||||
newProps.customIsUrl === this.state.isUrl) return;
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
|
||||
}
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onHomeserverBlur = (ev) => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.state.isUrl,
|
||||
});
|
||||
this.validateServer();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onHomeserverChange = (ev) => {
|
||||
const hsUrl = ev.target.value;
|
||||
this.setState({ hsUrl });
|
||||
}
|
||||
};
|
||||
|
||||
onIdentityServerBlur = (ev) => {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.state.isUrl,
|
||||
});
|
||||
this.validateServer();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onIdentityServerChange = (ev) => {
|
||||
const isUrl = ev.target.value;
|
||||
this.setState({ isUrl });
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
await this.validateServer();
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
|
@ -114,10 +154,24 @@ export default class ServerConfig extends React.PureComponent {
|
|||
showHelpPopup = () => {
|
||||
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const errorText = this.state.errorText
|
||||
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
|
||||
: null;
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
|
@ -127,22 +181,28 @@ export default class ServerConfig extends React.PureComponent {
|
|||
{ sub }
|
||||
</a>,
|
||||
})}
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.defaultIsUrl}
|
||||
value={this.state.isUrl}
|
||||
onBlur={this.onIdentityServerBlur}
|
||||
onChange={this.onIdentityServerChange}
|
||||
/>
|
||||
</div>
|
||||
{errorText}
|
||||
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.serverConfig.isUrl}
|
||||
value={this.state.isUrl}
|
||||
onBlur={this.onIdentityServerBlur}
|
||||
onChange={this.onIdentityServerChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>
|
||||
{submitButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
|
@ -32,8 +34,13 @@ export const TYPES = {
|
|||
label: () => _t('Free'),
|
||||
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
||||
description: () => _t('Join millions for free on the largest public server'),
|
||||
hsUrl: 'https://matrix.org',
|
||||
isUrl: 'https://vector.im',
|
||||
serverConfig: makeType(ValidatedServerConfig, {
|
||||
hsUrl: "https://matrix.org",
|
||||
hsName: "matrix.org",
|
||||
hsNameIsDifferent: false,
|
||||
isUrl: "https://vector.im",
|
||||
identityEnabled: true,
|
||||
}),
|
||||
},
|
||||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
|
@ -44,6 +51,7 @@ export const TYPES = {
|
|||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
identityServerUrl: "https://vector.im",
|
||||
},
|
||||
ADVANCED: {
|
||||
id: ADVANCED,
|
||||
|
@ -56,10 +64,11 @@ export const TYPES = {
|
|||
},
|
||||
};
|
||||
|
||||
export function getTypeFromHsUrl(hsUrl) {
|
||||
export function getTypeFromServerConfig(config) {
|
||||
const {hsUrl} = config;
|
||||
if (!hsUrl) {
|
||||
return null;
|
||||
} else if (hsUrl === TYPES.FREE.hsUrl) {
|
||||
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
|
||||
return FREE;
|
||||
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
||||
// This is an unlikely case to reach, as Modular defaults to hiding the
|
||||
|
@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent {
|
|||
selected: PropTypes.string,
|
||||
// Handler called when the selected type changes.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent {
|
|||
e.stopPropagation();
|
||||
const type = e.currentTarget.dataset.id;
|
||||
this.updateSelectedType(type);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
|
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -104,9 +105,13 @@ module.exports = React.createClass({
|
|||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
const urls = props.urls || [];
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
let urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
urls = props.urls || [];
|
||||
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
let defaultImageUrl = null;
|
||||
|
@ -133,40 +138,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
*/
|
||||
_getInitialLetter: function(name) {
|
||||
if (name.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
|
@ -176,20 +148,20 @@ module.exports = React.createClass({
|
|||
} = this.props;
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = this._getInitialLetter(name);
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{ initialLetter }
|
||||
</EmojiText>
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} />
|
||||
width={width} height={height} aria-hidden="true" />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
|
|||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from "../../../index";
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Avatar from '../../../Avatar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
@ -89,7 +89,6 @@ module.exports = React.createClass({
|
|||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
|
@ -98,41 +97,14 @@ module.exports = React.createClass({
|
|||
getRoomAvatarUrl: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
return Avatar.avatarUrlForRoom(
|
||||
props.room,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
const room = props.room;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
onRoomAvatarClick: function() {
|
||||
const avatarUrl = this.props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -29,6 +30,10 @@ export class TopLeftMenu extends React.Component {
|
|||
displayName: PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func,
|
||||
|
||||
// Optional function to collect a reference to the container
|
||||
// of this component directly.
|
||||
containerRef: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -61,44 +66,48 @@ export class TopLeftMenu extends React.Component {
|
|||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener">
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let homePageSection = null;
|
||||
let homePageItem = null;
|
||||
if (this.hasHomePage()) {
|
||||
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
|
||||
</ul>;
|
||||
homePageItem = <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage} tabIndex={0}>
|
||||
{_t("Home")}
|
||||
</li>;
|
||||
}
|
||||
|
||||
let signInOutSection;
|
||||
let signInOutItem;
|
||||
if (isGuest) {
|
||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
|
||||
</ul>;
|
||||
signInOutItem = <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn} tabIndex={0}>
|
||||
{_t("Sign in")}
|
||||
</li>;
|
||||
} else {
|
||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
|
||||
</ul>;
|
||||
signInOutItem = <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut} tabIndex={0}>
|
||||
{_t("Sign out")}
|
||||
</li>;
|
||||
}
|
||||
|
||||
return <div className="mx_TopLeftMenu">
|
||||
<div className="mx_TopLeftMenu_section_noIcon">
|
||||
const settingsItem = <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings} tabIndex={0}>
|
||||
{_t("Settings")}
|
||||
</li>;
|
||||
|
||||
return <div className="mx_TopLeftMenu mx_HiddenFocusable" tabIndex={0} ref={this.props.containerRef}>
|
||||
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
|
||||
<div>{this.props.displayName}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
|
||||
{hostingSignup}
|
||||
</div>
|
||||
{homePageSection}
|
||||
<ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
|
||||
{homePageItem}
|
||||
{settingsItem}
|
||||
{signInOutItem}
|
||||
</ul>
|
||||
{signInOutSection}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor {
|
|||
|
||||
return <div>
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
<div className="mx_DevTools_eventTypeStateKeyGroup">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
|
|
@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component {
|
|||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
this.closeCopiedTooltip = e.target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
|
@ -131,6 +132,12 @@ export default class ShareDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
||||
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
|
||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
|
|
@ -28,6 +28,7 @@ import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
|
|||
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
||||
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
||||
import sdk from "../../../index";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
export default class UserSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -67,7 +68,7 @@ export default class UserSettingsDialog extends React.Component {
|
|||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecurityUserSettingsTab />,
|
||||
));
|
||||
if (SettingsStore.getLabsFeatures().length > 0) {
|
||||
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
|
||||
tabs.push(new Tab(
|
||||
_td("Labs"),
|
||||
"mx_UserSettingsDialog_labsIcon",
|
||||
|
|
|
@ -63,6 +63,10 @@ export default function AccessibleButton(props) {
|
|||
};
|
||||
}
|
||||
|
||||
// Pass through the ref - used for keyboard shortcut access to some buttons
|
||||
restProps.ref = restProps.inputRef;
|
||||
delete restProps.inputRef;
|
||||
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
|
@ -89,6 +93,7 @@ export default function AccessibleButton(props) {
|
|||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: PropTypes.node,
|
||||
inputRef: PropTypes.func,
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 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 {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
const {element, children, addAlt, ...restProps} = props;
|
||||
|
||||
// fast path: simple regex to detect strings that don't contain
|
||||
// emoji and just return them
|
||||
if (containsEmoji(children)) {
|
||||
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
|
||||
return React.createElement(element, restProps);
|
||||
} else {
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
}
|
||||
|
||||
EmojiText.propTypes = {
|
||||
element: PropTypes.string,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EmojiText.defaultProps = {
|
||||
element: 'span',
|
||||
addAlt: true,
|
||||
};
|
|
@ -195,13 +195,13 @@ export default class ImageView extends React.Component {
|
|||
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
|
||||
<div className="mx_ImageView_labelWrapper">
|
||||
<div className="mx_ImageView_label">
|
||||
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" onClick={ this.rotateCounterClockwise }>
|
||||
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" title={_t("Rotate Left")} onClick={ this.rotateCounterClockwise }>
|
||||
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_ImageView_rotateClockwise" onClick={ this.rotateClockwise }>
|
||||
<AccessibleButton className="mx_ImageView_rotateClockwise" title={_t("Rotate Right")} onClick={ this.rotateClockwise }>
|
||||
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }>
|
||||
<AccessibleButton className="mx_ImageView_cancel" title={_t("Close")} onClick={ this.props.onFinished }>
|
||||
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
|
||||
</AccessibleButton>
|
||||
<div className="mx_ImageView_shim">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -13,11 +14,13 @@ 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';
|
||||
const MemberAvatar = require('../avatars/MemberAvatar.js');
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberEventListSummary',
|
||||
|
@ -105,7 +108,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
|
||||
const desc = this._renderCommaSeparatedList(descs);
|
||||
const desc = formatCommaSeparatedList(descs);
|
||||
|
||||
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
||||
});
|
||||
|
@ -114,13 +117,9 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
return (
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
<EmojiText>
|
||||
{ summaries.join(", ") }
|
||||
</EmojiText>
|
||||
{ summaries.join(", ") }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
@ -132,7 +131,7 @@ module.exports = React.createClass({
|
|||
* included before "and [n] others".
|
||||
*/
|
||||
_renderNameList: function(users) {
|
||||
return this._renderCommaSeparatedList(users, this.props.summaryLength);
|
||||
return formatCommaSeparatedList(users, this.props.summaryLength);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -283,35 +282,6 @@ module.exports = React.createClass({
|
|||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a written English string representing `items`, with an optional limit on
|
||||
* the number of items included in the result. If specified and if the length of
|
||||
*`items` is greater than the limit, the string "and n others" will be appended onto
|
||||
* the result.
|
||||
* If `items` is empty, returns the empty string. If there is only one item, return
|
||||
* it.
|
||||
* @param {string[]} items the items to construct a string from.
|
||||
* @param {number?} itemLimit the number by which to limit the list.
|
||||
* @returns {string} a string constructed by joining `items` with a comma between each
|
||||
* item, but with the last item appended as " and [lastItem]".
|
||||
*/
|
||||
_renderCommaSeparatedList(items, itemLimit) {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
} else if (items.length === 1) {
|
||||
return items[0];
|
||||
} else if (remaining > 0) {
|
||||
items = items.slice(0, itemLimit);
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
}
|
||||
},
|
||||
|
||||
_renderAvatars: function(roomMembers) {
|
||||
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,12 +22,14 @@ import dis from '../../../dispatcher';
|
|||
import EditorModel from '../../../editor/model';
|
||||
import {setCaretPosition} from '../../../editor/caret';
|
||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
||||
import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize';
|
||||
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import Autocomplete from '../rooms/Autocomplete';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import {renderModel} from '../../../editor/render';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class MessageEditor extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -40,22 +43,28 @@ export default class MessageEditor extends React.Component {
|
|||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(
|
||||
() => this._autocompleteRef,
|
||||
query => this.setState({query}),
|
||||
room,
|
||||
);
|
||||
this.model = new EditorModel(
|
||||
parseEvent(this.props.event),
|
||||
parseEvent(this.props.event, room),
|
||||
partCreator,
|
||||
this._updateEditorState,
|
||||
);
|
||||
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
||||
this.state = {
|
||||
autoComplete: null,
|
||||
room,
|
||||
};
|
||||
this._editorRef = null;
|
||||
this._autocompleteRef = null;
|
||||
this._hasModifications = false;
|
||||
}
|
||||
|
||||
_getRoom() {
|
||||
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
||||
}
|
||||
|
||||
_updateEditorState = (caret) => {
|
||||
|
@ -71,48 +80,99 @@ export default class MessageEditor extends React.Component {
|
|||
}
|
||||
|
||||
_onInput = (event) => {
|
||||
this._hasModifications = true;
|
||||
const sel = document.getSelection();
|
||||
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||
this.model.update(text, event.inputType, caret);
|
||||
}
|
||||
|
||||
_isCaretAtStart() {
|
||||
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
||||
return caret.offset === 0;
|
||||
}
|
||||
|
||||
_isCaretAtEnd() {
|
||||
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
||||
return caret.offset === text.length;
|
||||
}
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
// insert newline on Shift+Enter
|
||||
if (event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault(); // just in case the browser does support this
|
||||
document.execCommand("insertHTML", undefined, "\n");
|
||||
return;
|
||||
}
|
||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
if (!this.model.autoComplete) {
|
||||
return;
|
||||
if (this.model.autoComplete) {
|
||||
const autoComplete = this.model.autoComplete;
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
autoComplete.onEnter(event); break;
|
||||
case "ArrowUp":
|
||||
autoComplete.onUpArrow(event); break;
|
||||
case "ArrowDown":
|
||||
autoComplete.onDownArrow(event); break;
|
||||
case "Tab":
|
||||
autoComplete.onTab(event); break;
|
||||
case "Escape":
|
||||
autoComplete.onEscape(event); break;
|
||||
default:
|
||||
return; // don't preventDefault on anything else
|
||||
}
|
||||
event.preventDefault();
|
||||
} else if (event.key === "Enter") {
|
||||
this._sendEdit();
|
||||
event.preventDefault();
|
||||
} else if (event.key === "Escape") {
|
||||
this._cancelEdit();
|
||||
} else if (event.key === "ArrowUp") {
|
||||
if (this._hasModifications || !this._isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId());
|
||||
if (previousEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: previousEvent});
|
||||
event.preventDefault();
|
||||
}
|
||||
} else if (event.key === "ArrowDown") {
|
||||
if (this._hasModifications || !this._isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId());
|
||||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
const autoComplete = this.model.autoComplete;
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
autoComplete.onEnter(event); break;
|
||||
case "ArrowUp":
|
||||
autoComplete.onUpArrow(event); break;
|
||||
case "ArrowDown":
|
||||
autoComplete.onDownArrow(event); break;
|
||||
case "Tab":
|
||||
autoComplete.onTab(event); break;
|
||||
case "Escape":
|
||||
autoComplete.onEscape(event); break;
|
||||
default:
|
||||
return; // don't preventDefault on anything else
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
_onCancelClicked = () => {
|
||||
_cancelEdit = () => {
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
_onSaveClicked = () => {
|
||||
_sendEdit = () => {
|
||||
const newContent = {
|
||||
"msgtype": "m.text",
|
||||
"body": textSerialize(this.model),
|
||||
};
|
||||
if (requiresHtml(this.model)) {
|
||||
const contentBody = {
|
||||
msgtype: newContent.msgtype,
|
||||
body: ` * ${newContent.body}`,
|
||||
};
|
||||
const formattedBody = htmlSerializeIfNeeded(this.model);
|
||||
if (formattedBody) {
|
||||
newContent.format = "org.matrix.custom.html";
|
||||
newContent.formatted_body = htmlSerialize(this.model);
|
||||
newContent.formatted_body = formattedBody;
|
||||
contentBody.format = newContent.format;
|
||||
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
|
||||
}
|
||||
const content = Object.assign({
|
||||
"m.new_content": newContent,
|
||||
|
@ -120,12 +180,13 @@ export default class MessageEditor extends React.Component {
|
|||
"rel_type": "m.replace",
|
||||
"event_id": this.props.event.getId(),
|
||||
},
|
||||
}, newContent);
|
||||
}, contentBody);
|
||||
|
||||
const roomId = this.props.event.getRoomId();
|
||||
this.context.matrixClient.sendMessage(roomId, content);
|
||||
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
_onAutoCompleteConfirm = (completion) => {
|
||||
|
@ -138,6 +199,8 @@ export default class MessageEditor extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this._updateEditorState();
|
||||
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
|
||||
this._editorRef.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -157,7 +220,7 @@ export default class MessageEditor extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <div className="mx_MessageEditor">
|
||||
return <div className={classNames("mx_MessageEditor", this.props.className)}>
|
||||
{ autoComplete }
|
||||
<div
|
||||
className="mx_MessageEditor_editor"
|
||||
|
@ -166,10 +229,11 @@ export default class MessageEditor extends React.Component {
|
|||
onInput={this._onInput}
|
||||
onKeyDown={this._onKeyDown}
|
||||
ref={ref => this._editorRef = ref}
|
||||
aria-label={_t("Edit message")}
|
||||
></div>
|
||||
<div className="mx_MessageEditor_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={this._onCancelClicked}>{_t("Cancel")}</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this._onSaveClicked}>{_t("Save")}</AccessibleButton>
|
||||
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -63,6 +63,11 @@ export default class ReplyThread extends React.Component {
|
|||
static getParentEventId(ev) {
|
||||
if (!ev || ev.isRedacted()) return;
|
||||
|
||||
// XXX: For newer relations (annotations, replacements, etc.), we now
|
||||
// have a `getRelation` helper on the event, and you might assume it
|
||||
// could be used here for replies as well... However, the helper
|
||||
// currently assumes the relation has a `rel_type`, which older replies
|
||||
// do not, so this block is left as-is for now.
|
||||
const mRelatesTo = ev.getWireContent()['m.relates_to'];
|
||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
||||
|
|
56
src/components/views/elements/TextWithTooltip.js
Normal file
56
src/components/views/elements/TextWithTooltip.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
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';
|
||||
|
||||
export default class TextWithTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
class: PropTypes.string,
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
onMouseOut = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
|
||||
return (
|
||||
<span onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={this.props.class}>
|
||||
{this.props.children}
|
||||
<Tooltip
|
||||
label={this.props.tooltip}
|
||||
visible={this.state.hover}
|
||||
className={"mx_TextWithTooltip_tooltip"} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -79,6 +79,10 @@ module.exports = React.createClass({
|
|||
let offset = 0;
|
||||
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
||||
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
|
||||
} else {
|
||||
// The tooltip is larger than the parent height: figure out what offset
|
||||
// we need so that we're still centered.
|
||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||
}
|
||||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||
style.left = 6 + parentBox.right + window.pageXOffset;
|
||||
|
|
|
@ -117,7 +117,6 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
||||
|
@ -129,9 +128,9 @@ export default React.createClass({
|
|||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
</div>;
|
||||
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
|
|
|
@ -180,7 +180,6 @@ module.exports = React.createClass({
|
|||
this.props.groupMember.displayname || this.props.groupMember.userId
|
||||
);
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
|
@ -189,7 +188,7 @@ module.exports = React.createClass({
|
|||
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
{ avatarElement }
|
||||
<EmojiText element="h2">{ groupMemberName }</EmojiText>
|
||||
<h2>{ groupMemberName }</h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
|
|
|
@ -149,7 +149,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
@ -221,7 +220,7 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>
|
||||
{ avatarElement }
|
||||
|
||||
<EmojiText element="h2">{ groupRoomName }</EmojiText>
|
||||
<h2>{ groupRoomName }</h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
|
|
|
@ -145,6 +145,7 @@ function remoteRender(event) {
|
|||
a.target = data.target;
|
||||
a.download = data.download;
|
||||
a.style = data.style;
|
||||
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
|
||||
a.href = window.URL.createObjectURL(data.blob);
|
||||
a.appendChild(img);
|
||||
a.appendChild(document.createTextNode(data.textContent));
|
||||
|
|
|
@ -172,8 +172,8 @@ export default class MImageBody extends React.Component {
|
|||
// thumbnail resolution will be unnecessarily reduced.
|
||||
// custom timeline widths seems preferable.
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
const thumbWidth = 800 * pixelRatio;
|
||||
const thumbHeight = 600 * pixelRatio;
|
||||
const thumbWidth = Math.round(800 * pixelRatio);
|
||||
const thumbHeight = Math.round(600 * pixelRatio);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
|
|
|
@ -23,7 +23,7 @@ import dis from '../../../dispatcher';
|
|||
import Modal from '../../../Modal';
|
||||
import { createMenu } from '../../structures/ContextualMenu';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
|
||||
export default class MessageActionBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -148,12 +148,12 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
/>;
|
||||
if (this.isEditingEnabled()) {
|
||||
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
||||
title={_t("Edit")}
|
||||
onClick={this.onEditClick}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
if (this.isEditingEnabled() && canEditContent(this.props.mxEvent)) {
|
||||
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
||||
title={_t("Edit")}
|
||||
onClick={this.onEditClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className="mx_MessageActionBar">
|
||||
|
|
|
@ -90,6 +90,7 @@ module.exports = React.createClass({
|
|||
tileShape={this.props.tileShape}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
isEditing={this.props.isEditing}
|
||||
onHeightChanged={this.props.onHeightChanged} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ export default class ReactionDimension extends React.PureComponent {
|
|||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +45,7 @@ export default class ReactionDimension extends React.PureComponent {
|
|||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
|
@ -55,6 +57,10 @@ export default class ReactionDimension extends React.PureComponent {
|
|||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
|
@ -82,7 +88,7 @@ export default class ReactionDimension extends React.PureComponent {
|
|||
if (mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
return mxEvent.getContent()["m.relates_to"].key === option;
|
||||
return mxEvent.getRelation().key === option;
|
||||
});
|
||||
if (!reactionForOption) {
|
||||
continue;
|
||||
|
@ -107,7 +113,11 @@ export default class ReactionDimension extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
return reactions.getAnnotationsBySender()[userId];
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
}
|
||||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onOptionClick = (ev) => {
|
||||
|
@ -158,6 +168,7 @@ export default class ReactionDimension extends React.PureComponent {
|
|||
|
||||
return <span className="mx_ReactionDimension"
|
||||
title={this.props.title}
|
||||
aria-hidden={true}
|
||||
>
|
||||
{items}
|
||||
</span>;
|
||||
|
|
|
@ -34,6 +34,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
|
||||
|
@ -45,6 +46,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
|
@ -56,6 +58,10 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
|
@ -80,7 +86,11 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
return reactions.getAnnotationsBySender()[userId];
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
}
|
||||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -101,13 +111,13 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
if (mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
return mxEvent.getContent()["m.relates_to"].key === content;
|
||||
return mxEvent.getRelation().key === content;
|
||||
});
|
||||
return <ReactionsRowButton
|
||||
key={content}
|
||||
content={content}
|
||||
count={count}
|
||||
mxEvent={mxEvent}
|
||||
reactionEvents={events}
|
||||
myReactionEvent={myReactionEvent}
|
||||
/>;
|
||||
});
|
||||
|
|
|
@ -19,17 +19,28 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent: PropTypes.object,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tooltipVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
const { mxEvent, myReactionEvent, content } = this.props;
|
||||
if (myReactionEvent) {
|
||||
|
@ -48,18 +59,53 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onMouseOver = () => {
|
||||
this.setState({
|
||||
// To avoid littering the DOM with a tooltip for every reaction,
|
||||
// only render it on first use.
|
||||
tooltipRendered: true,
|
||||
tooltipVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
onMouseOut = () => {
|
||||
this.setState({
|
||||
tooltipVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { content, count, myReactionEvent } = this.props;
|
||||
const ReactionsRowButtonTooltip =
|
||||
sdk.getComponent('messages.ReactionsRowButtonTooltip');
|
||||
const { content, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const count = reactionEvents.size;
|
||||
if (!count) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
mx_ReactionsRowButton: true,
|
||||
mx_ReactionsRowButton_selected: !!myReactionEvent,
|
||||
});
|
||||
|
||||
let tooltip;
|
||||
if (this.state.tooltipRendered) {
|
||||
tooltip = <ReactionsRowButtonTooltip
|
||||
mxEvent={this.props.mxEvent}
|
||||
content={content}
|
||||
reactionEvents={reactionEvents}
|
||||
visible={this.state.tooltipVisible}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <span className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
{content} {count}
|
||||
{tooltip}
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
|
84
src/components/views/messages/ReactionsRowButtonTooltip.js
Normal file
84
src/components/views/messages/ReactionsRowButtonTooltip.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const { content, reactionEvents, mxEvent, visible } = this.props;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
let tooltipLabel;
|
||||
if (room) {
|
||||
const senders = [];
|
||||
for (const reactionEvent of reactionEvents) {
|
||||
const { name } = room.getMember(reactionEvent.getSender());
|
||||
senders.push(name);
|
||||
}
|
||||
const shortName = unicodeToShortcode(content);
|
||||
tooltipLabel = <div>{_t(
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||
{
|
||||
shortName,
|
||||
},
|
||||
{
|
||||
reactors: () => {
|
||||
return <div className="mx_ReactionsRowButtonTooltip_senders">
|
||||
{formatCommaSeparatedList(senders, 6)}
|
||||
</div>;
|
||||
},
|
||||
reactedWith: (sub) => {
|
||||
if (!shortName) {
|
||||
return null;
|
||||
}
|
||||
return <div className="mx_ReactionsRowButtonTooltip_reactedWith">
|
||||
{sub}
|
||||
</div>;
|
||||
},
|
||||
},
|
||||
)}</div>;
|
||||
}
|
||||
|
||||
let tooltip;
|
||||
if (tooltipLabel) {
|
||||
tooltip = <Tooltip
|
||||
tooltipClassName="mx_Tooltip_timeline"
|
||||
visible={visible}
|
||||
label={tooltipLabel}
|
||||
/>;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -95,7 +94,6 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
|
@ -117,7 +115,7 @@ export default React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
|
||||
const nameElem = name || '';
|
||||
|
||||
// Name + flair
|
||||
const nameFlair = <span>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,6 +23,7 @@ import ReactDOM from 'react-dom';
|
|||
import PropTypes from 'prop-types';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -88,7 +90,12 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
if (!this.props.isEditing) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
},
|
||||
|
||||
_applyFormatting() {
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
|
@ -123,8 +130,14 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this.calculateUrlPreview();
|
||||
componentDidUpdate: function(prevProps) {
|
||||
if (!this.props.isEditing) {
|
||||
const stoppedEditing = prevProps.isEditing && !this.props.isEditing;
|
||||
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
|
||||
if (messageWasEdited || stoppedEditing) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -140,14 +153,16 @@ module.exports = React.createClass({
|
|||
nextProps.replacingEventId !== this.props.replacingEventId ||
|
||||
nextProps.highlightLink !== this.props.highlightLink ||
|
||||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||
nextProps.isEditing !== this.props.isEditing ||
|
||||
nextState.links !== this.state.links ||
|
||||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
|
||||
nextState.widgetHidden !== this.state.widgetHidden);
|
||||
},
|
||||
|
||||
calculateUrlPreview: function() {
|
||||
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview && !this.state.links.length) {
|
||||
if (this.props.showUrlPreview) {
|
||||
let links = this.findLinks(this.refs.content.children);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
|
@ -425,8 +440,39 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onMouseEnterEditedMarker: function() {
|
||||
this.setState({editedMarkerHovered: true});
|
||||
},
|
||||
|
||||
_onMouseLeaveEditedMarker: function() {
|
||||
this.setState({editedMarkerHovered: false});
|
||||
},
|
||||
|
||||
_renderEditedMarker: function() {
|
||||
let editedTooltip;
|
||||
if (this.state.editedMarkerHovered) {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const editEvent = this.props.mxEvent.replacingEvent();
|
||||
const date = editEvent && formatDate(editEvent.getDate());
|
||||
editedTooltip = <Tooltip
|
||||
tooltipClassName="mx_Tooltip_timeline"
|
||||
label={_t("Edited at %(date)s", {date})}
|
||||
/>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key="editedMarker" className="mx_EventTile_edited"
|
||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
if (this.props.isEditing) {
|
||||
const MessageEditor = sdk.getComponent('elements.MessageEditor');
|
||||
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
|
||||
}
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
|
@ -436,6 +482,9 @@ module.exports = React.createClass({
|
|||
// Part of Replies fallback support
|
||||
stripReplyFallback: stripReply,
|
||||
});
|
||||
if (this.props.replacingEventId) {
|
||||
body = [body, this._renderEditedMarker()];
|
||||
}
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
body = <a href={this.props.highlightLink}>{ body }</a>;
|
||||
|
@ -462,12 +511,12 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||
*
|
||||
<EmojiText
|
||||
<span
|
||||
className="mx_MEmoteBody_sender"
|
||||
onClick={this.onEmoteSenderClick}
|
||||
>
|
||||
{ name }
|
||||
</EmojiText>
|
||||
</span>
|
||||
|
||||
{ body }
|
||||
{ widgets }
|
||||
|
|
|
@ -20,7 +20,6 @@ const React = require('react');
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const TextForEvent = require('../../../TextForEvent');
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualEvent',
|
||||
|
@ -31,11 +30,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
if (text == null || text.length === 0) return null;
|
||||
return (
|
||||
<EmojiText element="div" className="mx_TextualEvent">{ text }</EmojiText>
|
||||
<div className="mx_TextualEvent">{ text }</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
67
src/components/views/messages/ViewSourceEvent.js
Normal file
67
src/components/views/messages/ViewSourceEvent.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class ViewSourceEvent extends React.PureComponent {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
onToggle = (ev) => {
|
||||
ev.preventDefault();
|
||||
const { expanded } = this.state;
|
||||
this.setState({
|
||||
expanded: !expanded,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
||||
let content;
|
||||
if (expanded) {
|
||||
content = <pre>{JSON.stringify(mxEvent, null, 4)}</pre>;
|
||||
} else {
|
||||
content = <code>{`{ "type": ${mxEvent.getType()} }`}</code>;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", {
|
||||
mx_ViewSourceEvent_expanded: expanded,
|
||||
});
|
||||
|
||||
return <span className={classes}>
|
||||
{content}
|
||||
<a
|
||||
className="mx_ViewSourceEvent_toggle"
|
||||
href="#"
|
||||
onClick={this.onToggle}
|
||||
/>
|
||||
</span>;
|
||||
}
|
||||
}
|
|
@ -256,8 +256,6 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let position = 1;
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, i) => {
|
||||
|
@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
|
||||
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
|
||||
{ completionResult.provider.renderCompletions(completions) }
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
@ -111,7 +111,6 @@ const EntityTile = React.createClass({
|
|||
let nameEl;
|
||||
const {name} = this.props;
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
if (!this.props.suppressOnHover) {
|
||||
const activeAgo = this.props.presenceLastActiveAgo ?
|
||||
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
||||
|
@ -128,24 +127,24 @@ const EntityTile = React.createClass({
|
|||
}
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
||||
<div className="mx_EntityTile_name" dir="auto">
|
||||
{ name }
|
||||
</EmojiText>
|
||||
</div>
|
||||
{presenceLabel}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.subtextLabel) {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
||||
<div className="mx_EntityTile_name" dir="auto">
|
||||
{name}
|
||||
</EmojiText>
|
||||
</div>
|
||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
nameEl = (
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
|
||||
<div className="mx_EntityTile_name" dir="auto">{ name }</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -160,8 +160,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// show twelve hour timestamps
|
||||
isTwelveHour: PropTypes.bool,
|
||||
|
||||
// helper function to access relations for an event
|
||||
// helper function to access relations for this event
|
||||
getRelationsForEvent: PropTypes.func,
|
||||
|
||||
// whether to show reactions for this event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -198,7 +201,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const client = this.props.matrixClient;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
@ -223,7 +226,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const client = this.props.matrixClient;
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
@ -485,6 +488,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
getReactions() {
|
||||
if (
|
||||
!this.props.showReactions ||
|
||||
!this.props.getRelationsForEvent ||
|
||||
!SettingsStore.isFeatureEnabled("feature_reactions")
|
||||
) {
|
||||
|
@ -520,7 +524,10 @@ module.exports = withMatrixClient(React.createClass({
|
|||
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
|
||||
);
|
||||
|
||||
const tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
let tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
if (!tileHandler && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
tileHandler = "messages.ViewSourceEvent";
|
||||
}
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
if (!tileHandler) {
|
||||
|
@ -540,6 +547,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
const classes = classNames({
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_isEditing: this.props.isEditing,
|
||||
mx_EventTile_info: isInfoMessage,
|
||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
|
||||
|
@ -617,14 +625,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||
const actionBar = <MessageActionBar
|
||||
const actionBar = !this.props.isEditing ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
/>;
|
||||
/> : undefined;
|
||||
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
@ -674,14 +682,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</EmojiText>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
|
@ -780,6 +787,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
isEditing={this.props.isEditing}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
|
@ -789,7 +797,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ actionBar }
|
||||
</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 absolutely positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
|
|
|
@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let backButton;
|
||||
if (this.props.member.roomId) {
|
||||
|
@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<div className="mx_MemberInfo_name">
|
||||
{ backButton }
|
||||
{ e2eIconElement }
|
||||
<EmojiText element="h2">{ memberName }</EmojiText>
|
||||
<h2>{ memberName }</h2>
|
||||
</div>
|
||||
{ avatarElement }
|
||||
<div className="mx_MemberInfo_container">
|
||||
|
|
|
@ -40,21 +40,18 @@ import Analytics from '../../../Analytics';
|
|||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
import Markdown from '../../../Markdown';
|
||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
|
||||
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
||||
|
||||
import {
|
||||
asciiRegexp, unicodeRegexp, shortnameToUnicode,
|
||||
asciiList, mapUnicodeToShort, toShort,
|
||||
} from 'emojione';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
|
@ -62,10 +59,9 @@ import RoomViewStore from '../../../stores/RoomViewStore';
|
|||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
|
@ -144,7 +140,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
historyManager: ComposerHistoryManager;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -273,9 +268,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
case 'emoji':
|
||||
// XXX: apparently you can't return plain strings from serializer rules
|
||||
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
|
||||
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
|
||||
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
|
||||
return <b>{ obj.data.get('emojiUnicode') }</b>;
|
||||
// So instead we temporarily wrap emoji from RTE in a span.
|
||||
return <span>{ obj.data.get('emojiUnicode') }</span>;
|
||||
}
|
||||
return this.renderNode({
|
||||
node: obj,
|
||||
|
@ -335,7 +329,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -375,7 +368,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
||||
forComposerQuote: true,
|
||||
returnString: true,
|
||||
emojiOne: false,
|
||||
});
|
||||
const fragment = this.html.deserialize(html);
|
||||
// FIXME: do we want to put in a permalink to the original quote here?
|
||||
|
@ -483,6 +475,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
sendTyping(isTyping) {
|
||||
if (!SettingsStore.getValue('sendTypingNotifications')) return;
|
||||
if (SettingsStore.getValue('lowBandwidth')) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||
|
@ -538,17 +531,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
// Automatic replacement of plaintext emoji to Unicode emoji
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
// The first matched group includes just the matched plaintext emoji
|
||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emojiMatch) {
|
||||
// plaintext -> hex unicode
|
||||
const emojiUc = asciiList[emojiMatch[1]];
|
||||
// hex unicode -> shortname -> actual unicode
|
||||
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emoticonMatch) {
|
||||
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
|
||||
const unicodeEmoji = data ? data.unicode : '';
|
||||
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.startText.key,
|
||||
|
@ -561,54 +552,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// emojioneify any emoji
|
||||
let foundEmoji;
|
||||
do {
|
||||
foundEmoji = false;
|
||||
|
||||
for (const node of editorState.document.getTexts()) {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: node.key,
|
||||
offset: match.index,
|
||||
},
|
||||
focus: {
|
||||
key: node.key,
|
||||
offset: match.index + match[0].length,
|
||||
},
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
|
||||
// if we replaced an emoji, start again looking for more
|
||||
// emoji in the new editor state since doing the replacement
|
||||
// will change the node structure & offsets so we can't compute
|
||||
// insertion ranges from node.key / match.index anymore.
|
||||
foundEmoji = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (foundEmoji);
|
||||
|
||||
// work around weird bug where inserting emoji via the macOS
|
||||
// emoji picker can leave the selection stuck in the emoji's
|
||||
// child text. This seems to happen due to selection getting
|
||||
// moved in the normalisation phase after calculating these changes
|
||||
if (editorState.selection.anchor.key &&
|
||||
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
|
||||
change = change.moveToStartOfNextText();
|
||||
editorState = change.value;
|
||||
}
|
||||
|
||||
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
|
||||
let blockType = editorState.blocks.first().type;
|
||||
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
|
||||
|
@ -1046,6 +989,12 @@ export default class MessageComposerInput extends React.Component {
|
|||
return change.insertText('\n');
|
||||
}
|
||||
|
||||
if (this.autocomplete.countCompletions() > 0) {
|
||||
this.autocomplete.hide();
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
const editorState = this.state.editorState;
|
||||
|
||||
const lastBlock = editorState.blocks.last();
|
||||
|
@ -1087,7 +1036,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (cmd) {
|
||||
if (!cmd.error) {
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{
|
||||
|
@ -1165,11 +1113,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||
|
||||
this.historyManager.save(
|
||||
editorState,
|
||||
this.state.isRichTextEnabled ? 'rich' : 'markdown',
|
||||
);
|
||||
|
||||
if (commandText && commandText.startsWith('/me')) {
|
||||
if (replyingToEv) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1244,14 +1187,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
// and we must be at the edge of the document (up=start, down=end)
|
||||
if (up) {
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
} else {
|
||||
if (!selection.anchor.isAtEndOfNode(document)) return;
|
||||
}
|
||||
|
||||
const selected = this.selectHistory(up);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.moveAutocompleteSelection(up);
|
||||
|
@ -1259,54 +1204,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
selectHistory = async (up) => {
|
||||
const delta = up ? -1 : 1;
|
||||
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||
// We can't go any further - there isn't any more history, so nop.
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
currentlyComposedEditorState: this.state.editorState,
|
||||
});
|
||||
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||
// True when we return to the message being composed currently
|
||||
this.setState({
|
||||
editorState: this.state.currentlyComposedEditorState,
|
||||
});
|
||||
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||
return;
|
||||
}
|
||||
|
||||
let editorState;
|
||||
const historyItem = this.historyManager.getItem(delta);
|
||||
if (!historyItem) return;
|
||||
|
||||
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
|
||||
editorState = this.richToMdEditorState(historyItem.value);
|
||||
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
|
||||
editorState = this.mdToRichEditorState(historyItem.value);
|
||||
} else {
|
||||
editorState = historyItem.value;
|
||||
}
|
||||
|
||||
// Move selection to the end of the selected history
|
||||
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like emoji
|
||||
// should already have been done and persisted in the history.
|
||||
editorState = change.value;
|
||||
|
||||
this.suppressAutoComplete = true;
|
||||
|
||||
this.setState({ editorState }, ()=>{
|
||||
this._editor.focus();
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
onTab = async (e) => {
|
||||
this.setState({
|
||||
someCompletions: null,
|
||||
|
@ -1475,17 +1372,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
case 'emoji': {
|
||||
const { data } = node;
|
||||
const emojiUnicode = data.get('emojiUnicode');
|
||||
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
|
||||
const shortname = toShort(emojiUnicode);
|
||||
const className = classNames('mx_emojione', {
|
||||
mx_emojione_selected: isSelected,
|
||||
});
|
||||
const style = {};
|
||||
if (props.selected) style.border = '1px solid blue';
|
||||
return <img className={ className } src={ uri }
|
||||
title={ shortname } alt={ emojiUnicode } style={style}
|
||||
/>;
|
||||
return data.get('emojiUnicode');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component {
|
|||
if (!this.state.event) return null;
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
return <div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
{ '💬 ' + _t('Replying') }
|
||||
</EmojiText>
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||
onClick={cancelQuoting} />
|
||||
|
|
|
@ -147,7 +147,6 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
|
@ -191,10 +190,10 @@ module.exports = React.createClass({
|
|||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||
<div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
|
||||
|
|
|
@ -750,6 +750,7 @@ module.exports = React.createClass({
|
|||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.recent'],
|
||||
|
|
|
@ -342,7 +342,6 @@ module.exports = React.createClass({
|
|||
badge = <div className={badgeClasses}>{ badgeContent }</div>;
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
let subtextLabel;
|
||||
let tooltip;
|
||||
|
@ -354,14 +353,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||
|
||||
if (this.state.selected) {
|
||||
const nameSelected = <EmojiText>{ name }</EmojiText>;
|
||||
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
|
||||
} else {
|
||||
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
|
||||
}
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
|
||||
} else if (this.state.hover) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -212,15 +211,13 @@ module.exports = React.createClass({
|
|||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
return (
|
||||
<li className="mx_WhoIsTypingTile">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">
|
||||
<EmojiText>{ typingString }</EmojiText>
|
||||
{ typingString }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
} else if (this.state.loading) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.backupInfo) {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let clientBackupStatus;
|
||||
let restoreButtonCaption = _t("Restore from Backup");
|
||||
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
clientBackupStatus = <div>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>{_t("This device is backing up your keys. ")}<EmojiText>✅</EmojiText></p>
|
||||
<p>✅ {_t("This device is backing up your keys. ")}</p>
|
||||
</div>;
|
||||
} else {
|
||||
clientBackupStatus = <div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue