Merge branches 'develop' and 't3chguy/accesibility' of github.com:matrix-org/matrix-react-sdk into t3chguy/accesibility
						commit
						ab3e5c3b87
					
				|  | @ -99,6 +99,7 @@ | |||
| @import "./views/elements/_ResizeHandle.scss"; | ||||
| @import "./views/elements/_RichText.scss"; | ||||
| @import "./views/elements/_RoleButton.scss"; | ||||
| @import "./views/elements/_RoomAliasField.scss"; | ||||
| @import "./views/elements/_Spinner.scss"; | ||||
| @import "./views/elements/_SyntaxHighlight.scss"; | ||||
| @import "./views/elements/_TextWithTooltip.scss"; | ||||
|  |  | |||
|  | @ -14,8 +14,29 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| .mx_CreateRoomDialog_details_summary { | ||||
|     outline: none; | ||||
| .mx_CreateRoomDialog_details { | ||||
|     .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 { | ||||
|  | @ -36,3 +57,38 @@ limitations under the License. | |||
|     background-color: $primary-bg-color; | ||||
|     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; | ||||
| } | ||||
| 
 | ||||
| .mx_Field_postfix { | ||||
|     border-left: 1px solid $input-border-color; | ||||
| } | ||||
| 
 | ||||
| .mx_Field input, | ||||
| .mx_Field select, | ||||
| .mx_Field textarea { | ||||
|  |  | |||
|  | @ -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; | ||||
|     background-color: $message-action-bar-bg-color; | ||||
|     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 { | ||||
|         display: block; | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ import IdentityAuthClient from './IdentityAuthClient'; | |||
| export default class AddThreepid { | ||||
|     constructor() { | ||||
|         this.clientSecret = MatrixClientPeg.get().generateClientSecret(); | ||||
|         this.sessionId = null; | ||||
|         this.submitUrl = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -101,6 +103,7 @@ export default class AddThreepid { | |||
|             phoneCountry, phoneNumber, this.clientSecret, 1, | ||||
|         ).then((res) => { | ||||
|             this.sessionId = res.sid; | ||||
|             this.submitUrl = res.submit_url; | ||||
|             return res; | ||||
|         }, function(err) { | ||||
|             if (err.errcode === 'M_THREEPID_IN_USE') { | ||||
|  | @ -197,13 +200,23 @@ export default class AddThreepid { | |||
|      */ | ||||
|     async haveMsisdnToken(msisdnToken) { | ||||
|         const authClient = new IdentityAuthClient(); | ||||
|         const identityAccessToken = await authClient.getAccessToken(); | ||||
|         const result = await MatrixClientPeg.get().submitMsisdnToken( | ||||
|             this.sessionId, | ||||
|             this.clientSecret, | ||||
|             msisdnToken, | ||||
|             identityAccessToken, | ||||
|         ); | ||||
| 
 | ||||
|         let result; | ||||
|         if (this.submitUrl) { | ||||
|             result = await MatrixClientPeg.get().submitMsisdnTokenOtherUrl( | ||||
|                 this.submitUrl, | ||||
|                 this.sessionId, | ||||
|                 this.clientSecret, | ||||
|                 msisdnToken, | ||||
|             ); | ||||
|         } else { | ||||
|             result = await MatrixClientPeg.get().submitMsisdnToken( | ||||
|                 this.sessionId, | ||||
|                 this.clientSecret, | ||||
|                 msisdnToken, | ||||
|                 await authClient.getAccessToken(), | ||||
|             ); | ||||
|         } | ||||
|         if (result.errcode) { | ||||
|             throw result; | ||||
|         } | ||||
|  | @ -211,13 +224,11 @@ export default class AddThreepid { | |||
|         const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; | ||||
|         if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { | ||||
|             if (this.bind) { | ||||
|                 const authClient = new IdentityAuthClient(); | ||||
|                 const identityAccessToken = await authClient.getAccessToken(); | ||||
|                 await MatrixClientPeg.get().bindThreePid({ | ||||
|                     sid: this.sessionId, | ||||
|                     client_secret: this.clientSecret, | ||||
|                     id_server: identityServerDomain, | ||||
|                     id_access_token: identityAccessToken, | ||||
|                     id_access_token: await authClient.getAccessToken(), | ||||
|                 }); | ||||
|             } else { | ||||
|                 await MatrixClientPeg.get().addThreePidOnly({ | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2015, 2016 OpenMarket Ltd | ||||
| Copyright 2019 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -72,15 +73,21 @@ class PasswordReset { | |||
|      * 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". | ||||
|      */ | ||||
|     checkEmailLinkClicked() { | ||||
|         return this.client.setPassword({ | ||||
|             type: "m.login.email.identity", | ||||
|             threepid_creds: { | ||||
|                 sid: this.sessionId, | ||||
|                 client_secret: this.clientSecret, | ||||
|                 id_server: this.identityServerDomain, | ||||
|             }, | ||||
|         }, this.password).catch(function(err) { | ||||
|     async checkEmailLinkClicked() { | ||||
|         const creds = { | ||||
|             sid: this.sessionId, | ||||
|             client_secret: this.clientSecret, | ||||
|         }; | ||||
|         if (await this.doesServerRequireIdServerParam()) { | ||||
|             creds.id_server = this.identityServerDomain; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await this.client.setPassword({ | ||||
|                 type: "m.login.email.identity", | ||||
|                 threepid_creds: creds, | ||||
|             }, this.password); | ||||
|         } catch (err) { | ||||
|             if (err.httpStatus === 401) { | ||||
|                 err.message = _t('Failed to verify email address: make sure you clicked the link in the email'); | ||||
|             } else if (err.httpStatus === 404) { | ||||
|  | @ -90,7 +97,7 @@ class PasswordReset { | |||
|                 err.message += ` (Status ${err.httpStatus})`; | ||||
|             } | ||||
|             throw err; | ||||
|         }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -64,6 +64,7 @@ export default class CommandProvider extends AutocompleteProvider { | |||
|         return matches.map((result) => ({ | ||||
|             // 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 + ' '), | ||||
|             type: "command", | ||||
|             component: <TextualCompletion | ||||
|                 title={result.command} | ||||
|                 subtitle={result.args} | ||||
|  |  | |||
|  | @ -84,6 +84,7 @@ export default class CommunityProvider extends AutocompleteProvider { | |||
|             ]).map(({avatarUrl, groupId, name}) => ({ | ||||
|                 completion: groupId, | ||||
|                 suffix: ' ', | ||||
|                 type: "community", | ||||
|                 href: makeGroupPermalink(groupId), | ||||
|                 component: ( | ||||
|                     <PillCompletion initialComponent={ | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ export default class NotifProvider extends AutocompleteProvider { | |||
|             return [{ | ||||
|                 completion: '@room', | ||||
|                 completionId: '@room', | ||||
|                 type: "at-room", | ||||
|                 suffix: ' ', | ||||
|                 component: ( | ||||
|                     <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 { | ||||
|                     completion: displayAlias, | ||||
|                     completionId: displayAlias, | ||||
|                     type: "room", | ||||
|                     suffix: ' ', | ||||
|                     href: makeRoomPermalink(displayAlias), | ||||
|                     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.
 | ||||
|                     completion: user.rawDisplayName, | ||||
|                     completionId: user.userId, | ||||
|                     type: "user", | ||||
|                     suffix: (selection.beginning && range.start === 0) ? ': ' : ' ', | ||||
|                     href: makeUserPermalink(user.userId), | ||||
|                     component: ( | ||||
|  |  | |||
|  | @ -167,6 +167,7 @@ export default class ContextualMenu extends React.Component { | |||
| 
 | ||||
|         const menuClasses = classNames({ | ||||
|             'mx_ContextualMenu': true, | ||||
|             'mx_HiddenFocusable': true, // hide browser outline
 | ||||
|             'mx_ContextualMenu_left': !hasChevron && position.left, | ||||
|             'mx_ContextualMenu_right': !hasChevron && position.right, | ||||
|             'mx_ContextualMenu_top': !hasChevron && position.top, | ||||
|  |  | |||
|  | @ -271,6 +271,10 @@ export default createReactClass({ | |||
| 
 | ||||
|         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
 | ||||
|         // the routing url path logic together.
 | ||||
|         if (this.onAliasClick) { | ||||
|  | @ -870,9 +874,10 @@ export default createReactClass({ | |||
|             if (roomInfo.event_id && roomInfo.highlighted) { | ||||
|                 presentedId += "/" + roomInfo.event_id; | ||||
|             } | ||||
|             this.notifyNewScreen('room/' + presentedId); | ||||
|             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 modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); | ||||
| 
 | ||||
|         const [shouldCreate, name, noFederate] = await modal.finished; | ||||
|         const [shouldCreate, createOpts] = await modal.finished; | ||||
|         if (shouldCreate) { | ||||
|             const createOpts = {}; | ||||
|             if (name) createOpts.name = name; | ||||
|             if (noFederate) createOpts.creation_content = {'m.federate': false}; | ||||
|             createRoom({createOpts}).done(); | ||||
|         } | ||||
|     }, | ||||
|  | @ -1300,6 +1302,7 @@ export default createReactClass({ | |||
|             collapsedRhs: false, | ||||
|             currentRoomId: null, | ||||
|         }); | ||||
|         this.subTitleStatus = ''; | ||||
|         this._setPageSubtitle(); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -1315,6 +1318,7 @@ export default createReactClass({ | |||
|             collapsedRhs: false, | ||||
|             currentRoomId: null, | ||||
|         }); | ||||
|         this.subTitleStatus = ''; | ||||
|         this._setPageSubtitle(); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -1709,6 +1713,7 @@ export default createReactClass({ | |||
|         if (this.props.onNewScreen) { | ||||
|             this.props.onNewScreen(screen); | ||||
|         } | ||||
|         this._setPageSubtitle(); | ||||
|     }, | ||||
| 
 | ||||
|     onAliasClick: function(event, alias) { | ||||
|  | @ -1824,7 +1829,14 @@ export default createReactClass({ | |||
|     }, | ||||
| 
 | ||||
|     _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) { | ||||
|  | @ -1835,15 +1847,15 @@ export default createReactClass({ | |||
|             PlatformPeg.get().setNotificationCount(notifCount); | ||||
|         } | ||||
| 
 | ||||
|         let subtitle = ''; | ||||
|         this.subTitleStatus = ''; | ||||
|         if (state === "ERROR") { | ||||
|             subtitle += `[${_t("Offline")}] `; | ||||
|             this.subTitleStatus += `[${_t("Offline")}] `; | ||||
|         } | ||||
|         if (notifCount > 0) { | ||||
|             subtitle += `[${notifCount}]`; | ||||
|             this.subTitleStatus += `[${notifCount}]`; | ||||
|         } | ||||
| 
 | ||||
|         this._setPageSubtitle(subtitle); | ||||
|         this._setPageSubtitle(); | ||||
|     }, | ||||
| 
 | ||||
|     onCloseAllSettings() { | ||||
|  |  | |||
|  | @ -117,17 +117,18 @@ module.exports = createReactClass({ | |||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onVerify: function(ev) { | ||||
|     onVerify: async function(ev) { | ||||
|         ev.preventDefault(); | ||||
|         if (!this.reset) { | ||||
|             console.error("onVerify called before submitPasswordReset!"); | ||||
|             return; | ||||
|         } | ||||
|         this.reset.checkEmailLinkClicked().done((res) => { | ||||
|         try { | ||||
|             await this.reset.checkEmailLinkClicked(); | ||||
|             this.setState({ phase: PHASE_DONE }); | ||||
|         }, (err) => { | ||||
|         } catch (err) { | ||||
|             this.showErrorDialog(err.message); | ||||
|         }); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     onSubmitForm: async function(ev) { | ||||
|  |  | |||
|  | @ -420,6 +420,7 @@ export const MsisdnAuthEntry = createReactClass({ | |||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         this._submitUrl = null; | ||||
|         this._sid = null; | ||||
|         this._msisdn = null; | ||||
|         this._tokenBox = null; | ||||
|  | @ -442,6 +443,7 @@ export const MsisdnAuthEntry = createReactClass({ | |||
|             this.props.clientSecret, | ||||
|             1, // TODO: Multiple send attempts?
 | ||||
|         ).then((result) => { | ||||
|             this._submitUrl = result.submit_url; | ||||
|             this._sid = result.sid; | ||||
|             this._msisdn = result.msisdn; | ||||
|         }); | ||||
|  | @ -453,45 +455,52 @@ export const MsisdnAuthEntry = createReactClass({ | |||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _onFormSubmit: function(e) { | ||||
|     _onFormSubmit: async function(e) { | ||||
|         e.preventDefault(); | ||||
|         if (this.state.token == '') return; | ||||
| 
 | ||||
|             this.setState({ | ||||
|                 errorText: null, | ||||
|             }); | ||||
|         this.setState({ | ||||
|             errorText: null, | ||||
|         }); | ||||
| 
 | ||||
|         this.props.matrixClient.submitMsisdnToken( | ||||
|             this._sid, this.props.clientSecret, this.state.token, | ||||
|         ).then((result) => { | ||||
|             if (result.success) { | ||||
|                 const idServerParsedUrl = url.parse( | ||||
|                     this.props.matrixClient.getIdentityServerUrl(), | ||||
|         try { | ||||
|             let result; | ||||
|             if (this._submitUrl) { | ||||
|                 result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( | ||||
|                     this._submitUrl, this._sid, this.props.clientSecret, this.state.token, | ||||
|                 ); | ||||
|             } 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({ | ||||
|                     type: MsisdnAuthEntry.LOGIN_TYPE, | ||||
|                     // TODO: Remove `threepid_creds` once servers support proper UIA
 | ||||
|                     // See https://github.com/vector-im/riot-web/issues/10312
 | ||||
|                     threepid_creds: { | ||||
|                         sid: this._sid, | ||||
|                         client_secret: this.props.clientSecret, | ||||
|                         id_server: idServerParsedUrl.host, | ||||
|                     }, | ||||
|                     threepidCreds: { | ||||
|                         sid: this._sid, | ||||
|                         client_secret: this.props.clientSecret, | ||||
|                         id_server: idServerParsedUrl.host, | ||||
|                     }, | ||||
|                     threepid_creds: creds, | ||||
|                     threepidCreds: creds, | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.setState({ | ||||
|                     errorText: _t("Token incorrect"), | ||||
|                 }); | ||||
|             } | ||||
|         }).catch((e) => { | ||||
|         } catch (e) { | ||||
|             this.props.fail(e); | ||||
|             console.log("Failed to submit msisdn token"); | ||||
|         }).done(); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|  |  | |||
|  | @ -19,7 +19,9 @@ import createReactClass from 'create-react-class'; | |||
| import PropTypes from 'prop-types'; | ||||
| import sdk from '../../../index'; | ||||
| import SdkConfig from '../../../SdkConfig'; | ||||
| import withValidation from '../elements/Validation'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import MatrixClientPeg from '../../../MatrixClientPeg'; | ||||
| 
 | ||||
| export default createReactClass({ | ||||
|     displayName: 'CreateRoomDialog', | ||||
|  | @ -27,47 +29,164 @@ export default createReactClass({ | |||
|         onFinished: PropTypes.func.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|     getInitialState() { | ||||
|         const config = SdkConfig.get(); | ||||
|         // Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
 | ||||
|         this.defaultNoFederate = config.default_federate === false; | ||||
|         return { | ||||
|             isPublic: false, | ||||
|             name: "", | ||||
|             topic: "", | ||||
|             alias: "", | ||||
|             detailsOpen: false, | ||||
|             noFederate: config.default_federate === false, | ||||
|             nameIsValid: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onOk: function() { | ||||
|         this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked); | ||||
|     _roomCreateOptions() { | ||||
|         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() { | ||||
|         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() { | ||||
|         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); | ||||
|         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 ( | ||||
|             <BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} | ||||
|                 title={_t('Create Room')} | ||||
|                 title={title} | ||||
|             > | ||||
|                 <form onSubmit={this.onOk}> | ||||
|                     <div className="mx_Dialog_content"> | ||||
|                         <div className="mx_CreateRoomDialog_label"> | ||||
|                             <label htmlFor="textinput"> { _t('Room name (optional)') } </label> | ||||
|                         </div> | ||||
|                         <div className="mx_CreateRoomDialog_input_container"> | ||||
|                             <input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} /> | ||||
|                         </div> | ||||
|                         <br /> | ||||
| 
 | ||||
|                         <details className="mx_CreateRoomDialog_details"> | ||||
|                             <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> | ||||
|                         <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" /> | ||||
|                         <Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} /> | ||||
|                         <LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} /> | ||||
|                         { privateLabel } | ||||
|                         { publicLabel } | ||||
|                         { aliasField } | ||||
|                         <details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details"> | ||||
|                             <summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary> | ||||
|                             <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} /> | ||||
|                         </details> | ||||
|                     </div> | ||||
|                 </form> | ||||
|  |  | |||
|  | @ -41,6 +41,8 @@ export default class Field extends React.PureComponent { | |||
|         value: PropTypes.string.isRequired, | ||||
|         // Optional component to include inside the field before the input.
 | ||||
|         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
 | ||||
|         // changes.  Returns an object with `valid` boolean field
 | ||||
|         // 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
 | ||||
|         // tooltip itself.
 | ||||
|         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>.
 | ||||
|     }; | ||||
| 
 | ||||
|  | @ -143,8 +147,8 @@ export default class Field extends React.PureComponent { | |||
| 
 | ||||
|     render() { | ||||
|         const { | ||||
|             element, prefix, onValidate, children, tooltipContent, flagInvalid, | ||||
|             tooltipClassName, ...inputProps} = this.props; | ||||
|             element, prefix, postfix, className, onValidate, children, | ||||
|             tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props; | ||||
| 
 | ||||
|         const inputElement = element || "input"; | ||||
| 
 | ||||
|  | @ -163,9 +167,13 @@ export default class Field extends React.PureComponent { | |||
|         if (prefix) { | ||||
|             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 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
 | ||||
|             // don't animate it, as it looks a bit clunky and would add complexity to do
 | ||||
|             // properly.
 | ||||
|  | @ -192,6 +200,7 @@ export default class Field extends React.PureComponent { | |||
|             {prefixContainer} | ||||
|             {fieldInput} | ||||
|             <label htmlFor={this.props.id}>{this.props.label}</label> | ||||
|             {postfixContainer} | ||||
|             {fieldTooltip} | ||||
|         </div>; | ||||
|     } | ||||
|  |  | |||
|  | @ -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) => { | ||||
|         this._isIMEComposing = false; | ||||
|         // some browsers (chromium) 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
 | ||||
|         this._onInput({inputType: "insertCompositionText"}); | ||||
|         // 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.
 | ||||
| 
 | ||||
|         // 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) => { | ||||
|  |  | |||
|  | @ -127,6 +127,10 @@ export default class EditMessageComposer extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     _onKeyDown = (event) => { | ||||
|         // ignore any keypress while doing IME compositions
 | ||||
|         if (this._editorRef.isComposing(event)) { | ||||
|             return; | ||||
|         } | ||||
|         if (event.metaKey || event.altKey || event.shiftKey) { | ||||
|             return; | ||||
|         } | ||||
|  |  | |||
|  | @ -104,6 +104,10 @@ export default class SendMessageComposer extends React.Component { | |||
|     }; | ||||
| 
 | ||||
|     _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; | ||||
|         if (event.key === "Enter" && !hasModifier) { | ||||
|             this._sendMessage(); | ||||
|  |  | |||
|  | @ -100,19 +100,21 @@ export default class AutocompleteWrapperModel { | |||
|     _partForCompletion(completion) { | ||||
|         const {completionId} = completion; | ||||
|         const text = completion.completion; | ||||
|         const firstChr = completionId && completionId[0]; | ||||
|         switch (firstChr) { | ||||
|             case "@": { | ||||
|                 if (completionId === "@room") { | ||||
|                     return [this._partCreator.atRoomPill(completionId)]; | ||||
|                 } else { | ||||
|                     return this._partCreator.createMentionParts(this._partIndex, text, completionId); | ||||
|                 } | ||||
|             } | ||||
|             case "#": | ||||
|                 return [this._partCreator.roomPill(completionId)]; | ||||
|             // used for emoji and command completion replacement
 | ||||
|         switch (completion.type) { | ||||
|             case "room": | ||||
|                 return [this._partCreator.roomPill(completionId), this._partCreator.plain(completion.suffix)]; | ||||
|             case "at-room": | ||||
|                 return [this._partCreator.atRoomPill(completionId), this._partCreator.plain(completion.suffix)]; | ||||
|             case "user": | ||||
|                 // 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
 | ||||
|                 return this._partCreator.createMentionParts(this._partIndex, text, completionId); | ||||
|             case "command": | ||||
|                 // command needs special handling for auto complete, but also renders as plain texts
 | ||||
|                 return [this._partCreator.command(text)]; | ||||
|             default: | ||||
|                 // used for emoji and other plain text completion replacement
 | ||||
|                 return [this._partCreator.plain(text)]; | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -76,7 +76,7 @@ function parseHeader(el, partCreator) { | |||
|     return partCreator.plain("#".repeat(depth) + " "); | ||||
| } | ||||
| 
 | ||||
| function parseElement(n, partCreator, state) { | ||||
| function parseElement(n, partCreator, lastNode, state) { | ||||
|     switch (n.nodeName) { | ||||
|         case "H1": | ||||
|         case "H2": | ||||
|  | @ -90,7 +90,7 @@ function parseElement(n, partCreator, state) { | |||
|         case "BR": | ||||
|             return partCreator.newline(); | ||||
|         case "EM": | ||||
|             return partCreator.plain(`*${n.textContent}*`); | ||||
|             return partCreator.plain(`_${n.textContent}_`); | ||||
|         case "STRONG": | ||||
|             return partCreator.plain(`**${n.textContent}**`); | ||||
|         case "PRE": | ||||
|  | @ -107,6 +107,12 @@ function parseElement(n, partCreator, state) { | |||
|                 return partCreator.plain(`${indent}- `); | ||||
|             } | ||||
|         } | ||||
|         case "P": { | ||||
|             if (lastNode) { | ||||
|                 return partCreator.newline(); | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|         case "OL": | ||||
|         case "UL": | ||||
|             state.listDepth = (state.listDepth || 0) + 1; | ||||
|  | @ -183,7 +189,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { | |||
|         if (n.nodeType === Node.TEXT_NODE) { | ||||
|             newParts.push(...parseAtRoomMentions(n.nodeValue, partCreator)); | ||||
|         } else if (n.nodeType === Node.ELEMENT_NODE) { | ||||
|             const parseResult = parseElement(n, partCreator, state); | ||||
|             const parseResult = parseElement(n, partCreator, lastNode, state); | ||||
|             if (parseResult) { | ||||
|                 if (Array.isArray(parseResult)) { | ||||
|                     newParts.push(...parseResult); | ||||
|  | @ -200,10 +206,6 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { | |||
| 
 | ||||
|         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); | ||||
|         // when not decending (like for PRE), onNodeLeave won't be called to set lastNode
 | ||||
|         // so do that here.
 | ||||
|  |  | |||
|  | @ -456,15 +456,20 @@ export class CommandPartCreator extends PartCreator { | |||
|     createPartForInput(text, partIndex) { | ||||
|         // at beginning and starts with /? create
 | ||||
|         if (partIndex === 0 && text[0] === "/") { | ||||
|             return new CommandPart("", this._autoCompleteCreator); | ||||
|             // text will be inserted by model, so pass empty string
 | ||||
|             return this.command(""); | ||||
|         } else { | ||||
|             return super.createPartForInput(text, partIndex); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     command(text) { | ||||
|         return new CommandPart(text, this._autoCompleteCreator); | ||||
|     } | ||||
| 
 | ||||
|     deserializePart(part) { | ||||
|         if (part.type === "command") { | ||||
|             return new CommandPart(part.text, this._autoCompleteCreator); | ||||
|             return this.command(part.text); | ||||
|         } else { | ||||
|             return super.deserializePart(part); | ||||
|         } | ||||
|  |  | |||
|  | @ -1189,6 +1189,12 @@ | |||
|     "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.", | ||||
|     "<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", | ||||
|     "And %(count)s more...|other": "And %(count)s more...", | ||||
|     "ex. @bob:example.com": "ex. @bob:example.com", | ||||
|  | @ -1236,11 +1242,18 @@ | |||
|     "Community ID": "Community ID", | ||||
|     "example": "example", | ||||
|     "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", | ||||
|     "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", | ||||
|     "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. ", | ||||
|  | @ -1497,7 +1510,6 @@ | |||
|     "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", | ||||
|     "Enter username": "Enter username", | ||||
|     "Some characters not allowed": "Some characters not allowed", | ||||
|     "Email (optional)": "Email (optional)", | ||||
|     "Confirm": "Confirm", | ||||
|     "Phone (optional)": "Phone (optional)", | ||||
|  | @ -1723,7 +1735,6 @@ | |||
|     "NOT verified": "NOT verified", | ||||
|     "Blacklisted": "Blacklisted", | ||||
|     "verified": "verified", | ||||
|     "Name": "Name", | ||||
|     "Verification": "Verification", | ||||
|     "Ed25519 fingerprint": "Ed25519 fingerprint", | ||||
|     "User ID": "User ID", | ||||
|  |  | |||
|  | @ -94,7 +94,7 @@ describe('editor/deserialize', function() { | |||
|             const html = "<strong>bold</strong> and <em>emphasized</em> text"; | ||||
|             const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); | ||||
|             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() { | ||||
|             const html = 'click <a href="http://example.com/">this</a>!'; | ||||
|  | @ -105,10 +105,11 @@ describe('editor/deserialize', function() { | |||
|         it('multiple lines with paragraphs', function() { | ||||
|             const html = '<p>hello</p><p>world</p>'; | ||||
|             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[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() { | ||||
|             const html = 'hello<br>world'; | ||||
|  | @ -121,18 +122,19 @@ describe('editor/deserialize', function() { | |||
|         it('multiple lines mixing paragraphs and line breaks', function() { | ||||
|             const html = '<p>hello<br>warm</p><p>world</p>'; | ||||
|             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[1]).toStrictEqual({type: "newline", text: "\n"}); | ||||
|             expect(parts[2]).toStrictEqual({type: "plain", text: "warm"}); | ||||
|             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() { | ||||
|             const html = '<blockquote><p><em>wise</em><br><strong>words</strong></p></blockquote><p>indeed</p>'; | ||||
|             const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); | ||||
|             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[2]).toStrictEqual({type: "plain", text: "> **words**"}); | ||||
|             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 parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); | ||||
|             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"}); | ||||
|         }); | ||||
|         it('inline code', function() { | ||||
|  | @ -220,7 +222,7 @@ describe('editor/deserialize', function() { | |||
|             const html = "says <em>DON'T SHOUT</em>!"; | ||||
|             const parts = normalize(parseEvent(htmlMessage(html, "m.emote"), createPartCreator())); | ||||
|             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…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski