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/BottomLeftMenu.js
|
||||||
src/components/structures/CreateRoom.js
|
src/components/structures/CreateRoom.js
|
||||||
src/components/structures/MessagePanel.js
|
src/components/structures/MessagePanel.js
|
||||||
src/components/structures/NotificationPanel.js
|
|
||||||
src/components/structures/RoomDirectory.js
|
src/components/structures/RoomDirectory.js
|
||||||
src/components/structures/RoomStatusBar.js
|
src/components/structures/RoomStatusBar.js
|
||||||
src/components/structures/RoomView.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)
|
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)
|
[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",
|
"name": "matrix-react-sdk",
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -65,7 +65,8 @@
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.28.1",
|
"commonmark": "^0.28.1",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.0",
|
||||||
"emojione": "2.2.7",
|
"emojibase-data": "^4.0.0",
|
||||||
|
"emojibase-regex": "^3.0.0",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.5.6",
|
||||||
"flux": "2.1.1",
|
"flux": "2.1.1",
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"lolex": "2.3.2",
|
"lolex": "2.3.2",
|
||||||
"matrix-js-sdk": "1.1.0",
|
"matrix-js-sdk": "1.2.0",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
|
|
|
@ -32,6 +32,11 @@ body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
font-family: $monospace-font-family;
|
||||||
|
font-size: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.error, .warning {
|
.error, .warning {
|
||||||
color: $warning-color;
|
color: $warning-color;
|
||||||
}
|
}
|
||||||
|
@ -110,6 +115,14 @@ textarea {
|
||||||
color: $primary-fg-color;
|
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
|
// .mx_textinput is a container for a text input
|
||||||
// + some other controls like buttons, ...
|
// + some other controls like buttons, ...
|
||||||
// it has the appearance of a text box so the controls
|
// it has the appearance of a text box so the controls
|
||||||
|
@ -445,15 +458,6 @@ textarea {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_emojione {
|
|
||||||
height: 1em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_emojione_selected {
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
::-moz-selection {
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
color: $selection-fg-color;
|
color: $selection-fg-color;
|
||||||
|
@ -557,4 +561,3 @@ textarea {
|
||||||
.mx_Username_color8 {
|
.mx_Username_color8 {
|
||||||
color: $username-variant8-color;
|
color: $username-variant8-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
@import "./views/elements/_Spinner.scss";
|
@import "./views/elements/_Spinner.scss";
|
||||||
@import "./views/elements/_SyntaxHighlight.scss";
|
@import "./views/elements/_SyntaxHighlight.scss";
|
||||||
|
@import "./views/elements/_TextWithTooltip.scss";
|
||||||
@import "./views/elements/_ToggleSwitch.scss";
|
@import "./views/elements/_ToggleSwitch.scss";
|
||||||
@import "./views/elements/_ToolTipButton.scss";
|
@import "./views/elements/_ToolTipButton.scss";
|
||||||
@import "./views/elements/_Tooltip.scss";
|
@import "./views/elements/_Tooltip.scss";
|
||||||
|
@ -119,10 +120,12 @@
|
||||||
@import "./views/messages/_ReactionDimension.scss";
|
@import "./views/messages/_ReactionDimension.scss";
|
||||||
@import "./views/messages/_ReactionsRow.scss";
|
@import "./views/messages/_ReactionsRow.scss";
|
||||||
@import "./views/messages/_ReactionsRowButton.scss";
|
@import "./views/messages/_ReactionsRowButton.scss";
|
||||||
|
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
|
||||||
@import "./views/messages/_RoomAvatarEvent.scss";
|
@import "./views/messages/_RoomAvatarEvent.scss";
|
||||||
@import "./views/messages/_SenderProfile.scss";
|
@import "./views/messages/_SenderProfile.scss";
|
||||||
@import "./views/messages/_TextualEvent.scss";
|
@import "./views/messages/_TextualEvent.scss";
|
||||||
@import "./views/messages/_UnknownBody.scss";
|
@import "./views/messages/_UnknownBody.scss";
|
||||||
|
@import "./views/messages/_ViewSourceEvent.scss";
|
||||||
@import "./views/room_settings/_AliasSettings.scss";
|
@import "./views/room_settings/_AliasSettings.scss";
|
||||||
@import "./views/room_settings/_ColorSettings.scss";
|
@import "./views/room_settings/_ColorSettings.scss";
|
||||||
@import "./views/rooms/_AppsDrawer.scss";
|
@import "./views/rooms/_AppsDrawer.scss";
|
||||||
|
|
|
@ -79,3 +79,7 @@ limitations under the License.
|
||||||
.mx_Login_type_dropdown {
|
.mx_Login_type_dropdown {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Login_underlinedServerName {
|
||||||
|
border-bottom: 1px dashed $accent-color;
|
||||||
|
}
|
||||||
|
|
|
@ -35,3 +35,8 @@ limitations under the License.
|
||||||
.mx_ServerConfig_help:link {
|
.mx_ServerConfig_help:link {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_ServerConfig_error {
|
||||||
|
display: block;
|
||||||
|
color: $warning-color;
|
||||||
|
}
|
||||||
|
|
|
@ -82,8 +82,13 @@ limitations under the License.
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevTools_content .mx_Field_input + .mx_Field_input {
|
.mx_DevTools_eventTypeStateKeyGroup {
|
||||||
margin-left: 42px;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DevTools_content .mx_Field_input:first-of-type {
|
||||||
|
margin-right: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevTools_tgl {
|
.mx_DevTools_tgl {
|
||||||
|
|
|
@ -16,40 +16,61 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_MessageEditor {
|
.mx_MessageEditor {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $header-panel-bg-color;
|
padding: 3px;
|
||||||
padding: 11px 13px 7px 56px;
|
// 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 {
|
.mx_MessageEditor_editor {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px #e9edf1;
|
border: solid 1px $primary-hairline-color;
|
||||||
background-color: #ffffff;
|
background-color: $primary-bg-color;
|
||||||
padding: 10px;
|
padding: 3px 6px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
span {
|
span.mx_UserPill, span.mx_RoomPill {
|
||||||
display: inline-block;
|
padding-left: 21px;
|
||||||
padding: 0 5px;
|
position: relative;
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageEditor_buttons {
|
.mx_MessageEditor_buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: end;
|
justify-content: flex-end;
|
||||||
padding: 5px 0;
|
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 {
|
.mx_AccessibleButton {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
@ -62,3 +83,8 @@ limitations under the License.
|
||||||
height: 0;
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,15 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_EventTile_continuation {
|
.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 {
|
.mx_EventTile .mx_SenderProfile {
|
||||||
|
@ -72,6 +81,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_MessageTimestamp {
|
.mx_EventTile .mx_MessageTimestamp {
|
||||||
display: block;
|
display: block;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -108,7 +121,7 @@ limitations under the License.
|
||||||
/* HACK to override line-height which is already marked important elsewhere */
|
/* HACK to override line-height which is already marked important elsewhere */
|
||||||
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
||||||
font-size: 48px ! important;
|
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.
|
/* 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_UserPill,
|
||||||
.mx_EventTile_sending .mx_RoomPill,
|
.mx_EventTile_sending .mx_RoomPill {
|
||||||
.mx_EventTile_sending .mx_emojione {
|
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,6 +389,14 @@ limitations under the License.
|
||||||
left: 41px;
|
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 */
|
/* Various markdown overrides */
|
||||||
|
|
||||||
.mx_EventTile_content .markdown-body {
|
.mx_EventTile_content .markdown-body {
|
||||||
|
@ -404,6 +424,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_EventTile_content .markdown-body {
|
.mx_EventTile_content .markdown-body {
|
||||||
pre, code {
|
pre, code {
|
||||||
|
font-family: $monospace-font-family ! important;
|
||||||
// deliberate constants as we're behind an invert filter
|
// deliberate constants as we're behind an invert filter
|
||||||
color: #333;
|
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-bg-color: #1f6954;
|
||||||
$reaction-row-button-selected-border-color: $accent-color;
|
$reaction-row-button-selected-border-color: $accent-color;
|
||||||
|
|
||||||
|
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||||
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -33,21 +33,52 @@
|
||||||
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* latin-ext */
|
||||||
* Fira Mono
|
|
||||||
* Used for monospace copy, i.e. code
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Fira Mono';
|
font-family: 'Inconsolata';
|
||||||
src: url('$(res)/fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype');
|
font-style: normal;
|
||||||
font-weight: 400;
|
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-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-face {
|
||||||
font-family: 'Fira Mono';
|
font-family: "Twemoji Mozilla";
|
||||||
src: url('$(res)/fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype');
|
src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2');
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -1,10 +1,13 @@
|
||||||
// XXX: check this?
|
// XXX: check this?
|
||||||
/* Nunito lacks combining diacritics, so these will fall through
|
/* Nunito lacks combining diacritics, so these will fall through
|
||||||
to the next font. Helevetica's diacritics however do not combine
|
to the next font. Helevetica's diacritics however do not combine
|
||||||
nicely with Open Sans (on OSX, at least) and result in a huge
|
nicely (on OSX, at least) and result in a huge horizontal mess.
|
||||||
horizontal mess. Arial empirically gets it right, hence prioritising
|
Arial empirically gets it right, hence prioritising Arial here. */
|
||||||
Arial here. */
|
/* We fall through to Twemoji for emoji rather than falling through
|
||||||
$font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
|
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
|
// unified palette
|
||||||
// try to use these colors when possible
|
// 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-bg-color: #e9fff9;
|
||||||
$reaction-row-button-selected-border-color: $accent-color;
|
$reaction-row-button-selected-border-color: $accent-color;
|
||||||
|
|
||||||
|
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||||
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -1,28 +1,29 @@
|
||||||
#!/usr/bin/env node
|
#!/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 fs = require('fs');
|
||||||
|
|
||||||
const output = Object.keys(EMOJI_DATA).map(
|
const output = EMOJIBASE.map(
|
||||||
(key) => {
|
(datum) => {
|
||||||
const datum = EMOJI_DATA[key];
|
|
||||||
const newDatum = {
|
const newDatum = {
|
||||||
name: datum.name,
|
name: datum.annotation,
|
||||||
shortname: datum.shortname,
|
shortname: `:${datum.shortcodes[0]}:`,
|
||||||
category: datum.category,
|
category: datum.group,
|
||||||
emoji_order: datum.emoji_order,
|
emoji_order: datum.order,
|
||||||
};
|
};
|
||||||
if (datum.aliases.length > 0) {
|
if (datum.shortcodes.length > 1) {
|
||||||
newDatum.aliases = datum.aliases;
|
newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`);
|
||||||
}
|
}
|
||||||
if (datum.aliases_ascii.length > 0) {
|
if (datum.emoticon) {
|
||||||
newDatum.aliases_ascii = datum.aliases_ascii;
|
newDatum.aliases_ascii = [ datum.emoticon ];
|
||||||
}
|
}
|
||||||
return newDatum;
|
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
|
// Write to a file in src. Changes should be checked into git. This file is copied by
|
||||||
// babel using --copy-files
|
// babel using --copy-files
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
import {ContentRepo} from 'matrix-js-sdk';
|
import {ContentRepo} from 'matrix-js-sdk';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||||
|
@ -58,4 +59,71 @@ module.exports = {
|
||||||
}
|
}
|
||||||
return require('../res/img/' + images[total % images.length] + '.png');
|
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, {
|
Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, {
|
||||||
title: _t('Could not connect to the integration server'),
|
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;
|
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, {
|
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
||||||
title: _t("Invite new community members"),
|
title: _t("Invite new community members"),
|
||||||
description: description,
|
description: description,
|
||||||
placeholder: _t("Name or matrix ID"),
|
placeholder: _t("Name or Matrix ID"),
|
||||||
button: _t("Invite to Community"),
|
button: _t("Invite to Community"),
|
||||||
validAddressTypes: ['mx-user-id'],
|
validAddressTypes: ['mx-user-id'],
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
|
|
125
src/HtmlUtils.js
125
src/HtmlUtils.js
|
@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix';
|
||||||
import _linkifyElement from 'linkifyjs/element';
|
import _linkifyElement from 'linkifyjs/element';
|
||||||
import _linkifyString from 'linkifyjs/string';
|
import _linkifyString from 'linkifyjs/string';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
import emojione from 'emojione';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
|
|
||||||
emojione.imagePathSVG = 'emojione/svg/';
|
linkifyMatrix(linkify);
|
||||||
// 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';
|
|
||||||
|
|
||||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
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
|
// BMP, so this includes the ranges from 'letterlike symbols' to
|
||||||
// 'miscellaneous symbols and arrows' which should catch all of them
|
// 'miscellaneous symbols and arrows' which should catch all of them
|
||||||
// (with plenty of false positives, but that's OK)
|
// (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
|
// Regex pattern for whitespace characters
|
||||||
const WHITESPACE_REGEX = new RegExp("\\s", "g");
|
const WHITESPACE_REGEX = new RegExp("\\s", "g");
|
||||||
|
|
||||||
// And this is emojione's complete regex
|
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
|
||||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Return true if the given string contains emoji
|
* 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
|
* positives, but useful for fast-path testing strings to see if they
|
||||||
* need emojification.
|
* need emojification.
|
||||||
* unicodeToImage uses this function.
|
* unicodeToImage uses this function.
|
||||||
|
@ -71,62 +67,27 @@ export function containsEmoji(str) {
|
||||||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(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) {
|
export function unicodeToShortcode(char) {
|
||||||
if (addAlt === undefined) addAlt = true;
|
const data = EMOJIBASE.find(e => e.unicode === char);
|
||||||
|
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given one or more unicode characters (represented by unicode
|
* Returns the unicode character for an emoji shortcode
|
||||||
* character number), return an image node with the corresponding
|
|
||||||
* emoji.
|
|
||||||
*
|
*
|
||||||
* @param alt {string} String to use for the image alt text
|
* @param {String} shortcode The shortcode (such as :thumbup:)
|
||||||
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
|
* @return {String} The emoji character; null if none exists
|
||||||
* @param unicode {integer} One or more integers representing unicode characters
|
|
||||||
* @returns A img node with the corresponding emoji
|
|
||||||
*/
|
*/
|
||||||
export function charactersToImageNode(alt, useSvg, ...unicode) {
|
export function shortcodeToUnicode(shortcode) {
|
||||||
const fileName = unicode.map((u) => {
|
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||||
return u.toString(16);
|
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
|
||||||
}).join('-');
|
return data ? data.unicode : null;
|
||||||
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
|
|
||||||
const fileType = useSvg ? 'svg' : 'png';
|
|
||||||
return <img
|
|
||||||
alt={alt}
|
|
||||||
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processHtmlForSending(html: string): string {
|
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.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.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.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
|
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||||
*/
|
*/
|
||||||
export function bodyToHtml(content, highlights, opts={}) {
|
export function bodyToHtml(content, highlights, opts={}) {
|
||||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||||
|
|
||||||
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
|
|
||||||
let bodyHasEmoji = false;
|
let bodyHasEmoji = false;
|
||||||
|
|
||||||
let sanitizeParams = sanitizeHtmlParams;
|
let sanitizeParams = sanitizeHtmlParams;
|
||||||
|
@ -470,28 +428,12 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
|
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
|
||||||
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
|
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
|
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||||
if (isHtmlMessage) {
|
if (isHtmlMessage) {
|
||||||
isDisplayedWithHtml = true;
|
isDisplayedWithHtml = true;
|
||||||
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
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 {
|
} finally {
|
||||||
delete sanitizeParams.textFilter;
|
delete sanitizeParams.textFilter;
|
||||||
|
@ -503,7 +445,6 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
|
|
||||||
let emojiBody = false;
|
let emojiBody = false;
|
||||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
|
||||||
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
||||||
|
|
||||||
// Ignore spaces in body text. Emojis with spaces in between should
|
// Ignore spaces in body text. Emojis with spaces in between should
|
||||||
|
@ -515,29 +456,25 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
// presented as large.
|
// presented as large.
|
||||||
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
|
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
|
||||||
|
|
||||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length
|
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
|
||||||
// Prevent user pills expanding for users with only emoji in
|
// Prevent user pills expanding for users with only emoji in
|
||||||
// their username
|
// 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({
|
const className = classNames({
|
||||||
'mx_EventTile_body': true,
|
'mx_EventTile_body': true,
|
||||||
'mx_EventTile_bigEmoji': emojiBody,
|
'mx_EventTile_bigEmoji': emojiBody,
|
||||||
'markdown-body': isHtmlMessage,
|
'markdown-body': isHtmlMessage && !emojiBody,
|
||||||
});
|
});
|
||||||
|
|
||||||
return isDisplayedWithHtml ?
|
return isDisplayedWithHtml ?
|
||||||
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
<span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||||
<span className={className} dir="auto">{ strippedBody }</span>;
|
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
|
||||||
}
|
|
||||||
|
|
||||||
export function emojifyText(text, addAlt) {
|
|
||||||
return {
|
|
||||||
__html: unicodeToImage(escape(text), addAlt),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -33,6 +33,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||||
import PlatformPeg from "./PlatformPeg";
|
import PlatformPeg from "./PlatformPeg";
|
||||||
import { sendLoginRequest } from "./Login";
|
import { sendLoginRequest } from "./Login";
|
||||||
import * as StorageManager from './utils/StorageManager';
|
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
|
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||||
|
@ -499,7 +500,9 @@ async function startMatrixClient() {
|
||||||
|
|
||||||
Notifier.start();
|
Notifier.start();
|
||||||
UserActivity.sharedInstance().start();
|
UserActivity.sharedInstance().start();
|
||||||
|
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||||
Presence.start();
|
Presence.start();
|
||||||
|
}
|
||||||
DMRoomMap.makeShared().start();
|
DMRoomMap.makeShared().start();
|
||||||
ActiveWidgetStore.start();
|
ActiveWidgetStore.start();
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ class MatrixClientPeg {
|
||||||
// try to initialise e2e on the new client
|
// try to initialise e2e on the new client
|
||||||
try {
|
try {
|
||||||
// check that we have a version of the js-sdk which includes initCrypto
|
// 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();
|
await this.matrixClient.initCrypto();
|
||||||
StorageManager.setCryptoInitialised(true);
|
StorageManager.setCryptoInitialised(true);
|
||||||
}
|
}
|
||||||
|
@ -188,8 +188,7 @@ class MatrixClientPeg {
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
||||||
verificationMethods: [verificationMethods.SAS],
|
verificationMethods: [verificationMethods.SAS],
|
||||||
unstableClientRelationAggregation: aggregateRelations,
|
unstableClientRelationAggregation: aggregateRelations || enableEdits,
|
||||||
unstableClientRelationReplacements: enableEdits,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.matrixClient = createMatrixClient(opts);
|
this.matrixClient = createMatrixClient(opts);
|
||||||
|
|
|
@ -85,7 +85,11 @@ const Notifier = {
|
||||||
msg = '';
|
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);
|
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
|
||||||
|
|
||||||
// if displayNotification returns non-null, the platform supports
|
// 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, {
|
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||||
title: _t('Start a chat'),
|
title: _t('Start a chat'),
|
||||||
description: _t("Who would you like to communicate with?"),
|
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'],
|
validAddressTypes: ['mx-user-id', 'email'],
|
||||||
button: _t("Start Chat"),
|
button: _t("Start Chat"),
|
||||||
onFinished: _onStartChatFinished,
|
onFinished: _onStartChatFinished,
|
||||||
|
@ -58,7 +58,7 @@ export function showRoomInviteDialog(roomId) {
|
||||||
title: _t('Invite new room members'),
|
title: _t('Invite new room members'),
|
||||||
description: _t('Who would you like to add to this room?'),
|
description: _t('Who would you like to add to this room?'),
|
||||||
button: _t('Send Invites'),
|
button: _t('Send Invites'),
|
||||||
placeholder: _t("Email, name or matrix ID"),
|
placeholder: _t("Email, name or Matrix ID"),
|
||||||
onFinished: (shouldInvite, addrs) => {
|
onFinished: (shouldInvite, addrs) => {
|
||||||
_onRoomInviteFinished(roomId, shouldInvite, addrs);
|
_onRoomInviteFinished(roomId, shouldInvite, addrs);
|
||||||
},
|
},
|
||||||
|
|
|
@ -518,7 +518,7 @@ export const CommandMap = {
|
||||||
unban: new Command({
|
unban: new Command({
|
||||||
name: 'unban',
|
name: 'unban',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: _td('Unbans user with given id'),
|
description: _td('Unbans user with given ID'),
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
|
|
@ -19,47 +19,31 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import sdk from '../index';
|
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import type {Completion, SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
import _uniq from 'lodash/uniq';
|
import _uniq from 'lodash/uniq';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||||
|
|
||||||
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
import EmojiData from '../stripped-emoji.json';
|
import EmojiData from '../stripped-emoji.json';
|
||||||
|
|
||||||
const LIMIT = 20;
|
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
|
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
|
||||||
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
|
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
|
||||||
// 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 + ')');
|
|
||||||
|
|
||||||
|
// 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(
|
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
||||||
(a, b) => {
|
(a, b) => {
|
||||||
if (a.category === b.category) {
|
if (a.category === b.category) {
|
||||||
return a.emoji_order - b.emoji_order;
|
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) => {
|
).map((a, index) => {
|
||||||
return {
|
return {
|
||||||
|
@ -101,26 +85,20 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
return []; // don't give any suggestions if the user doesn't want them
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
let matchedString = command[0];
|
const 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;
|
|
||||||
}
|
|
||||||
completions = this.matcher.match(matchedString);
|
completions = this.matcher.match(matchedString);
|
||||||
|
|
||||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||||
|
|
||||||
const sorters = [];
|
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));
|
sorters.push((c) => score(matchedString, c.shortname));
|
||||||
// If the matchedString is not empty, sort by length of shortname. Example:
|
// If the matchedString is not empty, sort by length of shortname. Example:
|
||||||
// matchedString = ":bookmark"
|
// matchedString = ":bookmark"
|
||||||
|
@ -134,11 +112,11 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
completions = completions.map((result) => {
|
completions = completions.map((result) => {
|
||||||
const { shortname } = result;
|
const { shortname } = result;
|
||||||
const unicode = shortnameToUnicode(shortname);
|
const unicode = shortcodeToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
|
<PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
|
|
@ -265,7 +265,7 @@ const RoleUserList = React.createClass({
|
||||||
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
||||||
title: _t('Add users to the community summary'),
|
title: _t('Add users to the community summary'),
|
||||||
description: _t("Who would you like to add to this 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"),
|
button: _t("Add to summary"),
|
||||||
validAddressTypes: ['mx-user-id'],
|
validAddressTypes: ['mx-user-id'],
|
||||||
groupId: this.props.groupId,
|
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.
|
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||||
const yRetention = 1.0;
|
const yRetention = 1.0;
|
||||||
|
|
||||||
if (Math.abs(e.deltaX) < xyThreshold) {
|
if (Math.abs(e.deltaX) <= xyThreshold) {
|
||||||
// noinspection JSSuspiciousNameCombination
|
// noinspection JSSuspiciousNameCombination
|
||||||
this._scrollElement.scrollLeft += e.deltaY * yRetention;
|
this._scrollElement.scrollLeft += e.deltaY * yRetention;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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,
|
inputs: PropTypes.object,
|
||||||
|
|
||||||
// As js-sdk interactive-auth
|
// As js-sdk interactive-auth
|
||||||
makeRegistrationUrl: PropTypes.func,
|
requestEmailToken: PropTypes.func,
|
||||||
sessionId: PropTypes.string,
|
sessionId: PropTypes.string,
|
||||||
clientSecret: PropTypes.string,
|
clientSecret: PropTypes.string,
|
||||||
emailSid: PropTypes.string,
|
emailSid: PropTypes.string,
|
||||||
|
@ -96,6 +97,7 @@ export default React.createClass({
|
||||||
sessionId: this.props.sessionId,
|
sessionId: this.props.sessionId,
|
||||||
clientSecret: this.props.clientSecret,
|
clientSecret: this.props.clientSecret,
|
||||||
emailSid: this.props.emailSid,
|
emailSid: this.props.emailSid,
|
||||||
|
requestEmailToken: this.props.requestEmailToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._authLogic.attemptAuth().then((result) => {
|
this._authLogic.attemptAuth().then((result) => {
|
||||||
|
@ -202,7 +204,6 @@ export default React.createClass({
|
||||||
stageState={this.state.stageState}
|
stageState={this.state.stageState}
|
||||||
fail={this._onAuthStageFailed}
|
fail={this._onAuthStageFailed}
|
||||||
setEmailSid={this._setEmailSid}
|
setEmailSid={this._setEmailSid}
|
||||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
|
||||||
showContinue={!this.props.continueIsManaged}
|
showContinue={!this.props.continueIsManaged}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import CallMediaHandler from '../../CallMediaHandler';
|
import CallMediaHandler from '../../CallMediaHandler';
|
||||||
|
import { fixupColorFonts } from '../../utils/FontManager';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import sessionStore from '../../stores/SessionStore';
|
import sessionStore from '../../stores/SessionStore';
|
||||||
|
@ -118,6 +119,8 @@ const LoggedInView = React.createClass({
|
||||||
this._matrixClient.on("accountData", this.onAccountData);
|
this._matrixClient.on("accountData", this.onAccountData);
|
||||||
this._matrixClient.on("sync", this.onSync);
|
this._matrixClient.on("sync", this.onSync);
|
||||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
|
||||||
|
fixupColorFonts();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -322,6 +325,18 @@ const LoggedInView = React.createClass({
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
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) {
|
if (handled) {
|
||||||
|
|
|
@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils";
|
||||||
const AutoDiscovery = Matrix.AutoDiscovery;
|
|
||||||
|
|
||||||
// Disable warnings for now: we use deprecated bluebird functions
|
// Disable warnings for now: we use deprecated bluebird functions
|
||||||
// and need to migrate, but they spam the console with warnings.
|
// and need to migrate, but they spam the console with warnings.
|
||||||
|
@ -109,6 +108,7 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
config: PropTypes.object,
|
config: PropTypes.object,
|
||||||
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig),
|
||||||
ConferenceHandler: PropTypes.any,
|
ConferenceHandler: PropTypes.any,
|
||||||
onNewScreen: PropTypes.func,
|
onNewScreen: PropTypes.func,
|
||||||
registrationUrl: PropTypes.string,
|
registrationUrl: PropTypes.string,
|
||||||
|
@ -181,16 +181,8 @@ export default React.createClass({
|
||||||
// Parameters used in the registration dance with the IS
|
// Parameters used in the registration dance with the IS
|
||||||
register_client_secret: null,
|
register_client_secret: null,
|
||||||
register_session_id: null,
|
register_session_id: null,
|
||||||
register_hs_url: null,
|
|
||||||
register_is_url: null,
|
|
||||||
register_id_sid: 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
|
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||||
// and disable it when there are no dialogs
|
// and disable it when there are no dialogs
|
||||||
hideToSRUsers: false,
|
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() {
|
getFallbackHsUrl: function() {
|
||||||
|
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
|
||||||
return this.props.config.fallback_hs_url;
|
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();
|
|
||||||
} else {
|
} else {
|
||||||
return this.getDefaultIsUrl();
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultIsUrl() {
|
getServerProperties() {
|
||||||
return this.state.defaultIsUrl || "https://vector.im";
|
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() {
|
componentWillMount: function() {
|
||||||
|
@ -260,40 +229,6 @@ export default React.createClass({
|
||||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
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
|
// a thing to call showScreen with once login completes. this is kept
|
||||||
// outside this.state because updating it should never trigger a
|
// outside this.state because updating it should never trigger a
|
||||||
// rerender.
|
// rerender.
|
||||||
|
@ -374,8 +309,8 @@ export default React.createClass({
|
||||||
return Lifecycle.loadSession({
|
return Lifecycle.loadSession({
|
||||||
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
fragmentQueryParams: this.props.startingFragmentQueryParams,
|
||||||
enableGuest: this.props.enableGuest,
|
enableGuest: this.props.enableGuest,
|
||||||
guestHsUrl: this.getCurrentHsUrl(),
|
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
|
||||||
guestIsUrl: this.getCurrentIsUrl(),
|
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
});
|
});
|
||||||
}).then((loadedSession) => {
|
}).then((loadedSession) => {
|
||||||
|
@ -1827,44 +1762,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerConfigChange(config) {
|
onServerConfigChange(config) {
|
||||||
const newState = {};
|
this.setState({serverConfig: config});
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_makeRegistrationUrl: function(params) {
|
_makeRegistrationUrl: function(params) {
|
||||||
|
@ -1883,8 +1781,7 @@ export default React.createClass({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.view === VIEWS.LOADING ||
|
this.state.view === VIEWS.LOADING ||
|
||||||
this.state.view === VIEWS.LOGGING_IN ||
|
this.state.view === VIEWS.LOGGING_IN
|
||||||
this.state.loadingDefaultHomeserver
|
|
||||||
) {
|
) {
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
return (
|
return (
|
||||||
|
@ -1962,17 +1859,12 @@ export default React.createClass({
|
||||||
sessionId={this.state.register_session_id}
|
sessionId={this.state.register_session_id}
|
||||||
idSid={this.state.register_id_sid}
|
idSid={this.state.register_id_sid}
|
||||||
email={this.props.startingFragmentQueryParams.email}
|
email={this.props.startingFragmentQueryParams.email}
|
||||||
defaultServerName={this.getDefaultServerName()}
|
|
||||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
|
||||||
defaultIsUrl={this.getDefaultIsUrl()}
|
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
|
||||||
makeRegistrationUrl={this._makeRegistrationUrl}
|
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||||
onLoggedIn={this.onRegistered}
|
onLoggedIn={this.onRegistered}
|
||||||
onLoginClick={this.onLoginClick}
|
onLoginClick={this.onLoginClick}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
|
{...this.getServerProperties()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1982,14 +1874,11 @@ export default React.createClass({
|
||||||
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
|
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
|
||||||
return (
|
return (
|
||||||
<ForgotPassword
|
<ForgotPassword
|
||||||
defaultServerName={this.getDefaultServerName()}
|
|
||||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
|
||||||
defaultIsUrl={this.getDefaultIsUrl()}
|
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
|
||||||
onComplete={this.onLoginClick}
|
onComplete={this.onLoginClick}
|
||||||
onLoginClick={this.onLoginClick} />
|
onLoginClick={this.onLoginClick}
|
||||||
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
|
{...this.getServerProperties()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1999,16 +1888,11 @@ export default React.createClass({
|
||||||
<Login
|
<Login
|
||||||
onLoggedIn={Lifecycle.setLoggedIn}
|
onLoggedIn={Lifecycle.setLoggedIn}
|
||||||
onRegisterClick={this.onRegisterClick}
|
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()}
|
fallbackHsUrl={this.getFallbackHsUrl()}
|
||||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
|
{...this.getServerProperties()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../DateUtils';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
import SettingsStore from '../../settings/SettingsStore';
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||||
|
@ -95,6 +96,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// helper function to access relations for an event
|
// helper function to access relations for an event
|
||||||
getRelationsForEvent: PropTypes.func,
|
getRelationsForEvent: PropTypes.func,
|
||||||
|
|
||||||
|
// whether to show reactions for an event
|
||||||
|
showReactions: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
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.
|
/* check the scroll state and send out pagination requests if necessary.
|
||||||
*/
|
*/
|
||||||
checkFillState: function() {
|
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)
|
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');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
return false; // no tile = no show
|
return false; // no tile = no show
|
||||||
|
@ -450,14 +465,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
const MessageEditor = sdk.getComponent('elements.MessageEditor');
|
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const ret = [];
|
const ret = [];
|
||||||
|
|
||||||
if (this.props.editEvent && this.props.editEvent.getId() === mxEv.getId()) {
|
const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId();
|
||||||
return [<MessageEditor key={mxEv.getId()} event={mxEv} />];
|
|
||||||
}
|
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
let continuation = false;
|
let continuation = false;
|
||||||
|
|
||||||
|
@ -527,18 +538,20 @@ module.exports = React.createClass({
|
||||||
continuation={continuation}
|
continuation={continuation}
|
||||||
isRedacted={mxEv.isRedacted()}
|
isRedacted={mxEv.isRedacted()}
|
||||||
replacingEventId={mxEv.replacingEventId()}
|
replacingEventId={mxEv.replacingEventId()}
|
||||||
|
isEditing={isEditing}
|
||||||
onHeightChanged={this._onHeightChanged}
|
onHeightChanged={this._onHeightChanged}
|
||||||
readReceipts={readReceipts}
|
readReceipts={readReceipts}
|
||||||
readReceiptMap={this._readReceiptMap}
|
readReceiptMap={this._readReceiptMap}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
checkUnmounting={this._isUnmounting}
|
checkUnmounting={this._isUnmounting}
|
||||||
eventSendStatus={mxEv.status}
|
eventSendStatus={mxEv.replacementOrOwnStatus()}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
last={last}
|
last={last}
|
||||||
isSelectedEvent={highlight}
|
isSelectedEvent={highlight}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
showReactions={this.props.showReactions}
|
||||||
/>
|
/>
|
||||||
</li>,
|
</li>,
|
||||||
);
|
);
|
||||||
|
@ -714,7 +727,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
let whoIsTyping;
|
let whoIsTyping;
|
||||||
if (this.props.room) {
|
if (this.props.room && !this.props.tileShape) {
|
||||||
whoIsTyping = (<WhoIsTypingTile
|
whoIsTyping = (<WhoIsTypingTile
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
onShown={this._onTypingShown}
|
onShown={this._onTypingShown}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React = require('react');
|
||||||
const ReactDOM = require("react-dom");
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
const Matrix = require("matrix-js-sdk");
|
|
||||||
const sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
const dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the global notification list using a TimelinePanel
|
* Component which shows the global notification list using a TimelinePanel
|
||||||
|
|
|
@ -304,8 +304,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// return suitable content for the main (text) part of the status bar.
|
// return suitable content for the main (text) part of the status bar.
|
||||||
_getContent: function() {
|
_getContent: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
|
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this._shouldShowConnectionError()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { Group } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import RoomTile from "../views/rooms/RoomTile";
|
import RoomTile from "../views/rooms/RoomTile";
|
||||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||||
|
import {_t} from "../../languageHandler";
|
||||||
|
|
||||||
// turn this on for drop & drag console debugging galore
|
// turn this on for drop & drag console debugging galore
|
||||||
const debug = false;
|
const debug = false;
|
||||||
|
@ -42,6 +43,7 @@ const RoomSubList = React.createClass({
|
||||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
tagName: PropTypes.string,
|
tagName: PropTypes.string,
|
||||||
|
addRoomLabel: PropTypes.string,
|
||||||
|
|
||||||
order: PropTypes.string.isRequired,
|
order: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
@ -232,7 +234,11 @@ const RoomSubList = React.createClass({
|
||||||
let addRoomButton;
|
let addRoomButton;
|
||||||
if (this.props.onAddRoom) {
|
if (this.props.onAddRoom) {
|
||||||
addRoomButton = (
|
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}
|
membersLoaded={this.state.membersLoaded}
|
||||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
showReactions={true}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
let topUnreadMessagesBar = null;
|
let topUnreadMessagesBar = null;
|
||||||
|
|
|
@ -106,6 +106,9 @@ const TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// placeholder text to use if the timeline is empty
|
// placeholder text to use if the timeline is empty
|
||||||
empty: PropTypes.string,
|
empty: PropTypes.string,
|
||||||
|
|
||||||
|
// whether to show reactions for an event
|
||||||
|
showReactions: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -204,11 +207,11 @@ const TimelinePanel = React.createClass({
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
|
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
|
||||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
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.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||||
|
MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
|
||||||
MatrixClientPeg.get().on("sync", this.onSync);
|
MatrixClientPeg.get().on("sync", this.onSync);
|
||||||
|
|
||||||
this._initTimeline(this.props);
|
this._initTimeline(this.props);
|
||||||
|
@ -283,11 +286,11 @@ const TimelinePanel = React.createClass({
|
||||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
||||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||||
client.removeListener("Room.replaceEvent", this.onRoomReplaceEvent);
|
|
||||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
client.removeListener("Room.accountData", this.onAccountData);
|
client.removeListener("Room.accountData", this.onAccountData);
|
||||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||||
|
client.removeListener("Event.replaced", this.onEventReplaced);
|
||||||
client.removeListener("sync", this.onSync);
|
client.removeListener("sync", this.onSync);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -405,7 +408,13 @@ const TimelinePanel = React.createClass({
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
if (payload.action === "edit_event") {
|
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();
|
this.forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReplaceEvent: function(replacedEvent, room) {
|
onEventReplaced: function(replacedEvent, room) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
// ignore events for other rooms
|
// ignore events for other rooms
|
||||||
|
@ -553,6 +562,9 @@ const TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onEventDecrypted: function(ev) {
|
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
|
// 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
|
// haven't yet been decrypted. The event will have just been updated
|
||||||
// in place so we just need to re-render.
|
// in place so we just need to re-render.
|
||||||
|
@ -601,6 +613,8 @@ const TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
sendReadReceipt: function() {
|
sendReadReceipt: function() {
|
||||||
|
if (SettingsStore.getValue("lowBandwidth")) return;
|
||||||
|
|
||||||
if (!this.refs.messagePanel) return;
|
if (!this.refs.messagePanel) return;
|
||||||
if (!this.props.manageReadReceipts) return;
|
if (!this.props.manageReadReceipts) return;
|
||||||
// This happens on user_activity_end which is delayed, and it's
|
// This happens on user_activity_end which is delayed, and it's
|
||||||
|
@ -1261,6 +1275,7 @@ const TimelinePanel = React.createClass({
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
getRelationsForEvent={this.getRelationsForEvent}
|
getRelationsForEvent={this.getRelationsForEvent}
|
||||||
editEvent={this.state.editEvent}
|
editEvent={this.state.editEvent}
|
||||||
|
showReactions={this.props.showReactions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import Avatar from '../../Avatar';
|
import Avatar from '../../Avatar';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
import dis from "../../dispatcher";
|
||||||
|
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||||
|
|
||||||
const AVATAR_SIZE = 28;
|
const AVATAR_SIZE = 28;
|
||||||
|
|
||||||
|
@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component {
|
||||||
super();
|
super();
|
||||||
this.state = {
|
this.state = {
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
|
menuFunctions: null, // should be { close: fn }
|
||||||
profileInfo: null,
|
profileInfo: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
this._dispatcherRef = dis.register(this.onAction);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const profileInfo = await this._getProfileInfo();
|
const profileInfo = await this._getProfileInfo();
|
||||||
this.setState({profileInfo});
|
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() {
|
_getDisplayName() {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return _t("Guest");
|
return _t("Guest");
|
||||||
|
@ -88,7 +105,13 @@ export default class TopLeftMenuButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<BaseAvatar
|
||||||
idName={MatrixClientPeg.get().getUserId()}
|
idName={MatrixClientPeg.get().getUserId()}
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -98,7 +121,7 @@ export default class TopLeftMenuButton extends React.Component {
|
||||||
resizeMethod="crop"
|
resizeMethod="crop"
|
||||||
/>
|
/>
|
||||||
{ nameElement }
|
{ nameElement }
|
||||||
<span className="mx_TopLeftMenuButton_chevron"></span>
|
<span className="mx_TopLeftMenuButton_chevron" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -107,20 +130,26 @@ export default class TopLeftMenuButton extends React.Component {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.state.menuDisplayed && this.state.menuFunctions) {
|
||||||
|
this.state.menuFunctions.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const elementRect = e.currentTarget.getBoundingClientRect();
|
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||||
const x = elementRect.left;
|
const x = elementRect.left;
|
||||||
const y = elementRect.top + elementRect.height;
|
const y = elementRect.top + elementRect.height;
|
||||||
|
|
||||||
ContextualMenu.createMenu(TopLeftMenu, {
|
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
|
||||||
chevronFace: "none",
|
chevronFace: "none",
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
userId: MatrixClientPeg.get().getUserId(),
|
userId: MatrixClientPeg.get().getUserId(),
|
||||||
displayName: this._getDisplayName(),
|
displayName: this._getDisplayName(),
|
||||||
|
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
|
||||||
onFinished: () => {
|
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 sdk from '../../../index';
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
|
||||||
import PasswordReset from "../../../PasswordReset";
|
import PasswordReset from "../../../PasswordReset";
|
||||||
|
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
|
||||||
// Phases
|
// Phases
|
||||||
// Show controls to configure server details
|
// Show controls to configure server details
|
||||||
|
@ -40,28 +40,14 @@ module.exports = React.createClass({
|
||||||
displayName: 'ForgotPassword',
|
displayName: 'ForgotPassword',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// The default server name to use when the user hasn't specified
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
onServerConfigChange: PropTypes.func.isRequired,
|
||||||
// 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,
|
|
||||||
|
|
||||||
onLoginClick: PropTypes.func,
|
onLoginClick: PropTypes.func,
|
||||||
onComplete: PropTypes.func.isRequired,
|
onComplete: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
|
||||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
|
||||||
phase: PHASE_FORGOT,
|
phase: PHASE_FORGOT,
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
@ -70,11 +56,11 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
submitPasswordReset: function(email, password) {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_SENDING_EMAIL,
|
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.reset.resetPassword(email, password).done(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_EMAIL_SENT,
|
phase: PHASE_EMAIL_SENT,
|
||||||
|
@ -103,13 +89,6 @@ module.exports = React.createClass({
|
||||||
onSubmitForm: function(ev) {
|
onSubmitForm: function(ev) {
|
||||||
ev.preventDefault();
|
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) {
|
if (!this.state.email) {
|
||||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||||
} else if (!this.state.password || !this.state.password2) {
|
} else if (!this.state.password || !this.state.password2) {
|
||||||
|
@ -132,10 +111,7 @@ module.exports = React.createClass({
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.submitPasswordReset(
|
this.submitPasswordReset(this.state.email, this.state.password);
|
||||||
this.state.enteredHsUrl, this.state.enteredIsUrl,
|
|
||||||
this.state.email, this.state.password,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -148,19 +124,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerConfigChange: function(config) {
|
async onServerDetailsNextPhaseClick() {
|
||||||
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();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_FORGOT,
|
phase: PHASE_FORGOT,
|
||||||
});
|
});
|
||||||
|
@ -190,26 +154,19 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
renderServerDetails() {
|
renderServerDetails() {
|
||||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
||||||
|
|
||||||
if (SdkConfig.get()['disable_custom_urls']) {
|
if (SdkConfig.get()['disable_custom_urls']) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <ServerConfig
|
||||||
<ServerConfig
|
serverConfig={this.props.serverConfig}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
delayTimeMs={0}
|
||||||
customHsUrl={this.state.enteredHsUrl}
|
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||||
customIsUrl={this.state.enteredIsUrl}
|
submitText={_t("Next")}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
submitClass="mx_Login_submit"
|
||||||
delayTimeMs={0} />
|
/>;
|
||||||
<AccessibleButton className="mx_Login_submit"
|
|
||||||
onClick={this.onServerDetailsNextPhaseClick}
|
|
||||||
>
|
|
||||||
{_t("Next")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderForgot() {
|
renderForgot() {
|
||||||
|
@ -221,25 +178,22 @@ module.exports = React.createClass({
|
||||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let yourMatrixAccountText = _t('Your Matrix account');
|
let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) {
|
serverName: this.props.serverConfig.hsName,
|
||||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
|
||||||
serverName: this.props.defaultServerName,
|
|
||||||
});
|
});
|
||||||
} else {
|
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||||
try {
|
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||||
const parsedHsUrl = new URL(this.state.enteredHsUrl);
|
|
||||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
|
||||||
serverName: parsedHsUrl.hostname,
|
'underlinedServerName': () => {
|
||||||
|
return <TextWithTooltip
|
||||||
|
class="mx_Login_underlinedServerName"
|
||||||
|
tooltip={this.props.serverConfig.hsUrl}
|
||||||
|
>
|
||||||
|
{this.props.serverConfig.hsName}
|
||||||
|
</TextWithTooltip>;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} 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.
|
// If custom URLs are allowed, wire up the server details edit link.
|
||||||
|
|
|
@ -25,7 +25,7 @@ import sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login from '../../../Login';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||||
import { AutoDiscovery } from "matrix-js-sdk";
|
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
|
||||||
// For validating phone numbers without country codes
|
// For validating phone numbers without country codes
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||||
|
@ -59,19 +59,14 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onLoggedIn: PropTypes.func.isRequired,
|
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
|
// An error passed along from higher up explaining that something
|
||||||
// went wrong when finding the defaultHsUrl.
|
// went wrong. May be replaced with a different error within the
|
||||||
defaultServerDiscoveryError: PropTypes.string,
|
// 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
|
// Secondary HS which we try to log into if the user is using
|
||||||
// the default HS but login fails. Useful for migrating to a
|
// the default HS but login fails. Useful for migrating to a
|
||||||
// different homeserver without confusing users.
|
// different homeserver without confusing users.
|
||||||
|
@ -79,12 +74,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
defaultDeviceDisplayName: PropTypes.string,
|
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,
|
onRegisterClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// login shouldn't care how password recovery is done.
|
|
||||||
onForgotPasswordClick: PropTypes.func,
|
onForgotPasswordClick: PropTypes.func,
|
||||||
onServerConfigChange: PropTypes.func.isRequired,
|
onServerConfigChange: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -93,9 +89,6 @@ module.exports = React.createClass({
|
||||||
errorText: null,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
|
|
||||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
|
||||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
|
||||||
|
|
||||||
// used for preserving form values when changing homeserver
|
// used for preserving form values when changing homeserver
|
||||||
username: "",
|
username: "",
|
||||||
phoneCountry: null,
|
phoneCountry: null,
|
||||||
|
@ -105,10 +98,6 @@ module.exports = React.createClass({
|
||||||
phase: PHASE_LOGIN,
|
phase: PHASE_LOGIN,
|
||||||
// The current login flow, such as password, SSO, etc.
|
// The current login flow, such as password, SSO, etc.
|
||||||
currentFlow: "m.login.password",
|
currentFlow: "m.login.password",
|
||||||
|
|
||||||
// .well-known discovery
|
|
||||||
discoveryError: "",
|
|
||||||
findingHomeserver: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -132,6 +121,14 @@ module.exports = React.createClass({
|
||||||
this._unmounted = true;
|
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) {
|
onPasswordLoginError: function(errorText) {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText,
|
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) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
// Prevent people from submitting their password when homeserver
|
// Prevent people from submitting their password when something isn't right.
|
||||||
// discovery went wrong
|
if (this.isBusy() || this.hasError()) return;
|
||||||
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -164,7 +168,7 @@ module.exports = React.createClass({
|
||||||
const usingEmail = username.indexOf("@") > 0;
|
const usingEmail = username.indexOf("@") > 0;
|
||||||
if (error.httpStatus === 400 && usingEmail) {
|
if (error.httpStatus === 400 && usingEmail) {
|
||||||
errorText = _t('This homeserver does not support login using email address.');
|
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(
|
const errorTop = messageForResourceLimitError(
|
||||||
error.data.limit_type,
|
error.data.limit_type,
|
||||||
error.data.admin_contact, {
|
error.data.admin_contact, {
|
||||||
|
@ -194,11 +198,10 @@ module.exports = React.createClass({
|
||||||
<div>
|
<div>
|
||||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||||
<div className="mx_Login_smallError">
|
<div className="mx_Login_smallError">
|
||||||
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
|
{_t(
|
||||||
{
|
'Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
|
{hs: this.props.serverConfig.hsName},
|
||||||
})
|
)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -232,21 +235,26 @@ module.exports = React.createClass({
|
||||||
this.setState({ username: username });
|
this.setState({ username: username });
|
||||||
},
|
},
|
||||||
|
|
||||||
onUsernameBlur: function(username) {
|
onUsernameBlur: async function(username) {
|
||||||
|
const doWellknownLookup = username[0] === "@";
|
||||||
this.setState({
|
this.setState({
|
||||||
username: username,
|
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(':');
|
const serverName = username.split(':').slice(1).join(':');
|
||||||
try {
|
try {
|
||||||
// we have to append 'https://' to make the URL constructor happy
|
const result = await AutoDiscoveryUtils.validateServerName(serverName);
|
||||||
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
|
this.props.onServerConfigChange(result);
|
||||||
const url = new URL("https://" + serverName);
|
|
||||||
this._tryWellKnownDiscovery(url.hostname);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", 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) {
|
onRegisterClick: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.props.onRegisterClick();
|
this.props.onRegisterClick();
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerDetailsNextPhaseClick(ev) {
|
async onServerDetailsNextPhaseClick() {
|
||||||
ev.stopPropagation();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_LOGIN,
|
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) {
|
_initLoginLogic: function(hsUrl, isUrl) {
|
||||||
const self = this;
|
const self = this;
|
||||||
hsUrl = hsUrl || this.state.enteredHsUrl;
|
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
|
||||||
isUrl = isUrl || this.state.enteredIsUrl;
|
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, {
|
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
|
@ -378,8 +316,6 @@ module.exports = React.createClass({
|
||||||
this._loginLogic = loginLogic;
|
this._loginLogic = loginLogic;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
enteredHsUrl: hsUrl,
|
|
||||||
enteredIsUrl: isUrl,
|
|
||||||
busy: true,
|
busy: true,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
@ -445,8 +381,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (err.cors === 'rejected') {
|
if (err.cors === 'rejected') {
|
||||||
if (window.location.protocol === 'https:' &&
|
if (window.location.protocol === 'https:' &&
|
||||||
(this.state.enteredHsUrl.startsWith("http:") ||
|
(this.props.serverConfig.hsUrl.startsWith("http:") ||
|
||||||
!this.state.enteredHsUrl.startsWith("http"))
|
!this.props.serverConfig.hsUrl.startsWith("http"))
|
||||||
) {
|
) {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
{ _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.", {},
|
"is not blocking requests.", {},
|
||||||
{
|
{
|
||||||
'a': (sub) => {
|
'a': (sub) => {
|
||||||
return <a target="_blank" rel="noopener"
|
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
|
||||||
href={this.state.enteredHsUrl}
|
{ sub }
|
||||||
>{ sub }</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
|
@ -484,7 +420,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
renderServerComponent() {
|
renderServerComponent() {
|
||||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
||||||
|
|
||||||
if (SdkConfig.get()['disable_custom_urls']) {
|
if (SdkConfig.get()['disable_custom_urls']) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -494,28 +429,19 @@ module.exports = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverDetails = <ServerConfig
|
const serverDetailsProps = {};
|
||||||
customHsUrl={this.state.enteredHsUrl}
|
|
||||||
customIsUrl={this.state.enteredIsUrl}
|
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
|
||||||
delayTimeMs={250}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
let nextButton = null;
|
|
||||||
if (PHASES_ENABLED) {
|
if (PHASES_ENABLED) {
|
||||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||||
onClick={this.onServerDetailsNextPhaseClick}
|
serverDetailsProps.submitText = _t("Next");
|
||||||
>
|
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||||
{_t("Next")}
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <ServerConfig
|
||||||
{serverDetails}
|
serverConfig={this.props.serverConfig}
|
||||||
{nextButton}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
</div>;
|
delayTimeMs={250}
|
||||||
|
{...serverDetailsProps}
|
||||||
|
/>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLoginComponentForStep() {
|
renderLoginComponentForStep() {
|
||||||
|
@ -547,13 +473,6 @@ module.exports = React.createClass({
|
||||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
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 (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
|
@ -569,9 +488,8 @@ module.exports = React.createClass({
|
||||||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||||
loginIncorrect={this.state.loginIncorrect}
|
loginIncorrect={this.state.loginIncorrect}
|
||||||
hsName={hsName}
|
serverConfig={this.props.serverConfig}
|
||||||
hsUrl={this.state.enteredHsUrl}
|
disableSubmit={this.isBusy()}
|
||||||
disableSubmit={this.state.findingHomeserver}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -595,9 +513,9 @@ module.exports = React.createClass({
|
||||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
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;
|
let errorTextSection;
|
||||||
if (errorText) {
|
if (errorText) {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018, 2019 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||||
|
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
|
||||||
// Phases
|
// Phases
|
||||||
// Show controls to configure server details
|
// Show controls to configure server details
|
||||||
|
@ -46,18 +46,7 @@ module.exports = React.createClass({
|
||||||
sessionId: PropTypes.string,
|
sessionId: PropTypes.string,
|
||||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||||
idSid: PropTypes.string,
|
idSid: PropTypes.string,
|
||||||
// The default server name to use when the user hasn't specified
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
// 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,
|
|
||||||
brand: PropTypes.string,
|
brand: PropTypes.string,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
|
@ -66,7 +55,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
|
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
@ -87,8 +76,6 @@ module.exports = React.createClass({
|
||||||
// straight back into UI auth
|
// straight back into UI auth
|
||||||
doingUIAuth: Boolean(this.props.sessionId),
|
doingUIAuth: Boolean(this.props.sessionId),
|
||||||
serverType,
|
serverType,
|
||||||
hsUrl: this.props.customHsUrl,
|
|
||||||
isUrl: this.props.customIsUrl,
|
|
||||||
// Phase of the overall registration dialog.
|
// Phase of the overall registration dialog.
|
||||||
phase: PHASE_REGISTRATION,
|
phase: PHASE_REGISTRATION,
|
||||||
flows: null,
|
flows: null,
|
||||||
|
@ -100,18 +87,22 @@ module.exports = React.createClass({
|
||||||
this._replaceClient();
|
this._replaceClient();
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerConfigChange: function(config) {
|
componentWillReceiveProps(newProps) {
|
||||||
const newState = {};
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
if (config.hsUrl !== undefined) {
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
newState.hsUrl = config.hsUrl;
|
|
||||||
}
|
this._replaceClient(newProps.serverConfig);
|
||||||
if (config.isUrl !== undefined) {
|
|
||||||
newState.isUrl = config.isUrl;
|
// Handle cases where the user enters "https://matrix.org" for their server
|
||||||
}
|
// from the advanced option - we should default to FREE at that point.
|
||||||
this.props.onServerConfigChange(config);
|
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
|
||||||
this.setState(newState, () => {
|
if (serverType !== this.state.serverType) {
|
||||||
this._replaceClient();
|
// Reset the phase to default phase for the server type.
|
||||||
|
this.setState({
|
||||||
|
serverType,
|
||||||
|
phase: this.getDefaultPhaseForServerType(serverType),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultPhaseForServerType(type) {
|
getDefaultPhaseForServerType(type) {
|
||||||
|
@ -136,19 +127,17 @@ module.exports = React.createClass({
|
||||||
// the new type.
|
// the new type.
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ServerType.FREE: {
|
case ServerType.FREE: {
|
||||||
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
|
const { serverConfig } = ServerType.TYPES.FREE;
|
||||||
this.onServerConfigChange({
|
this.props.onServerConfigChange(serverConfig);
|
||||||
hsUrl,
|
|
||||||
isUrl,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ServerType.PREMIUM:
|
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:
|
case ServerType.ADVANCED:
|
||||||
this.onServerConfigChange({
|
// Use the default config from the config
|
||||||
hsUrl: this.props.defaultHsUrl,
|
this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
|
||||||
isUrl: this.props.defaultIsUrl,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,13 +147,15 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_replaceClient: async function() {
|
_replaceClient: async function(serverConfig) {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: null,
|
errorText: null,
|
||||||
});
|
});
|
||||||
|
if (!serverConfig) serverConfig = this.props.serverConfig;
|
||||||
|
const {hsUrl, isUrl} = serverConfig;
|
||||||
this._matrixClient = Matrix.createClient({
|
this._matrixClient = Matrix.createClient({
|
||||||
baseUrl: this.state.hsUrl,
|
baseUrl: hsUrl,
|
||||||
idBaseUrl: this.state.isUrl,
|
idBaseUrl: isUrl,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await this._makeRegisterRequest({});
|
await this._makeRegisterRequest({});
|
||||||
|
@ -189,12 +180,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onFormSubmit: function(formVals) {
|
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({
|
this.setState({
|
||||||
errorText: "",
|
errorText: "",
|
||||||
busy: true,
|
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) {
|
_onUIAuthFinished: async function(success, response, extra) {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
let msg = response.message || response.toString();
|
let msg = response.message || response.toString();
|
||||||
// can we give a better error message?
|
// can we give a better error message?
|
||||||
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||||
const errorTop = messageForResourceLimitError(
|
const errorTop = messageForResourceLimitError(
|
||||||
response.data.limit_type,
|
response.data.limit_type,
|
||||||
response.data.admin_contact, {
|
response.data.admin_contact, {
|
||||||
|
@ -302,8 +301,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerDetailsNextPhaseClick(ev) {
|
async onServerDetailsNextPhaseClick() {
|
||||||
ev.stopPropagation();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_REGISTRATION,
|
phase: PHASE_REGISTRATION,
|
||||||
});
|
});
|
||||||
|
@ -348,7 +346,6 @@ module.exports = React.createClass({
|
||||||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
||||||
|
|
||||||
if (SdkConfig.get()['disable_custom_urls']) {
|
if (SdkConfig.get()['disable_custom_urls']) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -365,47 +362,41 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverDetailsProps = {};
|
||||||
|
if (PHASES_ENABLED) {
|
||||||
|
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||||
|
serverDetailsProps.submitText = _t("Next");
|
||||||
|
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||||
|
}
|
||||||
|
|
||||||
let serverDetails = null;
|
let serverDetails = null;
|
||||||
switch (this.state.serverType) {
|
switch (this.state.serverType) {
|
||||||
case ServerType.FREE:
|
case ServerType.FREE:
|
||||||
break;
|
break;
|
||||||
case ServerType.PREMIUM:
|
case ServerType.PREMIUM:
|
||||||
serverDetails = <ModularServerConfig
|
serverDetails = <ModularServerConfig
|
||||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
serverConfig={this.props.serverConfig}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
|
||||||
delayTimeMs={250}
|
delayTimeMs={250}
|
||||||
|
{...serverDetailsProps}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
case ServerType.ADVANCED:
|
case ServerType.ADVANCED:
|
||||||
serverDetails = <ServerConfig
|
serverDetails = <ServerConfig
|
||||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
serverConfig={this.props.serverConfig}
|
||||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
|
||||||
delayTimeMs={250}
|
delayTimeMs={250}
|
||||||
|
{...serverDetailsProps}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextButton = null;
|
|
||||||
if (PHASES_ENABLED) {
|
|
||||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
|
||||||
onClick={this.onServerDetailsNextPhaseClick}
|
|
||||||
>
|
|
||||||
{_t("Next")}
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<ServerTypeSelector
|
<ServerTypeSelector
|
||||||
selected={this.state.serverType}
|
selected={this.state.serverType}
|
||||||
onChange={this.onServerTypeChange}
|
onChange={this.onServerTypeChange}
|
||||||
/>
|
/>
|
||||||
{serverDetails}
|
{serverDetails}
|
||||||
{nextButton}
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -424,7 +415,7 @@ module.exports = React.createClass({
|
||||||
makeRequest={this._makeRegisterRequest}
|
makeRequest={this._makeRegisterRequest}
|
||||||
onAuthFinished={this._onUIAuthFinished}
|
onAuthFinished={this._onUIAuthFinished}
|
||||||
inputs={this._getUIAuthInputs()}
|
inputs={this._getUIAuthInputs()}
|
||||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
requestEmailToken={this._requestEmailToken}
|
||||||
sessionId={this.props.sessionId}
|
sessionId={this.props.sessionId}
|
||||||
clientSecret={this.props.clientSecret}
|
clientSecret={this.props.clientSecret}
|
||||||
emailSid={this.props.idSid}
|
emailSid={this.props.idSid}
|
||||||
|
@ -446,13 +437,6 @@ module.exports = React.createClass({
|
||||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
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
|
return <RegistrationForm
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
|
@ -462,8 +446,7 @@ module.exports = React.createClass({
|
||||||
onRegisterClick={this.onFormSubmit}
|
onRegisterClick={this.onFormSubmit}
|
||||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||||
flows={this.state.flows}
|
flows={this.state.flows}
|
||||||
hsName={hsName}
|
serverConfig={this.props.serverConfig}
|
||||||
hsUrl={this.state.hsUrl}
|
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -474,7 +457,7 @@ module.exports = React.createClass({
|
||||||
const AuthPage = sdk.getComponent('auth.AuthPage');
|
const AuthPage = sdk.getComponent('auth.AuthPage');
|
||||||
|
|
||||||
let errorText;
|
let errorText;
|
||||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
const err = this.state.errorText;
|
||||||
if (err) {
|
if (err) {
|
||||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
* 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
|
* setEmailSid: m.login.email.identity only: a function to be called with the
|
||||||
* email sid after a token is requested.
|
* 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):
|
* Each component may also provide the following functions (beyond the standard React ones):
|
||||||
* focus: set the input focus appropriately in the form.
|
* focus: set the input focus appropriately in the form.
|
||||||
|
@ -365,7 +365,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
||||||
stageState: PropTypes.object.isRequired,
|
stageState: PropTypes.object.isRequired,
|
||||||
fail: PropTypes.func.isRequired,
|
fail: PropTypes.func.isRequired,
|
||||||
setEmailSid: PropTypes.func.isRequired,
|
setEmailSid: PropTypes.func.isRequired,
|
||||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
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() {
|
render: function() {
|
||||||
if (this.state.requestingToken) {
|
if (this.state.requestingToken) {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
|
@ -18,9 +18,15 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
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';
|
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.
|
* Configure the Modular server name.
|
||||||
*
|
*
|
||||||
|
@ -31,65 +37,107 @@ export default class ModularServerConfig extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onServerConfigChange: PropTypes.func,
|
onServerConfigChange: PropTypes.func,
|
||||||
|
|
||||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
// The current configuration that the user is expecting to change.
|
||||||
// they are used if the user has not overridden them with a custom URL.
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
// 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,
|
|
||||||
|
|
||||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
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 = {
|
static defaultProps = {
|
||||||
onServerConfigChange: function() {},
|
onServerConfigChange: function() {},
|
||||||
customHsUrl: "",
|
customHsUrl: "",
|
||||||
delayTimeMs: 0,
|
delayTimeMs: 0,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hsUrl: props.customHsUrl,
|
busy: false,
|
||||||
|
errorText: "",
|
||||||
|
hsUrl: props.serverConfig.hsUrl,
|
||||||
|
isUrl: props.serverConfig.isUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
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({
|
this.setState({
|
||||||
hsUrl: newProps.customHsUrl,
|
hsUrl,
|
||||||
|
isUrl,
|
||||||
|
busy: true,
|
||||||
|
errorText: "",
|
||||||
});
|
});
|
||||||
this.props.onServerConfigChange({
|
|
||||||
hsUrl: newProps.customHsUrl,
|
try {
|
||||||
isUrl: this.props.defaultIsUrl,
|
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) => {
|
onHomeserverBlur = (ev) => {
|
||||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||||
this.props.onServerConfigChange({
|
this.validateServer();
|
||||||
hsUrl: this.state.hsUrl,
|
|
||||||
isUrl: this.props.defaultIsUrl,
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
onHomeserverChange = (ev) => {
|
onHomeserverChange = (ev) => {
|
||||||
const hsUrl = ev.target.value;
|
const hsUrl = ev.target.value;
|
||||||
this.setState({ hsUrl });
|
this.setState({ hsUrl });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = async (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
await this.validateServer();
|
||||||
|
|
||||||
|
if (this.props.onAfterSubmit) {
|
||||||
|
this.props.onAfterSubmit();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_waitThenInvoke(existingTimeoutId, fn) {
|
_waitThenInvoke(existingTimeoutId, fn) {
|
||||||
if (existingTimeoutId) {
|
if (existingTimeoutId) {
|
||||||
|
@ -100,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Field = sdk.getComponent('elements.Field');
|
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 (
|
return (
|
||||||
<div className="mx_ServerConfig">
|
<div className="mx_ServerConfig">
|
||||||
|
@ -113,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent {
|
||||||
</a>,
|
</a>,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
|
||||||
<div className="mx_ServerConfig_fields">
|
<div className="mx_ServerConfig_fields">
|
||||||
<Field id="mx_ServerConfig_hsUrl"
|
<Field id="mx_ServerConfig_hsUrl"
|
||||||
label={_t("Server Name")}
|
label={_t("Server Name")}
|
||||||
placeholder={this.props.defaultHsUrl}
|
placeholder={this.props.serverConfig.hsUrl}
|
||||||
value={this.state.hsUrl}
|
value={this.state.hsUrl}
|
||||||
onBlur={this.onHomeserverBlur}
|
onBlur={this.onHomeserverBlur}
|
||||||
onChange={this.onHomeserverChange}
|
onChange={this.onHomeserverChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{submitButton}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2019 New Vector Ltd.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pure UI component which displays a username/password form.
|
* 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 = {
|
static defaultProps = {
|
||||||
onError: function() {},
|
onError: function() {},
|
||||||
onEditServerDetailsClick: null,
|
onEditServerDetailsClick: null,
|
||||||
|
@ -40,13 +59,12 @@ class PasswordLogin extends React.Component {
|
||||||
initialPhoneNumber: "",
|
initialPhoneNumber: "",
|
||||||
initialPassword: "",
|
initialPassword: "",
|
||||||
loginIncorrect: false,
|
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,
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -193,10 +211,7 @@ class PasswordLogin extends React.Component {
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
key="username_input"
|
key="username_input"
|
||||||
type="text"
|
type="text"
|
||||||
label={SdkConfig.get().disable_custom_urls ?
|
label={_t("Username")}
|
||||||
_t("Username on %(hs)s", {
|
|
||||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
|
||||||
}) : _t("Username")}
|
|
||||||
value={this.state.username}
|
value={this.state.username}
|
||||||
onChange={this.onUsernameChanged}
|
onChange={this.onUsernameChanged}
|
||||||
onBlur={this.onUsernameBlur}
|
onBlur={this.onUsernameBlur}
|
||||||
|
@ -258,20 +273,22 @@ class PasswordLogin extends React.Component {
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signInToText = _t('Sign in to your Matrix account');
|
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||||
if (this.props.hsName) {
|
serverName: this.props.serverConfig.hsName,
|
||||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
|
||||||
serverName: this.props.hsName,
|
|
||||||
});
|
});
|
||||||
} else {
|
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||||
try {
|
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
|
||||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
|
||||||
serverName: parsedHsUrl.hostname,
|
'underlinedServerName': () => {
|
||||||
|
return <TextWithTooltip
|
||||||
|
class="mx_Login_underlinedServerName"
|
||||||
|
tooltip={this.props.serverConfig.hsUrl}
|
||||||
|
>
|
||||||
|
{this.props.serverConfig.hsName}
|
||||||
|
</TextWithTooltip>;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let editLink = null;
|
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 SdkConfig from '../../../SdkConfig';
|
||||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||||
import withValidation from '../elements/Validation';
|
import withValidation from '../elements/Validation';
|
||||||
|
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
|
@ -51,11 +52,7 @@ module.exports = React.createClass({
|
||||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||||
onEditServerDetailsClick: PropTypes.func,
|
onEditServerDetailsClick: PropTypes.func,
|
||||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
// This is optional and only set if we used a server name to determine
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
// 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,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -515,20 +512,22 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let yourMatrixAccountText = _t('Create your Matrix account');
|
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||||
if (this.props.hsName) {
|
serverName: this.props.serverConfig.hsName,
|
||||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
|
||||||
serverName: this.props.hsName,
|
|
||||||
});
|
});
|
||||||
} else {
|
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||||
try {
|
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
|
||||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
|
||||||
serverName: parsedHsUrl.hostname,
|
'underlinedServerName': () => {
|
||||||
|
return <TextWithTooltip
|
||||||
|
class="mx_Login_underlinedServerName"
|
||||||
|
tooltip={this.props.serverConfig.hsUrl}
|
||||||
|
>
|
||||||
|
{this.props.serverConfig.hsName}
|
||||||
|
</TextWithTooltip>;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let editLink = null;
|
let editLink = null;
|
||||||
|
|
|
@ -20,6 +20,9 @@ import PropTypes from 'prop-types';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
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.
|
* 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 {
|
export default class ServerConfig extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onServerConfigChange: PropTypes.func,
|
onServerConfigChange: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
// The current configuration that the user is expecting to change.
|
||||||
// they are used if the user has not overridden them with a custom URL.
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
// 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,
|
|
||||||
|
|
||||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
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 = {
|
static defaultProps = {
|
||||||
onServerConfigChange: function() {},
|
onServerConfigChange: function() {},
|
||||||
customHsUrl: "",
|
|
||||||
customIsUrl: "",
|
|
||||||
delayTimeMs: 0,
|
delayTimeMs: 0,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hsUrl: props.customHsUrl,
|
busy: false,
|
||||||
isUrl: props.customIsUrl,
|
errorText: "",
|
||||||
|
hsUrl: props.serverConfig.hsUrl,
|
||||||
|
isUrl: props.serverConfig.isUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
if (newProps.customHsUrl === this.state.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||||
newProps.customIsUrl === this.state.isUrl) return;
|
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({
|
this.setState({
|
||||||
hsUrl: newProps.customHsUrl,
|
hsUrl,
|
||||||
isUrl: newProps.customIsUrl,
|
isUrl,
|
||||||
|
busy: true,
|
||||||
|
errorText: "",
|
||||||
});
|
});
|
||||||
this.props.onServerConfigChange({
|
|
||||||
hsUrl: newProps.customHsUrl,
|
try {
|
||||||
isUrl: newProps.customIsUrl,
|
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) => {
|
onHomeserverBlur = (ev) => {
|
||||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||||
this.props.onServerConfigChange({
|
this.validateServer();
|
||||||
hsUrl: this.state.hsUrl,
|
|
||||||
isUrl: this.state.isUrl,
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
onHomeserverChange = (ev) => {
|
onHomeserverChange = (ev) => {
|
||||||
const hsUrl = ev.target.value;
|
const hsUrl = ev.target.value;
|
||||||
this.setState({ hsUrl });
|
this.setState({ hsUrl });
|
||||||
}
|
};
|
||||||
|
|
||||||
onIdentityServerBlur = (ev) => {
|
onIdentityServerBlur = (ev) => {
|
||||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||||
this.props.onServerConfigChange({
|
this.validateServer();
|
||||||
hsUrl: this.state.hsUrl,
|
|
||||||
isUrl: this.state.isUrl,
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
onIdentityServerChange = (ev) => {
|
onIdentityServerChange = (ev) => {
|
||||||
const isUrl = ev.target.value;
|
const isUrl = ev.target.value;
|
||||||
this.setState({ isUrl });
|
this.setState({ isUrl });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = async (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
await this.validateServer();
|
||||||
|
|
||||||
|
if (this.props.onAfterSubmit) {
|
||||||
|
this.props.onAfterSubmit();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_waitThenInvoke(existingTimeoutId, fn) {
|
_waitThenInvoke(existingTimeoutId, fn) {
|
||||||
if (existingTimeoutId) {
|
if (existingTimeoutId) {
|
||||||
|
@ -114,10 +154,24 @@ export default class ServerConfig extends React.PureComponent {
|
||||||
showHelpPopup = () => {
|
showHelpPopup = () => {
|
||||||
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
||||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Field = sdk.getComponent('elements.Field');
|
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 (
|
return (
|
||||||
<div className="mx_ServerConfig">
|
<div className="mx_ServerConfig">
|
||||||
|
@ -127,22 +181,28 @@ export default class ServerConfig extends React.PureComponent {
|
||||||
{ sub }
|
{ sub }
|
||||||
</a>,
|
</a>,
|
||||||
})}
|
})}
|
||||||
|
{errorText}
|
||||||
|
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
|
||||||
<div className="mx_ServerConfig_fields">
|
<div className="mx_ServerConfig_fields">
|
||||||
<Field id="mx_ServerConfig_hsUrl"
|
<Field id="mx_ServerConfig_hsUrl"
|
||||||
label={_t("Homeserver URL")}
|
label={_t("Homeserver URL")}
|
||||||
placeholder={this.props.defaultHsUrl}
|
placeholder={this.props.serverConfig.hsUrl}
|
||||||
value={this.state.hsUrl}
|
value={this.state.hsUrl}
|
||||||
onBlur={this.onHomeserverBlur}
|
onBlur={this.onHomeserverBlur}
|
||||||
onChange={this.onHomeserverChange}
|
onChange={this.onHomeserverChange}
|
||||||
|
disabled={this.state.busy}
|
||||||
/>
|
/>
|
||||||
<Field id="mx_ServerConfig_isUrl"
|
<Field id="mx_ServerConfig_isUrl"
|
||||||
label={_t("Identity Server URL")}
|
label={_t("Identity Server URL")}
|
||||||
placeholder={this.props.defaultIsUrl}
|
placeholder={this.props.serverConfig.isUrl}
|
||||||
value={this.state.isUrl}
|
value={this.state.isUrl}
|
||||||
onBlur={this.onIdentityServerBlur}
|
onBlur={this.onIdentityServerBlur}
|
||||||
onChange={this.onIdentityServerChange}
|
onChange={this.onIdentityServerChange}
|
||||||
|
disabled={this.state.busy}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{submitButton}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import classnames from 'classnames';
|
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';
|
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'),
|
label: () => _t('Free'),
|
||||||
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
||||||
description: () => _t('Join millions for free on the largest public server'),
|
description: () => _t('Join millions for free on the largest public server'),
|
||||||
hsUrl: 'https://matrix.org',
|
serverConfig: makeType(ValidatedServerConfig, {
|
||||||
isUrl: 'https://vector.im',
|
hsUrl: "https://matrix.org",
|
||||||
|
hsName: "matrix.org",
|
||||||
|
hsNameIsDifferent: false,
|
||||||
|
isUrl: "https://vector.im",
|
||||||
|
identityEnabled: true,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
PREMIUM: {
|
PREMIUM: {
|
||||||
id: PREMIUM,
|
id: PREMIUM,
|
||||||
|
@ -44,6 +51,7 @@ export const TYPES = {
|
||||||
{sub}
|
{sub}
|
||||||
</a>,
|
</a>,
|
||||||
}),
|
}),
|
||||||
|
identityServerUrl: "https://vector.im",
|
||||||
},
|
},
|
||||||
ADVANCED: {
|
ADVANCED: {
|
||||||
id: ADVANCED,
|
id: ADVANCED,
|
||||||
|
@ -56,10 +64,11 @@ export const TYPES = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getTypeFromHsUrl(hsUrl) {
|
export function getTypeFromServerConfig(config) {
|
||||||
|
const {hsUrl} = config;
|
||||||
if (!hsUrl) {
|
if (!hsUrl) {
|
||||||
return null;
|
return null;
|
||||||
} else if (hsUrl === TYPES.FREE.hsUrl) {
|
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
|
||||||
return FREE;
|
return FREE;
|
||||||
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
||||||
// This is an unlikely case to reach, as Modular defaults to hiding the
|
// 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,
|
selected: PropTypes.string,
|
||||||
// Handler called when the selected type changes.
|
// Handler called when the selected type changes.
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const type = e.currentTarget.dataset.id;
|
const type = e.currentTarget.dataset.id;
|
||||||
this.updateSelectedType(type);
|
this.updateSelectedType(type);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import AvatarLogic from '../../../Avatar';
|
import AvatarLogic from '../../../Avatar';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -104,10 +105,14 @@ module.exports = React.createClass({
|
||||||
// work out the full set of urls to try to load. This is formed like so:
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
// imageUrls: [ props.url, props.urls, default image ]
|
// imageUrls: [ props.url, props.urls, default image ]
|
||||||
|
|
||||||
const urls = props.urls || [];
|
let urls = [];
|
||||||
|
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||||
|
urls = props.urls || [];
|
||||||
|
|
||||||
if (props.url) {
|
if (props.url) {
|
||||||
urls.unshift(props.url); // put in urls[0]
|
urls.unshift(props.url); // put in urls[0]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let defaultImageUrl = null;
|
let defaultImageUrl = null;
|
||||||
if (props.defaultToInitialLetter) {
|
if (props.defaultToInitialLetter) {
|
||||||
|
@ -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() {
|
render: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -176,20 +148,20 @@ module.exports = React.createClass({
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (imageUrl === this.state.defaultImageUrl) {
|
if (imageUrl === this.state.defaultImageUrl) {
|
||||||
const initialLetter = this._getInitialLetter(name);
|
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||||
const textNode = (
|
const textNode = (
|
||||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||||
style={{ fontSize: (width * 0.65) + "px",
|
style={{ fontSize: (width * 0.65) + "px",
|
||||||
width: width + "px",
|
width: width + "px",
|
||||||
lineHeight: height + "px" }}
|
lineHeight: height + "px" }}
|
||||||
>
|
>
|
||||||
{ initialLetter }
|
{ initialLetter }
|
||||||
</EmojiText>
|
</span>
|
||||||
);
|
);
|
||||||
const imgNode = (
|
const imgNode = (
|
||||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||||
alt="" title={title} onError={this.onError}
|
alt="" title={title} onError={this.onError}
|
||||||
width={width} height={height} />
|
width={width} height={height} aria-hidden="true" />
|
||||||
);
|
);
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
|
||||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from "../../../index";
|
import sdk from "../../../index";
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import Avatar from '../../../Avatar';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomAvatar',
|
displayName: 'RoomAvatar',
|
||||||
|
@ -89,7 +89,6 @@ module.exports = React.createClass({
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
), // highest priority
|
), // highest priority
|
||||||
this.getRoomAvatarUrl(props),
|
this.getRoomAvatarUrl(props),
|
||||||
this.getOneToOneAvatar(props), // lowest priority
|
|
||||||
].filter(function(url) {
|
].filter(function(url) {
|
||||||
return (url != null && url != "");
|
return (url != null && url != "");
|
||||||
});
|
});
|
||||||
|
@ -98,41 +97,14 @@ module.exports = React.createClass({
|
||||||
getRoomAvatarUrl: function(props) {
|
getRoomAvatarUrl: function(props) {
|
||||||
if (!props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
return props.room.getAvatarUrl(
|
return Avatar.avatarUrlForRoom(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
props.room,
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
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() {
|
onRoomAvatarClick: function() {
|
||||||
const avatarUrl = this.props.room.getAvatarUrl(
|
const avatarUrl = this.props.room.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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,
|
displayName: PropTypes.string.isRequired,
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
onFinished: PropTypes.func,
|
onFinished: PropTypes.func,
|
||||||
|
|
||||||
|
// Optional function to collect a reference to the container
|
||||||
|
// of this component directly.
|
||||||
|
containerRef: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -61,44 +66,48 @@ export class TopLeftMenu extends React.Component {
|
||||||
{_t(
|
{_t(
|
||||||
"<a>Upgrade</a> to your own domain", {},
|
"<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='' />
|
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let homePageSection = null;
|
let homePageItem = null;
|
||||||
if (this.hasHomePage()) {
|
if (this.hasHomePage()) {
|
||||||
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
homePageItem = <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage} tabIndex={0}>
|
||||||
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
|
{_t("Home")}
|
||||||
</ul>;
|
</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signInOutSection;
|
let signInOutItem;
|
||||||
if (isGuest) {
|
if (isGuest) {
|
||||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
signInOutItem = <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn} tabIndex={0}>
|
||||||
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
|
{_t("Sign in")}
|
||||||
</ul>;
|
</li>;
|
||||||
} else {
|
} else {
|
||||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
signInOutItem = <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut} tabIndex={0}>
|
||||||
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
|
{_t("Sign out")}
|
||||||
</ul>;
|
</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_TopLeftMenu">
|
const settingsItem = <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings} tabIndex={0}>
|
||||||
<div className="mx_TopLeftMenu_section_noIcon">
|
{_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>{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}
|
{hostingSignup}
|
||||||
</div>
|
</div>
|
||||||
{homePageSection}
|
|
||||||
<ul className="mx_TopLeftMenu_section_withIcon">
|
<ul className="mx_TopLeftMenu_section_withIcon">
|
||||||
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
|
{homePageItem}
|
||||||
|
{settingsItem}
|
||||||
|
{signInOutItem}
|
||||||
</ul>
|
</ul>
|
||||||
{signInOutSection}
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor {
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<div className="mx_DevTools_content">
|
<div className="mx_DevTools_content">
|
||||||
|
<div className="mx_DevTools_eventTypeStateKeyGroup">
|
||||||
{ this.textInput('eventType', _t('Event Type')) }
|
{ this.textInput('eventType', _t('Event Type')) }
|
||||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||||
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component {
|
||||||
top: y,
|
top: y,
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
}, false);
|
}, false);
|
||||||
e.target.onmouseleave = close;
|
// Drop a reference to this close handler for componentWillUnmount
|
||||||
|
this.closeCopiedTooltip = e.target.onmouseleave = close;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLinkSpecificEventCheckboxClick() {
|
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() {
|
render() {
|
||||||
let title;
|
let title;
|
||||||
let matrixToUrl;
|
let matrixToUrl;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
|
||||||
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
||||||
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
||||||
import sdk from "../../../index";
|
import sdk from "../../../index";
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
|
||||||
export default class UserSettingsDialog extends React.Component {
|
export default class UserSettingsDialog extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -67,7 +68,7 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
"mx_UserSettingsDialog_securityIcon",
|
"mx_UserSettingsDialog_securityIcon",
|
||||||
<SecurityUserSettingsTab />,
|
<SecurityUserSettingsTab />,
|
||||||
));
|
));
|
||||||
if (SettingsStore.getLabsFeatures().length > 0) {
|
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
_td("Labs"),
|
_td("Labs"),
|
||||||
"mx_UserSettingsDialog_labsIcon",
|
"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.tabIndex = restProps.tabIndex || "0";
|
||||||
restProps.role = "button";
|
restProps.role = "button";
|
||||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||||
|
@ -89,6 +93,7 @@ export default function AccessibleButton(props) {
|
||||||
*/
|
*/
|
||||||
AccessibleButton.propTypes = {
|
AccessibleButton.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
inputRef: PropTypes.func,
|
||||||
element: PropTypes.string,
|
element: PropTypes.string,
|
||||||
onClick: PropTypes.func.isRequired,
|
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" />
|
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
|
||||||
<div className="mx_ImageView_labelWrapper">
|
<div className="mx_ImageView_labelWrapper">
|
||||||
<div className="mx_ImageView_label">
|
<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" />
|
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
|
||||||
</AccessibleButton>
|
</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" />
|
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
|
||||||
</AccessibleButton>
|
</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') } />
|
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<div className="mx_ImageView_shim">
|
<div className="mx_ImageView_shim">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
const MemberAvatar = require('../avatars/MemberAvatar.js');
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MemberEventListSummary',
|
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 });
|
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
||||||
});
|
});
|
||||||
|
@ -114,13 +117,9 @@ module.exports = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||||
<EmojiText>
|
|
||||||
{ summaries.join(", ") }
|
{ summaries.join(", ") }
|
||||||
</EmojiText>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -132,7 +131,7 @@ module.exports = React.createClass({
|
||||||
* included before "and [n] others".
|
* included before "and [n] others".
|
||||||
*/
|
*/
|
||||||
_renderNameList: function(users) {
|
_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;
|
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) {
|
_renderAvatars: function(roomMembers) {
|
||||||
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
|
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 EditorModel from '../../../editor/model';
|
||||||
import {setCaretPosition} from '../../../editor/caret';
|
import {setCaretPosition} from '../../../editor/caret';
|
||||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
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 {parseEvent} from '../../../editor/deserialize';
|
||||||
import Autocomplete from '../rooms/Autocomplete';
|
import Autocomplete from '../rooms/Autocomplete';
|
||||||
import {PartCreator} from '../../../editor/parts';
|
import {PartCreator} from '../../../editor/parts';
|
||||||
import {renderModel} from '../../../editor/render';
|
import {renderModel} from '../../../editor/render';
|
||||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default class MessageEditor extends React.Component {
|
export default class MessageEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -40,22 +43,28 @@ export default class MessageEditor extends React.Component {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
const room = this._getRoom();
|
||||||
const partCreator = new PartCreator(
|
const partCreator = new PartCreator(
|
||||||
() => this._autocompleteRef,
|
() => this._autocompleteRef,
|
||||||
query => this.setState({query}),
|
query => this.setState({query}),
|
||||||
|
room,
|
||||||
);
|
);
|
||||||
this.model = new EditorModel(
|
this.model = new EditorModel(
|
||||||
parseEvent(this.props.event),
|
parseEvent(this.props.event, room),
|
||||||
partCreator,
|
partCreator,
|
||||||
this._updateEditorState,
|
this._updateEditorState,
|
||||||
);
|
);
|
||||||
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
|
||||||
this.state = {
|
this.state = {
|
||||||
autoComplete: null,
|
autoComplete: null,
|
||||||
room,
|
room,
|
||||||
};
|
};
|
||||||
this._editorRef = null;
|
this._editorRef = null;
|
||||||
this._autocompleteRef = null;
|
this._autocompleteRef = null;
|
||||||
|
this._hasModifications = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getRoom() {
|
||||||
|
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateEditorState = (caret) => {
|
_updateEditorState = (caret) => {
|
||||||
|
@ -71,18 +80,34 @@ export default class MessageEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onInput = (event) => {
|
_onInput = (event) => {
|
||||||
|
this._hasModifications = true;
|
||||||
const sel = document.getSelection();
|
const sel = document.getSelection();
|
||||||
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||||
this.model.update(text, event.inputType, caret);
|
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) => {
|
_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) {
|
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.model.autoComplete) {
|
if (this.model.autoComplete) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
const autoComplete = this.model.autoComplete;
|
const autoComplete = this.model.autoComplete;
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
|
@ -99,20 +124,55 @@ export default class MessageEditor extends React.Component {
|
||||||
return; // don't preventDefault on anything else
|
return; // don't preventDefault on anything else
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancelClicked = () => {
|
_cancelEdit = () => {
|
||||||
dis.dispatch({action: "edit_event", event: null});
|
dis.dispatch({action: "edit_event", event: null});
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSaveClicked = () => {
|
_sendEdit = () => {
|
||||||
const newContent = {
|
const newContent = {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
"body": textSerialize(this.model),
|
"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.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({
|
const content = Object.assign({
|
||||||
"m.new_content": newContent,
|
"m.new_content": newContent,
|
||||||
|
@ -120,12 +180,13 @@ export default class MessageEditor extends React.Component {
|
||||||
"rel_type": "m.replace",
|
"rel_type": "m.replace",
|
||||||
"event_id": this.props.event.getId(),
|
"event_id": this.props.event.getId(),
|
||||||
},
|
},
|
||||||
}, newContent);
|
}, contentBody);
|
||||||
|
|
||||||
const roomId = this.props.event.getRoomId();
|
const roomId = this.props.event.getRoomId();
|
||||||
this.context.matrixClient.sendMessage(roomId, content);
|
this.context.matrixClient.sendMessage(roomId, content);
|
||||||
|
|
||||||
dis.dispatch({action: "edit_event", event: null});
|
dis.dispatch({action: "edit_event", event: null});
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAutoCompleteConfirm = (completion) => {
|
_onAutoCompleteConfirm = (completion) => {
|
||||||
|
@ -138,6 +199,8 @@ export default class MessageEditor extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._updateEditorState();
|
this._updateEditorState();
|
||||||
|
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
|
||||||
|
this._editorRef.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -157,7 +220,7 @@ export default class MessageEditor extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
return <div className="mx_MessageEditor">
|
return <div className={classNames("mx_MessageEditor", this.props.className)}>
|
||||||
{ autoComplete }
|
{ autoComplete }
|
||||||
<div
|
<div
|
||||||
className="mx_MessageEditor_editor"
|
className="mx_MessageEditor_editor"
|
||||||
|
@ -166,10 +229,11 @@ export default class MessageEditor extends React.Component {
|
||||||
onInput={this._onInput}
|
onInput={this._onInput}
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this._onKeyDown}
|
||||||
ref={ref => this._editorRef = ref}
|
ref={ref => this._editorRef = ref}
|
||||||
|
aria-label={_t("Edit message")}
|
||||||
></div>
|
></div>
|
||||||
<div className="mx_MessageEditor_buttons">
|
<div className="mx_MessageEditor_buttons">
|
||||||
<AccessibleButton kind="secondary" onClick={this._onCancelClicked}>{_t("Cancel")}</AccessibleButton>
|
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
|
||||||
<AccessibleButton kind="primary" onClick={this._onSaveClicked}>{_t("Save")}</AccessibleButton>
|
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,11 @@ export default class ReplyThread extends React.Component {
|
||||||
static getParentEventId(ev) {
|
static getParentEventId(ev) {
|
||||||
if (!ev || ev.isRedacted()) return;
|
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'];
|
const mRelatesTo = ev.getWireContent()['m.relates_to'];
|
||||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||||
const mInReplyTo = 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;
|
let offset = 0;
|
||||||
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
||||||
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
|
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.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||||
style.left = 6 + parentBox.right + window.pageXOffset;
|
style.left = 6 + parentBox.right + window.pageXOffset;
|
||||||
|
|
|
@ -117,7 +117,6 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
|
|
||||||
const groupName = this.props.group.name || this.props.group.groupId;
|
const groupName = this.props.group.name || this.props.group.groupId;
|
||||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
const httpAvatarUrl = this.props.group.avatarUrl ?
|
||||||
|
@ -129,9 +128,9 @@ export default React.createClass({
|
||||||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
'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 }
|
{ groupName }
|
||||||
</EmojiText>;
|
</div>;
|
||||||
|
|
||||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
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
|
this.props.groupMember.displayname || this.props.groupMember.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberInfo">
|
<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" />
|
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
<EmojiText element="h2">{ groupMemberName }</EmojiText>
|
<h2>{ groupMemberName }</h2>
|
||||||
|
|
||||||
<div className="mx_MemberInfo_profile">
|
<div className="mx_MemberInfo_profile">
|
||||||
<div className="mx_MemberInfo_profileField">
|
<div className="mx_MemberInfo_profileField">
|
||||||
|
|
|
@ -149,7 +149,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
|
@ -221,7 +220,7 @@ module.exports = React.createClass({
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
|
|
||||||
<EmojiText element="h2">{ groupRoomName }</EmojiText>
|
<h2>{ groupRoomName }</h2>
|
||||||
|
|
||||||
<div className="mx_MemberInfo_profile">
|
<div className="mx_MemberInfo_profile">
|
||||||
<div className="mx_MemberInfo_profileField">
|
<div className="mx_MemberInfo_profileField">
|
||||||
|
|
|
@ -145,6 +145,7 @@ function remoteRender(event) {
|
||||||
a.target = data.target;
|
a.target = data.target;
|
||||||
a.download = data.download;
|
a.download = data.download;
|
||||||
a.style = data.style;
|
a.style = data.style;
|
||||||
|
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
|
||||||
a.href = window.URL.createObjectURL(data.blob);
|
a.href = window.URL.createObjectURL(data.blob);
|
||||||
a.appendChild(img);
|
a.appendChild(img);
|
||||||
a.appendChild(document.createTextNode(data.textContent));
|
a.appendChild(document.createTextNode(data.textContent));
|
||||||
|
|
|
@ -172,8 +172,8 @@ export default class MImageBody extends React.Component {
|
||||||
// thumbnail resolution will be unnecessarily reduced.
|
// thumbnail resolution will be unnecessarily reduced.
|
||||||
// custom timeline widths seems preferable.
|
// custom timeline widths seems preferable.
|
||||||
const pixelRatio = window.devicePixelRatio;
|
const pixelRatio = window.devicePixelRatio;
|
||||||
const thumbWidth = 800 * pixelRatio;
|
const thumbWidth = Math.round(800 * pixelRatio);
|
||||||
const thumbHeight = 600 * pixelRatio;
|
const thumbHeight = Math.round(600 * pixelRatio);
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import dis from '../../../dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { createMenu } from '../../structures/ContextualMenu';
|
import { createMenu } from '../../structures/ContextualMenu';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||||
|
|
||||||
export default class MessageActionBar extends React.PureComponent {
|
export default class MessageActionBar extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -148,13 +148,13 @@ export default class MessageActionBar extends React.PureComponent {
|
||||||
title={_t("Reply")}
|
title={_t("Reply")}
|
||||||
onClick={this.onReplyClick}
|
onClick={this.onReplyClick}
|
||||||
/>;
|
/>;
|
||||||
if (this.isEditingEnabled()) {
|
}
|
||||||
|
if (this.isEditingEnabled() && canEditContent(this.props.mxEvent)) {
|
||||||
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
||||||
title={_t("Edit")}
|
title={_t("Edit")}
|
||||||
onClick={this.onEditClick}
|
onClick={this.onEditClick}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="mx_MessageActionBar">
|
return <div className="mx_MessageActionBar">
|
||||||
{agreeDimensionReactionButtons}
|
{agreeDimensionReactionButtons}
|
||||||
|
|
|
@ -90,6 +90,7 @@ module.exports = React.createClass({
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
maxImageHeight={this.props.maxImageHeight}
|
maxImageHeight={this.props.maxImageHeight}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
|
isEditing={this.props.isEditing}
|
||||||
onHeightChanged={this.props.onHeightChanged} />;
|
onHeightChanged={this.props.onHeightChanged} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default class ReactionDimension extends React.PureComponent {
|
||||||
|
|
||||||
if (props.reactions) {
|
if (props.reactions) {
|
||||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
|
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +45,7 @@ export default class ReactionDimension extends React.PureComponent {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.reactions !== this.props.reactions) {
|
if (prevProps.reactions !== this.props.reactions) {
|
||||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
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.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
this.onReactionsChange();
|
this.onReactionsChange();
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,10 @@ export default class ReactionDimension extends React.PureComponent {
|
||||||
"Relations.add",
|
"Relations.add",
|
||||||
this.onReactionsChange,
|
this.onReactionsChange,
|
||||||
);
|
);
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.remove",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
this.props.reactions.removeListener(
|
this.props.reactions.removeListener(
|
||||||
"Relations.redaction",
|
"Relations.redaction",
|
||||||
this.onReactionsChange,
|
this.onReactionsChange,
|
||||||
|
@ -82,7 +88,7 @@ export default class ReactionDimension extends React.PureComponent {
|
||||||
if (mxEvent.isRedacted()) {
|
if (mxEvent.isRedacted()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return mxEvent.getContent()["m.relates_to"].key === option;
|
return mxEvent.getRelation().key === option;
|
||||||
});
|
});
|
||||||
if (!reactionForOption) {
|
if (!reactionForOption) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -107,7 +113,11 @@ export default class ReactionDimension extends React.PureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const userId = MatrixClientPeg.get().getUserId();
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
return reactions.getAnnotationsBySender()[userId];
|
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||||
|
if (!myReactions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [...myReactions.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
onOptionClick = (ev) => {
|
onOptionClick = (ev) => {
|
||||||
|
@ -158,6 +168,7 @@ export default class ReactionDimension extends React.PureComponent {
|
||||||
|
|
||||||
return <span className="mx_ReactionDimension"
|
return <span className="mx_ReactionDimension"
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
|
aria-hidden={true}
|
||||||
>
|
>
|
||||||
{items}
|
{items}
|
||||||
</span>;
|
</span>;
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
|
|
||||||
if (props.reactions) {
|
if (props.reactions) {
|
||||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
|
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.reactions !== this.props.reactions) {
|
if (prevProps.reactions !== this.props.reactions) {
|
||||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
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.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
this.onReactionsChange();
|
this.onReactionsChange();
|
||||||
}
|
}
|
||||||
|
@ -56,6 +58,10 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
"Relations.add",
|
"Relations.add",
|
||||||
this.onReactionsChange,
|
this.onReactionsChange,
|
||||||
);
|
);
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.remove",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
this.props.reactions.removeListener(
|
this.props.reactions.removeListener(
|
||||||
"Relations.redaction",
|
"Relations.redaction",
|
||||||
this.onReactionsChange,
|
this.onReactionsChange,
|
||||||
|
@ -80,7 +86,11 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const userId = MatrixClientPeg.get().getUserId();
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
return reactions.getAnnotationsBySender()[userId];
|
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||||
|
if (!myReactions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [...myReactions.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -101,13 +111,13 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
if (mxEvent.isRedacted()) {
|
if (mxEvent.isRedacted()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return mxEvent.getContent()["m.relates_to"].key === content;
|
return mxEvent.getRelation().key === content;
|
||||||
});
|
});
|
||||||
return <ReactionsRowButton
|
return <ReactionsRowButton
|
||||||
key={content}
|
key={content}
|
||||||
content={content}
|
content={content}
|
||||||
count={count}
|
|
||||||
mxEvent={mxEvent}
|
mxEvent={mxEvent}
|
||||||
|
reactionEvents={events}
|
||||||
myReactionEvent={myReactionEvent}
|
myReactionEvent={myReactionEvent}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,17 +19,28 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
export default class ReactionsRowButton extends React.PureComponent {
|
export default class ReactionsRowButton extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// The event we're displaying reactions for
|
// The event we're displaying reactions for
|
||||||
mxEvent: PropTypes.object.isRequired,
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
// The reaction content / key / emoji
|
||||||
content: PropTypes.string.isRequired,
|
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
|
// A possible Matrix event if the current user has voted for this type
|
||||||
myReactionEvent: PropTypes.object,
|
myReactionEvent: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
tooltipVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onClick = (ev) => {
|
onClick = (ev) => {
|
||||||
const { mxEvent, myReactionEvent, content } = this.props;
|
const { mxEvent, myReactionEvent, content } = this.props;
|
||||||
if (myReactionEvent) {
|
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() {
|
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({
|
const classes = classNames({
|
||||||
mx_ReactionsRowButton: true,
|
mx_ReactionsRowButton: true,
|
||||||
mx_ReactionsRowButton_selected: !!myReactionEvent,
|
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}
|
return <span className={classes}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseOut={this.onMouseOut}
|
||||||
>
|
>
|
||||||
{content} {count}
|
{content} {count}
|
||||||
|
{tooltip}
|
||||||
</span>;
|
</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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
|
||||||
import Flair from '../elements/Flair.js';
|
import Flair from '../elements/Flair.js';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -95,7 +94,6 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
const {mxEvent} = this.props;
|
const {mxEvent} = this.props;
|
||||||
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
||||||
const name = mxEvent.sender ? mxEvent.sender.name : 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
|
// Name + flair
|
||||||
const nameFlair = <span>
|
const nameFlair = <span>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 PropTypes from 'prop-types';
|
||||||
import highlight from 'highlight.js';
|
import highlight from 'highlight.js';
|
||||||
import * as HtmlUtils from '../../../HtmlUtils';
|
import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
|
import {formatDate} from '../../../DateUtils';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -88,7 +90,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
if (!this.props.isEditing) {
|
||||||
|
this._applyFormatting();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_applyFormatting() {
|
||||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
// 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,
|
// 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.
|
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||||
|
@ -123,8 +130,14 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function(prevProps) {
|
||||||
this.calculateUrlPreview();
|
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() {
|
componentWillUnmount: function() {
|
||||||
|
@ -140,14 +153,16 @@ module.exports = React.createClass({
|
||||||
nextProps.replacingEventId !== this.props.replacingEventId ||
|
nextProps.replacingEventId !== this.props.replacingEventId ||
|
||||||
nextProps.highlightLink !== this.props.highlightLink ||
|
nextProps.highlightLink !== this.props.highlightLink ||
|
||||||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||||
|
nextProps.isEditing !== this.props.isEditing ||
|
||||||
nextState.links !== this.state.links ||
|
nextState.links !== this.state.links ||
|
||||||
|
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
|
||||||
nextState.widgetHidden !== this.state.widgetHidden);
|
nextState.widgetHidden !== this.state.widgetHidden);
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateUrlPreview: function() {
|
calculateUrlPreview: function() {
|
||||||
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
//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);
|
let links = this.findLinks(this.refs.content.children);
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
// de-dup the links (but preserve ordering)
|
// 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() {
|
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 mxEvent = this.props.mxEvent;
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
||||||
|
@ -436,6 +482,9 @@ module.exports = React.createClass({
|
||||||
// Part of Replies fallback support
|
// Part of Replies fallback support
|
||||||
stripReplyFallback: stripReply,
|
stripReplyFallback: stripReply,
|
||||||
});
|
});
|
||||||
|
if (this.props.replacingEventId) {
|
||||||
|
body = [body, this._renderEditedMarker()];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.highlightLink) {
|
if (this.props.highlightLink) {
|
||||||
body = <a href={this.props.highlightLink}>{ body }</a>;
|
body = <a href={this.props.highlightLink}>{ body }</a>;
|
||||||
|
@ -462,12 +511,12 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||||
*
|
*
|
||||||
<EmojiText
|
<span
|
||||||
className="mx_MEmoteBody_sender"
|
className="mx_MEmoteBody_sender"
|
||||||
onClick={this.onEmoteSenderClick}
|
onClick={this.onEmoteSenderClick}
|
||||||
>
|
>
|
||||||
{ name }
|
{ name }
|
||||||
</EmojiText>
|
</span>
|
||||||
|
|
||||||
{ body }
|
{ body }
|
||||||
{ widgets }
|
{ widgets }
|
||||||
|
|
|
@ -20,7 +20,6 @@ const React = require('react');
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const TextForEvent = require('../../../TextForEvent');
|
const TextForEvent = require('../../../TextForEvent');
|
||||||
import sdk from '../../../index';
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'TextualEvent',
|
displayName: 'TextualEvent',
|
||||||
|
@ -31,11 +30,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||||
if (text == null || text.length === 0) return null;
|
if (text == null || text.length === 0) return null;
|
||||||
return (
|
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() {
|
render() {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
|
||||||
|
|
||||||
let position = 1;
|
let position = 1;
|
||||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
const completions = completionResult.completions.map((completion, i) => {
|
const completions = completionResult.completions.map((completion, i) => {
|
||||||
|
@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
return completions.length > 0 ? (
|
return completions.length > 0 ? (
|
||||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
<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) }
|
{ completionResult.provider.renderCompletions(completions) }
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
|
@ -111,7 +111,6 @@ const EntityTile = React.createClass({
|
||||||
let nameEl;
|
let nameEl;
|
||||||
const {name} = this.props;
|
const {name} = this.props;
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
if (!this.props.suppressOnHover) {
|
if (!this.props.suppressOnHover) {
|
||||||
const activeAgo = this.props.presenceLastActiveAgo ?
|
const activeAgo = this.props.presenceLastActiveAgo ?
|
||||||
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
||||||
|
@ -128,24 +127,24 @@ const EntityTile = React.createClass({
|
||||||
}
|
}
|
||||||
nameEl = (
|
nameEl = (
|
||||||
<div className="mx_EntityTile_details">
|
<div className="mx_EntityTile_details">
|
||||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
<div className="mx_EntityTile_name" dir="auto">
|
||||||
{ name }
|
{ name }
|
||||||
</EmojiText>
|
</div>
|
||||||
{presenceLabel}
|
{presenceLabel}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.props.subtextLabel) {
|
} else if (this.props.subtextLabel) {
|
||||||
nameEl = (
|
nameEl = (
|
||||||
<div className="mx_EntityTile_details">
|
<div className="mx_EntityTile_details">
|
||||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
<div className="mx_EntityTile_name" dir="auto">
|
||||||
{name}
|
{name}
|
||||||
</EmojiText>
|
</div>
|
||||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
nameEl = (
|
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
|
// show twelve hour timestamps
|
||||||
isTwelveHour: PropTypes.bool,
|
isTwelveHour: PropTypes.bool,
|
||||||
|
|
||||||
// helper function to access relations for an event
|
// helper function to access relations for this event
|
||||||
getRelationsForEvent: PropTypes.func,
|
getRelationsForEvent: PropTypes.func,
|
||||||
|
|
||||||
|
// whether to show reactions for this event
|
||||||
|
showReactions: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -198,7 +201,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const client = this.props.matrixClient;
|
const client = this.props.matrixClient;
|
||||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
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);
|
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -223,7 +226,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const client = this.props.matrixClient;
|
const client = this.props.matrixClient;
|
||||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
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);
|
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -485,6 +488,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
getReactions() {
|
getReactions() {
|
||||||
if (
|
if (
|
||||||
|
!this.props.showReactions ||
|
||||||
!this.props.getRelationsForEvent ||
|
!this.props.getRelationsForEvent ||
|
||||||
!SettingsStore.isFeatureEnabled("feature_reactions")
|
!SettingsStore.isFeatureEnabled("feature_reactions")
|
||||||
) {
|
) {
|
||||||
|
@ -520,7 +524,10 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
|
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
|
// This shouldn't happen: the caller should check we support this type
|
||||||
// before trying to instantiate us
|
// before trying to instantiate us
|
||||||
if (!tileHandler) {
|
if (!tileHandler) {
|
||||||
|
@ -540,6 +547,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_EventTile: true,
|
mx_EventTile: true,
|
||||||
|
mx_EventTile_isEditing: this.props.isEditing,
|
||||||
mx_EventTile_info: isInfoMessage,
|
mx_EventTile_info: isInfoMessage,
|
||||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||||
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
|
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
|
||||||
|
@ -617,14 +625,14 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||||
const actionBar = <MessageActionBar
|
const actionBar = !this.props.isEditing ? <MessageActionBar
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
reactions={this.state.reactions}
|
reactions={this.state.reactions}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
getTile={this.getTile}
|
getTile={this.getTile}
|
||||||
getReplyThread={this.getReplyThread}
|
getReplyThread={this.getReplyThread}
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
/>;
|
/> : undefined;
|
||||||
|
|
||||||
const timestamp = this.props.mxEvent.getTs() ?
|
const timestamp = this.props.mxEvent.getTs() ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
@ -674,14 +682,13 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
switch (this.props.tileShape) {
|
switch (this.props.tileShape) {
|
||||||
case 'notif': {
|
case 'notif': {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="mx_EventTile_roomName">
|
<div className="mx_EventTile_roomName">
|
||||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{ room ? room.name : '' }
|
{ room ? room.name : '' }
|
||||||
</EmojiText>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_EventTile_senderDetails">
|
<div className="mx_EventTile_senderDetails">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
|
@ -780,6 +787,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
|
isEditing={this.props.isEditing}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
|
@ -789,7 +797,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
// 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)
|
// the need for further z-indexing chaos)
|
||||||
}
|
}
|
||||||
|
|
|
@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
|
|
||||||
let backButton;
|
let backButton;
|
||||||
if (this.props.member.roomId) {
|
if (this.props.member.roomId) {
|
||||||
|
@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
<div className="mx_MemberInfo_name">
|
<div className="mx_MemberInfo_name">
|
||||||
{ backButton }
|
{ backButton }
|
||||||
{ e2eIconElement }
|
{ e2eIconElement }
|
||||||
<EmojiText element="h2">{ memberName }</EmojiText>
|
<h2>{ memberName }</h2>
|
||||||
</div>
|
</div>
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
<div className="mx_MemberInfo_container">
|
<div className="mx_MemberInfo_container">
|
||||||
|
|
|
@ -40,21 +40,18 @@ import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
import * as RichText from '../../../RichText';
|
|
||||||
import * as HtmlUtils from '../../../HtmlUtils';
|
import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||||
import Markdown from '../../../Markdown';
|
import Markdown from '../../../Markdown';
|
||||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
|
||||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
|
|
||||||
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
||||||
|
|
||||||
import {
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
asciiRegexp, unicodeRegexp, shortnameToUnicode,
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
asciiList, mapUnicodeToShort, toShort,
|
|
||||||
} from 'emojione';
|
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import {makeUserPermalink} from "../../../matrix-to";
|
import {makeUserPermalink} from "../../../matrix-to";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
|
@ -62,10 +59,9 @@ import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {ContentHelpers} from 'matrix-js-sdk';
|
import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
|
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
|
||||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
@ -144,7 +140,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
historyManager: ComposerHistoryManager;
|
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -273,9 +268,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
case 'emoji':
|
case 'emoji':
|
||||||
// XXX: apparently you can't return plain strings from serializer rules
|
// XXX: apparently you can't return plain strings from serializer rules
|
||||||
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
|
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
|
||||||
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
|
// So instead we temporarily wrap emoji from RTE in a span.
|
||||||
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
|
return <span>{ obj.data.get('emojiUnicode') }</span>;
|
||||||
return <b>{ obj.data.get('emojiUnicode') }</b>;
|
|
||||||
}
|
}
|
||||||
return this.renderNode({
|
return this.renderNode({
|
||||||
node: obj,
|
node: obj,
|
||||||
|
@ -335,7 +329,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -375,7 +368,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
||||||
forComposerQuote: true,
|
forComposerQuote: true,
|
||||||
returnString: true,
|
returnString: true,
|
||||||
emojiOne: false,
|
|
||||||
});
|
});
|
||||||
const fragment = this.html.deserialize(html);
|
const fragment = this.html.deserialize(html);
|
||||||
// FIXME: do we want to put in a permalink to the original quote here?
|
// 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) {
|
sendTyping(isTyping) {
|
||||||
if (!SettingsStore.getValue('sendTypingNotifications')) return;
|
if (!SettingsStore.getValue('sendTypingNotifications')) return;
|
||||||
|
if (SettingsStore.getValue('lowBandwidth')) return;
|
||||||
MatrixClientPeg.get().sendTyping(
|
MatrixClientPeg.get().sendTyping(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||||
|
@ -538,17 +531,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// Automatic replacement of plaintext emoji to Unicode emoji
|
// Automatic replacement of plaintext emoji to Unicode emoji
|
||||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
// The first matched group includes just the matched plaintext emoji
|
// The first matched group includes just the matched plaintext emoji
|
||||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||||
if (emojiMatch) {
|
if (emoticonMatch) {
|
||||||
// plaintext -> hex unicode
|
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
|
||||||
const emojiUc = asciiList[emojiMatch[1]];
|
const unicodeEmoji = data ? data.unicode : '';
|
||||||
// hex unicode -> shortname -> actual unicode
|
|
||||||
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
|
|
||||||
|
|
||||||
const range = Range.create({
|
const range = Range.create({
|
||||||
anchor: {
|
anchor: {
|
||||||
key: editorState.startText.key,
|
key: editorState.startText.key,
|
||||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||||
},
|
},
|
||||||
focus: {
|
focus: {
|
||||||
key: editorState.startText.key,
|
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) {
|
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
|
||||||
let blockType = editorState.blocks.first().type;
|
let blockType = editorState.blocks.first().type;
|
||||||
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
|
// 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');
|
return change.insertText('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.autocomplete.countCompletions() > 0) {
|
||||||
|
this.autocomplete.hide();
|
||||||
|
ev.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const editorState = this.state.editorState;
|
const editorState = this.state.editorState;
|
||||||
|
|
||||||
const lastBlock = editorState.blocks.last();
|
const lastBlock = editorState.blocks.last();
|
||||||
|
@ -1087,7 +1036,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
}, ()=>{
|
}, ()=>{
|
||||||
|
@ -1165,11 +1113,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||||
|
|
||||||
this.historyManager.save(
|
|
||||||
editorState,
|
|
||||||
this.state.isRichTextEnabled ? 'rich' : 'markdown',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (commandText && commandText.startsWith('/me')) {
|
if (commandText && commandText.startsWith('/me')) {
|
||||||
if (replyingToEv) {
|
if (replyingToEv) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
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)
|
// and we must be at the edge of the document (up=start, down=end)
|
||||||
if (up) {
|
if (up) {
|
||||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||||
} else {
|
|
||||||
if (!selection.anchor.isAtEndOfNode(document)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selected = this.selectHistory(up);
|
const editEvent = findEditableEvent(this.props.room, false);
|
||||||
if (selected) {
|
if (editEvent) {
|
||||||
// We're selecting history, so prevent the key event from doing anything else
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'edit_event',
|
||||||
|
event: editEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.moveAutocompleteSelection(up);
|
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) => {
|
onTab = async (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
someCompletions: null,
|
someCompletions: null,
|
||||||
|
@ -1475,17 +1372,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
case 'emoji': {
|
case 'emoji': {
|
||||||
const { data } = node;
|
const { data } = node;
|
||||||
const emojiUnicode = data.get('emojiUnicode');
|
return 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}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component {
|
||||||
if (!this.state.event) return null;
|
if (!this.state.event) return null;
|
||||||
|
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
|
||||||
|
|
||||||
return <div className="mx_ReplyPreview">
|
return <div className="mx_ReplyPreview">
|
||||||
<div className="mx_ReplyPreview_section">
|
<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') }
|
{ '💬 ' + _t('Replying') }
|
||||||
</EmojiText>
|
</div>
|
||||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||||
onClick={cancelQuoting} />
|
onClick={cancelQuoting} />
|
||||||
|
|
|
@ -147,7 +147,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
|
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
let cancelButton = null;
|
let cancelButton = null;
|
||||||
|
@ -191,10 +190,10 @@ module.exports = React.createClass({
|
||||||
roomName = this.props.room.name;
|
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 =
|
const name =
|
||||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
<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 }
|
{ searchStatus }
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
|
|
@ -750,6 +750,7 @@ module.exports = React.createClass({
|
||||||
order: "recent",
|
order: "recent",
|
||||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
|
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
|
||||||
|
addRoomLabel: _t("Start chat"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
list: this.state.lists['im.vector.fake.recent'],
|
list: this.state.lists['im.vector.fake.recent'],
|
||||||
|
|
|
@ -342,7 +342,6 @@ module.exports = React.createClass({
|
||||||
badge = <div className={badgeClasses}>{ badgeContent }</div>;
|
badge = <div className={badgeClasses}>{ badgeContent }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
let label;
|
let label;
|
||||||
let subtextLabel;
|
let subtextLabel;
|
||||||
let tooltip;
|
let tooltip;
|
||||||
|
@ -354,14 +353,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||||
|
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
|
||||||
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>;
|
|
||||||
}
|
|
||||||
} else if (this.state.hover) {
|
} else if (this.state.hover) {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
|
||||||
import WhoIsTyping from '../../../WhoIsTyping';
|
import WhoIsTyping from '../../../WhoIsTyping';
|
||||||
import Timer from '../../../utils/Timer';
|
import Timer from '../../../utils/Timer';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
@ -212,15 +211,13 @@ module.exports = React.createClass({
|
||||||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
return (<div className="mx_WhoIsTypingTile_empty" />);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="mx_WhoIsTypingTile">
|
<li className="mx_WhoIsTypingTile">
|
||||||
<div className="mx_WhoIsTypingTile_avatars">
|
<div className="mx_WhoIsTypingTile_avatars">
|
||||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_WhoIsTypingTile_label">
|
<div className="mx_WhoIsTypingTile_label">
|
||||||
<EmojiText>{ typingString }</EmojiText>
|
{ typingString }
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||||
} else if (this.state.loading) {
|
} else if (this.state.loading) {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else if (this.state.backupInfo) {
|
} else if (this.state.backupInfo) {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
|
||||||
let clientBackupStatus;
|
let clientBackupStatus;
|
||||||
let restoreButtonCaption = _t("Restore from Backup");
|
let restoreButtonCaption = _t("Restore from Backup");
|
||||||
|
|
||||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||||
clientBackupStatus = <div>
|
clientBackupStatus = <div>
|
||||||
<p>{encryptedMessageAreEncrypted}</p>
|
<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>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
clientBackupStatus = <div>
|
clientBackupStatus = <div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue