Merge branch 'develop' into luke/room-list-flux

This commit is contained in:
Luke Barnard 2018-02-05 11:39:39 +00:00 committed by GitHub
commit ff8fcb3139
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2892 additions and 143 deletions

View file

@ -55,11 +55,15 @@ export default React.createClass({
_checkGroupId: function(e) {
let error = null;
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
if (!this.state.groupId) {
error = _t("Community IDs cannot not be empty.");
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({
groupIdError: error,
// Reset createError to get rid of now stale error message
createError: null,
});
return error;
},

View file

@ -20,10 +20,11 @@ import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink} from "../../../matrix-to";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/room\/(([\#\!])[^\/]*)\/(\$[^\/]*)$/;
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
export default class Quote extends React.Component {
static isMessageUrl(url) {
@ -32,111 +33,156 @@ export default class Quote extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.object,
addRichQuote: PropTypes.func,
};
static propTypes = {
// The matrix.to url of the event
url: PropTypes.string,
// The original node that was rendered
node: PropTypes.instanceOf(Element),
// The parent event
parentEv: PropTypes.instanceOf(MatrixEvent),
// Whether this isn't the first Quote, and we're being nested
isNested: PropTypes.bool,
};
constructor(props, context) {
super(props, context);
this.state = {
// The event related to this quote
event: null,
show: !this.props.isNested,
// The event related to this quote and their nested rich quotes
events: [],
// Whether the top (oldest) event should be shown or spoilered
show: true,
// Whether an error was encountered fetching nested older event, show node if it does
err: false,
};
this.onQuoteClick = this.onQuoteClick.bind(this);
this.addRichQuote = this.addRichQuote.bind(this);
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
addRichQuote: this.addRichQuote,
};
}
parseUrl(url) {
if (!url) return;
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
const [, roomIdentifier, eventId] = matrixToMatch;
return {roomIdentifier, eventId};
}
componentWillReceiveProps(nextProps) {
let roomId;
let prefix;
let eventId;
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
if (!roomIdentifier || !eventId) return;
if (nextProps.url) {
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(nextProps.url) || [];
roomId = matrixToMatch[1]; // The room ID
prefix = matrixToMatch[2]; // The first character of prefix
eventId = matrixToMatch[3]; // The event ID
}
const room = prefix === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getAliases().includes(roomId);
}) : MatrixClientPeg.get().getRoom(roomId);
const room = this.getRoom(roomIdentifier);
if (!room) return;
// Only try and load the event if we know about the room
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
if (room) this.getEvent(room, eventId);
this.setState({ events: [] });
if (room) this.getEvent(room, eventId, true);
}
componentWillMount() {
this.componentWillReceiveProps(this.props);
}
async getEvent(room, eventId) {
let event = room.findEventById(eventId);
getRoom(id) {
const cli = MatrixClientPeg.get();
if (id[0] === '!') return cli.getRoom(id);
return cli.getRooms().find((r) => {
return r.getAliases().includes(id);
});
}
async getEvent(room, eventId, show) {
const event = room.findEventById(eventId);
if (event) {
this.setState({room, event});
this.addEvent(event, show);
return;
}
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
event = room.findEventById(eventId);
this.setState({room, event});
this.addEvent(room.findEventById(eventId), show);
}
addEvent(event, show) {
const events = [event].concat(this.state.events);
this.setState({events, show});
}
// addRichQuote(roomId, eventId) {
addRichQuote(href) {
const {roomIdentifier, eventId} = this.parseUrl(href);
if (!roomIdentifier || !eventId) {
this.setState({ err: true });
return;
}
const room = this.getRoom(roomIdentifier);
if (!room) {
this.setState({ err: true });
return;
}
this.getEvent(room, eventId, false);
}
onQuoteClick() {
this.setState({
show: true,
});
this.setState({ show: true });
}
render() {
const ev = this.state.event;
if (ev) {
if (this.state.show) {
const EventTile = sdk.getComponent('views.rooms.EventTile');
let dateSep = null;
const events = this.state.events.slice();
if (events.length) {
const evTiles = [];
const evDate = ev.getDate();
if (wantsDateSeparator(this.props.parentEv.getDate(), evDate)) {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
dateSep = <a href={this.props.url}><DateSeparator ts={evDate} /></a>;
}
if (!this.state.show) {
const oldestEv = events.shift();
const Pill = sdk.getComponent('elements.Pill');
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
return <blockquote className="mx_Quote">
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>;
evTiles.push(<blockquote className="mx_Quote" key="load">
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
})
}
</blockquote>);
}
return <div>
<a onClick={this.onQuoteClick} className="mx_Quote_show">{ _t('Quote') }</a>
<br />
</div>;
const EventTile = sdk.getComponent('views.rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
}
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>);
});
return <div>{ evTiles }</div>;
}
// Deliberately render nothing if the URL isn't recognised
return <div>
<a href={this.props.url}>{ _t('Quote') }</a>
<br />
</div>;
// in case we get an undefined/falsey node, replace it with null to make React happy
return this.props.node || null;
}
}

View file

@ -61,6 +61,10 @@ module.exports = React.createClass({
tileShape: PropTypes.string,
},
contextTypes: {
addRichQuote: PropTypes.func,
},
getInitialState: function() {
return {
// the URLs (if any) to be previewed with a LinkPreviewWidget
@ -202,18 +206,20 @@ module.exports = React.createClass({
// update the current node with one that's now taken its place
node = pillContainer;
} else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) {
// only allow this branch if we're not already in a quote, as fun as infinite nesting is.
const quoteContainer = document.createElement('span');
if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above
this.context.addRichQuote(href);
node.remove();
} else { // We're the first in the chain
const quoteContainer = document.createElement('span');
const quote =
<Quote url={href} parentEv={this.props.mxEvent} isNested={this.props.tileShape === 'quote'} />;
ReactDOM.render(quote, quoteContainer);
node.parentNode.replaceChild(quoteContainer, node);
const quote =
<Quote url={href} parentEv={this.props.mxEvent} node={node} />;
ReactDOM.render(quote, quoteContainer);
node.parentNode.replaceChild(quoteContainer, node);
node = quoteContainer;
}
pillified = true;
node = quoteContainer;
}
} else if (node.nodeType == Node.TEXT_NODE) {
const Pill = sdk.getComponent('elements.Pill');

View file

@ -58,8 +58,8 @@ module.exports = React.createClass({
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
state.remoteDomains = Object.keys(state.domainToAliases).filter((alias) => {
return alias !== localDomain;
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
return domain !== localDomain && state.domainToAliases[domain].length > 0;
});
if (canonicalAliasEvent) {

View file

@ -592,7 +592,7 @@ module.exports = withMatrixClient(React.createClass({
<div className={classes}>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<div className="mx_EventTile_line mx_EventTile_quote">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
@ -602,6 +602,7 @@ module.exports = withMatrixClient(React.createClass({
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onWidgetLoad={this.props.onWidgetLoad}
showUrlPreview={false} />
</div>
</div>

View file

@ -315,6 +315,30 @@ module.exports = React.createClass({
});
},
_getPending3PidInvites: function() {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) {
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
// any events without these keys are not valid 3pid invites, so we ignore them
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
for (let i = 0; i < requiredKeys.length; ++i) {
if (e.getContent()[requiredKeys[i]] === undefined) return false;
}
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) return false;
return true;
});
}
},
_makeMemberTiles: function(members, membership) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
@ -329,33 +353,13 @@ module.exports = React.createClass({
// Double XXX: Now it's really, really not the right home for this logic:
// we shouldn't even be passing in the 'membership' param to this function.
// Ew, ew, and ew.
// Triple XXX: This violates the size constraint, the output is expected/desired
// to be the same length as the members input array.
if (membership === "invite") {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const EntityTile = sdk.getComponent("rooms.EntityTile");
if (room) {
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
function(e) {
// any events without these keys are not valid 3pid invites, so we ignore them
const required_keys = ['key_validity_url', 'public_key', 'display_name'];
for (let i = 0; i < required_keys.length; ++i) {
if (e.getContent()[required_keys[i]] === undefined) return;
}
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) {
return;
}
memberList.push(
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />,
);
});
}
memberList.push(...this._getPending3PidInvites().map((e) => {
return <EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />;
}));
}
return memberList;
@ -374,7 +378,7 @@ module.exports = React.createClass({
},
_getChildCountInvited: function() {
return this.state.filteredInvitedMembers.length;
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
},
render: function() {

View file

@ -774,7 +774,8 @@ module.exports = React.createClass({
const aliasEvents = this.props.room.currentState.getStateEvents('m.room.aliases') || [];
let aliasCount = 0;
aliasEvents.forEach((event) => {
aliasCount += event.getContent().aliases.length;
const aliases = event.getContent().aliases || [];
aliasCount += aliases.length;
});
if (this.state.join_rule === "public" && aliasCount == 0) {