Merge remote-tracking branch 'upstream/develop' into hs/custom-notif-sounds

This commit is contained in:
Will Hunt 2019-05-31 10:44:30 +01:00
commit 9369e964fa
143 changed files with 3498 additions and 1773 deletions

View file

@ -4,7 +4,6 @@ src/component-index.js
src/components/structures/BottomLeftMenu.js
src/components/structures/CreateRoom.js
src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js
src/components/structures/RoomDirectory.js
src/components/structures/RoomStatusBar.js
src/components/structures/RoomView.js

View file

@ -1,3 +1,161 @@
Changes in [1.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.0) (2019-05-29)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.0-rc.1...v1.2.0)
* COLR font check fixes for release
[\#3041](https://github.com/matrix-org/matrix-react-sdk/pull/3041)
* Revert "Make the timeline less noisy for screen readers (mk II) #3019" for
release
[\#3036](https://github.com/matrix-org/matrix-react-sdk/pull/3036)
* Override font for usercontent download link for release
[\#3037](https://github.com/matrix-org/matrix-react-sdk/pull/3037)
Changes in [1.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.0-rc.1) (2019-05-23)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.2...v1.2.0-rc.1)
* Update from Weblate
[\#3023](https://github.com/matrix-org/matrix-react-sdk/pull/3023)
* Use the correct line-height for bold emoji
[\#3022](https://github.com/matrix-org/matrix-react-sdk/pull/3022)
* Make the timeline less noisy for screen readers (mk II)
[\#3019](https://github.com/matrix-org/matrix-react-sdk/pull/3019)
* Label message edit field as such for screen readers
[\#3020](https://github.com/matrix-org/matrix-react-sdk/pull/3020)
* Move checkmark to the front of key backup message
[\#3014](https://github.com/matrix-org/matrix-react-sdk/pull/3014)
* Revert "Make the timeline less noisy for screen readers"
[\#3017](https://github.com/matrix-org/matrix-react-sdk/pull/3017)
* Translate scroll movement if the deltaX is the same as the threshold
[\#3016](https://github.com/matrix-org/matrix-react-sdk/pull/3016)
* Make the timeline less noisy for screen readers
[\#3007](https://github.com/matrix-org/matrix-react-sdk/pull/3007)
* Windows emoji tweaks
[\#3015](https://github.com/matrix-org/matrix-react-sdk/pull/3015)
* Message editing: update link previews after editing
[\#3004](https://github.com/matrix-org/matrix-react-sdk/pull/3004)
* js-sdk interactive auth now sends email token
[\#3010](https://github.com/matrix-org/matrix-react-sdk/pull/3010)
* remove SBIX font and fallback to native emoji
[\#3011](https://github.com/matrix-org/matrix-react-sdk/pull/3011)
* Update from Weblate
[\#3012](https://github.com/matrix-org/matrix-react-sdk/pull/3012)
* load twemoji dynamically as colr or sbix; fix monospace
[\#3008](https://github.com/matrix-org/matrix-react-sdk/pull/3008)
* Guard against null rooms in `onEventDecrypted`
[\#3009](https://github.com/matrix-org/matrix-react-sdk/pull/3009)
* Only show reactions in main message timeline
[\#3005](https://github.com/matrix-org/matrix-react-sdk/pull/3005)
* Add voice labels for quick add room buttons
[\#3006](https://github.com/matrix-org/matrix-react-sdk/pull/3006)
* Update TopLeftMenu for accessibility: Keyboard shortcut, reduced screen
reader noise
[\#2994](https://github.com/matrix-org/matrix-react-sdk/pull/2994)
* Remove reacted with text when shortcode missing
[\#3003](https://github.com/matrix-org/matrix-react-sdk/pull/3003)
* Fixup: also change editor margin when last event and buttons are not
overlaying
[\#3002](https://github.com/matrix-org/matrix-react-sdk/pull/3002)
* Message editing: render avatars for pills in the editor
[\#2997](https://github.com/matrix-org/matrix-react-sdk/pull/2997)
* Replace emojione with twemoji + emojibase
[\#2995](https://github.com/matrix-org/matrix-react-sdk/pull/2995)
* Hide WhoIsTyping component if the MessagePanel is shaped e.g file grid
[\#3000](https://github.com/matrix-org/matrix-react-sdk/pull/3000)
* Close copy tooltip in edge cases correctly
[\#2999](https://github.com/matrix-org/matrix-react-sdk/pull/2999)
* Limit reaction sender tooltip to 6 people
[\#2998](https://github.com/matrix-org/matrix-react-sdk/pull/2998)
* Message editing: apply design
[\#2996](https://github.com/matrix-org/matrix-react-sdk/pull/2996)
* Add debug feature to show hidden events in timeline
[\#2993](https://github.com/matrix-org/matrix-react-sdk/pull/2993)
* Mute screen readers over reactions
[\#2986](https://github.com/matrix-org/matrix-react-sdk/pull/2986)
* Fix not being able to edit already edited messages
[\#2992](https://github.com/matrix-org/matrix-react-sdk/pull/2992)
* Add a basic tooltip showing who reacted
[\#2991](https://github.com/matrix-org/matrix-react-sdk/pull/2991)
* Message editing: show (edited) marker on edited messages, with tooltip
[\#2990](https://github.com/matrix-org/matrix-react-sdk/pull/2990)
* Update from Weblate
[\#2989](https://github.com/matrix-org/matrix-react-sdk/pull/2989)
* Message editing: only allow editing of text messages
[\#2988](https://github.com/matrix-org/matrix-react-sdk/pull/2988)
* Message editing: shift+enter for newline, enter to send
[\#2987](https://github.com/matrix-org/matrix-react-sdk/pull/2987)
* Apply Flex voodoo for devtools send event dialog
[\#2985](https://github.com/matrix-org/matrix-react-sdk/pull/2985)
* Fix some source strings noticed as incorrect by translators
[\#2984](https://github.com/matrix-org/matrix-react-sdk/pull/2984)
* Message editing: fix some bugs in cursor behaviour
[\#2983](https://github.com/matrix-org/matrix-react-sdk/pull/2983)
* Message editing: local echo & back-pagination
[\#2982](https://github.com/matrix-org/matrix-react-sdk/pull/2982)
* Listen for removed relations
[\#2981](https://github.com/matrix-org/matrix-react-sdk/pull/2981)
* Update from Weblate
[\#2980](https://github.com/matrix-org/matrix-react-sdk/pull/2980)
* Use `getRelation` helper
[\#2977](https://github.com/matrix-org/matrix-react-sdk/pull/2977)
* Add tooltips to rotate and close buttons in ImageView (#9686)
[\#2979](https://github.com/matrix-org/matrix-react-sdk/pull/2979)
* Message editing: smaller fixes
[\#2978](https://github.com/matrix-org/matrix-react-sdk/pull/2978)
* Message editing: adjust to js-sdk changes of marking original event as
replaced
[\#2973](https://github.com/matrix-org/matrix-react-sdk/pull/2973)
* Fix Single Sign-on
[\#2974](https://github.com/matrix-org/matrix-react-sdk/pull/2974)
* Initial support for editing messages
[\#2952](https://github.com/matrix-org/matrix-react-sdk/pull/2952)
* Check permission to invite before showing invite buttons/disable them
[\#2957](https://github.com/matrix-org/matrix-react-sdk/pull/2957)
* Support a backup room ID in PermalinkCreator
[\#2963](https://github.com/matrix-org/matrix-react-sdk/pull/2963)
* Always thumbnail for GIFs
[\#2962](https://github.com/matrix-org/matrix-react-sdk/pull/2962)
* Fix registration with email
[\#2967](https://github.com/matrix-org/matrix-react-sdk/pull/2967)
* Add configuration flag to disable minimum password requirements
[\#2947](https://github.com/matrix-org/matrix-react-sdk/pull/2947)
* Send and undo reaction events
[\#2954](https://github.com/matrix-org/matrix-react-sdk/pull/2954)
* Fix bug where email was not required where it shouldn't have been
[\#2961](https://github.com/matrix-org/matrix-react-sdk/pull/2961)
* add /rainbow and /rainbowme Slash Commands
[\#2958](https://github.com/matrix-org/matrix-react-sdk/pull/2958)
* Fix invite via MemberInfo
[\#2956](https://github.com/matrix-org/matrix-react-sdk/pull/2956)
* Close Room Settings upon Leave Room
[\#2955](https://github.com/matrix-org/matrix-react-sdk/pull/2955)
* Command to change avatar for a single room, including upload of mxc res
[\#2953](https://github.com/matrix-org/matrix-react-sdk/pull/2953)
* Add View Servers in Room to Devtools
[\#2804](https://github.com/matrix-org/matrix-react-sdk/pull/2804)
* Update 'Rooms' import RoomView.js file
[\#2951](https://github.com/matrix-org/matrix-react-sdk/pull/2951)
* Extract `ReactionDimension` out of `MessageActionBar`
[\#2950](https://github.com/matrix-org/matrix-react-sdk/pull/2950)
* Always default to the registration form
[\#2942](https://github.com/matrix-org/matrix-react-sdk/pull/2942)
* Check for `room` in all `Room.timeline*` handlers
[\#2945](https://github.com/matrix-org/matrix-react-sdk/pull/2945)
* Remove the karma junit reporter
[\#2944](https://github.com/matrix-org/matrix-react-sdk/pull/2944)
* yarn upgrade
[\#2943](https://github.com/matrix-org/matrix-react-sdk/pull/2943)
* Support changing options for .m.rule.tombstone push rule
[\#2798](https://github.com/matrix-org/matrix-react-sdk/pull/2798)
* Remove timeline explosion rageshake prompt
[\#2939](https://github.com/matrix-org/matrix-react-sdk/pull/2939)
* Add existing reactions below message
[\#2940](https://github.com/matrix-org/matrix-react-sdk/pull/2940)
* Fix lint errors in TimelinePanel
[\#2938](https://github.com/matrix-org/matrix-react-sdk/pull/2938)
* Add primary reactions to action bar
[\#2937](https://github.com/matrix-org/matrix-react-sdk/pull/2937)
Changes in [1.1.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.2) (2019-05-15)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.1...v1.1.2)

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "1.1.2",
"version": "1.2.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -65,7 +65,8 @@
"classnames": "^2.1.2",
"commonmark": "^0.28.1",
"counterpart": "^0.18.0",
"emojione": "2.2.7",
"emojibase-data": "^4.0.0",
"emojibase-regex": "^3.0.0",
"file-saver": "^1.3.3",
"filesize": "3.5.6",
"flux": "2.1.1",
@ -80,7 +81,7 @@
"linkifyjs": "^2.1.6",
"lodash": "^4.13.1",
"lolex": "2.3.2",
"matrix-js-sdk": "1.1.0",
"matrix-js-sdk": "1.2.0",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"png-chunks-extract": "^1.0.0",

View file

@ -32,6 +32,11 @@ body {
margin: 0px;
}
pre, code {
font-family: $monospace-font-family;
font-size: 100% !important;
}
.error, .warning {
color: $warning-color;
}
@ -110,6 +115,14 @@ textarea {
color: $primary-fg-color;
}
// This is used to hide the standard outline added by browsers for
// accessible (focusable) components. Not intended for buttons, but
// should be used on things like focusable containers where the outline
// is usually not helping anyone.
.mx_HiddenFocusable {
outline: none;
}
// .mx_textinput is a container for a text input
// + some other controls like buttons, ...
// it has the appearance of a text box so the controls
@ -445,15 +458,6 @@ textarea {
background-color: $primary-bg-color;
}
.mx_emojione {
height: 1em;
vertical-align: middle;
}
.mx_emojione_selected {
background-color: $accent-color;
}
::-moz-selection {
background-color: $accent-color;
color: $selection-fg-color;
@ -557,4 +561,3 @@ textarea {
.mx_Username_color8 {
color: $username-variant8-color;
}

View file

@ -98,6 +98,7 @@
@import "./views/elements/_RoleButton.scss";
@import "./views/elements/_Spinner.scss";
@import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_TextWithTooltip.scss";
@import "./views/elements/_ToggleSwitch.scss";
@import "./views/elements/_ToolTipButton.scss";
@import "./views/elements/_Tooltip.scss";
@ -119,10 +120,12 @@
@import "./views/messages/_ReactionDimension.scss";
@import "./views/messages/_ReactionsRow.scss";
@import "./views/messages/_ReactionsRowButton.scss";
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
@import "./views/messages/_RoomAvatarEvent.scss";
@import "./views/messages/_SenderProfile.scss";
@import "./views/messages/_TextualEvent.scss";
@import "./views/messages/_UnknownBody.scss";
@import "./views/messages/_ViewSourceEvent.scss";
@import "./views/room_settings/_AliasSettings.scss";
@import "./views/room_settings/_ColorSettings.scss";
@import "./views/rooms/_AppsDrawer.scss";

View file

@ -79,3 +79,7 @@ limitations under the License.
.mx_Login_type_dropdown {
min-width: 200px;
}
.mx_Login_underlinedServerName {
border-bottom: 1px dashed $accent-color;
}

View file

@ -35,3 +35,8 @@ limitations under the License.
.mx_ServerConfig_help:link {
opacity: 0.8;
}
.mx_ServerConfig_error {
display: block;
color: $warning-color;
}

View file

@ -82,8 +82,13 @@ limitations under the License.
display: inline-block;
}
.mx_DevTools_content .mx_Field_input + .mx_Field_input {
margin-left: 42px;
.mx_DevTools_eventTypeStateKeyGroup {
display: flex;
flex-wrap: wrap;
}
.mx_DevTools_content .mx_Field_input:first-of-type {
margin-right: 42px;
}
.mx_DevTools_tgl {

View file

@ -16,40 +16,61 @@ limitations under the License.
.mx_MessageEditor {
border-radius: 4px;
background-color: $header-panel-bg-color;
padding: 11px 13px 7px 56px;
padding: 3px;
// this is to try not make the text move but still have some
// padding around and in the editor.
// Actual values from fiddling around in inspector
margin: -7px -10px -5px -10px;
overflow: visible !important; // override mx_EventTile_content
.mx_MessageEditor_editor {
border-radius: 4px;
border: solid 1px #e9edf1;
background-color: #ffffff;
padding: 10px;
border: solid 1px $primary-hairline-color;
background-color: $primary-bg-color;
padding: 3px 6px;
white-space: pre-wrap;
word-wrap: break-word;
outline: none;
max-height: 200px;
overflow-x: auto;
span {
display: inline-block;
padding: 0 5px;
border-radius: 4px;
color: white;
span.mx_UserPill, span.mx_RoomPill {
padding-left: 21px;
position: relative;
// 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 {
display: flex;
flex-direction: row;
justify-content: end;
padding: 5px 0;
justify-content: flex-end;
padding: 5px;
position: absolute;
left: 0;
background: $header-panel-bg-color;
z-index: 100;
right: 0;
margin: 0 -110px 0 0;
padding-right: 147px;
.mx_AccessibleButton {
margin-left: 5px;
@ -62,3 +83,8 @@ limitations under the License.
height: 0;
}
}
.mx_EventTile_last .mx_MessageEditor_buttons {
position: static;
margin-right: -147px;
}

View file

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

View file

@ -74,3 +74,19 @@ limitations under the License.
animation: mx_fadeout 0.1s forwards;
}
}
.mx_Tooltip_timeline {
box-shadow: none;
background-color: $tooltip-timeline-bg-color;
color: $tooltip-timeline-fg-color;
text-align: center;
border: none;
border-radius: 3px;
font-size: 14px;
line-height: 1.2;
padding: 6px 8px;
.mx_Tooltip_chevron::after {
border-right-color: $tooltip-timeline-bg-color;
}
}

View 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;
}

View 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;
}
}

View file

@ -41,6 +41,15 @@ limitations under the License.
.mx_EventTile_continuation {
padding-top: 0px !important;
&.mx_EventTile_isEditing {
padding-top: 5px !important;
margin-top: -5px;
}
}
.mx_EventTile_isEditing {
background-color: $header-panel-bg-color;
}
.mx_EventTile .mx_SenderProfile {
@ -72,6 +81,10 @@ limitations under the License.
}
}
.mx_EventTile_isEditing .mx_MessageTimestamp {
visibility: hidden !important;
}
.mx_EventTile .mx_MessageTimestamp {
display: block;
visibility: hidden;
@ -108,7 +121,7 @@ limitations under the License.
/* HACK to override line-height which is already marked important elsewhere */
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
font-size: 48px ! important;
line-height: 48px ! important;
line-height: 57px ! important;
}
/* this is used for the tile for the event which is selected via the URL.
@ -149,8 +162,7 @@ limitations under the License.
}
.mx_EventTile_sending .mx_UserPill,
.mx_EventTile_sending .mx_RoomPill,
.mx_EventTile_sending .mx_emojione {
.mx_EventTile_sending .mx_RoomPill {
opacity: 0.5;
}
@ -377,6 +389,14 @@ limitations under the License.
left: 41px;
}
.mx_EventTile_content .mx_EventTile_edited {
user-select: none;
font-size: 12px;
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
}
/* Various markdown overrides */
.mx_EventTile_content .markdown-body {
@ -404,6 +424,7 @@ limitations under the License.
.mx_EventTile_content .markdown-body {
pre, code {
font-family: $monospace-font-family ! important;
// deliberate constants as we're behind an invert filter
color: #333;
}

View file

@ -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.

Binary file not shown.

View file

@ -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

View file

@ -157,6 +157,9 @@ $reaction-row-button-hover-border-color: $header-panel-text-primary-color;
$reaction-row-button-selected-bg-color: #1f6954;
$reaction-row-button-selected-border-color: $accent-color;
$tooltip-timeline-bg-color: $tagpanel-bg-color;
$tooltip-timeline-fg-color: #ffffff;
// ***** Mixins! *****
@define-mixin mx_DialogButton {

View file

@ -33,21 +33,52 @@
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
}
/*
* Fira Mono
* Used for monospace copy, i.e. code
*/
/* latin-ext */
@font-face {
font-family: 'Fira Mono';
src: url('$(res)/fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype');
font-family: 'Inconsolata';
font-style: normal;
font-weight: 400;
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji
* taken from https://github.com/mozilla/twemoji-colr
* using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to
* work on macOS
*/
/*
// except we now load it dynamically via FontManager to handle browsers
// which can't render COLR/CPAL still
@font-face {
font-family: 'Fira Mono';
src: url('$(res)/fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-family: "Twemoji Mozilla";
src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2');
}
*/

View file

@ -1,10 +1,13 @@
// XXX: check this?
/* Nunito lacks combining diacritics, so these will fall through
to the next font. Helevetica's diacritics however do not combine
nicely with Open Sans (on OSX, at least) and result in a huge
horizontal mess. Arial empirically gets it right, hence prioritising
Arial here. */
$font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
nicely (on OSX, at least) and result in a huge horizontal mess.
Arial empirically gets it right, hence prioritising Arial here. */
/* We fall through to Twemoji for emoji rather than falling through
to native Emoji fonts (if any) to ensure cross-browser consistency */
$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif;
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace;
// unified palette
// try to use these colors when possible
@ -265,6 +268,9 @@ $reaction-row-button-hover-border-color: $focus-bg-color;
$reaction-row-button-selected-bg-color: #e9fff9;
$reaction-row-button-selected-border-color: $accent-color;
$tooltip-timeline-bg-color: $tagpanel-bg-color;
$tooltip-timeline-fg-color: #ffffff;
// ***** Mixins! *****
@define-mixin mx_DialogButton {

View file

@ -1,28 +1,29 @@
#!/usr/bin/env node
const EMOJI_DATA = require('emojione/emoji.json');
const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList);
// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete
// provider.
const EMOJIBASE = require('emojibase-data/en/compact.json');
const fs = require('fs');
const output = Object.keys(EMOJI_DATA).map(
(key) => {
const datum = EMOJI_DATA[key];
const output = EMOJIBASE.map(
(datum) => {
const newDatum = {
name: datum.name,
shortname: datum.shortname,
category: datum.category,
emoji_order: datum.emoji_order,
name: datum.annotation,
shortname: `:${datum.shortcodes[0]}:`,
category: datum.group,
emoji_order: datum.order,
};
if (datum.aliases.length > 0) {
newDatum.aliases = datum.aliases;
if (datum.shortcodes.length > 1) {
newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`);
}
if (datum.aliases_ascii.length > 0) {
newDatum.aliases_ascii = datum.aliases_ascii;
if (datum.emoticon) {
newDatum.aliases_ascii = [ datum.emoticon ];
}
return newDatum;
}
).filter((datum) => {
return EMOJI_SUPPORTED.includes(datum.shortname);
});
);
// Write to a file in src. Changes should be checked into git. This file is copied by
// babel using --copy-files

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import {ContentRepo} from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';
module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
@ -58,4 +59,71 @@ module.exports = {
}
return require('../res/img/' + images[total % images.length] + '.png');
},
/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
* @param {string} name
* @return {string} the first letter
*/
getInitialLetter(name) {
if (name.length < 1) {
return undefined;
}
let idx = 0;
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
idx++;
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
let chars = 1;
const first = name.charCodeAt(idx);
// check if its 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;
},
};

View file

@ -361,7 +361,7 @@ async function _startCallApp(roomId, type) {
Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, {
title: _t('Could not connect to the integration server'),
description: _t('A conference call could not be started because the intgrations server is not available'),
description: _t('A conference call could not be started because the integrations server is not available'),
});
return;
}

View file

@ -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];
}
}

View file

@ -38,7 +38,7 @@ export function showGroupInviteDialog(groupId) {
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
title: _t("Invite new community members"),
description: description,
placeholder: _t("Name or matrix ID"),
placeholder: _t("Name or Matrix ID"),
button: _t("Invite to Community"),
validAddressTypes: ['mx-user-id'],
onFinished: (success, addrs) => {

View file

@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string';
import escape from 'lodash/escape';
import emojione from 'emojione';
import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg';
import url from 'url';
linkifyMatrix(linkify);
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOJIBASE_REGEX from 'emojibase-regex';
emojione.imagePathSVG = 'emojione/svg/';
// Store PNG path for displaying many flags at once (for increased performance over SVG)
emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis
emojione.imageType = 'svg';
linkifyMatrix(linkify);
// Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
// And there a bunch more symbol characters that emojione has within the
// And there a bunch more symbol characters that emojibase has within the
// BMP, so this includes the ranges from 'letterlike symbols' to
// 'miscellaneous symbols and arrows' which should catch all of them
// (with plenty of false positives, but that's OK)
@ -54,15 +50,15 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
// Regex pattern for whitespace characters
const WHITESPACE_REGEX = new RegExp("\\s", "g");
// And this is emojione's complete regex
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
/*
* Return true if the given string contains emoji
* Uses a much, much simpler regex than emojione's so will give false
* Uses a much, much simpler regex than emojibase's so will give false
* positives, but useful for fast-path testing strings to see if they
* need emojification.
* unicodeToImage uses this function.
@ -71,62 +67,27 @@ export function containsEmoji(str) {
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
}
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
* because we want to include emoji shortnames in title text
/**
* Returns the shortcode for an emoji character.
*
* @param {String} char The emoji character
* @return {String} The shortcode (such as :thumbup:)
*/
function unicodeToImage(str, addAlt) {
if (addAlt === undefined) addAlt = true;
let replaceWith; let unicode; let short; let fname;
const mappedUnicode = emojione.mapUnicodeToShort();
str = str.replace(emojione.regUnicode, function(unicodeChar) {
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
// if the unicodeChar doesnt exist just return the entire match
return unicodeChar;
} else {
// get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar];
short = mappedUnicode[unicode];
fname = emojione.emojioneList[short].fname;
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
const title = mappedUnicode[unicode];
if (addAlt) {
const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
} else {
replaceWith = `<img class="mx_emojione" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
}
return replaceWith;
}
});
return str;
export function unicodeToShortcode(char) {
const data = EMOJIBASE.find(e => e.unicode === char);
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
}
/**
* Given one or more unicode characters (represented by unicode
* character number), return an image node with the corresponding
* emoji.
* Returns the unicode character for an emoji shortcode
*
* @param alt {string} String to use for the image alt text
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
* @param unicode {integer} One or more integers representing unicode characters
* @returns A img node with the corresponding emoji
* @param {String} shortcode The shortcode (such as :thumbup:)
* @return {String} The emoji character; null if none exists
*/
export function charactersToImageNode(alt, useSvg, ...unicode) {
const fileName = unicode.map((u) => {
return u.toString(16);
}).join('-');
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
const fileType = useSvg ? 'svg' : 'png';
return <img
alt={alt}
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
/>;
export function shortcodeToUnicode(shortcode) {
shortcode = shortcode.slice(1, shortcode.length - 1);
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
return data ? data.unicode : null;
}
export function processHtmlForSending(html: string): string {
@ -433,13 +394,10 @@ class TextHighlighter extends BaseHighlighter {
* opts.disableBigEmoji: optional argument to disable the big emoji class.
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
* opts.returnString: return an HTML string rather than JSX elements
* opts.emojiOne: optional param to do emojiOne (default true)
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
*/
export function bodyToHtml(content, highlights, opts={}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
let bodyHasEmoji = false;
let sanitizeParams = sanitizeHtmlParams;
@ -470,28 +428,12 @@ export function bodyToHtml(content, highlights, opts={}) {
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
if (doEmojiOne) {
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
}
// Only generate safeBody if the message was sent as org.matrix.custom.html
if (isHtmlMessage) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
} else {
// ... or if there are emoji, which we insert as HTML alongside the
// escaped plaintext body.
if (bodyHasEmoji) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
}
}
// An HTML message with emoji
// or a plaintext message with emoji that was escaped and sanitized into
// HTML.
if (bodyHasEmoji) {
safeBody = unicodeToImage(safeBody);
}
} finally {
delete sanitizeParams.textFilter;
@ -503,7 +445,6 @@ export function bodyToHtml(content, highlights, opts={}) {
let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0;
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
// Ignore spaces in body text. Emojis with spaces in between should
@ -515,29 +456,25 @@ export function bodyToHtml(content, highlights, opts={}) {
// presented as large.
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
// their username
&& (content.formatted_body == undefined
|| !content.formatted_body.includes("https://matrix.to/"));
(
content.formatted_body == undefined ||
!content.formatted_body.includes("https://matrix.to/")
);
}
const className = classNames({
'mx_EventTile_body': true,
'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtmlMessage,
'markdown-body': isHtmlMessage && !emojiBody,
});
return isDisplayedWithHtml ?
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span className={className} dir="auto">{ strippedBody }</span>;
}
export function emojifyText(text, addAlt) {
return {
__html: unicodeToImage(escape(text), addAlt),
};
<span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
}
/**

View file

@ -33,6 +33,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore';
import PlatformPeg from "./PlatformPeg";
import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager';
import SettingsStore from "./settings/SettingsStore";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -499,7 +500,9 @@ async function startMatrixClient() {
Notifier.start();
UserActivity.sharedInstance().start();
if (!SettingsStore.getValue("lowBandwidth")) {
Presence.start();
}
DMRoomMap.makeShared().start();
ActiveWidgetStore.start();

View file

@ -119,7 +119,7 @@ class MatrixClientPeg {
// try to initialise e2e on the new client
try {
// check that we have a version of the js-sdk which includes initCrypto
if (this.matrixClient.initCrypto) {
if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto();
StorageManager.setCryptoInitialised(true);
}
@ -188,8 +188,7 @@ class MatrixClientPeg {
timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
verificationMethods: [verificationMethods.SAS],
unstableClientRelationAggregation: aggregateRelations,
unstableClientRelationReplacements: enableEdits,
unstableClientRelationAggregation: aggregateRelations || enableEdits,
};
this.matrixClient = createMatrixClient(opts);

View file

@ -85,7 +85,11 @@ const Notifier = {
msg = '';
}
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
let avatarUrl = null;
if (ev.sender && !SettingsStore.getValue("lowBandwidth")) {
avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop');
}
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
// if displayNotification returns non-null, the platform supports

View file

@ -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;
}
});
}

View file

@ -45,7 +45,7 @@ export function showStartChatInviteDialog() {
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: _t("Email, name or matrix ID"),
placeholder: _t("Email, name or Matrix ID"),
validAddressTypes: ['mx-user-id', 'email'],
button: _t("Start Chat"),
onFinished: _onStartChatFinished,
@ -58,7 +58,7 @@ export function showRoomInviteDialog(roomId) {
title: _t('Invite new room members'),
description: _t('Who would you like to add to this room?'),
button: _t('Send Invites'),
placeholder: _t("Email, name or matrix ID"),
placeholder: _t("Email, name or Matrix ID"),
onFinished: (shouldInvite, addrs) => {
_onRoomInviteFinished(roomId, shouldInvite, addrs);
},

View file

@ -518,7 +518,7 @@ export const CommandMap = {
unban: new Command({
name: 'unban',
args: '<user-id>',
description: _td('Unbans user with given id'),
description: _td('Unbans user with given ID'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);

View file

@ -19,47 +19,31 @@ limitations under the License.
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
import QueryMatcher from './QueryMatcher';
import sdk from '../index';
import {PillCompletion} from './Components';
import type {Completion, SelectionRange} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import EmojiData from '../stripped-emoji.json';
const LIMIT = 20;
const CATEGORY_ORDER = [
'people',
'food',
'objects',
'activity',
'nature',
'travel',
'flags',
'regional',
'symbols',
'modifier',
];
// Match for ":wink:" or ascii-style ";-)" provided by emojione
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
// that we need to support inputting multiple emoji with no space between them.
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g');
// We also need to match the non-zero-length prefixes to remove them from the final match,
// and update the range so that we don't replace the whitespace or the previous emoji.
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')');
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
// XXX: it's very unclear why we bother with this generated emojidata file.
// all it means is that we end up bloating the bundle with precomputed stuff
// which would be trivial to calculate and cache on demand.
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
(a, b) => {
if (a.category === b.category) {
return a.emoji_order - b.emoji_order;
}
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
return a.category - b.category;
},
).map((a, index) => {
return {
@ -101,26 +85,20 @@ export default class EmojiProvider extends AutocompleteProvider {
return []; // don't give any suggestions if the user doesn't want them
}
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
let matchedString = command[0];
// Remove prefix of any length (single whitespace or unicode emoji)
const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString);
if (prefixMatch) {
matchedString = matchedString.slice(prefixMatch[0].length);
range.start += prefixMatch[0].length;
}
const matchedString = command[0];
completions = this.matcher.match(matchedString);
// Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString));
const sorters = [];
// First, sort by score (Infinity if matchedString not in shortname)
// make sure that emoticons come first
sorters.push((c) => score(matchedString, c.aliases_ascii));
// then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname));
// If the matchedString is not empty, sort by length of shortname. Example:
// matchedString = ":bookmark"
@ -134,11 +112,11 @@ export default class EmojiProvider extends AutocompleteProvider {
completions = completions.map((result) => {
const { shortname } = result;
const unicode = shortnameToUnicode(shortname);
const unicode = shortcodeToUnicode(shortname);
return {
completion: unicode,
component: (
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
<PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
),
range,
};

View file

@ -265,7 +265,7 @@ const RoleUserList = React.createClass({
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
title: _t('Add users to the community summary'),
description: _t("Who would you like to add to this summary?"),
placeholder: _t("Name or matrix ID"),
placeholder: _t("Name or Matrix ID"),
button: _t("Add to summary"),
validAddressTypes: ['mx-user-id'],
groupId: this.props.groupId,

View file

@ -129,7 +129,7 @@ export default class IndicatorScrollbar extends React.Component {
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
const yRetention = 1.0;
if (Math.abs(e.deltaX) < xyThreshold) {
if (Math.abs(e.deltaX) <= xyThreshold) {
// noinspection JSSuspiciousNameCombination
this._scrollElement.scrollLeft += e.deltaY * yRetention;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -60,7 +61,7 @@ export default React.createClass({
inputs: PropTypes.object,
// As js-sdk interactive-auth
makeRegistrationUrl: PropTypes.func,
requestEmailToken: PropTypes.func,
sessionId: PropTypes.string,
clientSecret: PropTypes.string,
emailSid: PropTypes.string,
@ -96,6 +97,7 @@ export default React.createClass({
sessionId: this.props.sessionId,
clientSecret: this.props.clientSecret,
emailSid: this.props.emailSid,
requestEmailToken: this.props.requestEmailToken,
});
this._authLogic.attemptAuth().then((result) => {
@ -202,7 +204,6 @@ export default React.createClass({
stageState={this.state.stageState}
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
makeRegistrationUrl={this.props.makeRegistrationUrl}
showContinue={!this.props.continueIsManaged}
/>
);

View file

@ -24,6 +24,7 @@ import { DragDropContext } from 'react-beautiful-dnd';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import sdk from '../../index';
import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
@ -118,6 +119,8 @@ const LoggedInView = React.createClass({
this._matrixClient.on("accountData", this.onAccountData);
this._matrixClient.on("sync", this.onSync);
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
fixupColorFonts();
},
componentDidUpdate(prevProps) {
@ -322,6 +325,18 @@ const LoggedInView = React.createClass({
handled = true;
}
break;
case KeyCode.KEY_I:
// Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information"
// will have to do.
if (ctrlCmdOnly) {
dis.dispatch({
action: 'toggle_top_left_menu',
});
handled = true;
}
break;
}
if (handled) {

View file

@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
const AutoDiscovery = Matrix.AutoDiscovery;
import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils";
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
@ -109,6 +108,7 @@ export default React.createClass({
propTypes: {
config: PropTypes.object,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig),
ConferenceHandler: PropTypes.any,
onNewScreen: PropTypes.func,
registrationUrl: PropTypes.string,
@ -181,16 +181,8 @@ export default React.createClass({
// Parameters used in the registration dance with the IS
register_client_secret: null,
register_session_id: null,
register_hs_url: null,
register_is_url: null,
register_id_sid: null,
// Parameters used for setting up the authentication views
defaultServerName: this.props.config.default_server_name,
defaultHsUrl: this.props.config.default_hs_url,
defaultIsUrl: this.props.config.default_is_url,
defaultServerDiscoveryError: null,
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
hideToSRUsers: false,
@ -211,42 +203,19 @@ export default React.createClass({
};
},
getDefaultServerName: function() {
return this.state.defaultServerName;
},
getCurrentHsUrl: function() {
if (this.state.register_hs_url) {
return this.state.register_hs_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getHomeserverUrl();
} else {
return this.getDefaultHsUrl();
}
},
getDefaultHsUrl(defaultToMatrixDotOrg) {
defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
return this.state.defaultHsUrl;
},
getFallbackHsUrl: function() {
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
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 {
return this.getDefaultIsUrl();
return null;
}
},
getDefaultIsUrl() {
return this.state.defaultIsUrl || "https://vector.im";
getServerProperties() {
let props = this.state.serverConfig;
if (!props) props = this.props.serverConfig; // for unit tests
if (!props) props = SdkConfig.get()["validated_server_config"];
return {serverConfig: props};
},
componentWillMount: function() {
@ -260,40 +229,6 @@ export default React.createClass({
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
}
// Set up the default URLs (async)
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
this.setState({loadingDefaultHomeserver: true});
this._tryDiscoverDefaultHomeserver(this.getDefaultServerName());
} else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) {
// Ideally we would somehow only communicate this to the server admins, but
// given this is at login time we can't really do much besides hope that people
// will check their settings.
this.setState({
defaultServerName: null, // To un-hide any secrets people might be keeping
defaultServerDiscoveryError: _t(
"Invalid configuration: Cannot supply a default homeserver URL and " +
"a default server name",
),
});
}
// Set a default HS with query param `hs_url`
const paramHs = this.props.startingFragmentQueryParams.hs_url;
if (paramHs) {
console.log('Setting register_hs_url ', paramHs);
this.setState({
register_hs_url: paramHs,
});
}
// Set a default IS with query param `is_url`
const paramIs = this.props.startingFragmentQueryParams.is_url;
if (paramIs) {
console.log('Setting register_is_url ', paramIs);
this.setState({
register_is_url: paramIs,
});
}
// a thing to call showScreen with once login completes. this is kept
// outside this.state because updating it should never trigger a
// rerender.
@ -374,8 +309,8 @@ export default React.createClass({
return Lifecycle.loadSession({
fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(),
guestIsUrl: this.getCurrentIsUrl(),
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
}).then((loadedSession) => {
@ -1827,44 +1762,7 @@ export default React.createClass({
},
onServerConfigChange(config) {
const newState = {};
if (config.hsUrl) {
newState.register_hs_url = config.hsUrl;
}
if (config.isUrl) {
newState.register_is_url = config.isUrl;
}
this.setState(newState);
},
_tryDiscoverDefaultHomeserver: async function(serverName) {
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS) {
console.error("Failed to discover homeserver on startup:", discovery);
this.setState({
defaultServerDiscoveryError: discovery["m.homeserver"].error,
loadingDefaultHomeserver: false,
});
} else {
const hsUrl = discovery["m.homeserver"].base_url;
const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "https://vector.im";
this.setState({
defaultHsUrl: hsUrl,
defaultIsUrl: isUrl,
loadingDefaultHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
defaultServerDiscoveryError: _t("Unknown error discovering homeserver"),
loadingDefaultHomeserver: false,
});
}
this.setState({serverConfig: config});
},
_makeRegistrationUrl: function(params) {
@ -1883,8 +1781,7 @@ export default React.createClass({
if (
this.state.view === VIEWS.LOADING ||
this.state.view === VIEWS.LOGGING_IN ||
this.state.loadingDefaultHomeserver
this.state.view === VIEWS.LOGGING_IN
) {
const Spinner = sdk.getComponent('elements.Spinner');
return (
@ -1962,17 +1859,12 @@ export default React.createClass({
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
makeRegistrationUrl={this._makeRegistrationUrl}
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/>
);
}
@ -1982,14 +1874,11 @@ export default React.createClass({
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
return (
<ForgotPassword
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
onComplete={this.onLoginClick}
onLoginClick={this.onLoginClick} />
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/>
);
}
@ -1999,16 +1888,11 @@ export default React.createClass({
<Login
onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/>
);
}

View file

@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../DateUtils';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@ -95,6 +96,9 @@ module.exports = React.createClass({
// helper function to access relations for an event
getRelationsForEvent: PropTypes.func,
// whether to show reactions for an event
showReactions: PropTypes.bool,
},
componentWillMount: function() {
@ -230,6 +234,13 @@ module.exports = React.createClass({
}
},
scrollToEventIfNeeded: function(eventId) {
const node = this.eventNodes[eventId];
if (node) {
node.scrollIntoView({block: "nearest", behavior: "instant"});
}
},
/* check the scroll state and send out pagination requests if necessary.
*/
checkFillState: function() {
@ -248,6 +259,10 @@ module.exports = React.createClass({
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}
if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
return true;
}
const EventTile = sdk.getComponent('rooms.EventTile');
if (!EventTile.haveTileForEvent(mxEv)) {
return false; // no tile = no show
@ -450,14 +465,10 @@ module.exports = React.createClass({
_getTilesForEvent: function(prevEvent, mxEv, last) {
const EventTile = sdk.getComponent('rooms.EventTile');
const MessageEditor = sdk.getComponent('elements.MessageEditor');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const ret = [];
if (this.props.editEvent && this.props.editEvent.getId() === mxEv.getId()) {
return [<MessageEditor key={mxEv.getId()} event={mxEv} />];
}
const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId();
// is this a continuation of the previous message?
let continuation = false;
@ -527,18 +538,20 @@ module.exports = React.createClass({
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
isEditing={isEditing}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
eventSendStatus={mxEv.replacementOrOwnStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
/>
</li>,
);
@ -714,7 +727,7 @@ module.exports = React.createClass({
);
let whoIsTyping;
if (this.props.room) {
if (this.props.room && !this.props.tileShape) {
whoIsTyping = (<WhoIsTypingTile
room={this.props.room}
onShown={this._onTypingShown}

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,12 +16,9 @@ limitations under the License.
*/
const React = require('react');
const ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
const Matrix = require("matrix-js-sdk");
const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg");
const dis = require("../../dispatcher");
/*
* Component which shows the global notification list using a TimelinePanel

View file

@ -304,8 +304,6 @@ module.exports = React.createClass({
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this._shouldShowConnectionError()) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">

View file

@ -29,6 +29,7 @@ import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
// turn this on for drop & drag console debugging galore
const debug = false;
@ -42,6 +43,7 @@ const RoomSubList = React.createClass({
list: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string.isRequired,
tagName: PropTypes.string,
addRoomLabel: PropTypes.string,
order: PropTypes.string.isRequired,
@ -232,7 +234,11 @@ const RoomSubList = React.createClass({
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
<AccessibleButton
onClick={ this.props.onAddRoom }
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}

View file

@ -1832,6 +1832,7 @@ module.exports = React.createClass({
membersLoaded={this.state.membersLoaded}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
/>);
let topUnreadMessagesBar = null;

View file

@ -106,6 +106,9 @@ const TimelinePanel = React.createClass({
// placeholder text to use if the timeline is empty
empty: PropTypes.string,
// whether to show reactions for an event
showReactions: PropTypes.bool,
},
statics: {
@ -204,11 +207,11 @@ const TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
MatrixClientPeg.get().on("Room.replaceEvent", this.onRoomReplaceEvent);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props);
@ -283,11 +286,11 @@ const TimelinePanel = React.createClass({
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
client.removeListener("Room.redaction", this.onRoomRedaction);
client.removeListener("Room.replaceEvent", this.onRoomReplaceEvent);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
client.removeListener("Event.decrypted", this.onEventDecrypted);
client.removeListener("Event.replaced", this.onEventReplaced);
client.removeListener("sync", this.onSync);
}
},
@ -405,7 +408,13 @@ const TimelinePanel = React.createClass({
this.forceUpdate();
}
if (payload.action === "edit_event") {
this.setState({editEvent: payload.event});
this.setState({editEvent: payload.event}, () => {
if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded(
payload.event.getId(),
);
}
});
}
},
@ -507,7 +516,7 @@ const TimelinePanel = React.createClass({
this.forceUpdate();
},
onRoomReplaceEvent: function(replacedEvent, room) {
onEventReplaced: function(replacedEvent, room) {
if (this.unmounted) return;
// ignore events for other rooms
@ -553,6 +562,9 @@ const TimelinePanel = React.createClass({
},
onEventDecrypted: function(ev) {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
// Need to update as we don't display event tiles for events that
// haven't yet been decrypted. The event will have just been updated
// in place so we just need to re-render.
@ -601,6 +613,8 @@ const TimelinePanel = React.createClass({
},
sendReadReceipt: function() {
if (SettingsStore.getValue("lowBandwidth")) return;
if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
@ -1261,6 +1275,7 @@ const TimelinePanel = React.createClass({
resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent}
editEvent={this.state.editEvent}
showReactions={this.props.showReactions}
/>
);
},

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,6 +24,8 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
import MatrixClientPeg from '../../MatrixClientPeg';
import Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher";
import {focusCapturedRef} from "../../utils/Accessibility";
const AVATAR_SIZE = 28;
@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component {
super();
this.state = {
menuDisplayed: false,
menuFunctions: null, // should be { close: fn }
profileInfo: null,
};
@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component {
}
async componentDidMount() {
this._dispatcherRef = dis.register(this.onAction);
try {
const profileInfo = await this._getProfileInfo();
this.setState({profileInfo});
@ -68,6 +74,17 @@ export default class TopLeftMenuButton extends React.Component {
}
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
onAction = (payload) => {
// For accessibility
if (payload.action === "toggle_top_left_menu") {
if (this._buttonRef) this._buttonRef.click();
}
};
_getDisplayName() {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
@ -88,7 +105,13 @@ export default class TopLeftMenuButton extends React.Component {
}
return (
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
<AccessibleButton
className="mx_TopLeftMenuButton"
role="button"
onClick={this.onToggleMenu}
inputRef={(r) => this._buttonRef = r}
aria-label={_t("Your profile")}
>
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={name}
@ -98,7 +121,7 @@ export default class TopLeftMenuButton extends React.Component {
resizeMethod="crop"
/>
{ nameElement }
<span className="mx_TopLeftMenuButton_chevron"></span>
<span className="mx_TopLeftMenuButton_chevron" />
</AccessibleButton>
);
}
@ -107,20 +130,26 @@ export default class TopLeftMenuButton extends React.Component {
e.preventDefault();
e.stopPropagation();
if (this.state.menuDisplayed && this.state.menuFunctions) {
this.state.menuFunctions.close();
return;
}
const elementRect = e.currentTarget.getBoundingClientRect();
const x = elementRect.left;
const y = elementRect.top + elementRect.height;
ContextualMenu.createMenu(TopLeftMenu, {
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
chevronFace: "none",
left: x,
top: y,
userId: MatrixClientPeg.get().getUserId(),
displayName: this._getDisplayName(),
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
onFinished: () => {
this.setState({ menuDisplayed: false });
this.setState({ menuDisplayed: false, menuFunctions: null });
},
});
this.setState({ menuDisplayed: true });
this.setState({ menuDisplayed: true, menuFunctions });
}
}

View file

@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig";
import PasswordReset from "../../../PasswordReset";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// Phases
// Show controls to configure server details
@ -40,28 +40,14 @@ module.exports = React.createClass({
displayName: 'ForgotPassword',
propTypes: {
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about "your account".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onServerConfigChange: PropTypes.func.isRequired,
onLoginClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
phase: PHASE_FORGOT,
email: "",
password: "",
@ -70,11 +56,11 @@ module.exports = React.createClass({
};
},
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
submitPasswordReset: function(email, password) {
this.setState({
phase: PHASE_SENDING_EMAIL,
});
this.reset = new PasswordReset(hsUrl, identityUrl);
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
this.reset.resetPassword(email, password).done(() => {
this.setState({
phase: PHASE_EMAIL_SENT,
@ -103,13 +89,6 @@ module.exports = React.createClass({
onSubmitForm: function(ev) {
ev.preventDefault();
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) {
@ -132,10 +111,7 @@ module.exports = React.createClass({
button: _t('Continue'),
onFinished: (confirmed) => {
if (confirmed) {
this.submitPasswordReset(
this.state.enteredHsUrl, this.state.enteredIsUrl,
this.state.email, this.state.password,
);
this.submitPasswordReset(this.state.email, this.state.password);
}
},
});
@ -148,19 +124,7 @@ module.exports = React.createClass({
});
},
onServerConfigChange: function(config) {
const newState = {};
if (config.hsUrl !== undefined) {
newState.enteredHsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIsUrl = config.isUrl;
}
this.setState(newState);
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_FORGOT,
});
@ -190,26 +154,19 @@ module.exports = React.createClass({
renderServerDetails() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
if (SdkConfig.get()['disable_custom_urls']) {
return null;
}
return <div>
<ServerConfig
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.state.enteredHsUrl}
customIsUrl={this.state.enteredIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
<AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>
</div>;
return <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={0}
onAfterSubmit={this.onServerDetailsNextPhaseClick}
submitText={_t("Next")}
submitClass="mx_Login_submit"
/>;
},
renderForgot() {
@ -221,25 +178,22 @@ module.exports = React.createClass({
errorText = <div className="mx_Login_error">{ err }</div>;
}
let yourMatrixAccountText = _t('Your Matrix account');
if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) {
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: this.props.defaultServerName,
let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
} else {
try {
const parsedHsUrl = new URL(this.state.enteredHsUrl);
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} 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.

View file

@ -25,7 +25,7 @@ import sdk from '../../../index';
import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import { AutoDiscovery } from "matrix-js-sdk";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -59,19 +59,14 @@ module.exports = React.createClass({
propTypes: {
onLoggedIn: PropTypes.func.isRequired,
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about where to "sign in to".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
// went wrong. May be replaced with a different error within the
// Login component.
errorText: PropTypes.string,
// If true, the component will consider itself busy.
busy: PropTypes.bool,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
// Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a
// different homeserver without confusing users.
@ -79,12 +74,13 @@ module.exports = React.createClass({
defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done.
// login shouldn't know or care how registration, password recovery,
// etc is done.
onRegisterClick: PropTypes.func.isRequired,
// login shouldn't care how password recovery is done.
onForgotPasswordClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
},
getInitialState: function() {
@ -93,9 +89,6 @@ module.exports = React.createClass({
errorText: null,
loginIncorrect: false,
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
// used for preserving form values when changing homeserver
username: "",
phoneCountry: null,
@ -105,10 +98,6 @@ module.exports = React.createClass({
phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc.
currentFlow: "m.login.password",
// .well-known discovery
discoveryError: "",
findingHomeserver: false,
};
},
@ -132,6 +121,14 @@ module.exports = React.createClass({
this._unmounted = true;
},
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Ensure that we end up actually logging in to the right place
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
},
onPasswordLoginError: function(errorText) {
this.setState({
errorText,
@ -139,10 +136,17 @@ module.exports = React.createClass({
});
},
isBusy: function() {
return this.state.busy || this.props.busy;
},
hasError: function() {
return this.state.errorText || this.props.errorText;
},
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver
// discovery went wrong
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
// Prevent people from submitting their password when something isn't right.
if (this.isBusy() || this.hasError()) return;
this.setState({
busy: true,
@ -164,7 +168,7 @@ module.exports = React.createClass({
const usingEmail = username.indexOf("@") > 0;
if (error.httpStatus === 400 && usingEmail) {
errorText = _t('This homeserver does not support login using email address.');
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError(
error.data.limit_type,
error.data.admin_contact, {
@ -194,11 +198,10 @@ module.exports = React.createClass({
<div>
<div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
{
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
})
}
{_t(
'Please note you are logging into the %(hs)s server, not matrix.org.',
{hs: this.props.serverConfig.hsName},
)}
</div>
</div>
);
@ -232,21 +235,26 @@ module.exports = React.createClass({
this.setState({ username: username });
},
onUsernameBlur: function(username) {
onUsernameBlur: async function(username) {
const doWellknownLookup = username[0] === "@";
this.setState({
username: username,
discoveryError: null,
busy: doWellknownLookup, // unset later by the result of onServerConfigChange
errorText: null,
});
if (username[0] === "@") {
if (doWellknownLookup) {
const serverName = username.split(':').slice(1).join(':');
try {
// we have to append 'https://' to make the URL constructor happy
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
const url = new URL("https://" + serverName);
this._tryWellKnownDiscovery(url.hostname);
const result = await AutoDiscoveryUtils.validateServerName(serverName);
this.props.onServerConfigChange(result);
} catch (e) {
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
let message = _t("Failed to perform homeserver discovery");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({errorText: message, busy: false});
}
}
},
@ -274,32 +282,13 @@ module.exports = React.createClass({
}
},
onServerConfigChange: function(config) {
const self = this;
const newState = {
errorText: null, // reset err messages
};
if (config.hsUrl !== undefined) {
newState.enteredHsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIsUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, function() {
self._initLoginLogic(config.hsUrl || null, config.isUrl);
});
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_LOGIN,
});
@ -313,64 +302,13 @@ module.exports = React.createClass({
});
},
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
this.setState({
discoveryError: "",
findingHomeserver: false,
});
return;
}
this.setState({findingHomeserver: true});
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
this.setState({
discoveryError: discovery["m.homeserver"].error,
findingHomeserver: false,
});
} else if (state === AutoDiscovery.PROMPT) {
this.setState({
discoveryError: "",
findingHomeserver: false,
});
} else if (state === AutoDiscovery.SUCCESS) {
this.setState({
discoveryError: "",
findingHomeserver: false,
});
this.onServerConfigChange({
hsUrl: discovery["m.homeserver"].base_url,
isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "",
});
} else {
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
this.setState({
discoveryError: _t("Unknown failure discovering homeserver"),
findingHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
findingHomeserver: false,
discoveryError: _t("Unknown error discovering homeserver"),
});
}
},
_initLoginLogic: function(hsUrl, isUrl) {
const self = this;
hsUrl = hsUrl || this.state.enteredHsUrl;
isUrl = isUrl || this.state.enteredIsUrl;
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.props.serverConfig.isUrl;
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
// TODO: TravisR - Only use this if the homeserver is the default homeserver
const fallbackHsUrl = this.props.fallbackHsUrl;
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
@ -378,8 +316,6 @@ module.exports = React.createClass({
this._loginLogic = loginLogic;
this.setState({
enteredHsUrl: hsUrl,
enteredIsUrl: isUrl,
busy: true,
loginIncorrect: false,
});
@ -445,8 +381,8 @@ module.exports = React.createClass({
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
(this.state.enteredHsUrl.startsWith("http:") ||
!this.state.enteredHsUrl.startsWith("http"))
(this.props.serverConfig.hsUrl.startsWith("http:") ||
!this.props.serverConfig.hsUrl.startsWith("http"))
) {
errorText = <span>
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
@ -469,9 +405,9 @@ module.exports = React.createClass({
"is not blocking requests.", {},
{
'a': (sub) => {
return <a target="_blank" rel="noopener"
href={this.state.enteredHsUrl}
>{ sub }</a>;
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
{ sub }
</a>;
},
},
) }
@ -484,7 +420,6 @@ module.exports = React.createClass({
renderServerComponent() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
if (SdkConfig.get()['disable_custom_urls']) {
return null;
@ -494,28 +429,19 @@ module.exports = React.createClass({
return null;
}
const serverDetails = <ServerConfig
customHsUrl={this.state.enteredHsUrl}
customIsUrl={this.state.enteredIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={250}
/>;
let nextButton = null;
const serverDetailsProps = {};
if (PHASES_ENABLED) {
nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>;
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
return <div>
{serverDetails}
{nextButton}
</div>;
return <ServerConfig
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
},
renderLoginComponentForStep() {
@ -547,13 +473,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// If the current HS URL is the default HS URL, then we can label it
// with the default HS name (if it exists).
let hsName;
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
hsName = this.props.defaultServerName;
}
return (
<PasswordLogin
onSubmit={this.onPasswordLogin}
@ -569,9 +488,8 @@ module.exports = React.createClass({
onPhoneNumberBlur={this.onPhoneNumberBlur}
onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect}
hsName={hsName}
hsUrl={this.state.enteredHsUrl}
disableSubmit={this.state.findingHomeserver}
serverConfig={this.props.serverConfig}
disableSubmit={this.isBusy()}
/>
);
},
@ -595,9 +513,9 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
const errorText = this.state.errorText || this.props.errorText;
let errorTextSection;
if (errorText) {

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,16 +18,15 @@ limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import * as ServerType from '../../views/auth/ServerTypeSelector';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// Phases
// Show controls to configure server details
@ -46,18 +46,7 @@ module.exports = React.createClass({
sessionId: PropTypes.string,
makeRegistrationUrl: PropTypes.func.isRequired,
idSid: PropTypes.string,
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about "your account".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
brand: PropTypes.string,
email: PropTypes.string,
// registration shouldn't know or care how login is done.
@ -66,7 +55,7 @@ module.exports = React.createClass({
},
getInitialState: function() {
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
return {
busy: false,
@ -87,8 +76,6 @@ module.exports = React.createClass({
// straight back into UI auth
doingUIAuth: Boolean(this.props.sessionId),
serverType,
hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl,
// Phase of the overall registration dialog.
phase: PHASE_REGISTRATION,
flows: null,
@ -100,18 +87,22 @@ module.exports = React.createClass({
this._replaceClient();
},
onServerConfigChange: function(config) {
const newState = {};
if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, () => {
this._replaceClient();
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
this._replaceClient(newProps.serverConfig);
// Handle cases where the user enters "https://matrix.org" for their server
// from the advanced option - we should default to FREE at that point.
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
if (serverType !== this.state.serverType) {
// Reset the phase to default phase for the server type.
this.setState({
serverType,
phase: this.getDefaultPhaseForServerType(serverType),
});
}
},
getDefaultPhaseForServerType(type) {
@ -136,19 +127,17 @@ module.exports = React.createClass({
// the new type.
switch (type) {
case ServerType.FREE: {
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
this.onServerConfigChange({
hsUrl,
isUrl,
});
const { serverConfig } = ServerType.TYPES.FREE;
this.props.onServerConfigChange(serverConfig);
break;
}
case ServerType.PREMIUM:
// We can accept whatever server config was the default here as this essentially
// acts as a slightly different "custom server"/ADVANCED option.
break;
case ServerType.ADVANCED:
this.onServerConfigChange({
hsUrl: this.props.defaultHsUrl,
isUrl: this.props.defaultIsUrl,
});
// Use the default config from the config
this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
break;
}
@ -158,13 +147,15 @@ module.exports = React.createClass({
});
},
_replaceClient: async function() {
_replaceClient: async function(serverConfig) {
this.setState({
errorText: null,
});
if (!serverConfig) serverConfig = this.props.serverConfig;
const {hsUrl, isUrl} = serverConfig;
this._matrixClient = Matrix.createClient({
baseUrl: this.state.hsUrl,
idBaseUrl: this.state.isUrl,
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
try {
await this._makeRegisterRequest({});
@ -189,12 +180,6 @@ module.exports = React.createClass({
},
onFormSubmit: function(formVals) {
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
this.setState({
errorText: "",
busy: true,
@ -203,11 +188,25 @@ module.exports = React.createClass({
});
},
_requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) {
return this._matrixClient.requestRegisterEmailToken(
emailAddress,
clientSecret,
sendAttempt,
this.props.makeRegistrationUrl({
client_secret: clientSecret,
hs_url: this._matrixClient.getHomeserverUrl(),
is_url: this._matrixClient.getIdentityServerUrl(),
session_id: sessionId,
}),
);
},
_onUIAuthFinished: async function(success, response, extra) {
if (!success) {
let msg = response.message || response.toString();
// can we give a better error message?
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError(
response.data.limit_type,
response.data.admin_contact, {
@ -302,8 +301,7 @@ module.exports = React.createClass({
});
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation();
async onServerDetailsNextPhaseClick() {
this.setState({
phase: PHASE_REGISTRATION,
});
@ -348,7 +346,6 @@ module.exports = React.createClass({
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
const ServerConfig = sdk.getComponent("auth.ServerConfig");
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
if (SdkConfig.get()['disable_custom_urls']) {
return null;
@ -365,47 +362,41 @@ module.exports = React.createClass({
</div>;
}
const serverDetailsProps = {};
if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next");
serverDetailsProps.submitClass = "mx_Login_submit";
}
let serverDetails = null;
switch (this.state.serverType) {
case ServerType.FREE:
break;
case ServerType.PREMIUM:
serverDetails = <ModularServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
break;
case ServerType.ADVANCED:
serverDetails = <ServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.props.onServerConfigChange}
delayTimeMs={250}
{...serverDetailsProps}
/>;
break;
}
let nextButton = null;
if (PHASES_ENABLED) {
nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick}
>
{_t("Next")}
</AccessibleButton>;
}
return <div>
<ServerTypeSelector
selected={this.state.serverType}
onChange={this.onServerTypeChange}
/>
{serverDetails}
{nextButton}
</div>;
},
@ -424,7 +415,7 @@ module.exports = React.createClass({
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={this._getUIAuthInputs()}
makeRegistrationUrl={this.props.makeRegistrationUrl}
requestEmailToken={this._requestEmailToken}
sessionId={this.props.sessionId}
clientSecret={this.props.clientSecret}
emailSid={this.props.idSid}
@ -446,13 +437,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// If the current HS URL is the default HS URL, then we can label it
// with the default HS name (if it exists).
let hsName;
if (this.state.hsUrl === this.props.defaultHsUrl) {
hsName = this.props.defaultServerName;
}
return <RegistrationForm
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
@ -462,8 +446,7 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit}
onEditServerDetailsClick={onEditServerDetailsClick}
flows={this.state.flows}
hsName={hsName}
hsUrl={this.state.hsUrl}
serverConfig={this.props.serverConfig}
/>;
}
},
@ -474,7 +457,7 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent('auth.AuthPage');
let errorText;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
const err = this.state.errorText;
if (err) {
errorText = <div className="mx_Login_error">{ err }</div>;
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -57,7 +58,6 @@ import SettingsStore from "../../../settings/SettingsStore";
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
* makeRegistrationUrl A function that makes a registration URL
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
@ -365,7 +365,6 @@ export const EmailIdentityAuthEntry = React.createClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -374,38 +373,6 @@ export const EmailIdentityAuthEntry = React.createClass({
};
},
componentWillMount: function() {
if (this.props.stageState.emailSid === null) {
this.setState({requestingToken: true});
this._requestEmailToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
}
},
/*
* Requests a verification token by email.
*/
_requestEmailToken: function() {
const nextLink = this.props.makeRegistrationUrl({
client_secret: this.props.clientSecret,
hs_url: this.props.matrixClient.getHomeserverUrl(),
is_url: this.props.matrixClient.getIdentityServerUrl(),
session_id: this.props.authSessionId,
});
return this.props.matrixClient.requestRegisterEmailToken(
this.props.inputs.emailAddress,
this.props.clientSecret,
1, // TODO: Multiple send attempts?
nextLink,
).then((result) => {
this.props.setEmailSid(result.sid);
});
},
render: function() {
if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner");

View file

@ -18,9 +18,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?
/*
* Configure the Modular server name.
*
@ -31,65 +37,107 @@ export default class ModularServerConfig extends React.PureComponent {
static propTypes = {
onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
// This component always uses the default IS URL and doesn't allow it
// to be changed. We still receive it as a prop here to simplify
// consumers by still passing the IS URL via onServerConfigChange.
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
// The current configuration that the user is expecting to change.
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
}
// Called after the component calls onServerConfigChange
onAfterSubmit: PropTypes.func,
// Optional text for the submit button. If falsey, no button will be shown.
submitText: PropTypes.string,
// Optional class for the submit button. Only applies if the submit button
// is to be rendered.
submitClass: PropTypes.string,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
delayTimeMs: 0,
}
};
constructor(props) {
super(props);
this.state = {
hsUrl: props.customHsUrl,
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl) return;
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({
hsUrl: newProps.customHsUrl,
hsUrl,
isUrl,
busy: true,
errorText: "",
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: this.props.defaultIsUrl,
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.props.defaultIsUrl,
this.validateServer();
});
});
}
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
};
onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
await this.validateServer();
if (this.props.onAfterSubmit) {
this.props.onAfterSubmit();
}
};
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
@ -100,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent {
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const submitButton = this.props.submitText
? <AccessibleButton
element="button"
type="submit"
className={this.props.submitClass}
onClick={this.onSubmit}
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
: null;
return (
<div className="mx_ServerConfig">
@ -113,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent {
</a>,
},
)}
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.defaultHsUrl}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
/>
</div>
{submitButton}
</form>
</div>
);
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,11 +22,29 @@ import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
/**
* A pure UI component which displays a username/password form.
*/
class PasswordLogin extends React.Component {
export default class PasswordLogin extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
};
static defaultProps = {
onError: function() {},
onEditServerDetailsClick: null,
@ -40,13 +59,12 @@ class PasswordLogin extends React.Component {
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about where to "sign in to".
hsName: null,
hsUrl: "",
disableSubmit: false,
}
};
static LOGIN_FIELD_EMAIL = "login_field_email";
static LOGIN_FIELD_MXID = "login_field_mxid";
static LOGIN_FIELD_PHONE = "login_field_phone";
constructor(props) {
super(props);
@ -193,10 +211,7 @@ class PasswordLogin extends React.Component {
name="username" // make it a little easier for browser's remember-password
key="username_input"
type="text"
label={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
label={_t("Username")}
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
@ -258,20 +273,22 @@ class PasswordLogin extends React.Component {
</span>;
}
let signInToText = _t('Sign in to your Matrix account');
if (this.props.hsName) {
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} catch (e) {
// ignore
}
}
let editLink = null;
@ -353,27 +370,3 @@ class PasswordLogin extends React.Component {
);
}
}
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
hsUrl: PropTypes.string,
disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number';
@ -51,11 +52,7 @@ module.exports = React.createClass({
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
onEditServerDetailsClick: PropTypes.func,
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about "your account".
hsName: PropTypes.string,
hsUrl: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
},
getDefaultProps: function() {
@ -515,20 +512,22 @@ module.exports = React.createClass({
},
render: function() {
let yourMatrixAccountText = _t('Create your Matrix account');
if (this.props.hsName) {
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.hsName,
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
} catch (e) {
// ignore
}
}
let editLink = null;

View file

@ -20,6 +20,9 @@ import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
/*
* A pure UI component which displays the HS and IS to use.
@ -27,82 +30,119 @@ import { _t } from '../../../languageHandler';
export default class ServerConfig extends React.PureComponent {
static propTypes = {
onServerConfigChange: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
// The current configuration that the user is expecting to change.
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
}
// Called after the component calls onServerConfigChange
onAfterSubmit: PropTypes.func,
// Optional text for the submit button. If falsey, no button will be shown.
submitText: PropTypes.string,
// Optional class for the submit button. Only applies if the submit button
// is to be rendered.
submitClass: PropTypes.string,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
delayTimeMs: 0,
}
};
constructor(props) {
super(props);
this.state = {
hsUrl: props.customHsUrl,
isUrl: props.customIsUrl,
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl &&
newProps.customIsUrl === this.state.isUrl) return;
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
hsUrl,
isUrl,
busy: true,
errorText: "",
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
}
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
this.validateServer();
});
});
}
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
}
};
onIdentityServerBlur = (ev) => {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
this.props.onServerConfigChange({
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
this.validateServer();
});
});
}
};
onIdentityServerChange = (ev) => {
const isUrl = ev.target.value;
this.setState({ isUrl });
};
onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
await this.validateServer();
if (this.props.onAfterSubmit) {
this.props.onAfterSubmit();
}
};
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
@ -114,10 +154,24 @@ export default class ServerConfig extends React.PureComponent {
showHelpPopup = () => {
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
}
};
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const errorText = this.state.errorText
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
: null;
const submitButton = this.props.submitText
? <AccessibleButton
element="button"
type="submit"
className={this.props.submitClass}
onClick={this.onSubmit}
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
: null;
return (
<div className="mx_ServerConfig">
@ -127,22 +181,28 @@ export default class ServerConfig extends React.PureComponent {
{ sub }
</a>,
})}
{errorText}
<form onSubmit={this.onSubmit} autoComplete={false} action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.defaultHsUrl}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange}
disabled={this.state.busy}
/>
<Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")}
placeholder={this.props.defaultIsUrl}
placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl}
onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange}
disabled={this.state.busy}
/>
</div>
{submitButton}
</form>
</div>
);
}

View file

@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
@ -32,8 +34,13 @@ export const TYPES = {
label: () => _t('Free'),
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
description: () => _t('Join millions for free on the largest public server'),
hsUrl: 'https://matrix.org',
isUrl: 'https://vector.im',
serverConfig: makeType(ValidatedServerConfig, {
hsUrl: "https://matrix.org",
hsName: "matrix.org",
hsNameIsDifferent: false,
isUrl: "https://vector.im",
identityEnabled: true,
}),
},
PREMIUM: {
id: PREMIUM,
@ -44,6 +51,7 @@ export const TYPES = {
{sub}
</a>,
}),
identityServerUrl: "https://vector.im",
},
ADVANCED: {
id: ADVANCED,
@ -56,10 +64,11 @@ export const TYPES = {
},
};
export function getTypeFromHsUrl(hsUrl) {
export function getTypeFromServerConfig(config) {
const {hsUrl} = config;
if (!hsUrl) {
return null;
} else if (hsUrl === TYPES.FREE.hsUrl) {
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
return FREE;
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
// This is an unlikely case to reach, as Modular defaults to hiding the
@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent {
selected: PropTypes.string,
// Handler called when the selected type changes.
onChange: PropTypes.func.isRequired,
}
};
constructor(props) {
super(props);
@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent {
e.stopPropagation();
const type = e.currentTarget.dataset.id;
this.updateSelectedType(type);
}
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
@ -104,10 +105,14 @@ module.exports = React.createClass({
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]
const urls = props.urls || [];
let urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
urls = props.urls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
}
}
let defaultImageUrl = null;
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 its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
const second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const {
@ -176,20 +148,20 @@ module.exports = React.createClass({
} = this.props;
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name);
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
>
{ initialLetter }
</EmojiText>
</span>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
width={width} height={height} aria-hidden="true" />
);
if (onClick != null) {
return (

View file

@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import sdk from "../../../index";
import DMRoomMap from '../../../utils/DMRoomMap';
import Avatar from '../../../Avatar';
module.exports = React.createClass({
displayName: 'RoomAvatar',
@ -89,7 +89,6 @@ module.exports = React.createClass({
props.resizeMethod,
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props), // lowest priority
].filter(function(url) {
return (url != null && url != "");
});
@ -98,41 +97,14 @@ module.exports = React.createClass({
getRoomAvatarUrl: function(props) {
if (!props.room) return null;
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
return Avatar.avatarUrlForRoom(
props.room,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
},
getOneToOneAvatar: function(props) {
const room = props.room;
if (!room) {
return null;
}
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
otherMember = room.getMember(otherUserId);
} else {
// if the room is not marked as a 1:1, but only has max 2 members
// then still try to show any avatar (pref. other member)
otherMember = room.getAvatarFallbackMember();
}
if (otherMember) {
return otherMember.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
);
}
return null;
},
onRoomAvatarClick: function() {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),

View file

@ -1,5 +1,6 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -29,6 +30,10 @@ export class TopLeftMenu extends React.Component {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
// Optional function to collect a reference to the container
// of this component directly.
containerRef: PropTypes.func,
};
constructor() {
@ -61,44 +66,48 @@ export class TopLeftMenu extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageSection = null;
let homePageItem = null;
if (this.hasHomePage()) {
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
</ul>;
homePageItem = <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage} tabIndex={0}>
{_t("Home")}
</li>;
}
let signInOutSection;
let signInOutItem;
if (isGuest) {
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
</ul>;
signInOutItem = <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn} tabIndex={0}>
{_t("Sign in")}
</li>;
} else {
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
</ul>;
signInOutItem = <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut} tabIndex={0}>
{_t("Sign out")}
</li>;
}
return <div className="mx_TopLeftMenu">
<div className="mx_TopLeftMenu_section_noIcon">
const settingsItem = <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings} tabIndex={0}>
{_t("Settings")}
</li>;
return <div className="mx_TopLeftMenu mx_HiddenFocusable" tabIndex={0} ref={this.props.containerRef}>
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
{homePageSection}
<ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
{homePageItem}
{settingsItem}
{signInOutItem}
</ul>
{signInOutSection}
</div>;
}

View file

@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor {
return <div>
<div className="mx_DevTools_content">
<div className="mx_DevTools_eventTypeStateKeyGroup">
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
</div>
<br />

View file

@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component {
top: y,
message: successful ? _t('Copied!') : _t('Failed to copy'),
}, false);
e.target.onmouseleave = close;
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close;
}
onLinkSpecificEventCheckboxClick() {
@ -131,6 +132,12 @@ export default class ShareDialog extends React.Component {
}
}
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
render() {
let title;
let matrixToUrl;

View file

@ -28,6 +28,7 @@ import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
@ -67,7 +68,7 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab />,
));
if (SettingsStore.getLabsFeatures().length > 0) {
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",

View file

@ -63,6 +63,10 @@ export default function AccessibleButton(props) {
};
}
// Pass through the ref - used for keyboard shortcut access to some buttons
restProps.ref = restProps.inputRef;
delete restProps.inputRef;
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
restProps.className = (restProps.className ? restProps.className + " " : "") +
@ -89,6 +93,7 @@ export default function AccessibleButton(props) {
*/
AccessibleButton.propTypes = {
children: PropTypes.node,
inputRef: PropTypes.func,
element: PropTypes.string,
onClick: PropTypes.func.isRequired,

View file

@ -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,
};

View file

@ -195,13 +195,13 @@ export default class ImageView extends React.Component {
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
<div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label">
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" onClick={ this.rotateCounterClockwise }>
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" title={_t("Rotate Left")} onClick={ this.rotateCounterClockwise }>
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_rotateClockwise" onClick={ this.rotateClockwise }>
<AccessibleButton className="mx_ImageView_rotateClockwise" title={_t("Rotate Right")} onClick={ this.rotateClockwise }>
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }>
<AccessibleButton className="mx_ImageView_cancel" title={_t("Close")} onClick={ this.props.onFinished }>
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
</AccessibleButton>
<div className="mx_ImageView_shim">

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -13,11 +14,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
module.exports = React.createClass({
displayName: 'MemberEventListSummary',
@ -105,7 +108,7 @@ module.exports = React.createClass({
);
});
const desc = this._renderCommaSeparatedList(descs);
const desc = formatCommaSeparatedList(descs);
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
});
@ -114,13 +117,9 @@ module.exports = React.createClass({
return null;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
<EmojiText>
{ summaries.join(", ") }
</EmojiText>
</span>
);
},
@ -132,7 +131,7 @@ module.exports = React.createClass({
* included before "and [n] others".
*/
_renderNameList: function(users) {
return this._renderCommaSeparatedList(users, this.props.summaryLength);
return formatCommaSeparatedList(users, this.props.summaryLength);
},
/**
@ -283,35 +282,6 @@ module.exports = React.createClass({
return res;
},
/**
* Constructs a written English string representing `items`, with an optional limit on
* the number of items included in the result. If specified and if the length of
*`items` is greater than the limit, the string "and n others" will be appended onto
* the result.
* If `items` is empty, returns the empty string. If there is only one item, return
* it.
* @param {string[]} items the items to construct a string from.
* @param {number?} itemLimit the number by which to limit the list.
* @returns {string} a string constructed by joining `items` with a comma between each
* item, but with the last item appended as " and [lastItem]".
*/
_renderCommaSeparatedList(items, itemLimit) {
const remaining = itemLimit === undefined ? 0 : Math.max(
items.length - itemLimit, 0,
);
if (items.length === 0) {
return "";
} else if (items.length === 1) {
return items[0];
} else if (remaining > 0) {
items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
} else {
const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
}
},
_renderAvatars: function(roomMembers) {
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
return (

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,12 +22,14 @@ import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize';
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames';
export default class MessageEditor extends React.Component {
static propTypes = {
@ -40,22 +43,28 @@ export default class MessageEditor extends React.Component {
constructor(props, context) {
super(props, context);
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
room,
);
this.model = new EditorModel(
parseEvent(this.props.event),
parseEvent(this.props.event, room),
partCreator,
this._updateEditorState,
);
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
this.state = {
autoComplete: null,
room,
};
this._editorRef = null;
this._autocompleteRef = null;
this._hasModifications = false;
}
_getRoom() {
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
}
_updateEditorState = (caret) => {
@ -71,18 +80,34 @@ export default class MessageEditor extends React.Component {
}
_onInput = (event) => {
this._hasModifications = true;
const sel = document.getSelection();
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
this.model.update(text, event.inputType, caret);
}
_isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
}
_isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
}
_onKeyDown = (event) => {
// insert newline on Shift+Enter
if (event.shiftKey && event.key === "Enter") {
event.preventDefault(); // just in case the browser does support this
document.execCommand("insertHTML", undefined, "\n");
return;
}
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
if (event.metaKey || event.altKey || event.shiftKey) {
return;
}
if (!this.model.autoComplete) {
return;
}
if (this.model.autoComplete) {
const autoComplete = this.model.autoComplete;
switch (event.key) {
case "Enter":
@ -99,20 +124,55 @@ export default class MessageEditor extends React.Component {
return; // don't preventDefault on anything else
}
event.preventDefault();
} else if (event.key === "Enter") {
this._sendEdit();
event.preventDefault();
} else if (event.key === "Escape") {
this._cancelEdit();
} else if (event.key === "ArrowUp") {
if (this._hasModifications || !this._isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
}
} else if (event.key === "ArrowDown") {
if (this._hasModifications || !this._isCaretAtEnd()) {
return;
}
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.dispatch({action: 'focus_composer'});
}
event.preventDefault();
}
}
_onCancelClicked = () => {
_cancelEdit = () => {
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
}
_onSaveClicked = () => {
_sendEdit = () => {
const newContent = {
"msgtype": "m.text",
"body": textSerialize(this.model),
};
if (requiresHtml(this.model)) {
const contentBody = {
msgtype: newContent.msgtype,
body: ` * ${newContent.body}`,
};
const formattedBody = htmlSerializeIfNeeded(this.model);
if (formattedBody) {
newContent.format = "org.matrix.custom.html";
newContent.formatted_body = htmlSerialize(this.model);
newContent.formatted_body = formattedBody;
contentBody.format = newContent.format;
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
}
const content = Object.assign({
"m.new_content": newContent,
@ -120,12 +180,13 @@ export default class MessageEditor extends React.Component {
"rel_type": "m.replace",
"event_id": this.props.event.getId(),
},
}, newContent);
}, contentBody);
const roomId = this.props.event.getRoomId();
this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
}
_onAutoCompleteConfirm = (completion) => {
@ -138,6 +199,8 @@ export default class MessageEditor extends React.Component {
componentDidMount() {
this._updateEditorState();
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
this._editorRef.focus();
}
render() {
@ -157,7 +220,7 @@ export default class MessageEditor extends React.Component {
</div>;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div className="mx_MessageEditor">
return <div className={classNames("mx_MessageEditor", this.props.className)}>
{ autoComplete }
<div
className="mx_MessageEditor_editor"
@ -166,10 +229,11 @@ export default class MessageEditor extends React.Component {
onInput={this._onInput}
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
aria-label={_t("Edit message")}
></div>
<div className="mx_MessageEditor_buttons">
<AccessibleButton kind="secondary" onClick={this._onCancelClicked}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._onSaveClicked}>{_t("Save")}</AccessibleButton>
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
</div>
</div>;
}

View file

@ -63,6 +63,11 @@ export default class ReplyThread extends React.Component {
static getParentEventId(ev) {
if (!ev || ev.isRedacted()) return;
// XXX: For newer relations (annotations, replacements, etc.), we now
// have a `getRelation` helper on the event, and you might assume it
// could be used here for replies as well... However, the helper
// currently assumes the relation has a `rel_type`, which older replies
// do not, so this block is left as-is for now.
const mRelatesTo = ev.getWireContent()['m.relates_to'];
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
const mInReplyTo = mRelatesTo['m.in_reply_to'];

View 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>
);
}
}

View file

@ -79,6 +79,10 @@ module.exports = React.createClass({
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
} else {
// The tooltip is larger than the parent height: figure out what offset
// we need so that we're still centered.
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset;

View file

@ -117,7 +117,6 @@ export default React.createClass({
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ?
@ -129,9 +128,9 @@ export default React.createClass({
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
});
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
{ groupName }
</EmojiText>;
</div>;
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {

View file

@ -180,7 +180,6 @@ module.exports = React.createClass({
this.props.groupMember.displayname || this.props.groupMember.userId
);
const EmojiText = sdk.getComponent('elements.EmojiText');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
return (
<div className="mx_MemberInfo">
@ -189,7 +188,7 @@ module.exports = React.createClass({
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
{ avatarElement }
<EmojiText element="h2">{ groupMemberName }</EmojiText>
<h2>{ groupMemberName }</h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">

View file

@ -149,7 +149,6 @@ module.exports = React.createClass({
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
@ -221,7 +220,7 @@ module.exports = React.createClass({
</AccessibleButton>
{ avatarElement }
<EmojiText element="h2">{ groupRoomName }</EmojiText>
<h2>{ groupRoomName }</h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">

View file

@ -145,6 +145,7 @@ function remoteRender(event) {
a.target = data.target;
a.download = data.download;
a.style = data.style;
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
a.href = window.URL.createObjectURL(data.blob);
a.appendChild(img);
a.appendChild(document.createTextNode(data.textContent));

View file

@ -172,8 +172,8 @@ export default class MImageBody extends React.Component {
// thumbnail resolution will be unnecessarily reduced.
// custom timeline widths seems preferable.
const pixelRatio = window.devicePixelRatio;
const thumbWidth = 800 * pixelRatio;
const thumbHeight = 600 * pixelRatio;
const thumbWidth = Math.round(800 * pixelRatio);
const thumbHeight = Math.round(600 * pixelRatio);
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {

View file

@ -23,7 +23,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import { createMenu } from '../../structures/ContextualMenu';
import SettingsStore from '../../../settings/SettingsStore';
import { isContentActionable } from '../../../utils/EventUtils';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
@ -148,13 +148,13 @@ export default class MessageActionBar extends React.PureComponent {
title={_t("Reply")}
onClick={this.onReplyClick}
/>;
if (this.isEditingEnabled()) {
}
if (this.isEditingEnabled() && canEditContent(this.props.mxEvent)) {
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
title={_t("Edit")}
onClick={this.onEditClick}
/>;
}
}
return <div className="mx_MessageActionBar">
{agreeDimensionReactionButtons}

View file

@ -90,6 +90,7 @@ module.exports = React.createClass({
tileShape={this.props.tileShape}
maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing}
onHeightChanged={this.props.onHeightChanged} />;
},
});

View file

@ -37,6 +37,7 @@ export default class ReactionDimension extends React.PureComponent {
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.remove", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
}
@ -44,6 +45,7 @@ export default class ReactionDimension extends React.PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.reactions !== this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange);
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
this.onReactionsChange();
}
@ -55,6 +57,10 @@ export default class ReactionDimension extends React.PureComponent {
"Relations.add",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
@ -82,7 +88,7 @@ export default class ReactionDimension extends React.PureComponent {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getContent()["m.relates_to"].key === option;
return mxEvent.getRelation().key === option;
});
if (!reactionForOption) {
continue;
@ -107,7 +113,11 @@ export default class ReactionDimension extends React.PureComponent {
return null;
}
const userId = MatrixClientPeg.get().getUserId();
return reactions.getAnnotationsBySender()[userId];
const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) {
return null;
}
return [...myReactions.values()];
}
onOptionClick = (ev) => {
@ -158,6 +168,7 @@ export default class ReactionDimension extends React.PureComponent {
return <span className="mx_ReactionDimension"
title={this.props.title}
aria-hidden={true}
>
{items}
</span>;

View file

@ -34,6 +34,7 @@ export default class ReactionsRow extends React.PureComponent {
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.remove", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
@ -45,6 +46,7 @@ export default class ReactionsRow extends React.PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.reactions !== this.props.reactions) {
this.props.reactions.on("Relations.add", this.onReactionsChange);
this.props.reactions.on("Relations.remove", this.onReactionsChange);
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
this.onReactionsChange();
}
@ -56,6 +58,10 @@ export default class ReactionsRow extends React.PureComponent {
"Relations.add",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.remove",
this.onReactionsChange,
);
this.props.reactions.removeListener(
"Relations.redaction",
this.onReactionsChange,
@ -80,7 +86,11 @@ export default class ReactionsRow extends React.PureComponent {
return null;
}
const userId = MatrixClientPeg.get().getUserId();
return reactions.getAnnotationsBySender()[userId];
const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) {
return null;
}
return [...myReactions.values()];
}
render() {
@ -101,13 +111,13 @@ export default class ReactionsRow extends React.PureComponent {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getContent()["m.relates_to"].key === content;
return mxEvent.getRelation().key === content;
});
return <ReactionsRowButton
key={content}
content={content}
count={count}
mxEvent={mxEvent}
reactionEvents={events}
myReactionEvent={myReactionEvent}
/>;
});

View file

@ -19,17 +19,28 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
// A possible Matrix event if the current user has voted for this type
myReactionEvent: PropTypes.object,
}
constructor(props) {
super(props);
this.state = {
tooltipVisible: false,
};
}
onClick = (ev) => {
const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) {
@ -48,18 +59,53 @@ export default class ReactionsRowButton extends React.PureComponent {
}
};
onMouseOver = () => {
this.setState({
// To avoid littering the DOM with a tooltip for every reaction,
// only render it on first use.
tooltipRendered: true,
tooltipVisible: true,
});
}
onMouseOut = () => {
this.setState({
tooltipVisible: false,
});
}
render() {
const { content, count, myReactionEvent } = this.props;
const ReactionsRowButtonTooltip =
sdk.getComponent('messages.ReactionsRowButtonTooltip');
const { content, reactionEvents, myReactionEvent } = this.props;
const count = reactionEvents.size;
if (!count) {
return null;
}
const classes = classNames({
mx_ReactionsRowButton: true,
mx_ReactionsRowButton_selected: !!myReactionEvent,
});
let tooltip;
if (this.state.tooltipRendered) {
tooltip = <ReactionsRowButtonTooltip
mxEvent={this.props.mxEvent}
content={content}
reactionEvents={reactionEvents}
visible={this.state.tooltipVisible}
/>;
}
return <span className={classes}
onClick={this.onClick}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
{content} {count}
{tooltip}
</span>;
}
}

View 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;
}
}

View file

@ -19,7 +19,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
@ -95,7 +94,6 @@ export default React.createClass({
},
render() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const {mxEvent} = this.props;
const colorClass = getUserNameColorClass(mxEvent.getSender());
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
@ -117,7 +115,7 @@ export default React.createClass({
/>;
}
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
const nameElem = name || '';
// Name + flair
const nameFlair = <span>

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +23,7 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
import {formatDate} from '../../../DateUtils';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal';
@ -88,7 +90,12 @@ module.exports = React.createClass({
componentDidMount: function() {
this._unmounted = false;
if (!this.props.isEditing) {
this._applyFormatting();
}
},
_applyFormatting() {
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
@ -123,8 +130,14 @@ module.exports = React.createClass({
}
},
componentDidUpdate: function() {
this.calculateUrlPreview();
componentDidUpdate: function(prevProps) {
if (!this.props.isEditing) {
const stoppedEditing = prevProps.isEditing && !this.props.isEditing;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) {
this._applyFormatting();
}
}
},
componentWillUnmount: function() {
@ -140,14 +153,16 @@ module.exports = React.createClass({
nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextProps.isEditing !== this.props.isEditing ||
nextState.links !== this.state.links ||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
nextState.widgetHidden !== this.state.widgetHidden);
},
calculateUrlPreview: function() {
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview && !this.state.links.length) {
if (this.props.showUrlPreview) {
let links = this.findLinks(this.refs.content.children);
if (links.length) {
// de-dup the links (but preserve ordering)
@ -425,8 +440,39 @@ module.exports = React.createClass({
});
},
_onMouseEnterEditedMarker: function() {
this.setState({editedMarkerHovered: true});
},
_onMouseLeaveEditedMarker: function() {
this.setState({editedMarkerHovered: false});
},
_renderEditedMarker: function() {
let editedTooltip;
if (this.state.editedMarkerHovered) {
const Tooltip = sdk.getComponent('elements.Tooltip');
const editEvent = this.props.mxEvent.replacingEvent();
const date = editEvent && formatDate(editEvent.getDate());
editedTooltip = <Tooltip
tooltipClassName="mx_Tooltip_timeline"
label={_t("Edited at %(date)s", {date})}
/>;
}
return (
<div
key="editedMarker" className="mx_EventTile_edited"
onMouseEnter={this._onMouseEnterEditedMarker}
onMouseLeave={this._onMouseLeaveEditedMarker}
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
);
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this.props.isEditing) {
const MessageEditor = sdk.getComponent('elements.MessageEditor');
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
@ -436,6 +482,9 @@ module.exports = React.createClass({
// Part of Replies fallback support
stripReplyFallback: stripReply,
});
if (this.props.replacingEventId) {
body = [body, this._renderEditedMarker()];
}
if (this.props.highlightLink) {
body = <a href={this.props.highlightLink}>{ body }</a>;
@ -462,12 +511,12 @@ module.exports = React.createClass({
return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
<EmojiText
<span
className="mx_MEmoteBody_sender"
onClick={this.onEmoteSenderClick}
>
{ name }
</EmojiText>
</span>
&nbsp;
{ body }
{ widgets }

View file

@ -20,7 +20,6 @@ const React = require('react');
import PropTypes from 'prop-types';
const TextForEvent = require('../../../TextForEvent');
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'TextualEvent',
@ -31,11 +30,10 @@ module.exports = React.createClass({
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null;
return (
<EmojiText element="div" className="mx_TextualEvent">{ text }</EmojiText>
<div className="mx_TextualEvent">{ text }</div>
);
},
});

View 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>;
}
}

View file

@ -256,8 +256,6 @@ export default class Autocomplete extends React.Component {
}
render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let position = 1;
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => {
@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component {
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
{ completionResult.provider.renderCompletions(completions) }
</div>
) : null;

View file

@ -111,7 +111,6 @@ const EntityTile = React.createClass({
let nameEl;
const {name} = this.props;
const EmojiText = sdk.getComponent('elements.EmojiText');
if (!this.props.suppressOnHover) {
const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
@ -128,24 +127,24 @@ const EntityTile = React.createClass({
}
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
<div className="mx_EntityTile_name" dir="auto">
{ name }
</EmojiText>
</div>
{presenceLabel}
</div>
);
} else if (this.props.subtextLabel) {
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
<div className="mx_EntityTile_name" dir="auto">
{name}
</EmojiText>
</div>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
</div>
);
} else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
<div className="mx_EntityTile_name" dir="auto">{ name }</div>
);
}

View file

@ -160,8 +160,11 @@ module.exports = withMatrixClient(React.createClass({
// show twelve hour timestamps
isTwelveHour: PropTypes.bool,
// helper function to access relations for an event
// helper function to access relations for this event
getRelationsForEvent: PropTypes.func,
// whether to show reactions for this event
showReactions: PropTypes.bool,
},
getDefaultProps: function() {
@ -198,7 +201,7 @@ module.exports = withMatrixClient(React.createClass({
const client = this.props.matrixClient;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
}
},
@ -223,7 +226,7 @@ module.exports = withMatrixClient(React.createClass({
const client = this.props.matrixClient;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
}
},
@ -485,6 +488,7 @@ module.exports = withMatrixClient(React.createClass({
getReactions() {
if (
!this.props.showReactions ||
!this.props.getRelationsForEvent ||
!SettingsStore.isFeatureEnabled("feature_reactions")
) {
@ -520,7 +524,10 @@ module.exports = withMatrixClient(React.createClass({
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
);
const tileHandler = getHandlerTile(this.props.mxEvent);
let tileHandler = getHandlerTile(this.props.mxEvent);
if (!tileHandler && SettingsStore.getValue("showHiddenEventsInTimeline")) {
tileHandler = "messages.ViewSourceEvent";
}
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!tileHandler) {
@ -540,6 +547,7 @@ module.exports = withMatrixClient(React.createClass({
const classes = classNames({
mx_EventTile: true,
mx_EventTile_isEditing: this.props.isEditing,
mx_EventTile_info: isInfoMessage,
mx_EventTile_12hr: this.props.isTwelveHour,
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
@ -617,14 +625,14 @@ module.exports = withMatrixClient(React.createClass({
}
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
const actionBar = <MessageActionBar
const actionBar = !this.props.isEditing ? <MessageActionBar
mxEvent={this.props.mxEvent}
reactions={this.state.reactions}
permalinkCreator={this.props.permalinkCreator}
getTile={this.getTile}
getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange}
/>;
/> : undefined;
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -674,14 +682,13 @@ module.exports = withMatrixClient(React.createClass({
switch (this.props.tileShape) {
case 'notif': {
const EmojiText = sdk.getComponent('elements.EmojiText');
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</EmojiText>
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
@ -780,6 +787,7 @@ module.exports = withMatrixClient(React.createClass({
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
@ -789,7 +797,7 @@ module.exports = withMatrixClient(React.createClass({
{ actionBar }
</div>
{
// The avatar goes after the event tile as it's absolutly positioned to be over the
// The avatar goes after the event tile as it's absolutely positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
// the need for further z-indexing chaos)
}

View file

@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({
}
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const EmojiText = sdk.getComponent('elements.EmojiText');
let backButton;
if (this.props.member.roomId) {
@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo_name">
{ backButton }
{ e2eIconElement }
<EmojiText element="h2">{ memberName }</EmojiText>
<h2>{ memberName }</h2>
</div>
{ avatarElement }
<div className="mx_MemberInfo_container">

View file

@ -40,21 +40,18 @@ import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils';
import Autocomplete from './Autocomplete';
import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessages from '../../../ContentMessages';
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
import {
asciiRegexp, unicodeRegexp, shortnameToUnicode,
asciiList, mapUnicodeToShort, toShort,
} from 'emojione';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeUserPermalink} from "../../../matrix-to";
import ReplyPreview from "./ReplyPreview";
@ -62,10 +59,9 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk';
import AccessibleButton from '../elements/AccessibleButton';
import {findEditableEvent} from '../../../utils/EventUtils';
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
@ -144,7 +140,6 @@ export default class MessageComposerInput extends React.Component {
client: MatrixClient;
autocomplete: Autocomplete;
historyManager: ComposerHistoryManager;
constructor(props, context) {
super(props, context);
@ -273,9 +268,8 @@ export default class MessageComposerInput extends React.Component {
case 'emoji':
// XXX: apparently you can't return plain strings from serializer rules
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
return <b>{ obj.data.get('emojiUnicode') }</b>;
// So instead we temporarily wrap emoji from RTE in a span.
return <span>{ obj.data.get('emojiUnicode') }</span>;
}
return this.renderNode({
node: obj,
@ -335,7 +329,6 @@ export default class MessageComposerInput extends React.Component {
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
}
componentWillUnmount() {
@ -375,7 +368,6 @@ export default class MessageComposerInput extends React.Component {
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
forComposerQuote: true,
returnString: true,
emojiOne: false,
});
const fragment = this.html.deserialize(html);
// FIXME: do we want to put in a permalink to the original quote here?
@ -483,6 +475,7 @@ export default class MessageComposerInput extends React.Component {
sendTyping(isTyping) {
if (!SettingsStore.getValue('sendTypingNotifications')) return;
if (SettingsStore.getValue('lowBandwidth')) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT,
@ -538,17 +531,15 @@ export default class MessageComposerInput extends React.Component {
// Automatic replacement of plaintext emoji to Unicode emoji
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emojiMatch) {
// plaintext -> hex unicode
const emojiUc = asciiList[emojiMatch[1]];
// hex unicode -> shortname -> actual unicode
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emoticonMatch) {
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
const unicodeEmoji = data ? data.unicode : '';
const range = Range.create({
anchor: {
key: editorState.startText.key,
offset: currentStartOffset - emojiMatch[1].length - 1,
offset: currentStartOffset - emoticonMatch[1].length - 1,
},
focus: {
key: editorState.startText.key,
@ -561,54 +552,6 @@ export default class MessageComposerInput extends React.Component {
}
}
// emojioneify any emoji
let foundEmoji;
do {
foundEmoji = false;
for (const node of editorState.document.getTexts()) {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match;
EMOJI_REGEX.lastIndex = 0;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchor: {
key: node.key,
offset: match.index,
},
focus: {
key: node.key,
offset: match.index + match[0].length,
},
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
// if we replaced an emoji, start again looking for more
// emoji in the new editor state since doing the replacement
// will change the node structure & offsets so we can't compute
// insertion ranges from node.key / match.index anymore.
foundEmoji = true;
break;
}
}
}
} while (foundEmoji);
// work around weird bug where inserting emoji via the macOS
// emoji picker can leave the selection stuck in the emoji's
// child text. This seems to happen due to selection getting
// moved in the normalisation phase after calculating these changes
if (editorState.selection.anchor.key &&
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
change = change.moveToStartOfNextText();
editorState = change.value;
}
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
let blockType = editorState.blocks.first().type;
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
@ -1046,6 +989,12 @@ export default class MessageComposerInput extends React.Component {
return change.insertText('\n');
}
if (this.autocomplete.countCompletions() > 0) {
this.autocomplete.hide();
ev.preventDefault();
return true;
}
const editorState = this.state.editorState;
const lastBlock = editorState.blocks.last();
@ -1087,7 +1036,6 @@ export default class MessageComposerInput extends React.Component {
if (cmd) {
if (!cmd.error) {
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
this.setState({
editorState: this.createEditorState(),
}, ()=>{
@ -1165,11 +1113,6 @@ export default class MessageComposerInput extends React.Component {
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
let sendTextFn = ContentHelpers.makeTextMessage;
this.historyManager.save(
editorState,
this.state.isRichTextEnabled ? 'rich' : 'markdown',
);
if (commandText && commandText.startsWith('/me')) {
if (replyingToEv) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -1244,14 +1187,16 @@ export default class MessageComposerInput extends React.Component {
// and we must be at the edge of the document (up=start, down=end)
if (up) {
if (!selection.anchor.isAtStartOfNode(document)) return;
} else {
if (!selection.anchor.isAtEndOfNode(document)) return;
}
const selected = this.selectHistory(up);
if (selected) {
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
} else {
this.moveAutocompleteSelection(up);
@ -1259,54 +1204,6 @@ export default class MessageComposerInput extends React.Component {
}
};
selectHistory = async (up) => {
const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message
if (this.historyManager.currentIndex === this.historyManager.history.length) {
// We can't go any further - there isn't any more history, so nop.
if (!up) {
return;
}
this.setState({
currentlyComposedEditorState: this.state.editorState,
});
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
// True when we return to the message being composed currently
this.setState({
editorState: this.state.currentlyComposedEditorState,
});
this.historyManager.currentIndex = this.historyManager.history.length;
return;
}
let editorState;
const historyItem = this.historyManager.getItem(delta);
if (!historyItem) return;
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
editorState = this.richToMdEditorState(historyItem.value);
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
editorState = this.mdToRichEditorState(historyItem.value);
} else {
editorState = historyItem.value;
}
// Move selection to the end of the selected history
const change = editorState.change().moveToEndOfNode(editorState.document);
// We don't call this.onChange(change) now, as fixups on stuff like emoji
// should already have been done and persisted in the history.
editorState = change.value;
this.suppressAutoComplete = true;
this.setState({ editorState }, ()=>{
this._editor.focus();
});
return true;
};
onTab = async (e) => {
this.setState({
someCompletions: null,
@ -1475,17 +1372,7 @@ export default class MessageComposerInput extends React.Component {
}
case 'emoji': {
const { data } = node;
const emojiUnicode = data.get('emojiUnicode');
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
const shortname = toShort(emojiUnicode);
const className = classNames('mx_emojione', {
mx_emojione_selected: isSelected,
});
const style = {};
if (props.selected) style.border = '1px solid blue';
return <img className={ className } src={ uri }
title={ shortname } alt={ emojiUnicode } style={style}
/>;
return data.get('emojiUnicode');
}
}
};

View file

@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component {
if (!this.state.event) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
const EmojiText = sdk.getComponent('views.elements.EmojiText');
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
{ '💬 ' + _t('Replying') }
</EmojiText>
</div>
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
onClick={cancelQuoting} />

View file

@ -147,7 +147,6 @@ module.exports = React.createClass({
render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const EmojiText = sdk.getComponent('elements.EmojiText');
let searchStatus = null;
let cancelButton = null;
@ -191,10 +190,10 @@ module.exports = React.createClass({
roomName = this.props.room.name;
}
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
<div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
{ searchStatus }
</div>;

View file

@ -750,6 +750,7 @@ module.exports = React.createClass({
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
addRoomLabel: _t("Start chat"),
},
{
list: this.state.lists['im.vector.fake.recent'],

View file

@ -342,7 +342,6 @@ module.exports = React.createClass({
badge = <div className={badgeClasses}>{ badgeContent }</div>;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
let label;
let subtextLabel;
let tooltip;
@ -354,14 +353,7 @@ module.exports = React.createClass({
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
if (this.state.selected) {
const nameSelected = <EmojiText>{ name }</EmojiText>;
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
} else {
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
}
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
} else if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import WhoIsTyping from '../../../WhoIsTyping';
import Timer from '../../../utils/Timer';
import MatrixClientPeg from '../../../MatrixClientPeg';
@ -212,15 +211,13 @@ module.exports = React.createClass({
return (<div className="mx_WhoIsTypingTile_empty" />);
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<li className="mx_WhoIsTypingTile">
<div className="mx_WhoIsTypingTile_avatars">
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
</div>
<div className="mx_WhoIsTypingTile_label">
<EmojiText>{ typingString }</EmojiText>
{ typingString }
</div>
</li>
);

View file

@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
} else if (this.state.loading) {
return <Spinner />;
} else if (this.state.backupInfo) {
const EmojiText = sdk.getComponent('elements.EmojiText');
let clientBackupStatus;
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("This device is backing up your keys. ")}<EmojiText></EmojiText></p>
<p> {_t("This device is backing up your keys. ")}</p>
</div>;
} else {
clientBackupStatus = <div>

Some files were not shown because too many files have changed in this diff Show more