Merge branch 'travis/ftue/user-lists/4.5-3pids' into travis/ftue/user-lists/4.5.5-paste

This commit is contained in:
Travis Ralston 2020-01-08 21:21:03 -07:00
commit 35b27bd231
3 changed files with 296 additions and 35 deletions

View file

@ -184,6 +184,10 @@ limitations under the License.
.mx_DMInviteDialog_userTile_name {
vertical-align: top;
}
.mx_DMInviteDialog_userTile_threepidAvatar {
background-color: #ffffff; // this is fine without a var because it's for both themes
}
}
.mx_DMInviteDialog_userTile_remove {

View file

@ -1,31 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="69px" height="68px" viewBox="0 0 69 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="65.631203"
height="67.980705"
viewBox="0 0 65.631202 67.980705"
version="1.1"
id="svg32"
sodipodi:docname="icon-email-pill-avatar.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>at-sign</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1377"
id="namedview34"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="13.882353"
inkscape:cx="52.627789"
inkscape:cy="30.525891"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg32"/>
<!-- Generator: Sketch 58 (84663) - https://sketch.com -->
<title>at-sign</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-5.9%" y="-7.9%" width="111.8%" height="115.8%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
<title
id="title2">at-sign
</title>
<desc
id="desc4">Created with Sketch.
</desc>
<defs
id="defs19">
<filter
x="-0.059"
y="-0.079000004"
width="1.118"
height="1.158"
filterUnits="objectBoundingBox"
id="filter-1">
<feOffset
dx="0"
dy="2"
in="SourceAlpha"
result="shadowOffsetOuter1"
id="feOffset6"/>
<feGaussianBlur
stdDeviation="16"
in="shadowOffsetOuter1"
result="shadowBlurOuter1"
id="feGaussianBlur8"/>
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0"
type="matrix"
in="shadowBlurOuter1"
result="shadowMatrixOuter1"
id="feColorMatrix10"/>
<feMerge
id="feMerge16">
<feMergeNode
in="shadowMatrixOuter1"
id="feMergeNode12"/>
<feMergeNode
in="SourceGraphic"
id="feMergeNode14"/>
</feMerge>
</filter>
</defs>
<g id="FTUE" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="FTUE---User-list-suggestions" transform="translate(-5161.000000, -1379.000000)" stroke="#368BD6">
<g id="Group-14-Copy-10" transform="translate(4748.000000, 1168.000000)">
<g id="Web-Copy-6" filter="url(#filter-1)">
<g id="modal-copy" transform="translate(253.000000, 118.000000)">
<g id="content" transform="translate(40.000000, 107.000000)">
<g id="Group-43">
<g id="Group-15-Copy-6" transform="translate(143.000000, 6.000000)">
<g id="at-sign" transform="translate(5.500000, 6.500000)">
<circle id="Oval" cx="6.28571429" cy="5.71428571" r="2.28571429"></circle>
<path d="M8.57142857,3.42857143 L8.57142857,6.28571429 C8.57142857,7.23248814 9.33894043,8 10.2857143,8 C11.2324881,8 12,7.23248814 12,6.28571429 L12,5.71428571 C11.9998328,3.05880261 10.1703625,0.75337961 7.58436487,0.149884297 C4.9983672,-0.453611015 2.33741804,0.803881013 1.16186053,3.18498476 C-0.0136969889,5.5660885 0.605971794,8.4432307 2.65750183,10.1292957 C4.70903186,11.8153606 7.65171364,11.8659623 9.76,10.2514286" id="Path"></path>
</g>
<g
style="fill:none;fill-rule:evenodd;stroke:#368bd6;stroke-width:1;stroke-linecap:round;stroke-linejoin:round"
transform="matrix(3.4090685,0,0,3.4090685,-17679.973,-4776.9361)"
id="FTUE---User-list-suggestions">
<g
transform="translate(4748,1168)"
id="Group-14-Copy-10">
<g
style="filter:url(#filter-1)"
id="Web-Copy-6">
<g
transform="translate(253,118)"
id="modal-copy">
<g
transform="translate(40,107)"
id="content">
<g
id="Group-43">
<g
transform="translate(143,6)"
id="Group-15-Copy-6">
<g
transform="translate(5.5,6.5)"
id="at-sign">
<circle
r="2.2857144"
cy="5.7142859"
cx="6.2857141"
id="Oval"/>
<path
inkscape:connector-curvature="0"
id="Path"
d="M 8.5714286,3.4285714 V 6.2857143 C 8.5714286,7.2324881 9.3389404,8 10.285714,8 11.232488,8 12,7.2324881 12,6.2857143 V 5.7142857 C 11.999833,3.0588026 10.170363,0.75337961 7.5843649,0.1498843 4.9983672,-0.45361102 2.337418,0.80388101 1.1618605,3.1849848 -0.01369699,5.5660885 0.60597179,8.4432307 2.6575018,10.129296 4.7090319,11.815361 7.6517136,11.865962 9.76,10.251429"/>
</g>
</g>
</g>
@ -34,4 +134,4 @@
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -25,6 +25,11 @@ import {RoomMember} from "matrix-js-sdk/lib/matrix";
import * as humanize from "humanize";
import SdkConfig from "../../../SdkConfig";
import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo";
import * as Email from "../../../email";
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
import {abbreviateUrl} from "../../../utils/UrlUtils";
import dis from "../../../dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
// TODO: [TravisR] Make this generic for all kinds of invites
@ -84,6 +89,35 @@ class DirectoryMember extends Member {
}
}
class ThreepidMember extends Member {
_id: string;
constructor(id: string) {
super();
this._id = id;
}
// This is a getter that would be falsey on all other implementations. Until we have
// better type support in the react-sdk we can use this trick to determine the kind
// of 3PID we're dealing with, if any.
get isEmail(): boolean {
return this._id.includes('@');
}
// These next class members are for the Member interface
get name(): string {
return this._id;
}
get userId(): string {
return this._id;
}
getMxcAvatarUrl(): string {
return null;
}
}
class DMUserTile extends React.PureComponent {
static propTypes = {
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
@ -105,7 +139,7 @@ class DMUserTile extends React.PureComponent {
const avatarSize = 20;
const avatar = this.props.member.isEmail
? <img
className='mx_DMInviteDialog_userTile_avatar'
className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar'
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} />
: <BaseAvatar
@ -259,6 +293,9 @@ export default class DMInviteDialog extends React.PureComponent {
suggestions: this._buildSuggestions(),
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
tryingIdentityServer: false,
};
this._editorRef = createRef();
@ -362,7 +399,7 @@ export default class DMInviteDialog extends React.PureComponent {
if (this._debounceTimer) {
clearTimeout(this._debounceTimer);
}
this._debounceTimer = setTimeout(() => {
this._debounceTimer = setTimeout(async () => {
MatrixClientPeg.get().searchUserDirectory({term}).then(r => {
if (term !== this.state.filterText) {
// Discard the results - we were probably too slow on the server-side to make
@ -381,6 +418,62 @@ export default class DMInviteDialog extends React.PureComponent {
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)) {
// 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
}
}
}, 150); // 150ms debounce (human reaction time + some)
};
@ -419,6 +512,21 @@ export default class DMInviteDialog extends React.PureComponent {
}
};
_onUseDefaultIdentityServerClick = (e) => {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
// eslint-disable-next-line react-hooks/rules-of-hooks
useDefaultIdentityServer();
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
};
_onManageSettingsClick = (e) => {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
this._cancel();
};
_renderSection(kind: "recents"|"suggestions") {
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
@ -426,17 +534,27 @@ export default class DMInviteDialog extends React.PureComponent {
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
// Mix in the server results if we have any, but only if we're searching
if (this.state.filterText && this.state.serverResultsMixin && kind === 'suggestions') {
// only pick out the server results that aren't already covered though
const uniqueServerResults = this.state.serverResultsMixin
.filter(u => !sourceMembers.some(m => m.userId === u.userId));
// Mix in the server results if we have any, but only if we're searching. We track the additional
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
// the right members in them.
let additionalMembers = [];
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
if (this.state.filterText && hasMixins && kind === 'suggestions') {
// We don't want to duplicate members though, so just exclude anyone we've already seen.
const notAlreadyExists = (u: Member): boolean => {
return !sourceMembers.some(m => m.userId === u.userId)
&& !additionalMembers.some(m => m.userId === u.userId);
};
sourceMembers = sourceMembers.concat(uniqueServerResults);
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
additionalMembers = additionalMembers.concat(...uniqueServerResults);
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
}
// Hide the section if there's nothing to filter by
if (!sourceMembers || sourceMembers.length === 0) return null;
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
// Do some simple filtering on the input before going much further. If we get no results, say so.
if (this.state.filterText) {
@ -444,7 +562,7 @@ export default class DMInviteDialog extends React.PureComponent {
sourceMembers = sourceMembers
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
if (sourceMembers.length === 0) {
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
return (
<div className='mx_DMInviteDialog_section'>
<h3>{sectionName}</h3>
@ -454,6 +572,10 @@ export default class DMInviteDialog extends React.PureComponent {
}
}
// Now we mix in the additional members. Again, we presume these have already been filtered. We
// also assume they are more relevant than our suggestions and prepend them to the list.
sourceMembers = [...additionalMembers, ...sourceMembers];
// If we're going to hide one member behind 'show more', just use up the space of the button
// with the member's tile instead.
if (showNum === sourceMembers.length - 1) showNum++;
@ -512,6 +634,40 @@ export default class DMInviteDialog extends React.PureComponent {
);
}
_renderIdentityServerWarning() {
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
return null;
}
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
return (
<div className="mx_AddressPickerDialog_identityServer">{_t(
"Use an identity server to invite by email. " +
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
"or manage in <settings>Settings</settings>.",
{
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
},
{
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
},
)}</div>
);
} else {
return (
<div className="mx_AddressPickerDialog_identityServer">{_t(
"Use an identity server to invite by email. " +
"Manage in <settings>Settings</settings>.",
{}, {
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
},
)}</div>
);
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -535,6 +691,7 @@ export default class DMInviteDialog extends React.PureComponent {
</p>
<div className='mx_DMInviteDialog_addressBar'>
{this._renderEditor()}
{this._renderIdentityServerWarning()}
<AccessibleButton
kind="primary"
onClick={this._startDm}