Merge branches 'develop' and 't3chguy/community_member_invite_IS_text' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/community_member_invite_IS_text

pull/21833/head
Michael Telatynski 2019-09-12 12:53:32 +01:00
commit 6dc69afe67
38 changed files with 975 additions and 393 deletions

View File

@ -93,10 +93,10 @@
"qrcode-react": "^0.1.16",
"qs": "^6.6.0",
"querystring": "^0.2.0",
"react": "^15.6.0",
"react-addons-css-transition-group": "15.3.2",
"react": "^16.9.0",
"react-addons-css-transition-group": "15.6.2",
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^15.6.0",
"react-dom": "^16.9.0",
"react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#f644523",
"resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4",

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],
.mx_textinput {
color: $input-darker-fg-color;
background-color: $input-darker-bg-color;
background-color: $primary-bg-color;
border: none;
}
}
@ -330,7 +330,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_header {
position: relative;
margin-bottom: 20px;
margin-bottom: 10px;
}
.mx_Dialog_title {

View File

@ -125,3 +125,53 @@ limitations under the License.
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 {
max-width: 960px;
height: 100%;
padding: 20px;
}
.mx_RoomDirectory_dialog {
@ -35,17 +34,6 @@ limitations under the License.
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 {
flex: 1;
display: flex;
@ -84,9 +72,8 @@ limitations under the License.
}
.mx_RoomDirectory_roomAvatar {
width: 24px;
padding-left: 12px;
padding-right: 24px;
width: 32px;
padding-right: 14px;
vertical-align: top;
}
@ -94,6 +81,34 @@ limitations under the License.
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 {
display: inline-block;
font-weight: 600;
@ -103,22 +118,9 @@ limitations under the License.
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 {
cursor: initial;
color: $light-fg-color;
}
.mx_RoomDirectory_alias {
@ -126,13 +128,20 @@ limitations under the License.
color: $settings-grey-fg-color;
}
.mx_RoomDirectory_roomMemberCount {
text-align: right;
width: 100px;
padding-right: 10px;
}
.mx_RoomDirectory_table tr {
padding-bottom: 10px;
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.
*/
.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;
.mx_SearchBox {
flex: 1 1 0;
min-width: 0;
&.mx_SearchBox_blurred:not(:hover) {
background-color: transparent;
}
.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;
}
.mx_SearchBox {
flex: none;
}
/* hide resize handles next to collapsed / empty sublists */
.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
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

@ -46,7 +46,7 @@ export function showGroupInviteDialog(groupId) {
_onGroupInviteFinished(groupId, addrs).then(resolve, reject);
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
});
}
@ -81,7 +81,7 @@ export function showGroupAddRoomDialog(groupId) {
_onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject);
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
});
}

View File

@ -65,7 +65,7 @@ export default class IdentityAuthClient {
}
// Returns a promise that resolves to the access_token string from the IS
async getAccessToken(check=true) {
async getAccessToken({ check = true } = {}) {
if (!this.authEnabled) {
// The current IS doesn't support authentication
return null;

View File

@ -57,7 +57,7 @@ export function showStartChatInviteDialog() {
validAddressTypes: ['mx-user-id', 'email'],
button: _t("Start Chat"),
onFinished: _onStartDmFinished,
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
}
export function showRoomInviteDialog(roomId) {
@ -78,7 +78,7 @@ export function showRoomInviteDialog(roomId) {
onFinished: (shouldInvite, addrs) => {
_onRoomInviteFinished(roomId, shouldInvite, addrs);
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
}
/**

View File

@ -116,6 +116,7 @@ export async function startTermsFlow(
}
// if there's anything left to agree to, prompt the user
const numAcceptedBeforeAgreement = agreedUrlSet.size;
if (unagreedPoliciesAndServicePairs.length > 0) {
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
console.log("User has agreed to URLs", newlyAgreedUrls);
@ -125,8 +126,11 @@ export async function startTermsFlow(
console.log("User has already agreed to all required policies");
}
const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) };
await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms);
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length
if (agreedUrlSet.size !== numAcceptedBeforeAgreement) {
const newAcceptedTerms = {accepted: Array.from(agreedUrlSet)};
await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms);
}
const agreePromises = policiesAndServicePairs.map((policiesAndService) => {
// filter the agreed URL list for ones that are actually for this service

View File

@ -16,7 +16,7 @@ limitations under the License.
import IdentityAuthClient from './IdentityAuthClient';
export async function getThreepidBindStatus(client, filterMedium) {
export async function getThreepidsWithBindStatus(client, filterMedium) {
const userId = client.getUserId();
let { threepids } = await client.getThreePids();
@ -24,27 +24,33 @@ export async function getThreepidBindStatus(client, filterMedium) {
threepids = threepids.filter((a) => a.medium === filterMedium);
}
if (threepids.length > 0) {
// TODO: Handle terms agreement
// See https://github.com/vector-im/riot-web/issues/10522
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
// Check bind status assuming we have an IS and terms are agreed
if (threepids.length > 0 && !!client.getIdentityServerUrl()) {
try {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken({ check: false });
// Restructure for lookup query
const query = threepids.map(({ medium, address }) => [medium, address]);
const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
// Restructure for lookup query
const query = threepids.map(({ medium, address }) => [medium, address]);
const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
// Record which are already bound
for (const [medium, address, mxid] of lookupResults.threepids) {
if (mxid !== userId) {
continue;
// Record which are already bound
for (const [medium, address, mxid] of lookupResults.threepids) {
if (mxid !== userId) {
continue;
}
if (filterMedium && medium !== filterMedium) {
continue;
}
const threepid = threepids.find(e => e.medium === medium && e.address === address);
if (!threepid) continue;
threepid.bound = true;
}
if (filterMedium && medium !== filterMedium) {
continue;
} catch (e) {
// Ignore terms errors here and assume other flows handle this
if (!(e.errcode === "M_TERMS_NOT_SIGNED")) {
throw e;
}
const threepid = threepids.find(e => e.medium === medium && e.address === address);
if (!threepid) continue;
threepid.bound = true;
}
}

View File

@ -120,7 +120,7 @@ const CategoryRoomList = createReactClass({
});
});
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
},
render: function() {
@ -297,7 +297,7 @@ const RoleUserList = createReactClass({
});
});
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
},
render: function() {

View File

@ -81,6 +81,9 @@ const LeftPanel = createReactClass({
if (this.state.searchFilter !== nextState.searchFilter) {
return true;
}
if (this.state.searchExpanded !== nextState.searchExpanded) {
return true;
}
return false;
},
@ -203,12 +206,23 @@ const LeftPanel = createReactClass({
if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'});
}
this.setState({searchExpanded: false});
},
collectRoomList: function(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() {
const RoomList = sdk.getComponent('rooms.RoomList');
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
@ -217,6 +231,7 @@ const LeftPanel = createReactClass({
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
const SearchBox = sdk.getComponent('structures.SearchBox');
const CallPreview = sdk.getComponent('voip.CallPreview');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
let tagPanelContainer;
@ -240,11 +255,23 @@ const LeftPanel = createReactClass({
},
);
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
enableRoomSearchFocus={true}
placeholder={ _t('Filter room names') }
blurredPlaceholder={ _t('Filter') }
placeholder={ _t('Filter rooms…') }
onSearch={ this.onSearch }
onCleared={ this.onSearchCleared }
onFocus={this._onSearchFocus}
onBlur={this._onSearchBlur}
collapsed={this.props.collapsed} />);
let breadcrumbs;
@ -258,7 +285,10 @@ const LeftPanel = createReactClass({
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
<TopLeftMenuButton collapsed={ this.props.collapsed } />
{ breadcrumbs }
{ searchBox }
<div className="mx_LeftPanel_exploreAndFilterRow">
{ exploreButton }
{ searchBox }
</div>
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<RoomList
ref={this.collectRoomList}

View File

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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");
you may not use this file except in compliance with the License.
@ -140,6 +141,10 @@ module.exports = createReactClass({
getMoreRooms: function() {
if (!MatrixClientPeg.get()) return Promise.resolve();
this.setState({
loading: true,
});
const my_filter_string = this.state.filterString;
const my_server = this.state.roomServer;
// remember the next batch token when we sent the request
@ -321,12 +326,7 @@ module.exports = createReactClass({
}
},
onCreateRoomClicked: function() {
this.props.onFinished();
dis.dispatch({action: 'view_create_room'});
},
onJoinClick: function(alias) {
onJoinFromSearchClick: function(alias) {
// If we don't have a particular instance id selected, just show that rooms alias
if (!this.state.instanceId) {
// If the user specified an alias without a domain, add on whichever server is selected
@ -368,6 +368,39 @@ module.exports = createReactClass({
}
},
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) {
this.showRoom(null, alias, autoJoin);
},
@ -412,74 +445,70 @@ module.exports = createReactClass({
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 AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let previewButton;
let joinOrViewButton;
if (!this.state.publicRooms) return [];
const rooms = this.state.publicRooms;
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>,
if (room.world_readable && !hasJoinedRoom) {
previewButton = (
<AccessibleButton kind="secondary" onClick={() => this.onPreviewClick(room)}>{_t("Preview")}</AccessibleButton>
);
}
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) {
@ -530,20 +559,26 @@ module.exports = createReactClass({
let content;
if (this.state.error) {
content = this.state.error;
} else if (this.state.protocolsLoading || this.state.loading) {
} else if (this.state.protocolsLoading) {
content = <Loader />;
} 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
// otherwise we don't fetch more because we don't get a fill
// request from the scrollpanel because there isn't one
let spinner;
if (this.state.loading) {
spinner = <Loader />;
}
let scrollpanel_content;
if (rows.length == 0) {
if (rows.length === 0 && !this.state.loading) {
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
} else {
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows() }
{ rows }
</tbody>
</table>;
}
@ -555,6 +590,7 @@ module.exports = createReactClass({
startAtBottom={false}
>
{ scrollpanel_content }
{ spinner }
</ScrollPanel>;
}
@ -576,10 +612,9 @@ module.exports = createReactClass({
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
}
let placeholder = _t('Search for a room');
let placeholder = _t('Find a room…');
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) {
placeholder = instance_expected_field_type.placeholder;
}
@ -595,27 +630,31 @@ module.exports = createReactClass({
listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox
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}
/>
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
</div>;
}
const createRoomButton = (<AccessibleButton
onClick={this.onCreateRoomClicked}
className="mx_RoomDirectory_createRoom"
>{_t("Create new room")}</AccessibleButton>);
const explanation =
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
{a: sub => {
return (<AccessibleButton
kind="secondary"
onClick={this.onCreateRoomClick}
>{sub}</AccessibleButton>);
}},
);
return (
<BaseDialog
className={'mx_RoomDirectory_dialog'}
hasCancel={true}
onFinished={this.props.onFinished}
headerButton={createRoomButton}
title={_t("Room directory")}
title={_t("Explore rooms")}
>
<div className="mx_RoomDirectory">
<p>{explanation}</p>
<div className="mx_RoomDirectory_list">
{listHeader}
{content}

View File

@ -583,7 +583,7 @@ module.exports = createReactClass({
payload.data.description || payload.data.name);
break;
case 'picture_snapshot':
return ContentMessages.sharedInstance().sendContentListToRoom(
ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
);
break;
@ -624,6 +624,11 @@ module.exports = createReactClass({
showApps: payload.show,
});
break;
case 'reply_to_event':
if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) {
this.onCancelSearchClick();
}
break;
}
},

View File

@ -22,6 +22,7 @@ import { KeyCode } from '../../Keyboard';
import dis from '../../dispatcher';
import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
module.exports = createReactClass({
displayName: 'SearchBox',
@ -47,6 +48,7 @@ module.exports = createReactClass({
getInitialState: function() {
return {
searchTerm: "",
blurred: true,
};
},
@ -94,7 +96,18 @@ module.exports = createReactClass({
},
_onFocus: function(ev) {
this.setState({blurred: false});
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) {
@ -113,15 +126,21 @@ module.exports = createReactClass({
if (this.props.collapsed) {
return null;
}
const clearButton = this.state.searchTerm.length > 0 ?
const clearButton = !this.state.blurred ?
(<AccessibleButton key="button"
className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }>
</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 || "";
return (
<div className="mx_SearchBox mx_textinput">
<div className={classNames("mx_SearchBox", "mx_textinput", {"mx_SearchBox_blurred": this.state.blurred})}>
<input
key="searchfield"
type="text"
@ -131,7 +150,8 @@ module.exports = createReactClass({
onFocus={ this._onFocus }
onChange={ this.onChange }
onKeyDown={ this._onKeyDown }
placeholder={ this.props.placeholder }
onBlur={this._onBlur}
placeholder={ placeholder }
/>
{ clearButton }
</div>

View File

@ -685,20 +685,26 @@ const TimelinePanel = createReactClass({
}
this.lastRMSentEventId = this.state.readMarkerEventId;
const roomId = this.props.timelineSet.room.roomId;
const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId);
debuglog('TimelinePanel: Sending Read Markers for ',
this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
' hidden:' + hiddenRR,
);
MatrixClientPeg.get().setRoomReadMarkers(
this.props.timelineSet.room.roomId,
this.state.readMarkerEventId,
lastReadEvent, // Could be null, in which case no RR is sent
{hidden: hiddenRR},
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent,
{hidden: hiddenRR},
).catch((e) => {
console.error(e);
this.lastRRSentEventId = undefined;

View File

@ -93,7 +93,7 @@ module.exports = createReactClass({
// Phase of the overall login dialog.
phase: PHASE_LOGIN,
// 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 also track the server dead errors independently of the regular errors so
@ -372,6 +372,7 @@ module.exports = createReactClass({
this.setState({
busy: true,
currentFlow: null, // reset flow
loginIncorrect: false,
});
@ -565,6 +566,13 @@ module.exports = createReactClass({
},
_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
// 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
@ -574,7 +582,12 @@ module.exports = createReactClass({
// user's browser, let them log into their SSO provider, then redirect their browser
// to vector://vector which, of course, will not work.
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 = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onEditServerDetailsClick: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
@ -257,6 +258,7 @@ export default class PasswordLogin extends React.Component {
render() {
const Field = sdk.getComponent('elements.Field');
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let forgotPasswordJsx;
@ -273,33 +275,6 @@ export default class PasswordLogin extends React.Component {
</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({
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 (
<div>
<h3>
{signInToText}
{editLink}
</h3>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
<form onSubmit={this.onSubmitForm}>
{loginType}
{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

@ -112,6 +112,10 @@ export default class EditMessageComposer extends React.Component {
super(props, context);
this.model = null;
this._editorRef = null;
this.state = {
saveDisabled: true,
};
}
_setEditorRef = ref => {
@ -160,7 +164,7 @@ export default class EditMessageComposer extends React.Component {
dis.dispatch({action: 'focus_composer'});
}
_isModifiedOrSameAsOld(newContent) {
_isContentModified(newContent) {
// if nothing has changed then bail
const oldContent = this.props.editState.getEvent().getContent();
if (!this._editorRef.isModified() ||
@ -176,16 +180,18 @@ export default class EditMessageComposer extends React.Component {
const editedEvent = this.props.editState.getEvent();
const editContent = createEditContent(this.model, editedEvent);
const newContent = editContent["m.new_content"];
if (!this._isModifiedOrSameAsOld(newContent)) {
return;
}
const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit();
this.context.matrixClient.sendMessage(roomId, editContent);
// If content is modified then send an updated event into the room
if (this._isContentModified(newContent)) {
const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit();
this.context.matrixClient.sendMessage(roomId, editContent);
}
// close the event editing and focus composer
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
}
};
_cancelPreviousPendingEdit() {
const originalEvent = this.props.editState.getEvent();
@ -240,6 +246,16 @@ export default class EditMessageComposer extends React.Component {
return caretPosition;
}
_onChange = () => {
if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) {
return;
}
this.setState({
saveDisabled: false,
});
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this._onKeyDown}>
@ -249,10 +265,13 @@ export default class EditMessageComposer extends React.Component {
room={this._getRoom()}
initialCaret={this.props.editState.getCaret()}
label={_t("Edit message")}
onChange={this._onChange}
/>
<div className="mx_EditMessageComposer_buttons">
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit} disabled={this.state.saveDisabled}>
{_t("Save")}
</AccessibleButton>
</div>
</div>);
}

View File

@ -48,6 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import E2EIcon from "./E2EIcon";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {EventTimeline} from "matrix-js-sdk";
module.exports = createReactClass({
displayName: 'MemberInfo',
@ -64,6 +65,7 @@ module.exports = createReactClass({
mute: false,
modifyLevel: false,
synapseDeactivate: false,
redactMessages: false,
},
muted: false,
isTargetMod: false,
@ -356,6 +358,74 @@ module.exports = createReactClass({
});
},
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() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
return new Promise((resolve) => {
@ -572,7 +642,10 @@ module.exports = createReactClass({
_calculateOpsPermissions: async function(member) {
const defaultPerms = {
can: {},
can: {
// Calculate permissions for Synapse before doing the PL checks
synapseDeactivate: await this.context.matrixClient.isSynapseAdministrator(),
},
muted: false,
};
const room = this.context.matrixClient.getRoom(member.roomId);
@ -586,9 +659,10 @@ module.exports = createReactClass({
const them = member;
return {
can: await this._calculateCanPermissions(
me, them, powerLevels.getContent(),
),
can: {
...defaultPerms.can,
...await this._calculateCanPermissions(me, them, powerLevels.getContent()),
},
muted: this._isMuted(them, powerLevels.getContent()),
isTargetMod: them.powerLevel > powerLevels.getContent().users_default,
};
@ -602,11 +676,9 @@ module.exports = createReactClass({
mute: false,
modifyLevel: false,
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;
if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
@ -623,6 +695,7 @@ module.exports = createReactClass({
can.mute = me.powerLevel >= editPowerLevel;
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevelMax = me.powerLevel;
can.redactMessages = me.powerLevel >= powerLevels.redact;
return can;
},
@ -812,6 +885,7 @@ module.exports = createReactClass({
let banButton;
let muteButton;
let giveModButton;
let redactButton;
let synapseDeactivateButton;
let spinner;
@ -892,6 +966,15 @@ module.exports = createReactClass({
</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) {
let label = _t("Ban");
if (this.props.member.membership === 'ban') {
@ -932,7 +1015,7 @@ module.exports = createReactClass({
}
let adminTools;
if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton) {
if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton || redactButton) {
adminTools =
<div>
<h3>{ _t("Admin Tools") }</h3>
@ -941,6 +1024,7 @@ module.exports = createReactClass({
{ muteButton }
{ kickButton }
{ banButton }
{ redactButton }
{ giveModButton }
{ synapseDeactivateButton }
</div>

View File

@ -37,18 +37,19 @@ export default class ReplyPreview extends React.Component {
constructor(props, context) {
super(props, context);
this.unmounted = false;
this.state = {
event: null,
event: RoomViewStore.getQuotingEvent(),
};
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate();
}
componentWillUnmount() {
this.unmounted = true;
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
@ -56,6 +57,8 @@ export default class ReplyPreview extends React.Component {
}
_onRoomViewStoreUpdate() {
if (this.unmounted) return;
const event = RoomViewStore.getQuotingEvent();
if (this.state.event !== event) {
this.setState({ event });

View File

@ -758,7 +758,7 @@ module.exports = createReactClass({
headerItems: this._getHeaderItems('im.vector.fake.recent'),
order: "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)

View File

@ -22,7 +22,7 @@ import sdk from '../../../index';
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import dis from "../../../dispatcher";
import { getThreepidBindStatus } from '../../../boundThreepids';
import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {SERVICE_TYPES} from "matrix-js-sdk";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
@ -93,18 +93,11 @@ export default class SetIdServer extends React.Component {
onAction = (payload) => {
// We react to changes in the ID server in the event the user is staring at this form
// when changing their identity server on another device. If the user is trying to change
// it in two places, we'll end up stomping all over their input, but at that point we
// should question our UX which led to them doing that.
// when changing their identity server on another device.
if (payload.action !== "id_server_changed") return;
const fullUrl = MatrixClientPeg.get().getIdentityServerUrl();
let abbr = '';
if (fullUrl) abbr = abbreviateUrl(fullUrl);
this.setState({
currentClientIdServer: fullUrl,
idServer: abbr,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
});
};
@ -137,15 +130,21 @@ export default class SetIdServer extends React.Component {
MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: fullUrl,
});
this.setState({idServer: '', busy: false, error: null});
this.setState({
busy: false,
error: null,
currentClientIdServer: fullUrl,
idServer: '',
});
};
_checkIdServer = async (e) => {
e.preventDefault();
const { idServer, currentClientIdServer } = this.state;
this.setState({busy: true, checking: true, error: null});
const fullUrl = unabbreviateUrl(this.state.idServer);
const fullUrl = unabbreviateUrl(idServer);
let errStr = await checkIdentityServerUrl(fullUrl);
if (!errStr) {
@ -157,20 +156,49 @@ export default class SetIdServer extends React.Component {
const authClient = new IdentityAuthClient(fullUrl);
await authClient.getAccessToken();
let save = true;
// Double check that the identity server even has terms of service.
const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) {
this._showNoTermsWarning(fullUrl);
return;
let terms;
try {
terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
} catch (e) {
console.error(e);
if (e.cors === "rejected" || e.httpStatus === 404) {
terms = null;
} else {
throw e;
}
}
this._saveIdServer(fullUrl);
if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) {
const [confirmed] = await this._showNoTermsWarning(fullUrl);
save = confirmed;
}
// Show a general warning, possibly with details about any bound
// 3PIDs that would be left behind.
if (save && currentClientIdServer && fullUrl !== currentClientIdServer) {
const [confirmed] = await this._showServerChangeWarning({
title: _t("Change identity server"),
unboundMessage: _t(
"Disconnect from the identity server <current /> and " +
"connect to <new /> instead?", {},
{
current: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
new: sub => <b>{abbreviateUrl(idServer)}</b>,
},
),
button: _t("Continue"),
});
save = confirmed;
}
if (save) {
this._saveIdServer(fullUrl);
}
} catch (e) {
console.error(e);
if (e.cors === "rejected" || e.httpStatus === 404) {
this._showNoTermsWarning(fullUrl);
return;
}
errStr = _t("Terms of service not accepted or the identity server is invalid.");
}
}
@ -179,13 +207,12 @@ export default class SetIdServer extends React.Component {
checking: false,
error: errStr,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
idServer: this.state.idServer,
});
};
_showNoTermsWarning(fullUrl) {
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Identity server has no terms of service"),
description: (
<div>
@ -198,54 +225,67 @@ export default class SetIdServer extends React.Component {
</div>
),
button: _t("Continue"),
onFinished: async (confirmed) => {
if (!confirmed) return;
this._saveIdServer(fullUrl);
},
});
return finished;
}
_onDisconnectClicked = async () => {
this.setState({disconnectBusy: true});
try {
const threepids = await getThreepidBindStatus(MatrixClientPeg.get());
const boundThreepids = threepids.filter(tp => tp.bound);
let message;
if (boundThreepids.length) {
message = _t(
"You are currently sharing email addresses or phone numbers on the identity " +
"server <idserver />. You will need to reconnect to <idserver2 /> to stop " +
"sharing them.", {},
{
idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>,
// XXX: https://github.com/vector-im/riot-web/issues/9086
idserver2: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>,
},
);
} else {
message = _t(
const [confirmed] = await this._showServerChangeWarning({
title: _t("Disconnect identity server"),
unboundMessage: _t(
"Disconnect from the identity server <idserver />?", {},
{idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>},
);
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, {
title: _t("Disconnect Identity Server"),
description: message,
),
button: _t("Disconnect"),
onFinished: (confirmed) => {
if (confirmed) {
this._disconnectIdServer();
}
},
});
if (confirmed) {
this._disconnectIdServer();
}
} finally {
this.setState({disconnectBusy: false});
}
};
async _showServerChangeWarning({ title, unboundMessage, button }) {
const threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get());
const boundThreepids = threepids.filter(tp => tp.bound);
let message;
let danger = false;
if (boundThreepids.length) {
message = <div>
<p>{_t(
"You are still <b>sharing your personal data</b> on the identity " +
"server <idserver />.", {},
{
idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>,
b: sub => <b>{sub}</b>,
},
)}</p>
<p>{_t(
"We recommend that you remove your email addresses and phone numbers " +
"from the identity server before disconnecting.",
)}</p>
</div>;
danger = true;
button = _t("Disconnect anyway");
} else {
message = unboundMessage;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, {
title,
description: message,
button,
cancelButton: _t("Go back"),
danger,
});
return finished;
}
_disconnectIdServer = () => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {

View File

@ -23,8 +23,8 @@ import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
import * as Email from "../../../../email";
import AddThreepid from "../../../../AddThreepid";
const sdk = require('../../../../index');
const Modal = require("../../../../Modal");
import sdk from '../../../../index';
import Modal from '../../../../Modal';
/*
TODO: Improve the UX for everything in here.
@ -113,11 +113,15 @@ export class ExistingEmailAddress extends React.Component {
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
static propTypes = {
emails: PropTypes.array.isRequired,
onEmailsChange: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
emails: [],
verifying: false,
addTask: null,
continueDisabled: false,
@ -125,16 +129,9 @@ export default class EmailAddresses extends React.Component {
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({emails: addresses.threepids.filter((a) => a.medium === 'email')});
});
}
_onRemoved = (address) => {
this.setState({emails: this.state.emails.filter((e) => e !== address)});
const emails = this.props.emails.filter((e) => e !== address);
this.props.onEmailsChange(emails);
};
_onChangeNewEmailAddress = (e) => {
@ -184,12 +181,16 @@ export default class EmailAddresses extends React.Component {
this.state.addTask.checkEmailLinkClicked().then(() => {
const email = this.state.newEmailAddress;
this.setState({
emails: [...this.state.emails, {address: email, medium: "email"}],
addTask: null,
continueDisabled: false,
verifying: false,
newEmailAddress: "",
});
const emails = [
...this.props.emails,
{ address: email, medium: "email" },
];
this.props.onEmailsChange(emails);
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
@ -204,7 +205,7 @@ export default class EmailAddresses extends React.Component {
};
render() {
const existingEmailElements = this.state.emails.map((e) => {
const existingEmailElements = this.props.emails.map((e) => {
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
});

View File

@ -23,8 +23,8 @@ import Field from "../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton";
import AddThreepid from "../../../../AddThreepid";
import CountryDropdown from "../../auth/CountryDropdown";
const sdk = require('../../../../index');
const Modal = require("../../../../Modal");
import sdk from '../../../../index';
import Modal from '../../../../Modal';
/*
TODO: Improve the UX for everything in here.
@ -108,11 +108,15 @@ export class ExistingPhoneNumber extends React.Component {
}
export default class PhoneNumbers extends React.Component {
constructor() {
super();
static propTypes = {
msisdns: PropTypes.array.isRequired,
onMsisdnsChange: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
msisdns: [],
verifying: false,
verifyError: false,
verifyMsisdn: "",
@ -124,16 +128,9 @@ export default class PhoneNumbers extends React.Component {
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({msisdns: addresses.threepids.filter((a) => a.medium === 'msisdn')});
});
}
_onRemoved = (address) => {
this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)});
const msisdns = this.props.msisdns.filter((e) => e !== address);
this.props.onMsisdnsChange(msisdns);
};
_onChangeNewPhoneNumber = (e) => {
@ -181,7 +178,6 @@ export default class PhoneNumbers extends React.Component {
const token = this.state.newPhoneNumberCode;
this.state.addTask.haveMsisdnToken(token).then(() => {
this.setState({
msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}],
addTask: null,
continueDisabled: false,
verifying: false,
@ -190,6 +186,11 @@ export default class PhoneNumbers extends React.Component {
newPhoneNumber: "",
newPhoneNumberCode: "",
});
const msisdns = [
...this.props.msisdns,
{ address: this.state.verifyMsisdn, medium: "msisdn" },
];
this.props.onMsisdnsChange(msisdns);
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
@ -210,7 +211,7 @@ export default class PhoneNumbers extends React.Component {
};
render() {
const existingPhoneElements = this.state.msisdns.map((p) => {
const existingPhoneElements = this.props.msisdns.map((p) => {
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
});

View File

@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import { getThreepidBindStatus } from '../../../../boundThreepids';
/*
TODO: Improve the UX for everything in here.
@ -59,6 +58,11 @@ export class EmailAddress extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
const { bound } = nextProps.email;
this.setState({ bound });
}
async changeBinding({ bind, label, errorTitle }) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.email;
@ -187,27 +191,14 @@ export class EmailAddress extends React.Component {
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
this.state = {
loaded: false,
emails: [],
};
}
async componentWillMount() {
const client = MatrixClientPeg.get();
const emails = await getThreepidBindStatus(client, 'email');
this.setState({ emails });
static propTypes = {
emails: PropTypes.array.isRequired,
}
render() {
let content;
if (this.state.emails.length > 0) {
content = this.state.emails.map((e) => {
if (this.props.emails.length > 0) {
content = this.props.emails.map((e) => {
return <EmailAddress email={e} key={e.address} />;
});
} else {

View File

@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import { getThreepidBindStatus } from '../../../../boundThreepids';
/*
TODO: Improve the UX for everything in here.
@ -51,6 +50,11 @@ export class PhoneNumber extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
const { bound } = nextProps.msisdn;
this.setState({ bound });
}
async changeBinding({ bind, label, errorTitle }) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.msisdn;
@ -206,27 +210,14 @@ export class PhoneNumber extends React.Component {
}
export default class PhoneNumbers extends React.Component {
constructor() {
super();
this.state = {
loaded: false,
msisdns: [],
};
}
async componentWillMount() {
const client = MatrixClientPeg.get();
const msisdns = await getThreepidBindStatus(client, 'msisdn');
this.setState({ msisdns });
static propTypes = {
msisdns: PropTypes.array.isRequired,
}
render() {
let content;
if (this.state.msisdns.length > 0) {
content = this.state.msisdns.map((e) => {
if (this.props.msisdns.length > 0) {
content = this.props.msisdns.map((e) => {
return <PhoneNumber msisdn={e} key={e.address} />;
});
} else {

View File

@ -148,7 +148,18 @@ export default class RolesRoomSettingsTab extends React.Component {
parentObj[keyPath[keyPath.length - 1]] = value;
}
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent);
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, {
title: _t('Error changing power level requirement'),
description: _t(
"An error occurred changing the room's power level requirements. Ensure you have sufficient " +
"permissions and try again.",
),
});
});
};
_onUserPowerLevelChanged = (value, powerLevelKey) => {

View File

@ -37,6 +37,7 @@ import {Service, startTermsFlow} from "../../../../../Terms";
import {SERVICE_TYPES} from "matrix-js-sdk";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
export default class GeneralUserSettingsTab extends React.Component {
static propTypes = {
@ -58,17 +59,20 @@ export default class GeneralUserSettingsTab extends React.Component {
// agreedUrls, // From the startTermsFlow callback
// resolve, // Promise resolve function for startTermsFlow callback
},
emails: [],
msisdns: [],
};
this.dispatcherRef = dis.register(this._onAction);
}
async componentWillMount() {
const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam();
const cli = MatrixClientPeg.get();
const serverRequiresIdServer = await cli.doesServerRequireIdServerParam();
this.setState({serverRequiresIdServer});
// Check to see if terms need accepting
this._checkTerms();
this._getThreepidState();
}
componentWillUnmount() {
@ -78,10 +82,31 @@ export default class GeneralUserSettingsTab extends React.Component {
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
this._checkTerms();
this._getThreepidState();
}
};
_onEmailsChange = (emails) => {
this.setState({ emails });
}
_onMsisdnsChange = (msisdns) => {
this.setState({ msisdns });
}
async _getThreepidState() {
const cli = MatrixClientPeg.get();
// Check to see if terms need accepting
this._checkTerms();
// Need to get 3PIDs generally for Account section and possibly also for
// Discovery (assuming we have an IS and terms are agreed).
const threepids = await getThreepidsWithBindStatus(cli);
this.setState({ emails: threepids.filter((a) => a.medium === 'email') });
this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') });
}
async _checkTerms() {
if (!this.state.haveIdServer) {
this.setState({idServerHasUnsignedTerms: false});
@ -91,7 +116,7 @@ export default class GeneralUserSettingsTab extends React.Component {
// By starting the terms flow we get the logic for checking which terms the user has signed
// for free. So we might as well use that for our own purposes.
const authClient = new IdentityAuthClient();
const idAccessToken = await authClient.getAccessToken(/*check=*/false);
const idAccessToken = await authClient.getAccessToken({ check: false });
startTermsFlow([new Service(
SERVICE_TYPES.IS,
MatrixClientPeg.get().getIdentityServerUrl(),
@ -200,10 +225,16 @@ export default class GeneralUserSettingsTab extends React.Component {
if (this.state.haveIdServer || this.state.serverRequiresIdServer === false) {
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
<EmailAddresses
emails={this.state.emails}
onEmailsChange={this._onEmailsChange}
/>
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers />
<PhoneNumbers
msisdns={this.state.msisdns}
onMsisdnsChange={this._onMsisdnsChange}
/>
</div>;
} else if (this.state.serverRequiresIdServer === null) {
threepidSection = <Spinner />;
@ -279,10 +310,10 @@ export default class GeneralUserSettingsTab extends React.Component {
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
<EmailAddresses emails={this.state.emails} />
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers />
<PhoneNumbers msisdns={this.state.msisdns} />
</div> : null;
return (

View File

@ -54,6 +54,7 @@ export default class LabsUserSettingsTab extends React.Component {
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
</div>
</div>
);

View File

@ -367,6 +367,7 @@
"Show hidden events in timeline": "Show hidden events in timeline",
"Low bandwidth mode": "Low bandwidth mode",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading report": "Uploading report",
@ -554,14 +555,19 @@
"Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)",
"Could not connect to Identity Server": "Could not connect to Identity Server",
"Checking server": "Checking server",
"Change identity server": "Change identity server",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Disconnect from the identity server <current /> and connect to <new /> instead?",
"Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.",
"Identity server has no terms of service": "Identity server has no terms of service",
"The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.",
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
"You are currently sharing email addresses or phone numbers on the identity server <idserver />. You will need to reconnect to <idserver2 /> to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server <idserver />. You will need to reconnect to <idserver2 /> to stop sharing them.",
"Disconnect identity server": "Disconnect identity server",
"Disconnect from the identity server <idserver />?": "Disconnect from the identity server <idserver />?",
"Disconnect Identity Server": "Disconnect Identity Server",
"Disconnect": "Disconnect",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "You are still <b>sharing your personal data</b> on the identity server <idserver />.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.",
"Disconnect anyway": "Disconnect anyway",
"Go back": "Go back",
"Identity Server (%(server)s)": "Identity Server (%(server)s)",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.",
@ -690,6 +696,8 @@
"Failed to unban": "Failed to unban",
"Unban": "Unban",
"Banned by %(displayName)s": "Banned by %(displayName)s",
"Error changing power level requirement": "Error changing power level requirement",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
"Error changing power level": "Error changing power level",
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.",
"Default role": "Default role",
@ -789,6 +797,12 @@
"Unban this user?": "Unban this user?",
"Ban this user?": "Ban this 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?",
"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",
@ -808,6 +822,7 @@
"Share Link to User": "Share Link to User",
"User Options": "User Options",
"Direct chats": "Direct chats",
"Remove recent messages": "Remove recent messages",
"Unmute": "Unmute",
"Mute": "Mute",
"Revoke Moderator": "Revoke Moderator",
@ -1290,7 +1305,6 @@
"If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.",
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback",
"Go back": "Go back",
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
"Failed to upgrade room": "Failed to upgrade room",
"The room upgrade could not be completed": "The room upgrade could not be completed",
@ -1457,8 +1471,6 @@
"Username": "Username",
"Phone": "Phone",
"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 />",
"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?",
"No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.",
@ -1494,6 +1506,8 @@
"Premium": "Premium",
"Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
"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.",
"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.",
@ -1545,7 +1559,9 @@
"Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities",
"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",
"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'?",
@ -1582,8 +1598,12 @@
"Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room",
"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",
"Search for a room": "Search for a room",
"Search for a room like #example": "Search for a room like #example",
"Preview": "Preview",
"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",
"<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>.",

View File

@ -394,4 +394,11 @@ export const SETTINGS = {
// This is a tri-state value, where `null` means "prompt the user".
default: null,
},
"sendReadReceipts": {
supportedLevels: LEVELS_ROOM_SETTINGS,
displayName: _td(
"Send read receipts for messages (requires compatible homeserver to disable)",
),
default: true,
},
};

View File

@ -113,9 +113,19 @@ class RoomViewStore extends Store {
});
break;
case 'reply_to_event':
this._setState({
replyingToEvent: payload.event,
});
// If currently viewed room does not match the room in which we wish to reply then change rooms
// this can happen when performing a search across all rooms
if (payload.event && payload.event.getRoomId() !== this._state.roomId) {
dis.dispatch({
action: 'view_room',
room_id: payload.event.getRoomId(),
replyingToEvent: payload.event,
});
} else {
this._setState({
replyingToEvent: payload.event,
});
}
break;
case 'open_room_settings': {
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
@ -147,6 +157,11 @@ class RoomViewStore extends Store {
isEditingSettings: false,
};
// Allow being given an event to be replied to when switching rooms but sanity check its for this room
if (payload.replyingToEvent && payload.replyingToEvent.getRoomId() === payload.room_id) {
newState.replyingToEvent = payload.replyingToEvent;
}
if (this._state.forwardingEvent) {
dis.dispatch({
action: 'send_event',

View File

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

View File

@ -1857,6 +1857,11 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.4.tgz#9cf2de494ca84060a2a8d2854edd6dfb0445f386"
integrity sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w==
chain-function@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc"
integrity sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==
chalk@2.4.2, "chalk@^1.1.3 || 2.x", chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -2562,6 +2567,13 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-helpers@^3.2.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-serialize@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@ -6165,7 +6177,7 @@ promise@^7.0.3, promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -6344,10 +6356,12 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-addons-css-transition-group@15.3.2:
version "15.3.2"
resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz#d8fa52bec9bb61bdfde8b9e4652b80297cbff667"
integrity sha1-2PpSvsm7Yb396LnkZSuAKXy/9mc=
react-addons-css-transition-group@15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.6.2.tgz#9e4376bcf40b5217d14ec68553081cee4b08a6d6"
integrity sha1-nkN2vPQLUhfRTsaFUwgc7ksIptY=
dependencies:
react-transition-group "^1.2.0"
react-beautiful-dnd@^4.0.1:
version "4.0.1"
@ -6365,16 +6379,6 @@ react-beautiful-dnd@^4.0.1:
redux-thunk "^2.2.0"
reselect "^3.0.1"
react-dom@^15.6.0:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=
dependencies:
fbjs "^0.8.9"
loose-envify "^1.1.0"
object-assign "^4.1.0"
prop-types "^15.5.10"
react-dom@^16.4.2:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
@ -6385,6 +6389,16 @@ react-dom@^16.4.2:
prop-types "^15.6.2"
scheduler "^0.13.6"
react-dom@^16.9.0:
version "16.9.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962"
integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.15.0"
"react-gemini-scrollbar@github:matrix-org/react-gemini-scrollbar#f644523":
version "2.1.5"
resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/f64452388011d37d8a4427ba769153c30700ab8c"
@ -6428,16 +6442,16 @@ react-redux@^5.0.6:
react-is "^16.6.0"
react-lifecycles-compat "^3.0.0"
react@^15.6.0:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=
react-transition-group@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"
integrity sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==
dependencies:
create-react-class "^15.6.0"
fbjs "^0.8.9"
loose-envify "^1.1.0"
object-assign "^4.1.0"
prop-types "^15.5.10"
chain-function "^1.0.0"
dom-helpers "^3.2.0"
loose-envify "^1.3.1"
prop-types "^15.5.6"
warning "^3.0.0"
react@^16.4.2:
version "16.8.6"
@ -6449,6 +6463,15 @@ react@^16.4.2:
prop-types "^15.6.2"
scheduler "^0.13.6"
react@^16.9.0:
version "16.9.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"
integrity sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
read-pkg-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
@ -6901,6 +6924,14 @@ scheduler@^0.13.6:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.15.0.tgz#6bfcf80ff850b280fed4aeecc6513bc0b4f17f8e"
integrity sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@ -8134,6 +8165,13 @@ walk@^2.3.9:
dependencies:
foreachasync "^3.0.0"
warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=
dependencies:
loose-envify "^1.0.0"
watchpack@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"