From d5c399dfd9f5265230f44fb92fd124a12fdbc5ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 12:56:20 +0000 Subject: [PATCH 01/10] Fix Left Panel layout being wrong when filtering with 0 rooms --- src/components/structures/LeftPanel.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4445ff3ff8..52d461edcc 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -46,6 +46,7 @@ interface IProps { } interface IState { + isFiltering: boolean; showBreadcrumbs: boolean; showGroupFilterPanel: boolean; } @@ -70,6 +71,7 @@ export default class LeftPanel extends React.Component { super(props); this.state = { + isFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), showBreadcrumbs: BreadcrumbsStore.instance.visible, showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; @@ -102,9 +104,10 @@ export default class LeftPanel extends React.Component { }; private onBreadcrumbsUpdate = () => { - const newVal = BreadcrumbsStore.instance.visible; - if (newVal !== this.state.showBreadcrumbs) { - this.setState({showBreadcrumbs: newVal}); + const showBreadcrumbs = BreadcrumbsStore.instance.visible; + const isFiltering = !!RoomListStore.instance.getFirstNameFilterCondition(); + if (showBreadcrumbs !== this.state.showBreadcrumbs || isFiltering !== this.state.isFiltering) { + this.setState({showBreadcrumbs, isFiltering}); // Update the sticky headers too as the breadcrumbs will be popping in or out. if (!this.listContainerRef.current) return; // ignore: no headers to sticky From 187901004d0a97818b95f2a184cb731f9b5bd2e8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:01:40 +0000 Subject: [PATCH 02/10] Fix the Join rooms prompt not showing up due to missing updates --- src/components/structures/LeftPanel.tsx | 9 +++------ src/components/views/rooms/RoomList.tsx | 9 ++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 52d461edcc..4445ff3ff8 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -46,7 +46,6 @@ interface IProps { } interface IState { - isFiltering: boolean; showBreadcrumbs: boolean; showGroupFilterPanel: boolean; } @@ -71,7 +70,6 @@ export default class LeftPanel extends React.Component { super(props); this.state = { - isFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), showBreadcrumbs: BreadcrumbsStore.instance.visible, showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; @@ -104,10 +102,9 @@ export default class LeftPanel extends React.Component { }; private onBreadcrumbsUpdate = () => { - const showBreadcrumbs = BreadcrumbsStore.instance.visible; - const isFiltering = !!RoomListStore.instance.getFirstNameFilterCondition(); - if (showBreadcrumbs !== this.state.showBreadcrumbs || isFiltering !== this.state.isFiltering) { - this.setState({showBreadcrumbs, isFiltering}); + const newVal = BreadcrumbsStore.instance.visible; + if (newVal !== this.state.showBreadcrumbs) { + this.setState({showBreadcrumbs: newVal}); // Update the sticky headers too as the breadcrumbs will be popping in or out. if (!this.listContainerRef.current) return; // ignore: no headers to sticky diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index d952c137cd..3e9a8ac102 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -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 { this.state = { sublists: {}, + isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), }; this.dispatcherRef = defaultDispatcher.register(this.onAction); @@ -253,7 +255,8 @@ export default class RoomList extends React.PureComponent { 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,7 +278,7 @@ export default class RoomList extends React.PureComponent { const newSublists = objectWithOnly(newLists, newListIds); const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); - this.setState({sublists}, () => { + this.setState({sublists, isNameFiltering}, () => { this.props.onResize(); }); } @@ -370,7 +373,7 @@ export default class RoomList extends React.PureComponent { public render() { let explorePrompt: JSX.Element; if (!this.props.isMinimized) { - if (RoomListStore.instance.getFirstNameFilterCondition()) { + if (this.state.isNameFiltering) { explorePrompt =
{_t("Can't see what you’re looking for?")}
From d3fee540c5b8223b80301791e9bb38ac19dd68ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:08:09 +0000 Subject: [PATCH 03/10] Update Room List filter copy --- src/components/structures/RoomSearch.tsx | 4 ++-- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 526aecddd7..a64e40bc65 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent { 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 { if (this.props.isMinimized) { icon = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 830d3cdee4..701bee457f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2372,8 +2372,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 Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", "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 our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", From a481f3bdf1d6be30538185fda44718acb9d3863b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:21:20 +0000 Subject: [PATCH 04/10] Iterate the filtering prompt --- res/css/views/rooms/_RoomList.scss | 9 ++++++- src/components/views/rooms/RoomList.tsx | 35 ++++++++++++++++++++++--- src/i18n/strings/en_EN.json | 1 + 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 78e7307bc0..6ea99585d2 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -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'); } } diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 3e9a8ac102..de54fabc53 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -284,6 +284,10 @@ export default class RoomList extends React.PureComponent { } }; + private onStartChat = () => { + dis.dispatch({action: "view_create_chat"}); + }; + private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -335,8 +339,9 @@ export default class RoomList extends React.PureComponent { 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] || []; @@ -376,7 +381,18 @@ export default class RoomList extends React.PureComponent { if (this.state.isNameFiltering) { explorePrompt =
{_t("Can't see what you’re looking for?")}
- + + {_t("Start a new chat")} + + {_t("Explore all public rooms")}
; @@ -388,7 +404,18 @@ export default class RoomList extends React.PureComponent { if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) { explorePrompt =
{_t("Use the + to make a new room or explore existing ones below")}
- + + {_t("Start a new chat")} + + {_t("Explore all public rooms")}
; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 701bee457f..3bfa962216 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1391,6 +1391,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", From d0513406ee1d78ca82f144a8e72f59a23050965a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:36:17 +0000 Subject: [PATCH 05/10] Pass filter text when clicking explore/dm prompt --- src/RoomInvite.js | 4 +- src/components/structures/MatrixChat.tsx | 7 +- src/components/structures/RoomDirectory.js | 4 +- src/components/views/dialogs/InviteDialog.js | 230 ++++++++++-------- .../views/elements/DirectorySearchBox.js | 30 ++- src/components/views/rooms/RoomList.tsx | 6 +- 6 files changed, 153 insertions(+), 128 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 7eb7f5dbb2..06d3fb04e8 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -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, ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 22cd73eff7..b2c94e4a8b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -653,8 +653,9 @@ export default class MatrixChat extends React.PureComponent { } 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 { this.chatCreateOrReuse(payload.user_id); break; case 'view_create_chat': - showStartChatInviteDialog(); + showStartChatInviteDialog(payload.initialText || ""); break; case 'view_invite': showRoomInviteDialog(payload.roomId); diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index ece70e3a8f..e3323b05fa 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -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}
; diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 99878569d3..9b7c5803a1 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -308,10 +308,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; @@ -338,7 +342,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), @@ -356,6 +360,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): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room @@ -687,6 +697,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}); @@ -697,113 +816,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) }; diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js index c2e8e4fd68..644b69417b 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.js @@ -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
- - { joinButton } - -
; + + { joinButton } + + ; } } @@ -109,4 +114,5 @@ DirectorySearchBox.propTypes = { onJoinClick: PropTypes.func, placeholder: PropTypes.string, showJoinButton: PropTypes.bool, + initialText: PropTypes.string, }; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index de54fabc53..6e677f2b01 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -285,11 +285,13 @@ export default class RoomList extends React.PureComponent { }; private onStartChat = () => { - dis.dispatch({action: "view_create_chat"}); + 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[] { From 0bee4bd72bbdde493a0eed3a55d07944e219897d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:45:50 +0000 Subject: [PATCH 06/10] Update `Confirm` password placeholder --- src/components/views/auth/RegistrationForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 419443984a..5245d1a921 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -461,7 +461,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} From b3ccabbe6bfdfae580862307c6739b06652f6118 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 14:00:40 +0000 Subject: [PATCH 07/10] Clear recaptcha error on reattempts --- src/components/views/auth/CaptchaForm.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js index 5cce93f0b8..e2d7d594fa 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.js @@ -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({ From edb5e10506cd79349adff8e7d16dc8810b311abd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 15:07:57 +0000 Subject: [PATCH 08/10] Iterate registration to simplify it based on usertesting --- .../auth/_InteractiveAuthEntryComponents.scss | 29 +++++++++ res/img/element-icons/email-prompt.svg | 13 ++++ .../structures/auth/Registration.js | 63 ++++++++++++------- .../auth/InteractiveAuthEntryComponents.js | 8 +-- src/components/views/auth/RegistrationForm.js | 32 ---------- 5 files changed, 86 insertions(+), 59 deletions(-) create mode 100644 res/img/element-icons/email-prompt.svg diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 05cddf2c48..0a5ac9b2bc 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -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; } diff --git a/res/img/element-icons/email-prompt.svg b/res/img/element-icons/email-prompt.svg new file mode 100644 index 0000000000..19b8f82449 --- /dev/null +++ b/res/img/element-icons/email-prompt.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 630e04da9c..777d57344d 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -502,17 +502,9 @@ export default class Registration extends React.Component { 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 - // config as the user may need to change servers to resolve the error). - if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { - return
- -
; + // 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; } const serverDetailsProps = {}; @@ -582,17 +574,6 @@ export default class Registration extends React.Component { ; } 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 ; } 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': () => { + return ; + }, + }); + } + + // 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 = ( + + {_t('Change')} + + ); + } + body =

{ _t('Create your account') }

{ errorText } { serverDeadSection } { this.renderServerComponent() } +

+ {yourMatrixAccountText} + {editLink} +

{ this.renderRegisterComponent() } { goBack } { signIn } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index f49e6959fb..6628ca7120 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -421,12 +421,12 @@ export class EmailIdentityAuthEntry extends React.Component { return ; } else { return ( -
-

{ _t("An email has been sent to %(emailAddress)s", - { emailAddress: (sub) => { this.props.inputs.emailAddress } }, +

+

{ _t("A confirmation email has been sent to %(emailAddress)s", + { emailAddress: (sub) => { this.props.inputs.emailAddress } }, ) }

-

{ _t("Please check your email to continue registration.") }

+

{ _t("Open the link in the email to continue registration.") }

); } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 5245d1a921..70c1017427 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -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, @@ -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': () => { - return ; - }, - }); - } - - let editLink = null; - if (this.props.onEditServerDetailsClick) { - editLink = - {_t('Change')} - ; - } - const registerButton = ( ); @@ -575,10 +547,6 @@ export default class RegistrationForm extends React.Component { return (
-

- {yourMatrixAccountText} - {editLink} -

{this.renderUsername()} From a2958f8f99e9b142cac482d888d6f9fcc0bc2e53 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 16:45:46 +0000 Subject: [PATCH 09/10] i18n --- src/i18n/strings/en_EN.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3bfa962216..d9bdd82c42 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2211,8 +2211,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:", @@ -2249,8 +2249,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 ": "Create your Matrix account on ", "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.", @@ -2474,6 +2472,8 @@ "Log in to your new account.": "Log in to your new account.", "You can now close this window or log in to your new account.": "You can now close this window or log in 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 ": "Create your Matrix account on ", "Create your account": "Create your account", "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", "Use Recovery Key": "Use Recovery Key", From 0911007c77e35efbc0fd00712ffcdcfcaf025034 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 17:12:27 +0000 Subject: [PATCH 10/10] fix issue with server selector introduced in this PR --- src/components/structures/auth/Registration.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 777d57344d..80bf3b72cd 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -507,6 +507,19 @@ export default class Registration extends React.Component { 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 + // config as the user may need to change servers to resolve the error). + if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { + return
+ +
; + } + const serverDetailsProps = {}; if (PHASES_ENABLED) { serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; @@ -704,10 +717,10 @@ export default class Registration extends React.Component { { errorText } { serverDeadSection } { this.renderServerComponent() } -

+ { this.state.phase !== PHASE_SERVER_DETAILS &&

{yourMatrixAccountText} {editLink} -

+ } { this.renderRegisterComponent() } { goBack } { signIn }