mirror of https://github.com/vector-im/riot-web
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