Merge pull request #5418 from matrix-org/t3chguy/fix/muggle-hustle
Small delight tweaks to improve rough corners in the apppull/21833/head
						commit
						9b2143742a
					
				|  | @ -14,6 +14,35 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| .mx_InteractiveAuthEntryComponents_emailWrapper { | ||||
|     padding-right: 60px; | ||||
|     position: relative; | ||||
|     margin-top: 32px; | ||||
|     margin-bottom: 32px; | ||||
| 
 | ||||
|     &::before, &::after { | ||||
|         position: absolute; | ||||
|         width: 116px; | ||||
|         height: 116px; | ||||
|         content: ""; | ||||
|         right: -10px; | ||||
|     } | ||||
| 
 | ||||
|     &::before { | ||||
|         background-color: rgba(244, 246, 250, 0.91); | ||||
|         border-radius: 50%; | ||||
|         top: -20px; | ||||
|     } | ||||
| 
 | ||||
|     &::after { | ||||
|         background-image: url('$(res)/img/element-icons/email-prompt.svg'); | ||||
|         background-repeat: no-repeat; | ||||
|         background-position: center; | ||||
|         background-size: contain; | ||||
|         top: -25px; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .mx_InteractiveAuthEntryComponents_msisdnWrapper { | ||||
|     text-align: center; | ||||
| } | ||||
|  |  | |||
|  | @ -33,7 +33,6 @@ limitations under the License. | |||
| 
 | ||||
|     div:first-child { | ||||
|         font-weight: $font-semi-bold; | ||||
|         margin-bottom: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .mx_AccessibleButton { | ||||
|  | @ -41,6 +40,7 @@ limitations under the License. | |||
|         position: relative; | ||||
|         padding: 0 0 0 24px; | ||||
|         font-size: inherit; | ||||
|         margin-top: 8px; | ||||
| 
 | ||||
|         &::before { | ||||
|             content: ''; | ||||
|  | @ -53,6 +53,13 @@ limitations under the License. | |||
|             mask-position: center; | ||||
|             mask-size: contain; | ||||
|             mask-repeat: no-repeat; | ||||
|         } | ||||
| 
 | ||||
|         &.mx_RoomList_explorePrompt_startChat::before { | ||||
|             mask-image: url('$(res)/img/element-icons/feedback.svg'); | ||||
|         } | ||||
| 
 | ||||
|         &.mx_RoomList_explorePrompt_explore::before { | ||||
|             mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| <svg width="57" height="77" viewBox="0 0 57 77" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M5.55298 38.9352H4C1.79086 38.9352 0 40.726 0 42.9352V72.0304C0 74.2396 1.79086 76.0304 4 76.0304H53C55.2091 76.0304 57 74.2396 57 72.0304V42.9352C57 40.726 55.2091 38.9352 53 38.9352H51.365V41.6473H5.55298V38.9352ZM26.9753 61.3068L3.10141 43.4482C2.33137 42.8721 2.73876 41.6474 3.70041 41.6474H28.459H53.3841C54.3282 41.6474 54.7464 42.8352 54.0107 43.4268L31.8776 61.2212C30.4545 62.3653 28.4374 62.4005 26.9753 61.3068Z" fill="#8A8C8E"/> | ||||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M49.5885 33.0898C48.9384 33.2156 48.2703 33.2911 47.5885 33.3119V34.706V44.4238V54.1415H49.5885V44.4238V34.706V33.0898ZM36.5604 14.2706H13.7177C10.9562 14.2706 8.71765 16.5092 8.71765 19.2706V34.706V44.4238V54.1415H10.7177V44.4238V34.706V19.2706C10.7177 17.6138 12.0608 16.2706 13.7177 16.2706H35.5616C35.8354 15.571 36.1706 14.9022 36.5604 14.2706Z" fill="#8A8C8E"/> | ||||
| <path d="M16.6589 30.5414H37.4826" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||||
| <line x1="16.2706" y1="37.8708" x2="40.6473" y2="37.8708" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||||
| <line x1="16.2706" y1="44.812" x2="40.6473" y2="44.812" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||||
| <circle cx="47.2003" cy="20.8237" r="9.71771" fill="#FE2928"/> | ||||
| <rect x="45.812" y="14.5765" width="2.77649" height="8.32946" rx="1" fill="white"/> | ||||
| <rect x="45.812" y="24.2943" width="2.77649" height="2.77649" rx="1" fill="white"/> | ||||
| <line x1="27.3766" y1="1" x2="27.3766" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||||
| <line x1="34.3179" y1="6.55298" x2="34.3179" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||||
| <line x1="20.4354" y1="6.55298" x2="20.4354" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.0 KiB | 
|  | @ -40,11 +40,11 @@ export function inviteMultipleToRoom(roomId, addrs) { | |||
|     return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); | ||||
| } | ||||
| 
 | ||||
| export function showStartChatInviteDialog() { | ||||
| export function showStartChatInviteDialog(initialText) { | ||||
|     // This dialog handles the room creation internally - we don't need to worry about it.
 | ||||
|     const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); | ||||
|     Modal.createTrackedDialog( | ||||
|         'Start DM', '', InviteDialog, {kind: KIND_DM}, | ||||
|         'Start DM', '', InviteDialog, {kind: KIND_DM, initialText}, | ||||
|         /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, | ||||
|     ); | ||||
| } | ||||
|  |  | |||
|  | @ -653,8 +653,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|             } | ||||
|             case Action.ViewRoomDirectory: { | ||||
|                 const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); | ||||
|                 Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, | ||||
|                     'mx_RoomDirectory_dialogWrapper', false, true); | ||||
|                 Modal.createTrackedDialog('Room directory', '', RoomDirectory, { | ||||
|                     initialText: payload.initialText, | ||||
|                 }, 'mx_RoomDirectory_dialogWrapper', false, true); | ||||
| 
 | ||||
|                 // View the welcome or home page if we need something to look at
 | ||||
|                 this.viewSomethingBehindModal(); | ||||
|  | @ -677,7 +678,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                 this.chatCreateOrReuse(payload.user_id); | ||||
|                 break; | ||||
|             case 'view_create_chat': | ||||
|                 showStartChatInviteDialog(); | ||||
|                 showStartChatInviteDialog(payload.initialText || ""); | ||||
|                 break; | ||||
|             case 'view_invite': | ||||
|                 showRoomInviteDialog(payload.roomId); | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ function track(action) { | |||
| 
 | ||||
| export default class RoomDirectory extends React.Component { | ||||
|     static propTypes = { | ||||
|         initialText: PropTypes.string, | ||||
|         onFinished: PropTypes.func.isRequired, | ||||
|     }; | ||||
| 
 | ||||
|  | @ -61,7 +62,7 @@ export default class RoomDirectory extends React.Component { | |||
|             error: null, | ||||
|             instanceId: undefined, | ||||
|             roomServer: MatrixClientPeg.getHomeserverName(), | ||||
|             filterString: null, | ||||
|             filterString: this.props.initialText || "", | ||||
|             selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") | ||||
|                 ? selectedCommunityId | ||||
|                 : null, | ||||
|  | @ -686,6 +687,7 @@ export default class RoomDirectory extends React.Component { | |||
|                     onJoinClick={this.onJoinFromSearchClick} | ||||
|                     placeholder={placeholder} | ||||
|                     showJoinButton={showJoinButton} | ||||
|                     initialText={this.props.initialText} | ||||
|                 /> | ||||
|                 {dropdown} | ||||
|             </div>; | ||||
|  |  | |||
|  | @ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> { | |||
|                 onBlur={this.onBlur} | ||||
|                 onChange={this.onChange} | ||||
|                 onKeyDown={this.onKeyDown} | ||||
|                 placeholder={_t("Search")} | ||||
|                 placeholder={_t("Filter")} | ||||
|                 autoComplete="off" | ||||
|             /> | ||||
|         ); | ||||
|  | @ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> { | |||
|         if (this.props.isMinimized) { | ||||
|             icon = ( | ||||
|                 <AccessibleButton | ||||
|                     title={_t("Search rooms")} | ||||
|                     title={_t("Filter rooms and people")} | ||||
|                     className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle" | ||||
|                     onClick={this.openSearch} | ||||
|                 /> | ||||
|  |  | |||
|  | @ -502,6 +502,11 @@ export default class Registration extends React.Component { | |||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         // Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
 | ||||
|         if (this.state.phase !== PHASE_SERVER_DETAILS && this.state.doingUIAuth && !this.state.serverErrorIsFatal) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         // If we're on a different phase, we only show the server type selector,
 | ||||
|         // which is always shown if we allow custom URLs at all.
 | ||||
|         // (if there's a fatal server error, we need to show the full server
 | ||||
|  | @ -582,17 +587,6 @@ export default class Registration extends React.Component { | |||
|                 <Spinner /> | ||||
|             </div>; | ||||
|         } else if (this.state.flows.length) { | ||||
|             let onEditServerDetailsClick = null; | ||||
|             // If custom URLs are allowed and we haven't selected the Free server type, wire
 | ||||
|             // up the server details edit link.
 | ||||
|             if ( | ||||
|                 PHASES_ENABLED && | ||||
|                 !SdkConfig.get()['disable_custom_urls'] && | ||||
|                 this.state.serverType !== ServerType.FREE | ||||
|             ) { | ||||
|                 onEditServerDetailsClick = this.onEditServerDetailsClick; | ||||
|             } | ||||
| 
 | ||||
|             return <RegistrationForm | ||||
|                 defaultUsername={this.state.formVals.username} | ||||
|                 defaultEmail={this.state.formVals.email} | ||||
|  | @ -600,7 +594,6 @@ export default class Registration extends React.Component { | |||
|                 defaultPhoneNumber={this.state.formVals.phoneNumber} | ||||
|                 defaultPassword={this.state.formVals.password} | ||||
|                 onRegisterClick={this.onFormSubmit} | ||||
|                 onEditServerDetailsClick={onEditServerDetailsClick} | ||||
|                 flows={this.state.flows} | ||||
|                 serverConfig={this.props.serverConfig} | ||||
|                 canSubmit={!this.state.serverErrorIsFatal} | ||||
|  | @ -686,11 +679,48 @@ export default class Registration extends React.Component { | |||
|                 { regDoneText } | ||||
|             </div>; | ||||
|         } else { | ||||
|             let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { | ||||
|                 serverName: this.props.serverConfig.hsName, | ||||
|             }); | ||||
|             if (this.props.serverConfig.hsNameIsDifferent) { | ||||
|                 const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); | ||||
| 
 | ||||
|                 yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, { | ||||
|                     'underlinedServerName': () => { | ||||
|                         return <TextWithTooltip | ||||
|                             class="mx_Login_underlinedServerName" | ||||
|                             tooltip={this.props.serverConfig.hsUrl} | ||||
|                         > | ||||
|                             {this.props.serverConfig.hsName} | ||||
|                         </TextWithTooltip>; | ||||
|                     }, | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
 | ||||
|             // wire up the server details edit link.
 | ||||
|             let editLink = null; | ||||
|             if (PHASES_ENABLED && | ||||
|                 !SdkConfig.get()['disable_custom_urls'] && | ||||
|                 this.state.serverType !== ServerType.FREE && | ||||
|                 !this.state.doingUIAuth | ||||
|             ) { | ||||
|                 editLink = ( | ||||
|                     <a className="mx_AuthBody_editServerDetails" href="#" onClick={this.onEditServerDetailsClick}> | ||||
|                         {_t('Change')} | ||||
|                     </a> | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             body = <div> | ||||
|                 <h2>{ _t('Create your account') }</h2> | ||||
|                 { errorText } | ||||
|                 { serverDeadSection } | ||||
|                 { this.renderServerComponent() } | ||||
|                 { this.state.phase !== PHASE_SERVER_DETAILS && <h3> | ||||
|                     {yourMatrixAccountText} | ||||
|                     {editLink} | ||||
|                 </h3> } | ||||
|                 { this.renderRegisterComponent() } | ||||
|                 { goBack } | ||||
|                 { signIn } | ||||
|  |  | |||
|  | @ -102,6 +102,10 @@ export default class CaptchaForm extends React.Component { | |||
|         console.log("Loaded recaptcha script."); | ||||
|         try { | ||||
|             this._renderRecaptcha(DIV_ID); | ||||
|             // clear error if re-rendered
 | ||||
|             this.setState({ | ||||
|                 errorText: null, | ||||
|             }); | ||||
|             CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded"); | ||||
|         } catch (e) { | ||||
|             this.setState({ | ||||
|  |  | |||
|  | @ -421,12 +421,12 @@ export class EmailIdentityAuthEntry extends React.Component { | |||
|             return <Spinner />; | ||||
|         } else { | ||||
|             return ( | ||||
|                 <div> | ||||
|                     <p>{ _t("An email has been sent to %(emailAddress)s", | ||||
|                         { emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> }, | ||||
|                 <div className="mx_InteractiveAuthEntryComponents_emailWrapper"> | ||||
|                     <p>{ _t("A confirmation email has been sent to %(emailAddress)s", | ||||
|                         { emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> }, | ||||
|                     ) } | ||||
|                     </p> | ||||
|                     <p>{ _t("Please check your email to continue registration.") }</p> | ||||
|                     <p>{ _t("Open the link in the email to continue registration.") }</p> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
|  |  | |||
|  | @ -51,7 +51,6 @@ export default class RegistrationForm extends React.Component { | |||
|         defaultUsername: PropTypes.string, | ||||
|         defaultPassword: PropTypes.string, | ||||
|         onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
 | ||||
|         onEditServerDetailsClick: PropTypes.func, | ||||
|         flows: PropTypes.arrayOf(PropTypes.object).isRequired, | ||||
|         serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, | ||||
|         canSubmit: PropTypes.bool, | ||||
|  | @ -461,7 +460,7 @@ export default class RegistrationForm extends React.Component { | |||
|             ref={field => this[FIELD_PASSWORD_CONFIRM] = field} | ||||
|             type="password" | ||||
|             autoComplete="new-password" | ||||
|             label={_t("Confirm")} | ||||
|             label={_t("Confirm password")} | ||||
|             value={this.state.passwordConfirm} | ||||
|             onChange={this.onPasswordConfirmChange} | ||||
|             onValidate={this.onPasswordConfirmValidate} | ||||
|  | @ -513,33 +512,6 @@ export default class RegistrationForm extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { | ||||
|             serverName: this.props.serverConfig.hsName, | ||||
|         }); | ||||
|         if (this.props.serverConfig.hsNameIsDifferent) { | ||||
|             const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); | ||||
| 
 | ||||
|             yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, { | ||||
|                 'underlinedServerName': () => { | ||||
|                     return <TextWithTooltip | ||||
|                         class="mx_Login_underlinedServerName" | ||||
|                         tooltip={this.props.serverConfig.hsUrl} | ||||
|                     > | ||||
|                         {this.props.serverConfig.hsName} | ||||
|                     </TextWithTooltip>; | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         let editLink = null; | ||||
|         if (this.props.onEditServerDetailsClick) { | ||||
|             editLink = <a className="mx_AuthBody_editServerDetails" | ||||
|                 href="#" onClick={this.props.onEditServerDetailsClick} | ||||
|             > | ||||
|                 {_t('Change')} | ||||
|             </a>; | ||||
|         } | ||||
| 
 | ||||
|         const registerButton = ( | ||||
|             <input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} /> | ||||
|         ); | ||||
|  | @ -575,10 +547,6 @@ export default class RegistrationForm extends React.Component { | |||
| 
 | ||||
|         return ( | ||||
|             <div> | ||||
|                 <h3> | ||||
|                     {yourMatrixAccountText} | ||||
|                     {editLink} | ||||
|                 </h3> | ||||
|                 <form onSubmit={this.onSubmit}> | ||||
|                     <div className="mx_AuthBody_fieldRow"> | ||||
|                         {this.renderUsername()} | ||||
|  |  | |||
|  | @ -309,10 +309,14 @@ export default class InviteDialog extends React.PureComponent { | |||
| 
 | ||||
|         // The room ID this dialog is for. Only required for KIND_INVITE.
 | ||||
|         roomId: PropTypes.string, | ||||
| 
 | ||||
|         // Initial value to populate the filter with
 | ||||
|         initialText: PropTypes.string, | ||||
|     }; | ||||
| 
 | ||||
|     static defaultProps = { | ||||
|         kind: KIND_DM, | ||||
|         initialText: "", | ||||
|     }; | ||||
| 
 | ||||
|     _debounceTimer: number = null; | ||||
|  | @ -339,7 +343,7 @@ export default class InviteDialog extends React.PureComponent { | |||
| 
 | ||||
|         this.state = { | ||||
|             targets: [], // array of Member objects (see interface above)
 | ||||
|             filterText: "", | ||||
|             filterText: this.props.initialText, | ||||
|             recents: InviteDialog.buildRecents(alreadyInvited), | ||||
|             numRecentsShown: INITIAL_ROOMS_SHOWN, | ||||
|             suggestions: this._buildSuggestions(alreadyInvited), | ||||
|  | @ -357,6 +361,12 @@ export default class InviteDialog extends React.PureComponent { | |||
|         this._editorRef = createRef(); | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         if (this.props.initialText) { | ||||
|             this._updateSuggestions(this.props.initialText); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} { | ||||
|         const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
 | ||||
| 
 | ||||
|  | @ -693,6 +703,115 @@ export default class InviteDialog extends React.PureComponent { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     _updateSuggestions = async (term) => { | ||||
|         MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { | ||||
|             if (term !== this.state.filterText) { | ||||
|                 // Discard the results - we were probably too slow on the server-side to make
 | ||||
|                 // these results useful. This is a race we want to avoid because we could overwrite
 | ||||
|                 // more accurate results.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!r.results) r.results = []; | ||||
| 
 | ||||
|             // While we're here, try and autocomplete a search result for the mxid itself
 | ||||
|             // if there's no matches (and the input looks like a mxid).
 | ||||
|             if (term[0] === '@' && term.indexOf(':') > 1) { | ||||
|                 try { | ||||
|                     const profile = await MatrixClientPeg.get().getProfileInfo(term); | ||||
|                     if (profile) { | ||||
|                         // If we have a profile, we have enough information to assume that
 | ||||
|                         // the mxid can be invited - add it to the list. We stick it at the
 | ||||
|                         // top so it is most obviously presented to the user.
 | ||||
|                         r.results.splice(0, 0, { | ||||
|                             user_id: term, | ||||
|                             display_name: profile['displayname'], | ||||
|                             avatar_url: profile['avatar_url'], | ||||
|                         }); | ||||
|                     } | ||||
|                 } catch (e) { | ||||
|                     console.warn("Non-fatal error trying to make an invite for a user ID"); | ||||
|                     console.warn(e); | ||||
| 
 | ||||
|                     // Add a result anyways, just without a profile. We stick it at the
 | ||||
|                     // top so it is most obviously presented to the user.
 | ||||
|                     r.results.splice(0, 0, { | ||||
|                         user_id: term, | ||||
|                         display_name: term, | ||||
|                         avatar_url: null, | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             this.setState({ | ||||
|                 serverResultsMixin: r.results.map(u => ({ | ||||
|                     userId: u.user_id, | ||||
|                     user: new DirectoryMember(u), | ||||
|                 })), | ||||
|             }); | ||||
|         }).catch(e => { | ||||
|             console.error("Error searching user directory:"); | ||||
|             console.error(e); | ||||
|             this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
 | ||||
|         }); | ||||
| 
 | ||||
|         // Whenever we search the directory, also try to search the identity server. It's
 | ||||
|         // all debounced the same anyways.
 | ||||
|         if (!this.state.canUseIdentityServer) { | ||||
|             // The user doesn't have an identity server set - warn them of that.
 | ||||
|             this.setState({tryingIdentityServer: true}); | ||||
|             return; | ||||
|         } | ||||
|         if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) { | ||||
|             // Start off by suggesting the plain email while we try and resolve it
 | ||||
|             // to a real account.
 | ||||
|             this.setState({ | ||||
|                 // per above: the userId is a lie here - it's just a regular identifier
 | ||||
|                 threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], | ||||
|             }); | ||||
|             try { | ||||
|                 const authClient = new IdentityAuthClient(); | ||||
|                 const token = await authClient.getAccessToken(); | ||||
|                 if (term !== this.state.filterText) return; // abandon hope
 | ||||
| 
 | ||||
|                 const lookup = await MatrixClientPeg.get().lookupThreePid( | ||||
|                     'email', | ||||
|                     term, | ||||
|                     undefined, // callback
 | ||||
|                     token, | ||||
|                 ); | ||||
|                 if (term !== this.state.filterText) return; // abandon hope
 | ||||
| 
 | ||||
|                 if (!lookup || !lookup.mxid) { | ||||
|                     // We weren't able to find anyone - we're already suggesting the plain email
 | ||||
|                     // as an alternative, so do nothing.
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 // We append the user suggestion to give the user an option to click
 | ||||
|                 // the email anyways, and so we don't cause things to jump around. In
 | ||||
|                 // theory, the user would see the user pop up and think "ah yes, that
 | ||||
|                 // person!"
 | ||||
|                 const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); | ||||
|                 if (term !== this.state.filterText || !profile) return; // abandon hope
 | ||||
|                 this.setState({ | ||||
|                     threepidResultsMixin: [...this.state.threepidResultsMixin, { | ||||
|                         user: new DirectoryMember({ | ||||
|                             user_id: lookup.mxid, | ||||
|                             display_name: profile.displayname, | ||||
|                             avatar_url: profile.avatar_url, | ||||
|                         }), | ||||
|                         userId: lookup.mxid, | ||||
|                     }], | ||||
|                 }); | ||||
|             } catch (e) { | ||||
|                 console.error("Error searching identity server:"); | ||||
|                 console.error(e); | ||||
|                 this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
 | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     _updateFilter = (e) => { | ||||
|         const term = e.target.value; | ||||
|         this.setState({filterText: term}); | ||||
|  | @ -703,113 +822,8 @@ export default class InviteDialog extends React.PureComponent { | |||
|         if (this._debounceTimer) { | ||||
|             clearTimeout(this._debounceTimer); | ||||
|         } | ||||
|         this._debounceTimer = setTimeout(async () => { | ||||
|             MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { | ||||
|                 if (term !== this.state.filterText) { | ||||
|                     // Discard the results - we were probably too slow on the server-side to make
 | ||||
|                     // these results useful. This is a race we want to avoid because we could overwrite
 | ||||
|                     // more accurate results.
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (!r.results) r.results = []; | ||||
| 
 | ||||
|                 // While we're here, try and autocomplete a search result for the mxid itself
 | ||||
|                 // if there's no matches (and the input looks like a mxid).
 | ||||
|                 if (term[0] === '@' && term.indexOf(':') > 1) { | ||||
|                     try { | ||||
|                         const profile = await MatrixClientPeg.get().getProfileInfo(term); | ||||
|                         if (profile) { | ||||
|                             // If we have a profile, we have enough information to assume that
 | ||||
|                             // the mxid can be invited - add it to the list. We stick it at the
 | ||||
|                             // top so it is most obviously presented to the user.
 | ||||
|                             r.results.splice(0, 0, { | ||||
|                                 user_id: term, | ||||
|                                 display_name: profile['displayname'], | ||||
|                                 avatar_url: profile['avatar_url'], | ||||
|                             }); | ||||
|                         } | ||||
|                     } catch (e) { | ||||
|                         console.warn("Non-fatal error trying to make an invite for a user ID"); | ||||
|                         console.warn(e); | ||||
| 
 | ||||
|                         // Add a result anyways, just without a profile. We stick it at the
 | ||||
|                         // top so it is most obviously presented to the user.
 | ||||
|                         r.results.splice(0, 0, { | ||||
|                             user_id: term, | ||||
|                             display_name: term, | ||||
|                             avatar_url: null, | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 this.setState({ | ||||
|                     serverResultsMixin: r.results.map(u => ({ | ||||
|                         userId: u.user_id, | ||||
|                         user: new DirectoryMember(u), | ||||
|                     })), | ||||
|                 }); | ||||
|             }).catch(e => { | ||||
|                 console.error("Error searching user directory:"); | ||||
|                 console.error(e); | ||||
|                 this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
 | ||||
|             }); | ||||
| 
 | ||||
|             // Whenever we search the directory, also try to search the identity server. It's
 | ||||
|             // all debounced the same anyways.
 | ||||
|             if (!this.state.canUseIdentityServer) { | ||||
|                 // The user doesn't have an identity server set - warn them of that.
 | ||||
|                 this.setState({tryingIdentityServer: true}); | ||||
|                 return; | ||||
|             } | ||||
|             if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) { | ||||
|                 // Start off by suggesting the plain email while we try and resolve it
 | ||||
|                 // to a real account.
 | ||||
|                 this.setState({ | ||||
|                     // per above: the userId is a lie here - it's just a regular identifier
 | ||||
|                     threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], | ||||
|                 }); | ||||
|                 try { | ||||
|                     const authClient = new IdentityAuthClient(); | ||||
|                     const token = await authClient.getAccessToken(); | ||||
|                     if (term !== this.state.filterText) return; // abandon hope
 | ||||
| 
 | ||||
|                     const lookup = await MatrixClientPeg.get().lookupThreePid( | ||||
|                         'email', | ||||
|                         term, | ||||
|                         undefined, // callback
 | ||||
|                         token, | ||||
|                     ); | ||||
|                     if (term !== this.state.filterText) return; // abandon hope
 | ||||
| 
 | ||||
|                     if (!lookup || !lookup.mxid) { | ||||
|                         // We weren't able to find anyone - we're already suggesting the plain email
 | ||||
|                         // as an alternative, so do nothing.
 | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // We append the user suggestion to give the user an option to click
 | ||||
|                     // the email anyways, and so we don't cause things to jump around. In
 | ||||
|                     // theory, the user would see the user pop up and think "ah yes, that
 | ||||
|                     // person!"
 | ||||
|                     const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); | ||||
|                     if (term !== this.state.filterText || !profile) return; // abandon hope
 | ||||
|                     this.setState({ | ||||
|                         threepidResultsMixin: [...this.state.threepidResultsMixin, { | ||||
|                             user: new DirectoryMember({ | ||||
|                                 user_id: lookup.mxid, | ||||
|                                 display_name: profile.displayname, | ||||
|                                 avatar_url: profile.avatar_url, | ||||
|                             }), | ||||
|                             userId: lookup.mxid, | ||||
|                         }], | ||||
|                     }); | ||||
|                 } catch (e) { | ||||
|                     console.error("Error searching identity server:"); | ||||
|                     console.error(e); | ||||
|                     this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
 | ||||
|                 } | ||||
|             } | ||||
|         this._debounceTimer = setTimeout(() => { | ||||
|             this._updateSuggestions(term); | ||||
|         }, 150); // 150ms debounce (human reaction time + some)
 | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,8 +20,8 @@ import * as sdk from '../../../index'; | |||
| import { _t } from '../../../languageHandler'; | ||||
| 
 | ||||
| export default class DirectorySearchBox extends React.Component { | ||||
|     constructor() { | ||||
|         super(); | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|         this._collectInput = this._collectInput.bind(this); | ||||
|         this._onClearClick = this._onClearClick.bind(this); | ||||
|         this._onChange = this._onChange.bind(this); | ||||
|  | @ -31,7 +31,7 @@ export default class DirectorySearchBox extends React.Component { | |||
|         this.input = null; | ||||
| 
 | ||||
|         this.state = { | ||||
|             value: '', | ||||
|             value: this.props.initialText || '', | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  | @ -90,15 +90,20 @@ export default class DirectorySearchBox extends React.Component { | |||
|         } | ||||
| 
 | ||||
|         return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}> | ||||
|                 <input type="text" name="dirsearch" value={this.state.value} | ||||
|                     className="mx_textinput_icon mx_textinput_search" | ||||
|                     ref={this._collectInput} | ||||
|                     onChange={this._onChange} onKeyUp={this._onKeyUp} | ||||
|                     placeholder={this.props.placeholder} autoFocus | ||||
|                 /> | ||||
|                 { joinButton } | ||||
|                 <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick}></AccessibleButton> | ||||
|             </div>; | ||||
|             <input | ||||
|                 type="text" | ||||
|                 name="dirsearch" | ||||
|                 value={this.state.value} | ||||
|                 className="mx_textinput_icon mx_textinput_search" | ||||
|                 ref={this._collectInput} | ||||
|                 onChange={this._onChange} | ||||
|                 onKeyUp={this._onKeyUp} | ||||
|                 placeholder={this.props.placeholder} | ||||
|                 autoFocus | ||||
|             /> | ||||
|             { joinButton } | ||||
|             <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} /> | ||||
|         </div>; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -109,4 +114,5 @@ DirectorySearchBox.propTypes = { | |||
|     onJoinClick: PropTypes.func, | ||||
|     placeholder: PropTypes.string, | ||||
|     showJoinButton: PropTypes.bool, | ||||
|     initialText: PropTypes.string, | ||||
| }; | ||||
|  |  | |||
|  | @ -58,6 +58,7 @@ interface IProps { | |||
| 
 | ||||
| interface IState { | ||||
|     sublists: ITagMap; | ||||
|     isNameFiltering: boolean; | ||||
| } | ||||
| 
 | ||||
| const TAG_ORDER: TagID[] = [ | ||||
|  | @ -183,6 +184,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
| 
 | ||||
|         this.state = { | ||||
|             sublists: {}, | ||||
|             isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), | ||||
|         }; | ||||
| 
 | ||||
|         this.dispatcherRef = defaultDispatcher.register(this.onAction); | ||||
|  | @ -253,7 +255,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|             return CustomRoomTagStore.getTags()[t]; | ||||
|         }); | ||||
| 
 | ||||
|         let doUpdate = arrayHasDiff(previousListIds, newListIds); | ||||
|         const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition(); | ||||
|         let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds); | ||||
|         if (!doUpdate) { | ||||
|             // so we didn't have the visible sublists change, but did the contents of those
 | ||||
|             // sublists change significantly enough to break the sticky headers? Probably, so
 | ||||
|  | @ -275,14 +278,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|             const newSublists = objectWithOnly(newLists, newListIds); | ||||
|             const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); | ||||
| 
 | ||||
|             this.setState({sublists}, () => { | ||||
|             this.setState({sublists, isNameFiltering}, () => { | ||||
|                 this.props.onResize(); | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private onStartChat = () => { | ||||
|         const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; | ||||
|         dis.dispatch({ action: "view_create_chat", initialText }); | ||||
|     }; | ||||
| 
 | ||||
|     private onExplore = () => { | ||||
|         dis.fire(Action.ViewRoomDirectory); | ||||
|         const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; | ||||
|         dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); | ||||
|     }; | ||||
| 
 | ||||
|     private renderCommunityInvites(): TemporaryTile[] { | ||||
|  | @ -332,8 +341,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|             return p; | ||||
|         }, [] as TagID[]); | ||||
| 
 | ||||
|         // show a skeleton UI if the user is in no rooms
 | ||||
|         const showSkeleton = Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length); | ||||
|         // show a skeleton UI if the user is in no rooms and they are not filtering
 | ||||
|         const showSkeleton = !this.state.isNameFiltering && | ||||
|             Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length); | ||||
| 
 | ||||
|         for (const orderedTagId of tagOrder) { | ||||
|             const orderedRooms = this.state.sublists[orderedTagId] || []; | ||||
|  | @ -370,10 +380,21 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|     public render() { | ||||
|         let explorePrompt: JSX.Element; | ||||
|         if (!this.props.isMinimized) { | ||||
|             if (RoomListStore.instance.getFirstNameFilterCondition()) { | ||||
|             if (this.state.isNameFiltering) { | ||||
|                 explorePrompt = <div className="mx_RoomList_explorePrompt"> | ||||
|                     <div>{_t("Can't see what you’re looking for?")}</div> | ||||
|                     <AccessibleButton kind="link" onClick={this.onExplore}> | ||||
|                     <AccessibleButton | ||||
|                         className="mx_RoomList_explorePrompt_startChat" | ||||
|                         kind="link" | ||||
|                         onClick={this.onStartChat} | ||||
|                     > | ||||
|                         {_t("Start a new chat")} | ||||
|                     </AccessibleButton> | ||||
|                     <AccessibleButton | ||||
|                         className="mx_RoomList_explorePrompt_explore" | ||||
|                         kind="link" | ||||
|                         onClick={this.onExplore} | ||||
|                     > | ||||
|                         {_t("Explore all public rooms")} | ||||
|                     </AccessibleButton> | ||||
|                 </div>; | ||||
|  | @ -385,7 +406,18 @@ export default class RoomList extends React.PureComponent<IProps, IState> { | |||
|                 if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) { | ||||
|                     explorePrompt = <div className="mx_RoomList_explorePrompt"> | ||||
|                         <div>{_t("Use the + to make a new room or explore existing ones below")}</div> | ||||
|                         <AccessibleButton kind="link" onClick={this.onExplore}> | ||||
|                         <AccessibleButton | ||||
|                             className="mx_RoomList_explorePrompt_startChat" | ||||
|                             kind="link" | ||||
|                             onClick={this.onStartChat} | ||||
|                         > | ||||
|                             {_t("Start a new chat")} | ||||
|                         </AccessibleButton> | ||||
|                         <AccessibleButton | ||||
|                             className="mx_RoomList_explorePrompt_explore" | ||||
|                             kind="link" | ||||
|                             onClick={this.onExplore} | ||||
|                         > | ||||
|                             {_t("Explore all public rooms")} | ||||
|                         </AccessibleButton> | ||||
|                     </div>; | ||||
|  |  | |||
|  | @ -1390,6 +1390,7 @@ | |||
|     "Historical": "Historical", | ||||
|     "Custom Tag": "Custom Tag", | ||||
|     "Can't see what you’re looking for?": "Can't see what you’re looking for?", | ||||
|     "Start a new chat": "Start a new chat", | ||||
|     "Explore all public rooms": "Explore all public rooms", | ||||
|     "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", | ||||
|     "%(count)s results|other": "%(count)s results", | ||||
|  | @ -2208,8 +2209,8 @@ | |||
|     "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", | ||||
|     "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies", | ||||
|     "Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:", | ||||
|     "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s", | ||||
|     "Please check your email to continue registration.": "Please check your email to continue registration.", | ||||
|     "A confirmation email has been sent to %(emailAddress)s": "A confirmation email has been sent to %(emailAddress)s", | ||||
|     "Open the link in the email to continue registration.": "Open the link in the email to continue registration.", | ||||
|     "Token incorrect": "Token incorrect", | ||||
|     "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", | ||||
|     "Please enter the code it contains:": "Please enter the code it contains:", | ||||
|  | @ -2246,8 +2247,6 @@ | |||
|     "Enter username": "Enter username", | ||||
|     "Email (optional)": "Email (optional)", | ||||
|     "Phone (optional)": "Phone (optional)", | ||||
|     "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", | ||||
|     "Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />", | ||||
|     "Register": "Register", | ||||
|     "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.", | ||||
|     "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.", | ||||
|  | @ -2370,8 +2369,9 @@ | |||
|     "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", | ||||
|     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", | ||||
|     "Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s", | ||||
|     "Filter": "Filter", | ||||
|     "Clear filter": "Clear filter", | ||||
|     "Search rooms": "Search rooms", | ||||
|     "Filter rooms and people": "Filter rooms and people", | ||||
|     "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.", | ||||
|     "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.", | ||||
|     "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.", | ||||
|  | @ -2470,6 +2470,8 @@ | |||
|     "<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.", | ||||
|     "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.", | ||||
|     "Registration Successful": "Registration Successful", | ||||
|     "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", | ||||
|     "Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />", | ||||
|     "Create your account": "Create your account", | ||||
|     "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", | ||||
|     "Use Recovery Key": "Use Recovery Key", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski