Merge branches 'develop' and 't3chguy/accesibility' of github.com:matrix-org/matrix-react-sdk into t3chguy/accesibility
This commit is contained in:
commit
ab3e5c3b87
27 changed files with 596 additions and 124 deletions
|
@ -99,6 +99,7 @@
|
||||||
@import "./views/elements/_ResizeHandle.scss";
|
@import "./views/elements/_ResizeHandle.scss";
|
||||||
@import "./views/elements/_RichText.scss";
|
@import "./views/elements/_RichText.scss";
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
|
@import "./views/elements/_RoomAliasField.scss";
|
||||||
@import "./views/elements/_Spinner.scss";
|
@import "./views/elements/_Spinner.scss";
|
||||||
@import "./views/elements/_SyntaxHighlight.scss";
|
@import "./views/elements/_SyntaxHighlight.scss";
|
||||||
@import "./views/elements/_TextWithTooltip.scss";
|
@import "./views/elements/_TextWithTooltip.scss";
|
||||||
|
|
|
@ -14,8 +14,29 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CreateRoomDialog_details_summary {
|
.mx_CreateRoomDialog_details {
|
||||||
outline: none;
|
.mx_CreateRoomDialog_details_summary {
|
||||||
|
outline: none;
|
||||||
|
list-style: none;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $accent-color;
|
||||||
|
|
||||||
|
// list-style doesn't do it for webkit
|
||||||
|
&::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
margin: 5px 0;
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateRoomDialog_label {
|
.mx_CreateRoomDialog_label {
|
||||||
|
@ -36,3 +57,38 @@ limitations under the License.
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needed to make the alias field only grow as wide as needed
|
||||||
|
// as opposed to full width
|
||||||
|
.mx_CreateRoomDialog_aliasContainer {
|
||||||
|
display: flex;
|
||||||
|
// put margin on container so it can collapse with siblings
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
.mx_RoomAliasField {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog {
|
||||||
|
|
||||||
|
&.mx_Dialog_fixedWidth {
|
||||||
|
width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsFlag {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsFlag_label {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ToggleSwitch {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,10 @@ limitations under the License.
|
||||||
border-right: 1px solid $input-border-color;
|
border-right: 1px solid $input-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Field_postfix {
|
||||||
|
border-left: 1px solid $input-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_Field input,
|
.mx_Field input,
|
||||||
.mx_Field select,
|
.mx_Field select,
|
||||||
.mx_Field textarea {
|
.mx_Field textarea {
|
||||||
|
|
56
res/css/views/elements/_RoomAliasField.scss
Normal file
56
res/css/views/elements/_RoomAliasField.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_RoomAliasField {
|
||||||
|
// if parent is a flex container, this allows the
|
||||||
|
// width to be as wide as needed, and not 100%
|
||||||
|
flex: 0 1 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 150px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: $greyed-fg-color;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Field_prefix, .mx_Field_postfix {
|
||||||
|
color: $greyed-fg-color;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 9px 10px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Field_postfix {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
// this allows the domain name to show
|
||||||
|
// as long as it doesn't make the input shrink
|
||||||
|
// if it's too big, it shows an ellipsis
|
||||||
|
// 180: 28 for prefix, 152 for input
|
||||||
|
max-width: calc(100% - 180px);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,9 @@ limitations under the License.
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $message-action-bar-bg-color;
|
background-color: $message-action-bar-bg-color;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
// equal to z-index of mx_ReplyPreview and mx_RoomView_statusArea (1000)
|
||||||
|
// but as it appears after them in the DOM, will appear on top.
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
&.mx_MessageComposerFormatBar_shown {
|
&.mx_MessageComposerFormatBar_shown {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -35,6 +35,8 @@ import IdentityAuthClient from './IdentityAuthClient';
|
||||||
export default class AddThreepid {
|
export default class AddThreepid {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
||||||
|
this.sessionId = null;
|
||||||
|
this.submitUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,6 +103,7 @@ export default class AddThreepid {
|
||||||
phoneCountry, phoneNumber, this.clientSecret, 1,
|
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
|
this.submitUrl = res.submit_url;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.errcode === 'M_THREEPID_IN_USE') {
|
if (err.errcode === 'M_THREEPID_IN_USE') {
|
||||||
|
@ -197,13 +200,23 @@ export default class AddThreepid {
|
||||||
*/
|
*/
|
||||||
async haveMsisdnToken(msisdnToken) {
|
async haveMsisdnToken(msisdnToken) {
|
||||||
const authClient = new IdentityAuthClient();
|
const authClient = new IdentityAuthClient();
|
||||||
const identityAccessToken = await authClient.getAccessToken();
|
|
||||||
const result = await MatrixClientPeg.get().submitMsisdnToken(
|
let result;
|
||||||
this.sessionId,
|
if (this.submitUrl) {
|
||||||
this.clientSecret,
|
result = await MatrixClientPeg.get().submitMsisdnTokenOtherUrl(
|
||||||
msisdnToken,
|
this.submitUrl,
|
||||||
identityAccessToken,
|
this.sessionId,
|
||||||
);
|
this.clientSecret,
|
||||||
|
msisdnToken,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await MatrixClientPeg.get().submitMsisdnToken(
|
||||||
|
this.sessionId,
|
||||||
|
this.clientSecret,
|
||||||
|
msisdnToken,
|
||||||
|
await authClient.getAccessToken(),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (result.errcode) {
|
if (result.errcode) {
|
||||||
throw result;
|
throw result;
|
||||||
}
|
}
|
||||||
|
@ -211,13 +224,11 @@ export default class AddThreepid {
|
||||||
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
if (this.bind) {
|
if (this.bind) {
|
||||||
const authClient = new IdentityAuthClient();
|
|
||||||
const identityAccessToken = await authClient.getAccessToken();
|
|
||||||
await MatrixClientPeg.get().bindThreePid({
|
await MatrixClientPeg.get().bindThreePid({
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: identityServerDomain,
|
id_server: identityServerDomain,
|
||||||
id_access_token: identityAccessToken,
|
id_access_token: await authClient.getAccessToken(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await MatrixClientPeg.get().addThreePidOnly({
|
await MatrixClientPeg.get().addThreePidOnly({
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -72,15 +73,21 @@ class PasswordReset {
|
||||||
* with a "message" property which contains a human-readable message detailing why
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
||||||
*/
|
*/
|
||||||
checkEmailLinkClicked() {
|
async checkEmailLinkClicked() {
|
||||||
return this.client.setPassword({
|
const creds = {
|
||||||
type: "m.login.email.identity",
|
sid: this.sessionId,
|
||||||
threepid_creds: {
|
client_secret: this.clientSecret,
|
||||||
sid: this.sessionId,
|
};
|
||||||
client_secret: this.clientSecret,
|
if (await this.doesServerRequireIdServerParam()) {
|
||||||
id_server: this.identityServerDomain,
|
creds.id_server = this.identityServerDomain;
|
||||||
},
|
}
|
||||||
}, this.password).catch(function(err) {
|
|
||||||
|
try {
|
||||||
|
await this.client.setPassword({
|
||||||
|
type: "m.login.email.identity",
|
||||||
|
threepid_creds: creds,
|
||||||
|
}, this.password);
|
||||||
|
} catch (err) {
|
||||||
if (err.httpStatus === 401) {
|
if (err.httpStatus === 401) {
|
||||||
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
||||||
} else if (err.httpStatus === 404) {
|
} else if (err.httpStatus === 404) {
|
||||||
|
@ -90,7 +97,7 @@ class PasswordReset {
|
||||||
err.message += ` (Status ${err.httpStatus})`;
|
err.message += ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
return matches.map((result) => ({
|
return matches.map((result) => ({
|
||||||
// If the command is the same as the one they entered, we don't want to discard their arguments
|
// If the command is the same as the one they entered, we don't want to discard their arguments
|
||||||
completion: result.command === command[1] ? command[0] : (result.command + ' '),
|
completion: result.command === command[1] ? command[0] : (result.command + ' '),
|
||||||
|
type: "command",
|
||||||
component: <TextualCompletion
|
component: <TextualCompletion
|
||||||
title={result.command}
|
title={result.command}
|
||||||
subtitle={result.args}
|
subtitle={result.args}
|
||||||
|
|
|
@ -84,6 +84,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||||
]).map(({avatarUrl, groupId, name}) => ({
|
]).map(({avatarUrl, groupId, name}) => ({
|
||||||
completion: groupId,
|
completion: groupId,
|
||||||
suffix: ' ',
|
suffix: ' ',
|
||||||
|
type: "community",
|
||||||
href: makeGroupPermalink(groupId),
|
href: makeGroupPermalink(groupId),
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion initialComponent={
|
<PillCompletion initialComponent={
|
||||||
|
|
|
@ -42,6 +42,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
return [{
|
return [{
|
||||||
completion: '@room',
|
completion: '@room',
|
||||||
completionId: '@room',
|
completionId: '@room',
|
||||||
|
type: "at-room",
|
||||||
suffix: ' ',
|
suffix: ' ',
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
|
||||||
|
|
|
@ -89,6 +89,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
completionId: displayAlias,
|
completionId: displayAlias,
|
||||||
|
type: "room",
|
||||||
suffix: ' ',
|
suffix: ' ',
|
||||||
href: makeRoomPermalink(displayAlias),
|
href: makeRoomPermalink(displayAlias),
|
||||||
component: (
|
component: (
|
||||||
|
|
|
@ -114,6 +114,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
// relies on the length of the entity === length of the text in the decoration.
|
// relies on the length of the entity === length of the text in the decoration.
|
||||||
completion: user.rawDisplayName,
|
completion: user.rawDisplayName,
|
||||||
completionId: user.userId,
|
completionId: user.userId,
|
||||||
|
type: "user",
|
||||||
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
|
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
|
||||||
href: makeUserPermalink(user.userId),
|
href: makeUserPermalink(user.userId),
|
||||||
component: (
|
component: (
|
||||||
|
|
|
@ -167,6 +167,7 @@ export default class ContextualMenu extends React.Component {
|
||||||
|
|
||||||
const menuClasses = classNames({
|
const menuClasses = classNames({
|
||||||
'mx_ContextualMenu': true,
|
'mx_ContextualMenu': true,
|
||||||
|
'mx_HiddenFocusable': true, // hide browser outline
|
||||||
'mx_ContextualMenu_left': !hasChevron && position.left,
|
'mx_ContextualMenu_left': !hasChevron && position.left,
|
||||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||||
|
|
|
@ -271,6 +271,10 @@ export default createReactClass({
|
||||||
|
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
|
|
||||||
|
// object field used for tracking the status info appended to the title tag.
|
||||||
|
// we don't do it as react state as i'm scared about triggering needless react refreshes.
|
||||||
|
this.subTitleStatus = '';
|
||||||
|
|
||||||
// this can technically be done anywhere but doing this here keeps all
|
// this can technically be done anywhere but doing this here keeps all
|
||||||
// the routing url path logic together.
|
// the routing url path logic together.
|
||||||
if (this.onAliasClick) {
|
if (this.onAliasClick) {
|
||||||
|
@ -870,9 +874,10 @@ export default createReactClass({
|
||||||
if (roomInfo.event_id && roomInfo.highlighted) {
|
if (roomInfo.event_id && roomInfo.highlighted) {
|
||||||
presentedId += "/" + roomInfo.event_id;
|
presentedId += "/" + roomInfo.event_id;
|
||||||
}
|
}
|
||||||
this.notifyNewScreen('room/' + presentedId);
|
|
||||||
newState.ready = true;
|
newState.ready = true;
|
||||||
this.setState(newState);
|
this.setState(newState, ()=>{
|
||||||
|
this.notifyNewScreen('room/' + presentedId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -962,11 +967,8 @@ export default createReactClass({
|
||||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
|
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
|
||||||
|
|
||||||
const [shouldCreate, name, noFederate] = await modal.finished;
|
const [shouldCreate, createOpts] = await modal.finished;
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
const createOpts = {};
|
|
||||||
if (name) createOpts.name = name;
|
|
||||||
if (noFederate) createOpts.creation_content = {'m.federate': false};
|
|
||||||
createRoom({createOpts}).done();
|
createRoom({createOpts}).done();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1300,6 +1302,7 @@ export default createReactClass({
|
||||||
collapsedRhs: false,
|
collapsedRhs: false,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
|
this.subTitleStatus = '';
|
||||||
this._setPageSubtitle();
|
this._setPageSubtitle();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1315,6 +1318,7 @@ export default createReactClass({
|
||||||
collapsedRhs: false,
|
collapsedRhs: false,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
|
this.subTitleStatus = '';
|
||||||
this._setPageSubtitle();
|
this._setPageSubtitle();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1709,6 +1713,7 @@ export default createReactClass({
|
||||||
if (this.props.onNewScreen) {
|
if (this.props.onNewScreen) {
|
||||||
this.props.onNewScreen(screen);
|
this.props.onNewScreen(screen);
|
||||||
}
|
}
|
||||||
|
this._setPageSubtitle();
|
||||||
},
|
},
|
||||||
|
|
||||||
onAliasClick: function(event, alias) {
|
onAliasClick: function(event, alias) {
|
||||||
|
@ -1824,7 +1829,14 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_setPageSubtitle: function(subtitle='') {
|
_setPageSubtitle: function(subtitle='') {
|
||||||
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`;
|
if (this.state.currentRoomId) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const room = client && client.getRoom(this.state.currentRoomId);
|
||||||
|
if (room) {
|
||||||
|
subtitle = `| ${ room.name } ${subtitle}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle} ${this.subTitleStatus}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStatusIndicator: function(state, prevState) {
|
updateStatusIndicator: function(state, prevState) {
|
||||||
|
@ -1835,15 +1847,15 @@ export default createReactClass({
|
||||||
PlatformPeg.get().setNotificationCount(notifCount);
|
PlatformPeg.get().setNotificationCount(notifCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitle = '';
|
this.subTitleStatus = '';
|
||||||
if (state === "ERROR") {
|
if (state === "ERROR") {
|
||||||
subtitle += `[${_t("Offline")}] `;
|
this.subTitleStatus += `[${_t("Offline")}] `;
|
||||||
}
|
}
|
||||||
if (notifCount > 0) {
|
if (notifCount > 0) {
|
||||||
subtitle += `[${notifCount}]`;
|
this.subTitleStatus += `[${notifCount}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setPageSubtitle(subtitle);
|
this._setPageSubtitle();
|
||||||
},
|
},
|
||||||
|
|
||||||
onCloseAllSettings() {
|
onCloseAllSettings() {
|
||||||
|
|
|
@ -117,17 +117,18 @@ module.exports = createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onVerify: function(ev) {
|
onVerify: async function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!this.reset) {
|
if (!this.reset) {
|
||||||
console.error("onVerify called before submitPasswordReset!");
|
console.error("onVerify called before submitPasswordReset!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.reset.checkEmailLinkClicked().done((res) => {
|
try {
|
||||||
|
await this.reset.checkEmailLinkClicked();
|
||||||
this.setState({ phase: PHASE_DONE });
|
this.setState({ phase: PHASE_DONE });
|
||||||
}, (err) => {
|
} catch (err) {
|
||||||
this.showErrorDialog(err.message);
|
this.showErrorDialog(err.message);
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmitForm: async function(ev) {
|
onSubmitForm: async function(ev) {
|
||||||
|
|
|
@ -420,6 +420,7 @@ export const MsisdnAuthEntry = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._submitUrl = null;
|
||||||
this._sid = null;
|
this._sid = null;
|
||||||
this._msisdn = null;
|
this._msisdn = null;
|
||||||
this._tokenBox = null;
|
this._tokenBox = null;
|
||||||
|
@ -442,6 +443,7 @@ export const MsisdnAuthEntry = createReactClass({
|
||||||
this.props.clientSecret,
|
this.props.clientSecret,
|
||||||
1, // TODO: Multiple send attempts?
|
1, // TODO: Multiple send attempts?
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
|
this._submitUrl = result.submit_url;
|
||||||
this._sid = result.sid;
|
this._sid = result.sid;
|
||||||
this._msisdn = result.msisdn;
|
this._msisdn = result.msisdn;
|
||||||
});
|
});
|
||||||
|
@ -453,45 +455,52 @@ export const MsisdnAuthEntry = createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onFormSubmit: function(e) {
|
_onFormSubmit: async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.state.token == '') return;
|
if (this.state.token == '') return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: null,
|
errorText: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.matrixClient.submitMsisdnToken(
|
try {
|
||||||
this._sid, this.props.clientSecret, this.state.token,
|
let result;
|
||||||
).then((result) => {
|
if (this._submitUrl) {
|
||||||
if (result.success) {
|
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||||
const idServerParsedUrl = url.parse(
|
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||||
this.props.matrixClient.getIdentityServerUrl(),
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
result = await this.props.matrixClient.submitMsisdnToken(
|
||||||
|
this._sid, this.props.clientSecret, this.state.token,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (result.success) {
|
||||||
|
const creds = {
|
||||||
|
sid: this._sid,
|
||||||
|
client_secret: this.props.clientSecret,
|
||||||
|
};
|
||||||
|
if (await this.props.matrixClient.doesServerRequireIdServerParam()) {
|
||||||
|
const idServerParsedUrl = url.parse(
|
||||||
|
this.props.matrixClient.getIdentityServerUrl(),
|
||||||
|
);
|
||||||
|
creds.id_server = idServerParsedUrl.host;
|
||||||
|
}
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||||
// See https://github.com/vector-im/riot-web/issues/10312
|
// See https://github.com/vector-im/riot-web/issues/10312
|
||||||
threepid_creds: {
|
threepid_creds: creds,
|
||||||
sid: this._sid,
|
threepidCreds: creds,
|
||||||
client_secret: this.props.clientSecret,
|
|
||||||
id_server: idServerParsedUrl.host,
|
|
||||||
},
|
|
||||||
threepidCreds: {
|
|
||||||
sid: this._sid,
|
|
||||||
client_secret: this.props.clientSecret,
|
|
||||||
id_server: idServerParsedUrl.host,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: _t("Token incorrect"),
|
errorText: _t("Token incorrect"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch((e) => {
|
} catch (e) {
|
||||||
this.props.fail(e);
|
this.props.fail(e);
|
||||||
console.log("Failed to submit msisdn token");
|
console.log("Failed to submit msisdn token");
|
||||||
}).done();
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
|
|
@ -19,7 +19,9 @@ import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import withValidation from '../elements/Validation';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'CreateRoomDialog',
|
displayName: 'CreateRoomDialog',
|
||||||
|
@ -27,47 +29,164 @@ export default createReactClass({
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
getInitialState() {
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
return {
|
||||||
this.defaultNoFederate = config.default_federate === false;
|
isPublic: false,
|
||||||
|
name: "",
|
||||||
|
topic: "",
|
||||||
|
alias: "",
|
||||||
|
detailsOpen: false,
|
||||||
|
noFederate: config.default_federate === false,
|
||||||
|
nameIsValid: false,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onOk: function() {
|
_roomCreateOptions() {
|
||||||
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
const createOpts = {};
|
||||||
|
createOpts.name = this.state.name;
|
||||||
|
if (this.state.isPublic) {
|
||||||
|
createOpts.visibility = "public";
|
||||||
|
createOpts.preset = "public_chat";
|
||||||
|
// to prevent createRoom from enabling guest access
|
||||||
|
createOpts['initial_state'] = [];
|
||||||
|
const {alias} = this.state;
|
||||||
|
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||||
|
createOpts['room_alias_name'] = localPart;
|
||||||
|
}
|
||||||
|
if (this.state.topic) {
|
||||||
|
createOpts.topic = this.state.topic;
|
||||||
|
}
|
||||||
|
if (this.state.noFederate) {
|
||||||
|
createOpts.creation_content = {'m.federate': false};
|
||||||
|
}
|
||||||
|
return createOpts;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
||||||
|
// move focus to first field when showing dialog
|
||||||
|
this._nameFieldRef.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
||||||
|
},
|
||||||
|
|
||||||
|
onOk: async function() {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.blur();
|
||||||
|
}
|
||||||
|
await this._nameFieldRef.validate({allowEmpty: false});
|
||||||
|
if (this._aliasFieldRef) {
|
||||||
|
await this._aliasFieldRef.validate({allowEmpty: false});
|
||||||
|
}
|
||||||
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
|
await new Promise(resolve => this.setState({}, resolve));
|
||||||
|
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
||||||
|
this.props.onFinished(true, this._roomCreateOptions());
|
||||||
|
} else {
|
||||||
|
let field;
|
||||||
|
if (!this.state.nameIsValid) {
|
||||||
|
field = this._nameFieldRef;
|
||||||
|
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
||||||
|
field = this._aliasFieldRef;
|
||||||
|
}
|
||||||
|
if (field) {
|
||||||
|
field.focus();
|
||||||
|
field.validate({ allowEmpty: false, focused: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancel: function() {
|
onCancel: function() {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onNameChange(ev) {
|
||||||
|
this.setState({name: ev.target.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
onTopicChange(ev) {
|
||||||
|
this.setState({topic: ev.target.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
onPublicChange(isPublic) {
|
||||||
|
this.setState({isPublic});
|
||||||
|
},
|
||||||
|
|
||||||
|
onAliasChange(alias) {
|
||||||
|
this.setState({alias});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDetailsToggled(ev) {
|
||||||
|
this.setState({detailsOpen: ev.target.open});
|
||||||
|
},
|
||||||
|
|
||||||
|
onNoFederateChange(noFederate) {
|
||||||
|
this.setState({noFederate});
|
||||||
|
},
|
||||||
|
|
||||||
|
collectDetailsRef(ref) {
|
||||||
|
this._detailsRef = ref;
|
||||||
|
},
|
||||||
|
|
||||||
|
async onNameValidate(fieldState) {
|
||||||
|
const result = await this._validateRoomName(fieldState);
|
||||||
|
this.setState({nameIsValid: result.valid});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
_validateRoomName: withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test: async ({ value }) => !!value,
|
||||||
|
invalid: () => _t("Please enter a name for the room"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
const Field = sdk.getComponent('views.elements.Field');
|
||||||
|
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||||
|
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||||
|
|
||||||
|
let privateLabel;
|
||||||
|
let publicLabel;
|
||||||
|
let aliasField;
|
||||||
|
if (this.state.isPublic) {
|
||||||
|
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||||
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
|
aliasField = (
|
||||||
|
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||||
|
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||||
title={_t('Create Room')}
|
title={title}
|
||||||
>
|
>
|
||||||
<form onSubmit={this.onOk}>
|
<form onSubmit={this.onOk}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_CreateRoomDialog_label">
|
<Field id="name" ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
<Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
|
||||||
</div>
|
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
||||||
<div className="mx_CreateRoomDialog_input_container">
|
{ privateLabel }
|
||||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
{ publicLabel }
|
||||||
</div>
|
{ aliasField }
|
||||||
<br />
|
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
||||||
|
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
||||||
<details className="mx_CreateRoomDialog_details">
|
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
|
||||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
|
||||||
<div>
|
|
||||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
|
||||||
<label htmlFor="checkbox">
|
|
||||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
|
||||||
<br />
|
|
||||||
({ _t('This setting cannot be changed later!') })
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -41,6 +41,8 @@ export default class Field extends React.PureComponent {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
// Optional component to include inside the field before the input.
|
// Optional component to include inside the field before the input.
|
||||||
prefix: PropTypes.node,
|
prefix: PropTypes.node,
|
||||||
|
// Optional component to include inside the field after the input.
|
||||||
|
postfix: PropTypes.node,
|
||||||
// The callback called whenever the contents of the field
|
// The callback called whenever the contents of the field
|
||||||
// changes. Returns an object with `valid` boolean field
|
// changes. Returns an object with `valid` boolean field
|
||||||
// and a `feedback` react component field to provide feedback
|
// and a `feedback` react component field to provide feedback
|
||||||
|
@ -54,6 +56,8 @@ export default class Field extends React.PureComponent {
|
||||||
// If specified alongside tooltipContent, the class name to apply to the
|
// If specified alongside tooltipContent, the class name to apply to the
|
||||||
// tooltip itself.
|
// tooltip itself.
|
||||||
tooltipClassName: PropTypes.string,
|
tooltipClassName: PropTypes.string,
|
||||||
|
// If specified, an additional class name to apply to the field container
|
||||||
|
className: PropTypes.string,
|
||||||
// All other props pass through to the <input>.
|
// All other props pass through to the <input>.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -143,8 +147,8 @@ export default class Field extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
element, prefix, onValidate, children, tooltipContent, flagInvalid,
|
element, prefix, postfix, className, onValidate, children,
|
||||||
tooltipClassName, ...inputProps} = this.props;
|
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
|
||||||
|
|
||||||
const inputElement = element || "input";
|
const inputElement = element || "input";
|
||||||
|
|
||||||
|
@ -163,9 +167,13 @@ export default class Field extends React.PureComponent {
|
||||||
if (prefix) {
|
if (prefix) {
|
||||||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||||
}
|
}
|
||||||
|
let postfixContainer = null;
|
||||||
|
if (postfix) {
|
||||||
|
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
||||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
|
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
|
||||||
// If we have a prefix element, leave the label always at the top left and
|
// If we have a prefix element, leave the label always at the top left and
|
||||||
// don't animate it, as it looks a bit clunky and would add complexity to do
|
// don't animate it, as it looks a bit clunky and would add complexity to do
|
||||||
// properly.
|
// properly.
|
||||||
|
@ -192,6 +200,7 @@ export default class Field extends React.PureComponent {
|
||||||
{prefixContainer}
|
{prefixContainer}
|
||||||
{fieldInput}
|
{fieldInput}
|
||||||
<label htmlFor={this.props.id}>{this.props.label}</label>
|
<label htmlFor={this.props.id}>{this.props.label}</label>
|
||||||
|
{postfixContainer}
|
||||||
{fieldTooltip}
|
{fieldTooltip}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
125
src/components/views/elements/RoomAliasField.js
Normal file
125
src/components/views/elements/RoomAliasField.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import withValidation from './Validation';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
export default class RoomAliasField extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
domain: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {isValid: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
_asFullAlias(localpart) {
|
||||||
|
return `#${localpart}:${this.props.domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const Field = sdk.getComponent('views.elements.Field');
|
||||||
|
const poundSign = (<span>#</span>);
|
||||||
|
const aliasPostfix = ":" + this.props.domain;
|
||||||
|
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||||
|
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
label={_t("Room alias")}
|
||||||
|
className="mx_RoomAliasField"
|
||||||
|
prefix={poundSign}
|
||||||
|
postfix={domain}
|
||||||
|
id={this.props.id}
|
||||||
|
ref={ref => this._fieldRef = ref}
|
||||||
|
onValidate={this._onValidate}
|
||||||
|
placeholder={_t("e.g. my-room")}
|
||||||
|
onChange={this._onChange}
|
||||||
|
maxLength={maxlength} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onChange = (ev) => {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onValidate = async (fieldState) => {
|
||||||
|
const result = await this._validationRules(fieldState);
|
||||||
|
this.setState({isValid: result.valid});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
_validationRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "safeLocalpart",
|
||||||
|
test: async ({ value }) => {
|
||||||
|
if (!value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const fullAlias = this._asFullAlias(value);
|
||||||
|
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||||
|
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
||||||
|
encodeURI(fullAlias) === fullAlias;
|
||||||
|
},
|
||||||
|
invalid: () => _t("Some characters not allowed"),
|
||||||
|
}, {
|
||||||
|
key: "required",
|
||||||
|
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||||
|
invalid: () => _t("Please provide a room alias"),
|
||||||
|
}, {
|
||||||
|
key: "taken",
|
||||||
|
test: async ({value}) => {
|
||||||
|
if (!value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
try {
|
||||||
|
await client.getRoomIdForAlias(this._asFullAlias(value));
|
||||||
|
// we got a room id, so the alias is taken
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
// any server error code will do,
|
||||||
|
// either it M_NOT_FOUND or the alias is invalid somehow,
|
||||||
|
// in which case we don't want to show the invalid message
|
||||||
|
return !!err.errcode;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
valid: () => _t("This alias is available to use"),
|
||||||
|
invalid: () => _t("This alias is already in use"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
get isValid() {
|
||||||
|
return this.state.isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(options) {
|
||||||
|
return this._fieldRef.validate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this._fieldRef.focus();
|
||||||
|
}
|
||||||
|
}
|
|
@ -169,9 +169,32 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
|
|
||||||
_onCompositionEnd = (event) => {
|
_onCompositionEnd = (event) => {
|
||||||
this._isIMEComposing = false;
|
this._isIMEComposing = false;
|
||||||
// some browsers (chromium) don't fire an input event after ending a composition
|
// some browsers (Chrome) don't fire an input event after ending a composition,
|
||||||
// so trigger a model update after the composition is done by calling the input handler
|
// so trigger a model update after the composition is done by calling the input handler.
|
||||||
this._onInput({inputType: "insertCompositionText"});
|
|
||||||
|
// however, modifying the DOM (caused by the editor model update) from the compositionend handler seems
|
||||||
|
// to confuse the IME in Chrome, likely causing https://github.com/vector-im/riot-web/issues/10913 ,
|
||||||
|
// so we do it async
|
||||||
|
|
||||||
|
// however, doing this async seems to break things in Safari for some reason, so browser sniff.
|
||||||
|
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
const isSafari = ua.includes('safari/') && !ua.includes('chrome/');
|
||||||
|
|
||||||
|
if (isSafari) {
|
||||||
|
this._onInput({inputType: "insertCompositionText"});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._onInput({inputType: "insertCompositionText"});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isComposing(event) {
|
||||||
|
// checking the event.isComposing flag just in case any browser out there
|
||||||
|
// emits events related to the composition after compositionend
|
||||||
|
// has been fired
|
||||||
|
return !!(this._isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPaste = (event) => {
|
_onPaste = (event) => {
|
||||||
|
|
|
@ -127,6 +127,10 @@ export default class EditMessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown = (event) => {
|
_onKeyDown = (event) => {
|
||||||
|
// ignore any keypress while doing IME compositions
|
||||||
|
if (this._editorRef.isComposing(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.metaKey || event.altKey || event.shiftKey) {
|
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,10 @@ export default class SendMessageComposer extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (event) => {
|
_onKeyDown = (event) => {
|
||||||
|
// ignore any keypress while doing IME compositions
|
||||||
|
if (this._editorRef.isComposing(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||||
if (event.key === "Enter" && !hasModifier) {
|
if (event.key === "Enter" && !hasModifier) {
|
||||||
this._sendMessage();
|
this._sendMessage();
|
||||||
|
|
|
@ -100,19 +100,21 @@ export default class AutocompleteWrapperModel {
|
||||||
_partForCompletion(completion) {
|
_partForCompletion(completion) {
|
||||||
const {completionId} = completion;
|
const {completionId} = completion;
|
||||||
const text = completion.completion;
|
const text = completion.completion;
|
||||||
const firstChr = completionId && completionId[0];
|
switch (completion.type) {
|
||||||
switch (firstChr) {
|
case "room":
|
||||||
case "@": {
|
return [this._partCreator.roomPill(completionId), this._partCreator.plain(completion.suffix)];
|
||||||
if (completionId === "@room") {
|
case "at-room":
|
||||||
return [this._partCreator.atRoomPill(completionId)];
|
return [this._partCreator.atRoomPill(completionId), this._partCreator.plain(completion.suffix)];
|
||||||
} else {
|
case "user":
|
||||||
return this._partCreator.createMentionParts(this._partIndex, text, completionId);
|
// not using suffix here, because we also need to calculate
|
||||||
}
|
// the suffix when clicking a display name to insert a mention,
|
||||||
}
|
// which happens in createMentionParts
|
||||||
case "#":
|
return this._partCreator.createMentionParts(this._partIndex, text, completionId);
|
||||||
return [this._partCreator.roomPill(completionId)];
|
case "command":
|
||||||
// used for emoji and command completion replacement
|
// command needs special handling for auto complete, but also renders as plain texts
|
||||||
|
return [this._partCreator.command(text)];
|
||||||
default:
|
default:
|
||||||
|
// used for emoji and other plain text completion replacement
|
||||||
return [this._partCreator.plain(text)];
|
return [this._partCreator.plain(text)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ function parseHeader(el, partCreator) {
|
||||||
return partCreator.plain("#".repeat(depth) + " ");
|
return partCreator.plain("#".repeat(depth) + " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseElement(n, partCreator, state) {
|
function parseElement(n, partCreator, lastNode, state) {
|
||||||
switch (n.nodeName) {
|
switch (n.nodeName) {
|
||||||
case "H1":
|
case "H1":
|
||||||
case "H2":
|
case "H2":
|
||||||
|
@ -90,7 +90,7 @@ function parseElement(n, partCreator, state) {
|
||||||
case "BR":
|
case "BR":
|
||||||
return partCreator.newline();
|
return partCreator.newline();
|
||||||
case "EM":
|
case "EM":
|
||||||
return partCreator.plain(`*${n.textContent}*`);
|
return partCreator.plain(`_${n.textContent}_`);
|
||||||
case "STRONG":
|
case "STRONG":
|
||||||
return partCreator.plain(`**${n.textContent}**`);
|
return partCreator.plain(`**${n.textContent}**`);
|
||||||
case "PRE":
|
case "PRE":
|
||||||
|
@ -107,6 +107,12 @@ function parseElement(n, partCreator, state) {
|
||||||
return partCreator.plain(`${indent}- `);
|
return partCreator.plain(`${indent}- `);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "P": {
|
||||||
|
if (lastNode) {
|
||||||
|
return partCreator.newline();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "OL":
|
case "OL":
|
||||||
case "UL":
|
case "UL":
|
||||||
state.listDepth = (state.listDepth || 0) + 1;
|
state.listDepth = (state.listDepth || 0) + 1;
|
||||||
|
@ -183,7 +189,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
|
||||||
if (n.nodeType === Node.TEXT_NODE) {
|
if (n.nodeType === Node.TEXT_NODE) {
|
||||||
newParts.push(...parseAtRoomMentions(n.nodeValue, partCreator));
|
newParts.push(...parseAtRoomMentions(n.nodeValue, partCreator));
|
||||||
} else if (n.nodeType === Node.ELEMENT_NODE) {
|
} else if (n.nodeType === Node.ELEMENT_NODE) {
|
||||||
const parseResult = parseElement(n, partCreator, state);
|
const parseResult = parseElement(n, partCreator, lastNode, state);
|
||||||
if (parseResult) {
|
if (parseResult) {
|
||||||
if (Array.isArray(parseResult)) {
|
if (Array.isArray(parseResult)) {
|
||||||
newParts.push(...parseResult);
|
newParts.push(...parseResult);
|
||||||
|
@ -200,10 +206,6 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
|
||||||
|
|
||||||
parts.push(...newParts);
|
parts.push(...newParts);
|
||||||
|
|
||||||
// extra newline after quote, only if there something behind it...
|
|
||||||
if (lastNode && lastNode.nodeName === "BLOCKQUOTE") {
|
|
||||||
parts.push(partCreator.newline());
|
|
||||||
}
|
|
||||||
const decend = checkDecendInto(n);
|
const decend = checkDecendInto(n);
|
||||||
// when not decending (like for PRE), onNodeLeave won't be called to set lastNode
|
// when not decending (like for PRE), onNodeLeave won't be called to set lastNode
|
||||||
// so do that here.
|
// so do that here.
|
||||||
|
|
|
@ -456,15 +456,20 @@ export class CommandPartCreator extends PartCreator {
|
||||||
createPartForInput(text, partIndex) {
|
createPartForInput(text, partIndex) {
|
||||||
// at beginning and starts with /? create
|
// at beginning and starts with /? create
|
||||||
if (partIndex === 0 && text[0] === "/") {
|
if (partIndex === 0 && text[0] === "/") {
|
||||||
return new CommandPart("", this._autoCompleteCreator);
|
// text will be inserted by model, so pass empty string
|
||||||
|
return this.command("");
|
||||||
} else {
|
} else {
|
||||||
return super.createPartForInput(text, partIndex);
|
return super.createPartForInput(text, partIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command(text) {
|
||||||
|
return new CommandPart(text, this._autoCompleteCreator);
|
||||||
|
}
|
||||||
|
|
||||||
deserializePart(part) {
|
deserializePart(part) {
|
||||||
if (part.type === "command") {
|
if (part.type === "command") {
|
||||||
return new CommandPart(part.text, this._autoCompleteCreator);
|
return this.command(part.text);
|
||||||
} else {
|
} else {
|
||||||
return super.deserializePart(part);
|
return super.deserializePart(part);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1189,6 +1189,12 @@
|
||||||
"Custom level": "Custom level",
|
"Custom level": "Custom level",
|
||||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
||||||
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||||
|
"Room alias": "Room alias",
|
||||||
|
"e.g. my-room": "e.g. my-room",
|
||||||
|
"Some characters not allowed": "Some characters not allowed",
|
||||||
|
"Please provide a room alias": "Please provide a room alias",
|
||||||
|
"This alias is available to use": "This alias is available to use",
|
||||||
|
"This alias is already in use": "This alias is already in use",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
"And %(count)s more...|other": "And %(count)s more...",
|
"And %(count)s more...|other": "And %(count)s more...",
|
||||||
"ex. @bob:example.com": "ex. @bob:example.com",
|
"ex. @bob:example.com": "ex. @bob:example.com",
|
||||||
|
@ -1236,11 +1242,18 @@
|
||||||
"Community ID": "Community ID",
|
"Community ID": "Community ID",
|
||||||
"example": "example",
|
"example": "example",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
|
"Please enter a name for the room": "Please enter a name for the room",
|
||||||
|
"Set a room alias to easily share your room with other people.": "Set a room alias to easily share your room with other people.",
|
||||||
|
"This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.",
|
||||||
|
"Create a public room": "Create a public room",
|
||||||
|
"Create a private room": "Create a private room",
|
||||||
|
"Name": "Name",
|
||||||
|
"Topic (optional)": "Topic (optional)",
|
||||||
|
"Make this room public": "Make this room public",
|
||||||
|
"Hide advanced": "Hide advanced",
|
||||||
|
"Show advanced": "Show advanced",
|
||||||
|
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)",
|
||||||
"Create Room": "Create Room",
|
"Create Room": "Create Room",
|
||||||
"Room name (optional)": "Room name (optional)",
|
|
||||||
"Advanced options": "Advanced options",
|
|
||||||
"Block users on other matrix homeservers from joining this room": "Block users on other matrix homeservers from joining this room",
|
|
||||||
"This setting cannot be changed later!": "This setting cannot be changed later!",
|
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this",
|
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this",
|
||||||
"You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ",
|
"You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ",
|
||||||
|
@ -1497,7 +1510,6 @@
|
||||||
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
||||||
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
||||||
"Enter username": "Enter username",
|
"Enter username": "Enter username",
|
||||||
"Some characters not allowed": "Some characters not allowed",
|
|
||||||
"Email (optional)": "Email (optional)",
|
"Email (optional)": "Email (optional)",
|
||||||
"Confirm": "Confirm",
|
"Confirm": "Confirm",
|
||||||
"Phone (optional)": "Phone (optional)",
|
"Phone (optional)": "Phone (optional)",
|
||||||
|
@ -1723,7 +1735,6 @@
|
||||||
"NOT verified": "NOT verified",
|
"NOT verified": "NOT verified",
|
||||||
"Blacklisted": "Blacklisted",
|
"Blacklisted": "Blacklisted",
|
||||||
"verified": "verified",
|
"verified": "verified",
|
||||||
"Name": "Name",
|
|
||||||
"Verification": "Verification",
|
"Verification": "Verification",
|
||||||
"Ed25519 fingerprint": "Ed25519 fingerprint",
|
"Ed25519 fingerprint": "Ed25519 fingerprint",
|
||||||
"User ID": "User ID",
|
"User ID": "User ID",
|
||||||
|
|
|
@ -94,7 +94,7 @@ describe('editor/deserialize', function() {
|
||||||
const html = "<strong>bold</strong> and <em>emphasized</em> text";
|
const html = "<strong>bold</strong> and <em>emphasized</em> text";
|
||||||
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
expect(parts.length).toBe(1);
|
expect(parts.length).toBe(1);
|
||||||
expect(parts[0]).toStrictEqual({type: "plain", text: "**bold** and *emphasized* text"});
|
expect(parts[0]).toStrictEqual({type: "plain", text: "**bold** and _emphasized_ text"});
|
||||||
});
|
});
|
||||||
it('hyperlink', function() {
|
it('hyperlink', function() {
|
||||||
const html = 'click <a href="http://example.com/">this</a>!';
|
const html = 'click <a href="http://example.com/">this</a>!';
|
||||||
|
@ -105,10 +105,11 @@ describe('editor/deserialize', function() {
|
||||||
it('multiple lines with paragraphs', function() {
|
it('multiple lines with paragraphs', function() {
|
||||||
const html = '<p>hello</p><p>world</p>';
|
const html = '<p>hello</p><p>world</p>';
|
||||||
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
expect(parts.length).toBe(3);
|
expect(parts.length).toBe(4);
|
||||||
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
||||||
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
expect(parts[2]).toStrictEqual({type: "plain", text: "world"});
|
expect(parts[2]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "plain", text: "world"});
|
||||||
});
|
});
|
||||||
it('multiple lines with line breaks', function() {
|
it('multiple lines with line breaks', function() {
|
||||||
const html = 'hello<br>world';
|
const html = 'hello<br>world';
|
||||||
|
@ -121,18 +122,19 @@ describe('editor/deserialize', function() {
|
||||||
it('multiple lines mixing paragraphs and line breaks', function() {
|
it('multiple lines mixing paragraphs and line breaks', function() {
|
||||||
const html = '<p>hello<br>warm</p><p>world</p>';
|
const html = '<p>hello<br>warm</p><p>world</p>';
|
||||||
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
expect(parts.length).toBe(5);
|
expect(parts.length).toBe(6);
|
||||||
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
||||||
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
expect(parts[2]).toStrictEqual({type: "plain", text: "warm"});
|
expect(parts[2]).toStrictEqual({type: "plain", text: "warm"});
|
||||||
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
expect(parts[4]).toStrictEqual({type: "plain", text: "world"});
|
expect(parts[4]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[5]).toStrictEqual({type: "plain", text: "world"});
|
||||||
});
|
});
|
||||||
it('quote', function() {
|
it('quote', function() {
|
||||||
const html = '<blockquote><p><em>wise</em><br><strong>words</strong></p></blockquote><p>indeed</p>';
|
const html = '<blockquote><p><em>wise</em><br><strong>words</strong></p></blockquote><p>indeed</p>';
|
||||||
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
expect(parts.length).toBe(6);
|
expect(parts.length).toBe(6);
|
||||||
expect(parts[0]).toStrictEqual({type: "plain", text: "> *wise*"});
|
expect(parts[0]).toStrictEqual({type: "plain", text: "> _wise_"});
|
||||||
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
expect(parts[2]).toStrictEqual({type: "plain", text: "> **words**"});
|
expect(parts[2]).toStrictEqual({type: "plain", text: "> **words**"});
|
||||||
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
@ -159,7 +161,7 @@ describe('editor/deserialize', function() {
|
||||||
const html = "<em>formatted</em> message for @room";
|
const html = "<em>formatted</em> message for @room";
|
||||||
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
expect(parts.length).toBe(2);
|
expect(parts.length).toBe(2);
|
||||||
expect(parts[0]).toStrictEqual({type: "plain", text: "*formatted* message for "});
|
expect(parts[0]).toStrictEqual({type: "plain", text: "_formatted_ message for "});
|
||||||
expect(parts[1]).toStrictEqual({type: "at-room-pill", text: "@room"});
|
expect(parts[1]).toStrictEqual({type: "at-room-pill", text: "@room"});
|
||||||
});
|
});
|
||||||
it('inline code', function() {
|
it('inline code', function() {
|
||||||
|
@ -220,7 +222,7 @@ describe('editor/deserialize', function() {
|
||||||
const html = "says <em>DON'T SHOUT</em>!";
|
const html = "says <em>DON'T SHOUT</em>!";
|
||||||
const parts = normalize(parseEvent(htmlMessage(html, "m.emote"), createPartCreator()));
|
const parts = normalize(parseEvent(htmlMessage(html, "m.emote"), createPartCreator()));
|
||||||
expect(parts.length).toBe(1);
|
expect(parts.length).toBe(1);
|
||||||
expect(parts[0]).toStrictEqual({type: "plain", text: "/me says *DON'T SHOUT*!"});
|
expect(parts[0]).toStrictEqual({type: "plain", text: "/me says _DON'T SHOUT_!"});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue