Merge branch 'release-v1.5.2'

This commit is contained in:
RiotRobot 2019-09-12 13:01:56 +01:00
commit cc072fb256
19 changed files with 656 additions and 191 deletions

View file

@ -1,3 +1,23 @@
Changes in [1.5.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.5.2) (2019-09-12)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.5.2-rc.1...v1.5.2)
* Fix register page selector buttons growing too wide
[\#3427](https://github.com/matrix-org/matrix-react-sdk/pull/3427)
* Left panel: visual fixes
[\#3426](https://github.com/matrix-org/matrix-react-sdk/pull/3426)
* Hide the change HS url button on SSO login flow if custom urls disabled
[\#3425](https://github.com/matrix-org/matrix-react-sdk/pull/3425)
* RoomDirectory: show spinner if loading more results
[\#3424](https://github.com/matrix-org/matrix-react-sdk/pull/3424)
Changes in [1.5.2-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.5.2-rc.1) (2019-09-11)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.5.1...v1.5.2-rc.1)
* Merge first pass of First Time User Experience to release branch
[\#3420](https://github.com/matrix-org/matrix-react-sdk/pull/3420)
Changes in [1.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.5.1) (2019-08-05) Changes in [1.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.5.1) (2019-08-05)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.5.0-rc.1...v1.5.1) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.5.0-rc.1...v1.5.1)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "1.5.1", "version": "1.5.2",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -84,7 +84,7 @@
"linkifyjs": "^2.1.6", "linkifyjs": "^2.1.6",
"lodash": "^4.17.14", "lodash": "^4.17.14",
"lolex": "2.3.2", "lolex": "2.3.2",
"matrix-js-sdk": "2.3.0", "matrix-js-sdk": "2.3.1",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"pako": "^1.0.5", "pako": "^1.0.5",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",

View file

@ -171,7 +171,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search], :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search],
.mx_textinput { .mx_textinput {
color: $input-darker-fg-color; color: $input-darker-fg-color;
background-color: $input-darker-bg-color; background-color: $primary-bg-color;
border: none; border: none;
} }
} }
@ -330,7 +330,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_header { .mx_Dialog_header {
position: relative; position: relative;
margin-bottom: 20px; margin-bottom: 10px;
} }
.mx_Dialog_title { .mx_Dialog_title {

View file

@ -125,3 +125,53 @@ limitations under the License.
margin-top: 12px; margin-top: 12px;
} }
} }
.mx_LeftPanel_exploreAndFilterRow {
display: flex;
.mx_SearchBox {
flex: 1 1 0;
min-width: 0;
margin: 4px 9px 1px 9px;
}
}
.mx_LeftPanel_explore {
flex: 0 0 50%;
overflow: hidden;
transition: flex-basis 0.2s;
box-sizing: border-box;
&.mx_LeftPanel_explore_hidden {
flex-basis: 0;
}
.mx_AccessibleButton {
font-size: 14px;
margin: 4px 0 1px 9px;
padding: 9px;
padding-left: 42px;
font-weight: 600;
color: $notice-secondary-color;
position: relative;
border-radius: 4px;
&:hover {
background-color: $primary-bg-color;
}
&::before {
cursor: pointer;
mask: url('$(res)/img/explore.svg');
mask-repeat: no-repeat;
mask-position: center center;
content: "";
left: 14px;
top: 10px;
width: 16px;
height: 16px;
background-color: $notice-secondary-color;
position: absolute;
}
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
.mx_RoomDirectory_dialogWrapper > .mx_Dialog { .mx_RoomDirectory_dialogWrapper > .mx_Dialog {
max-width: 960px; max-width: 960px;
height: 100%; height: 100%;
padding: 20px;
} }
.mx_RoomDirectory_dialog { .mx_RoomDirectory_dialog {
@ -35,17 +34,6 @@ limitations under the License.
flex: 1; flex: 1;
} }
.mx_RoomDirectory_createRoom {
background-color: $button-bg-color;
border-radius: 4px;
padding: 8px;
color: $button-fg-color;
font-weight: 600;
position: absolute;
top: 0;
left: 0;
}
.mx_RoomDirectory_list { .mx_RoomDirectory_list {
flex: 1; flex: 1;
display: flex; display: flex;
@ -84,9 +72,8 @@ limitations under the License.
} }
.mx_RoomDirectory_roomAvatar { .mx_RoomDirectory_roomAvatar {
width: 24px; width: 32px;
padding-left: 12px; padding-right: 14px;
padding-right: 24px;
vertical-align: top; vertical-align: top;
} }
@ -94,6 +81,34 @@ limitations under the License.
padding-bottom: 16px; padding-bottom: 16px;
} }
.mx_RoomDirectory_roomMemberCount {
color: $light-fg-color;
width: 60px;
padding: 0 10px;
text-align: center;
&::before {
background-color: $light-fg-color;
display: inline-block;
vertical-align: text-top;
margin-right: 2px;
content: "";
mask: url('$(res)/img/feather-customised/user.svg');
mask-repeat: no-repeat;
mask-position: center;
// scale it down and make the size slightly bigger (16 instead of 14px)
// to avoid rendering artifacts
mask-size: 80%;
width: 16px;
height: 16px;
}
}
.mx_RoomDirectory_join, .mx_RoomDirectory_preview {
width: 80px;
text-align: center;
}
.mx_RoomDirectory_name { .mx_RoomDirectory_name {
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
@ -103,22 +118,9 @@ limitations under the License.
display: inline-block; display: inline-block;
} }
.mx_RoomDirectory_perm {
display: inline;
padding-left: 5px;
padding-right: 5px;
margin-right: 5px;
height: 15px;
border-radius: 11px;
background-color: $plinth-bg-color;
text-transform: uppercase;
font-weight: 600;
font-size: 11px;
color: $accent-color;
}
.mx_RoomDirectory_topic { .mx_RoomDirectory_topic {
cursor: initial; cursor: initial;
color: $light-fg-color;
} }
.mx_RoomDirectory_alias { .mx_RoomDirectory_alias {
@ -126,13 +128,20 @@ limitations under the License.
color: $settings-grey-fg-color; color: $settings-grey-fg-color;
} }
.mx_RoomDirectory_roomMemberCount {
text-align: right;
width: 100px;
padding-right: 10px;
}
.mx_RoomDirectory_table tr { .mx_RoomDirectory_table tr {
padding-bottom: 10px; padding-bottom: 10px;
cursor: pointer; cursor: pointer;
} }
.mx_RoomDirectory .mx_RoomView_MessageList {
padding: 0;
}
.mx_RoomDirectory p {
font-size: 14px;
margin-top: 0;
.mx_AccessibleButton {
padding: 0;
}
}

View file

@ -14,12 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SearchBox_closeButton { .mx_SearchBox {
cursor: pointer; flex: 1 1 0;
background-image: url('$(res)/img/icons-close.svg'); min-width: 0;
background-repeat: no-repeat;
width: 16px; &.mx_SearchBox_blurred:not(:hover) {
height: 16px; background-color: transparent;
background-position: center; }
padding: 9px;
.mx_SearchBox_closeButton {
cursor: pointer;
background-image: url('$(res)/img/icons-close.svg');
background-repeat: no-repeat;
width: 16px;
height: 16px;
background-position: center;
padding: 9px;
}
} }

View file

@ -27,10 +27,6 @@ limitations under the License.
position: relative; position: relative;
} }
.mx_SearchBox {
flex: none;
}
/* hide resize handles next to collapsed / empty sublists */ /* hide resize handles next to collapsed / empty sublists */
.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { .mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
display: none; display: none;

97
res/img/explore.svg Normal file
View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="21.124001"
height="14.896"
viewBox="0 0 21.124001 14.896"
version="1.1"
id="svg21"
sodipodi:docname="explore.svg"
inkscape:version="0.92.4 (unknown)">
<metadata
id="metadata25">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview23"
showgrid="false"
fit-margin-top="0.5"
fit-margin-left="0.5"
fit-margin-right="0.5"
fit-margin-bottom="0.5"
inkscape:zoom="3.1891892"
inkscape:cx="-23.683763"
inkscape:cy="5.4480001"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg21" />
<defs
id="defs15">
<filter
id="a"
width="1.118"
height="1.158"
x="-0.059"
y="-0.079000004"
filterUnits="objectBoundingBox">
<feOffset
dy="2"
in="SourceAlpha"
result="shadowOffsetOuter1"
id="feOffset2" />
<feGaussianBlur
in="shadowOffsetOuter1"
result="shadowBlurOuter1"
stdDeviation="16"
id="feGaussianBlur4" />
<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"
id="feColorMatrix6" />
<feMerge
id="feMerge12">
<feMergeNode
in="shadowMatrixOuter1"
id="feMergeNode8" />
<feMergeNode
in="SourceGraphic"
id="feMergeNode10" />
</feMerge>
</filter>
</defs>
<g
transform="translate(-91.438,-120.552)"
id="g19"
style="fill:none;fill-rule:evenodd;stroke:#61708b;stroke-width:1.29999995;stroke-linecap:round;filter:url(#a)">
<path
d="m 98,122 h 13 m -13,6 h 13 m -13,6 h 13 M 93,122 h 0.5 m -0.5,6 h 0.5 m -0.5,6 h 0.5"
id="path17"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -82,6 +82,9 @@ const LeftPanel = React.createClass({
if (this.state.searchFilter !== nextState.searchFilter) { if (this.state.searchFilter !== nextState.searchFilter) {
return true; return true;
} }
if (this.state.searchExpanded !== nextState.searchExpanded) {
return true;
}
return false; return false;
}, },
@ -204,12 +207,23 @@ const LeftPanel = React.createClass({
if (source === "keyboard") { if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
} }
this.setState({searchExpanded: false});
}, },
collectRoomList: function(ref) { collectRoomList: function(ref) {
this._roomList = ref; this._roomList = ref;
}, },
_onSearchFocus: function() {
this.setState({searchExpanded: true});
},
_onSearchBlur: function(event) {
if (event.target.value.length === 0) {
this.setState({searchExpanded: false});
}
},
render: function() { render: function() {
const RoomList = sdk.getComponent('rooms.RoomList'); const RoomList = sdk.getComponent('rooms.RoomList');
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
@ -218,6 +232,7 @@ const LeftPanel = React.createClass({
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
const SearchBox = sdk.getComponent('structures.SearchBox'); const SearchBox = sdk.getComponent('structures.SearchBox');
const CallPreview = sdk.getComponent('voip.CallPreview'); const CallPreview = sdk.getComponent('voip.CallPreview');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel"); const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
let tagPanelContainer; let tagPanelContainer;
@ -241,11 +256,23 @@ const LeftPanel = React.createClass({
}, },
); );
let exploreButton;
if (!this.props.collapsed) {
exploreButton = (
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}</AccessibleButton>
</div>
);
}
const searchBox = (<SearchBox const searchBox = (<SearchBox
enableRoomSearchFocus={true} enableRoomSearchFocus={true}
placeholder={ _t('Filter room names') } blurredPlaceholder={ _t('Filter') }
placeholder={ _t('Filter rooms…') }
onSearch={ this.onSearch } onSearch={ this.onSearch }
onCleared={ this.onSearchCleared } onCleared={ this.onSearchCleared }
onFocus={this._onSearchFocus}
onBlur={this._onSearchBlur}
collapsed={this.props.collapsed} />); collapsed={this.props.collapsed} />);
let breadcrumbs; let breadcrumbs;
@ -259,7 +286,10 @@ const LeftPanel = React.createClass({
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }> <aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
<TopLeftMenuButton collapsed={ this.props.collapsed } /> <TopLeftMenuButton collapsed={ this.props.collapsed } />
{ breadcrumbs } { breadcrumbs }
{ searchBox } <div className="mx_LeftPanel_exploreAndFilterRow">
{ exploreButton }
{ searchBox }
</div>
<CallPreview ConferenceHandler={VectorConferenceHandler} /> <CallPreview ConferenceHandler={VectorConferenceHandler} />
<RoomList <RoomList
ref={this.collectRoomList} ref={this.collectRoomList}

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -141,6 +142,10 @@ module.exports = React.createClass({
getMoreRooms: function() { getMoreRooms: function() {
if (!MatrixClientPeg.get()) return Promise.resolve(); if (!MatrixClientPeg.get()) return Promise.resolve();
this.setState({
loading: true,
});
const my_filter_string = this.state.filterString; const my_filter_string = this.state.filterString;
const my_server = this.state.roomServer; const my_server = this.state.roomServer;
// remember the next batch token when we sent the request // remember the next batch token when we sent the request
@ -322,12 +327,7 @@ module.exports = React.createClass({
} }
}, },
onCreateRoomClicked: function() { onJoinFromSearchClick: function(alias) {
this.props.onFinished();
dis.dispatch({action: 'view_create_room'});
},
onJoinClick: function(alias) {
// If we don't have a particular instance id selected, just show that rooms alias // If we don't have a particular instance id selected, just show that rooms alias
if (!this.state.instanceId) { if (!this.state.instanceId) {
// If the user specified an alias without a domain, add on whichever server is selected // If the user specified an alias without a domain, add on whichever server is selected
@ -369,6 +369,39 @@ module.exports = React.createClass({
} }
}, },
onPreviewClick: function(room) {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: true,
});
},
onViewClick: function(room) {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: false,
});
},
onJoinClick: function(room) {
this.props.onFinished();
MatrixClientPeg.get().joinRoom(room.room_id);
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
joining: true,
});
},
onCreateRoomClick: function(room) {
this.props.onFinished();
dis.dispatch({action: 'view_create_room'});
},
showRoomAlias: function(alias, autoJoin=false) { showRoomAlias: function(alias, autoJoin=false) {
this.showRoom(null, alias, autoJoin); this.showRoom(null, alias, autoJoin);
}, },
@ -413,74 +446,70 @@ module.exports = React.createClass({
dis.dispatch(payload); dis.dispatch(payload);
}, },
getRows: function() { getRow(room) {
const client = MatrixClientPeg.get();
const clientRoom = client.getRoom(room.room_id);
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
const isGuest = client.isGuest();
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let previewButton;
let joinOrViewButton;
if (!this.state.publicRooms) return []; if (room.world_readable && !hasJoinedRoom) {
previewButton = (
const rooms = this.state.publicRooms; <AccessibleButton kind="secondary" onClick={() => this.onPreviewClick(room)}>{_t("Preview")}</AccessibleButton>
const rows = [];
const self = this;
let guestRead; let guestJoin; let perms;
for (let i = 0; i < rooms.length; i++) {
guestRead = null;
guestJoin = null;
if (rooms[i].world_readable) {
guestRead = (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
);
}
if (rooms[i].guest_can_join) {
guestJoin = (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
);
}
perms = null;
if (guestRead || guestJoin) {
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
}
let name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = rooms[i].topic || '';
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
rows.push(
<tr key={ rooms[i].room_id }
onClick={self.onRoomClicked.bind(self, rooms[i])}
// cancel onMouseDown otherwise shift-clicking highlights text
onMouseDown={(ev) => {ev.preventDefault();}}
>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop'
name={ name } idName={ name }
url={ ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
rooms[i].avatar_url, 24, 24, "crop") } />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic"
onClick={ function(e) { e.stopPropagation(); } }
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(rooms[i]) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ rooms[i].num_joined_members }
</td>
</tr>,
); );
} }
return rows; if (hasJoinedRoom) {
joinOrViewButton = (
<AccessibleButton kind="secondary" onClick={() => this.onViewClick(room)}>{_t("View")}</AccessibleButton>
);
} else if (!isGuest || room.guest_can_join) {
joinOrViewButton = (
<AccessibleButton kind="primary" onClick={() => this.onJoinClick(room)}>{_t("Join")}</AccessibleButton>
);
}
let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = room.topic || '';
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
const avatarUrl = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
room.avatar_url, 32, 32, "crop",
);
return (
<tr key={ room.room_id }
onClick={() => this.onRoomClicked(room)}
// cancel onMouseDown otherwise shift-clicking highlights text
onMouseDown={(ev) => {ev.preventDefault();}}
>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={32} height={32} resizeMethod='crop'
name={ name } idName={ name }
url={ avatarUrl } />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
<div className="mx_RoomDirectory_topic"
onClick={ (ev) => { ev.stopPropagation(); } }
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ room.num_joined_members }
</td>
<td className="mx_RoomDirectory_preview">{previewButton}</td>
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
</tr>
);
}, },
collectScrollPanel: function(element) { collectScrollPanel: function(element) {
@ -531,20 +560,26 @@ module.exports = React.createClass({
let content; let content;
if (this.state.error) { if (this.state.error) {
content = this.state.error; content = this.state.error;
} else if (this.state.protocolsLoading || this.state.loading) { } else if (this.state.protocolsLoading) {
content = <Loader />; content = <Loader />;
} else { } else {
const rows = this.getRows(); const rows = (this.state.publicRooms || []).map(room => this.getRow(room));
// we still show the scrollpanel, at least for now, because // we still show the scrollpanel, at least for now, because
// otherwise we don't fetch more because we don't get a fill // otherwise we don't fetch more because we don't get a fill
// request from the scrollpanel because there isn't one // request from the scrollpanel because there isn't one
let spinner;
if (this.state.loading) {
spinner = <Loader />;
}
let scrollpanel_content; let scrollpanel_content;
if (rows.length == 0) { if (rows.length === 0 && !this.state.loading) {
scrollpanel_content = <i>{ _t('No rooms to show') }</i>; scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
} else { } else {
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table"> scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
<tbody> <tbody>
{ this.getRows() } { rows }
</tbody> </tbody>
</table>; </table>;
} }
@ -556,6 +591,7 @@ module.exports = React.createClass({
startAtBottom={false} startAtBottom={false}
> >
{ scrollpanel_content } { scrollpanel_content }
{ spinner }
</ScrollPanel>; </ScrollPanel>;
} }
@ -577,10 +613,9 @@ module.exports = React.createClass({
instance_expected_field_type = this.protocols[protocolName].field_types[last_field]; instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
} }
let placeholder = _t('Find a room…');
let placeholder = _t('Search for a room');
if (!this.state.instanceId) { if (!this.state.instanceId) {
placeholder = _t('Search for a room like #example') + ':' + this.state.roomServer; placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
} else if (instance_expected_field_type) { } else if (instance_expected_field_type) {
placeholder = instance_expected_field_type.placeholder; placeholder = instance_expected_field_type.placeholder;
} }
@ -596,27 +631,31 @@ module.exports = React.createClass({
listHeader = <div className="mx_RoomDirectory_listheader"> listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox <DirectorySearchBox
className="mx_RoomDirectory_searchbox" className="mx_RoomDirectory_searchbox"
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick} onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
placeholder={placeholder} showJoinButton={showJoinButton} placeholder={placeholder} showJoinButton={showJoinButton}
/> />
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} /> <NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
</div>; </div>;
} }
const explanation =
const createRoomButton = (<AccessibleButton _t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
onClick={this.onCreateRoomClicked} {a: sub => {
className="mx_RoomDirectory_createRoom" return (<AccessibleButton
>{_t("Create new room")}</AccessibleButton>); kind="secondary"
onClick={this.onCreateRoomClick}
>{sub}</AccessibleButton>);
}},
);
return ( return (
<BaseDialog <BaseDialog
className={'mx_RoomDirectory_dialog'} className={'mx_RoomDirectory_dialog'}
hasCancel={true} hasCancel={true}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
headerButton={createRoomButton} title={_t("Explore rooms")}
title={_t("Room directory")}
> >
<div className="mx_RoomDirectory"> <div className="mx_RoomDirectory">
<p>{explanation}</p>
<div className="mx_RoomDirectory_list"> <div className="mx_RoomDirectory_list">
{listHeader} {listHeader}
{content} {content}

View file

@ -21,6 +21,7 @@ import { KeyCode } from '../../Keyboard';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'SearchBox', displayName: 'SearchBox',
@ -46,6 +47,7 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
searchTerm: "", searchTerm: "",
blurred: true,
}; };
}, },
@ -93,7 +95,18 @@ module.exports = React.createClass({
}, },
_onFocus: function(ev) { _onFocus: function(ev) {
this.setState({blurred: false});
ev.target.select(); ev.target.select();
if (this.props.onFocus) {
this.props.onFocus(ev);
}
},
_onBlur: function(ev) {
this.setState({blurred: true});
if (this.props.onBlur) {
this.props.onBlur(ev);
}
}, },
_clearSearch: function(source) { _clearSearch: function(source) {
@ -112,15 +125,21 @@ module.exports = React.createClass({
if (this.props.collapsed) { if (this.props.collapsed) {
return null; return null;
} }
const clearButton = this.state.searchTerm.length > 0 ? const clearButton = !this.state.blurred ?
(<AccessibleButton key="button" (<AccessibleButton key="button"
className="mx_SearchBox_closeButton" className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }> onClick={ () => {this._clearSearch("button"); } }>
</AccessibleButton>) : undefined; </AccessibleButton>) : undefined;
// show a shorter placeholder when blurred, if requested
// this is used for the room filter field that has
// the explore button next to it when blurred
const placeholder = this.state.blurred ?
(this.props.blurredPlaceholder || this.props.placeholder) :
this.props.placeholder;
const className = this.props.className || ""; const className = this.props.className || "";
return ( return (
<div className="mx_SearchBox mx_textinput"> <div className={classNames("mx_SearchBox", "mx_textinput", {"mx_SearchBox_blurred": this.state.blurred})}>
<input <input
key="searchfield" key="searchfield"
type="text" type="text"
@ -130,7 +149,8 @@ module.exports = React.createClass({
onFocus={ this._onFocus } onFocus={ this._onFocus }
onChange={ this.onChange } onChange={ this.onChange }
onKeyDown={ this._onKeyDown } onKeyDown={ this._onKeyDown }
placeholder={ this.props.placeholder } onBlur={this._onBlur}
placeholder={ placeholder }
/> />
{ clearButton } { clearButton }
</div> </div>

View file

@ -94,7 +94,7 @@ module.exports = React.createClass({
// Phase of the overall login dialog. // Phase of the overall login dialog.
phase: PHASE_LOGIN, phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc. // The current login flow, such as password, SSO, etc.
currentFlow: "m.login.password", currentFlow: null, // we need to load the flows from the server
// We perform liveliness checks later, but for now suppress the errors. // We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so // We also track the server dead errors independently of the regular errors so
@ -373,6 +373,7 @@ module.exports = React.createClass({
this.setState({ this.setState({
busy: true, busy: true,
currentFlow: null, // reset flow
loginIncorrect: false, loginIncorrect: false,
}); });
@ -566,6 +567,13 @@ module.exports = React.createClass({
}, },
_renderSsoStep: function(url) { _renderSsoStep: function(url) {
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// XXX: This link does *not* have a target="_blank" because single sign-on relies on // XXX: This link does *not* have a target="_blank" because single sign-on relies on
// redirecting the user back to a URI once they're logged in. On the web, this means // redirecting the user back to a URI once they're logged in. On the web, this means
// we use the same window and redirect back to riot. On electron, this actually // we use the same window and redirect back to riot. On electron, this actually
@ -575,7 +583,12 @@ module.exports = React.createClass({
// user's browser, let them log into their SSO provider, then redirect their browser // user's browser, let them log into their SSO provider, then redirect their browser
// to vector://vector which, of course, will not work. // to vector://vector which, of course, will not work.
return ( return (
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a> <div>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick} />
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
</div>
); );
}, },

View file

@ -31,6 +31,7 @@ export default class PasswordLogin extends React.Component {
static propTypes = { static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password) onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func, onError: PropTypes.func,
onEditServerDetailsClick: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn() onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string, initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string, initialPhoneCountry: PropTypes.string,
@ -257,6 +258,7 @@ export default class PasswordLogin extends React.Component {
render() { render() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let forgotPasswordJsx; let forgotPasswordJsx;
@ -273,33 +275,6 @@ export default class PasswordLogin extends React.Component {
</span>; </span>;
} }
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
const pwFieldClass = classNames({ const pwFieldClass = classNames({
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
}); });
@ -342,10 +317,8 @@ export default class PasswordLogin extends React.Component {
return ( return (
<div> <div>
<h3> <SignInToText serverConfig={this.props.serverConfig}
{signInToText} onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
{editLink}
</h3>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
{loginType} {loginType}
{loginField} {loginField}

View file

@ -0,0 +1,62 @@
/*
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 {_t} from "../../../languageHandler";
import sdk from "../../../index";
import PropTypes from "prop-types";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
export default class SignInToText extends React.PureComponent {
static propTypes = {
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onEditServerDetailsClick: PropTypes.func,
};
render() {
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
return <h3>
{signInToText}
{editLink}
</h3>;
}
}

View file

@ -45,6 +45,8 @@ import MultiInviter from "../../../utils/MultiInviter";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import E2EIcon from "./E2EIcon"; import E2EIcon from "./E2EIcon";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {EventTimeline} from "matrix-js-sdk";
module.exports = withMatrixClient(React.createClass({ module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
@ -61,6 +63,8 @@ module.exports = withMatrixClient(React.createClass({
ban: false, ban: false,
mute: false, mute: false,
modifyLevel: false, modifyLevel: false,
synapseDeactivate: false,
redactMessages: false,
}, },
muted: false, muted: false,
isTargetMod: false, isTargetMod: false,
@ -211,8 +215,8 @@ module.exports = withMatrixClient(React.createClass({
} }
}, },
_updateStateForNewMember: function(member) { _updateStateForNewMember: async function(member) {
const newState = this._calculateOpsPermissions(member); const newState = await this._calculateOpsPermissions(member);
newState.devicesLoading = true; newState.devicesLoading = true;
newState.devices = null; newState.devices = null;
this.setState(newState); this.setState(newState);
@ -349,6 +353,74 @@ module.exports = withMatrixClient(React.createClass({
}); });
}, },
onRedactAllMessages: async function() {
const {roomId, userId} = this.props.member;
const room = this.context.matrixClient.getRoom(roomId);
if (!room) {
return;
}
let timeline = room.getLiveTimeline();
let eventsToRedact = [];
while (timeline) {
eventsToRedact = timeline.getEvents().reduce((events, event) => {
if (event.getSender() === userId && !event.isRedacted()) {
return events.concat(event);
} else {
return events;
}
}, eventsToRedact);
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
const count = eventsToRedact.length;
const user = this.props.member.name;
if (count === 0) {
const InfoDialog = sdk.getComponent("dialogs.InfoDialog");
Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, {
title: _t("No recent messages by %(user)s found", {user}),
description:
<div>
<p>{ _t("Try scrolling up in the timeline to see if there are any earlier ones.") }</p>
</div>,
});
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const confirmed = await new Promise((resolve) => {
Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, {
title: _t("Remove recent messages by %(user)s", {user}),
description:
<div>
<p>{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }</p>
<p>{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }</p>
</div>,
button: _t("Remove %(count)s messages", {count}),
onFinished: resolve,
});
});
if (!confirmed) {
return;
}
// Submitting a large number of redactions freezes the UI,
// so first yield to allow to rerender after closing the dialog.
await Promise.resolve();
console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`);
await Promise.all(eventsToRedact.map(async event => {
try {
await this.context.matrixClient.redactEvent(roomId, event.getId());
} catch (err) {
// log and swallow errors
console.error("Could not redact", event.getId());
console.error(err);
}
}));
console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`);
}
},
_warnSelfDemote: function() { _warnSelfDemote: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
return new Promise((resolve) => { return new Promise((resolve) => {
@ -460,6 +532,25 @@ module.exports = withMatrixClient(React.createClass({
}); });
}, },
onSynapseDeactivate: function() {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
title: _t("Deactivate user?"),
description:
<div>{ _t(
"Deactivating this user will log them out and prevent them from logging back in. Additionally, " +
"they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to " +
"deactivate this user?"
) }</div>,
button: _t("Deactivate user"),
danger: true,
onFinished: (accepted) => {
if (!accepted) return;
this.context.matrixClient.deactivateSynapseUser(this.props.member.userId);
},
});
},
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
this.props.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( this.props.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
@ -544,7 +635,7 @@ module.exports = withMatrixClient(React.createClass({
}); });
}, },
_calculateOpsPermissions: function(member) { _calculateOpsPermissions: async function(member) {
const defaultPerms = { const defaultPerms = {
can: {}, can: {},
muted: false, muted: false,
@ -560,7 +651,7 @@ module.exports = withMatrixClient(React.createClass({
const them = member; const them = member;
return { return {
can: this._calculateCanPermissions( can: await this._calculateCanPermissions(
me, them, powerLevels.getContent(), me, them, powerLevels.getContent(),
), ),
muted: this._isMuted(them, powerLevels.getContent()), muted: this._isMuted(them, powerLevels.getContent()),
@ -568,7 +659,7 @@ module.exports = withMatrixClient(React.createClass({
}; };
}, },
_calculateCanPermissions: function(me, them, powerLevels) { _calculateCanPermissions: async function(me, them, powerLevels) {
const isMe = me.userId === them.userId; const isMe = me.userId === them.userId;
const can = { const can = {
kick: false, kick: false,
@ -576,7 +667,12 @@ module.exports = withMatrixClient(React.createClass({
mute: false, mute: false,
modifyLevel: false, modifyLevel: false,
modifyLevelMax: 0, modifyLevelMax: 0,
redactMessages: false,
}; };
// Calculate permissions for Synapse before doing the PL checks
can.synapseDeactivate = await this.context.matrixClient.isSynapseAdministrator();
const canAffectUser = them.powerLevel < me.powerLevel || isMe; const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) { if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
@ -593,6 +689,7 @@ module.exports = withMatrixClient(React.createClass({
can.mute = me.powerLevel >= editPowerLevel; can.mute = me.powerLevel >= editPowerLevel;
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel); can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevelMax = me.powerLevel; can.modifyLevelMax = me.powerLevel;
can.redactMessages = me.powerLevel >= powerLevels.redact;
return can; return can;
}, },
@ -782,6 +879,8 @@ module.exports = withMatrixClient(React.createClass({
let banButton; let banButton;
let muteButton; let muteButton;
let giveModButton; let giveModButton;
let redactButton;
let synapseDeactivateButton;
let spinner; let spinner;
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) { if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
@ -858,6 +957,15 @@ module.exports = withMatrixClient(React.createClass({
</AccessibleButton> </AccessibleButton>
); );
} }
if (this.state.can.redactMessages) {
redactButton = (
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onRedactAllMessages}>
{ _t("Remove recent messages") }
</AccessibleButton>
);
}
if (this.state.can.ban) { if (this.state.can.ban) {
let label = _t("Ban"); let label = _t("Ban");
if (this.props.member.membership === 'ban') { if (this.props.member.membership === 'ban') {
@ -886,8 +994,19 @@ module.exports = withMatrixClient(React.createClass({
</AccessibleButton>; </AccessibleButton>;
} }
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
// someone does figure out how to bypass this check the worst that happens is an error.
const sameHomeserver = this.props.member.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`);
if (this.state.can.synapseDeactivate && sameHomeserver) {
synapseDeactivateButton = (
<AccessibleButton onClick={this.onSynapseDeactivate} className="mx_MemberInfo_field">
{_t("Deactivate user")}
</AccessibleButton>
);
}
let adminTools; let adminTools;
if (kickButton || banButton || muteButton || giveModButton) { if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton || redactButton) {
adminTools = adminTools =
<div> <div>
<h3>{ _t("Admin Tools") }</h3> <h3>{ _t("Admin Tools") }</h3>
@ -896,7 +1015,9 @@ module.exports = withMatrixClient(React.createClass({
{ muteButton } { muteButton }
{ kickButton } { kickButton }
{ banButton } { banButton }
{ redactButton }
{ giveModButton } { giveModButton }
{ synapseDeactivateButton }
</div> </div>
</div>; </div>;
} }

View file

@ -758,7 +758,7 @@ module.exports = React.createClass({
headerItems: this._getHeaderItems('im.vector.fake.recent'), headerItems: this._getHeaderItems('im.vector.fake.recent'),
order: "recent", order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
onAddRoom: () => {dis.dispatch({action: 'view_room_directory'})}, onAddRoom: () => {dis.dispatch({action: 'view_create_room'});},
}, },
]; ];
const tagSubLists = Object.keys(this.state.lists) const tagSubLists = Object.keys(this.state.lists)

View file

@ -724,11 +724,20 @@
"Unban this user?": "Unban this user?", "Unban this user?": "Unban this user?",
"Ban this user?": "Ban this user?", "Ban this user?": "Ban this user?",
"Failed to ban user": "Failed to ban user", "Failed to ban user": "Failed to ban user",
"No recent messages by %(user)s found": "No recent messages by %(user)s found",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.",
"Remove recent messages by %(user)s": "Remove recent messages by %(user)s",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?",
"For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.",
"Remove %(count)s messages|other": "Remove %(count)s messages",
"Demote yourself?": "Demote yourself?", "Demote yourself?": "Demote yourself?",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
"Demote": "Demote", "Demote": "Demote",
"Failed to mute user": "Failed to mute user", "Failed to mute user": "Failed to mute user",
"Failed to toggle moderator status": "Failed to toggle moderator status", "Failed to toggle moderator status": "Failed to toggle moderator status",
"Deactivate user?": "Deactivate user?",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?",
"Deactivate user": "Deactivate user",
"Failed to change power level": "Failed to change power level", "Failed to change power level": "Failed to change power level",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
"No devices with registered encryption keys": "No devices with registered encryption keys", "No devices with registered encryption keys": "No devices with registered encryption keys",
@ -739,6 +748,7 @@
"Share Link to User": "Share Link to User", "Share Link to User": "Share Link to User",
"User Options": "User Options", "User Options": "User Options",
"Direct chats": "Direct chats", "Direct chats": "Direct chats",
"Remove recent messages": "Remove recent messages",
"Unmute": "Unmute", "Unmute": "Unmute",
"Mute": "Mute", "Mute": "Mute",
"Revoke Moderator": "Revoke Moderator", "Revoke Moderator": "Revoke Moderator",
@ -1383,9 +1393,6 @@
"Username": "Username", "Username": "Username",
"Phone": "Phone", "Phone": "Phone",
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>", "Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
"Change": "Change",
"Sign in with": "Sign in with", "Sign in with": "Sign in with",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Use an email address to recover your account": "Use an email address to recover your account", "Use an email address to recover your account": "Use an email address to recover your account",
@ -1407,6 +1414,7 @@
"Phone (optional)": "Phone (optional)", "Phone (optional)": "Phone (optional)",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />", "Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
"Change": "Change",
"Use an email address to recover your account.": "Use an email address to recover your account.", "Use an email address to recover your account.": "Use an email address to recover your account.",
"Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.",
"Other servers": "Other servers", "Other servers": "Other servers",
@ -1419,6 +1427,8 @@
"Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>", "Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
"Other": "Other", "Other": "Other",
"Find other public servers or use a custom server": "Find other public servers or use a custom server", "Find other public servers or use a custom server": "Find other public servers or use a custom server",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.", "Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
"Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.", "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.",
@ -1470,7 +1480,9 @@
"Community %(groupId)s not found": "Community %(groupId)s not found", "Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities", "This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s", "Failed to load %(groupId)s": "Failed to load %(groupId)s",
"Filter room names": "Filter room names", "Explore": "Explore",
"Filter": "Filter",
"Filter rooms…": "Filter rooms…",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
@ -1507,8 +1519,12 @@
"Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room",
"Fetching third party location failed": "Fetching third party location failed", "Fetching third party location failed": "Fetching third party location failed",
"Unable to look up room ID from server": "Unable to look up room ID from server", "Unable to look up room ID from server": "Unable to look up room ID from server",
"Search for a room": "Search for a room", "Preview": "Preview",
"Search for a room like #example": "Search for a room like #example", "View": "View",
"Find a room…": "Find a room…",
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Explore rooms": "Explore rooms",
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.": "<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.", "<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.": "<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.", "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",

View file

@ -55,6 +55,11 @@ describe('Login', function() {
it('should show form with change server link', function() { it('should show form with change server link', function() {
const root = render(); const root = render();
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
currentFlow: "m.login.password",
});
const form = ReactTestUtils.findRenderedComponentWithType( const form = ReactTestUtils.findRenderedComponentWithType(
root, root,
sdk.getComponent('auth.PasswordLogin'), sdk.getComponent('auth.PasswordLogin'),
@ -75,6 +80,11 @@ describe('Login', function() {
const root = render(); const root = render();
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
currentFlow: "m.login.password",
});
const form = ReactTestUtils.findRenderedComponentWithType( const form = ReactTestUtils.findRenderedComponentWithType(
root, root,
sdk.getComponent('auth.PasswordLogin'), sdk.getComponent('auth.PasswordLogin'),

View file

@ -4958,10 +4958,10 @@ mathml-tag-names@^2.0.1:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw==
matrix-js-sdk@2.3.0: matrix-js-sdk@2.3.1:
version "2.3.0" version "2.3.1"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.3.0.tgz#ed04172add2e31c532dc87e2f38c26c2a63191c6" resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.3.1.tgz#c0ebe90d43611cf28422317ec0c04f5d41acb2ad"
integrity sha512-jeswie7cWK7+XxcD+pQ7LplWnWkOQDa+x6y7FUUnxCdEvaj38cE5Obo9bPMjFgOln2hISlLdR8fzMNE9F4oUJA== integrity sha512-lf2pGHp0o4bDVrSZ5ReLAkMMiX9PngGMxNAtzztdDvQ20lfYZvhwif9PUbi3tt8kwXlfs7s34eWxz5Rg37mdGg==
dependencies: dependencies:
another-json "^0.2.0" another-json "^0.2.0"
babel-runtime "^6.26.0" babel-runtime "^6.26.0"