Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/appFixes

This commit is contained in:
Richard Lewis 2017-08-09 14:07:45 +01:00
commit 185379b037
10 changed files with 178 additions and 71 deletions

View file

@ -49,6 +49,12 @@ export default class RoomProvider extends AutocompleteProvider {
async getCompletions(query: string, selection: {start: number, end: number}, force = false) { async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
// Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/riot-web/issues/4762)
if (/^(\/join|\/leave)/.test(query)) {
return [];
}
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
let completions = []; let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force); const {command, range} = this.getCurrentCommand(query, selection, force);

View file

@ -48,13 +48,21 @@ export default class UserProvider extends AutocompleteProvider {
async getCompletions(query: string, selection: {start: number, end: number}, force = false) { async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
// Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/riot-web/issues/4762)
if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) {
return [];
}
let completions = []; let completions = [];
let {command, range} = this.getCurrentCommand(query, selection, force); let {command, range} = this.getCurrentCommand(query, selection, force);
if (command) { if (command) {
completions = this.matcher.match(command[0]).map((user) => { completions = this.matcher.match(command[0]).map((user) => {
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
return { return {
completion: displayName, // Length of completion should equal length of text in decorator. draft-js
// relies on the length of the entity === length of the text in the decoration.
completion: user.rawDisplayName.replace(' (IRC)', ''),
suffix: range.start === 0 ? ': ' : ' ', suffix: range.start === 0 ? ': ' : ' ',
href: 'https://matrix.to/#/' + user.userId, href: 'https://matrix.to/#/' + user.userId,
component: ( component: (

View file

@ -131,9 +131,6 @@ module.exports = React.createClass({
// the master view we are showing. // the master view we are showing.
view: VIEWS.LOADING, view: VIEWS.LOADING,
// a thing to call showScreen with once login completes.
screenAfterLogin: this.props.initialScreenAfterLogin,
// What the LoggedInView would be showing if visible // What the LoggedInView would be showing if visible
page_type: null, page_type: null,
@ -147,8 +144,6 @@ module.exports = React.createClass({
collapse_lhs: false, collapse_lhs: false,
collapse_rhs: false, collapse_rhs: false,
ready: false,
width: 10000,
leftOpacity: 1.0, leftOpacity: 1.0,
middleOpacity: 1.0, middleOpacity: 1.0,
rightOpacity: 1.0, rightOpacity: 1.0,
@ -274,6 +269,15 @@ module.exports = React.createClass({
register_hs_url: paramHs, register_hs_url: paramHs,
}); });
} }
// a thing to call showScreen with once login completes. this is kept
// outside this.state because updating it should never trigger a
// rerender.
this._screenAfterLogin = this.props.initialScreenAfterLogin;
this._windowWidth = 10000;
this.handleResize();
window.addEventListener('resize', this.handleResize);
}, },
componentDidMount: function() { componentDidMount: function() {
@ -294,9 +298,6 @@ module.exports = React.createClass({
linkifyMatrix.onGroupClick = this.onGroupClick; linkifyMatrix.onGroupClick = this.onGroupClick;
} }
window.addEventListener('resize', this.handleResize);
this.handleResize();
const teamServerConfig = this.props.config.teamServerConfig || {}; const teamServerConfig = this.props.config.teamServerConfig || {};
Lifecycle.initRtsClient(teamServerConfig.teamServerURL); Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
@ -312,13 +313,12 @@ module.exports = React.createClass({
// if the user has followed a login or register link, don't reanimate // if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page // the old creds, but rather go straight to the relevant page
const firstScreen = this.state.screenAfterLogin ? const firstScreen = this._screenAfterLogin ?
this.state.screenAfterLogin.screen : null; this._screenAfterLogin.screen : null;
if (firstScreen === 'login' || if (firstScreen === 'login' ||
firstScreen === 'register' || firstScreen === 'register' ||
firstScreen === 'forgot_password') { firstScreen === 'forgot_password') {
this.setState({loading: false});
this._showScreenAfterLogin(); this._showScreenAfterLogin();
return; return;
} }
@ -992,14 +992,12 @@ module.exports = React.createClass({
_showScreenAfterLogin: function() { _showScreenAfterLogin: function() {
// If screenAfterLogin is set, use that, then null it so that a second login will // If screenAfterLogin is set, use that, then null it so that a second login will
// result in view_home_page, _user_settings or _room_directory // result in view_home_page, _user_settings or _room_directory
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { if (this._screenAfterLogin && this._screenAfterLogin.screen) {
this.showScreen( this.showScreen(
this.state.screenAfterLogin.screen, this._screenAfterLogin.screen,
this.state.screenAfterLogin.params, this._screenAfterLogin.params,
); );
// XXX: is this necessary? `showScreen` should do it for us. this._screenAfterLogin = null;
this.notifyNewScreen(this.state.screenAfterLogin.screen);
this.setState({screenAfterLogin: null});
} else if (localStorage && localStorage.getItem('mx_last_room_id')) { } else if (localStorage && localStorage.getItem('mx_last_room_id')) {
// Before defaulting to directory, show the last viewed room // Before defaulting to directory, show the last viewed room
dis.dispatch({ dis.dispatch({
@ -1276,20 +1274,20 @@ module.exports = React.createClass({
const hideRhsThreshold = 820; const hideRhsThreshold = 820;
const showRhsThreshold = 820; const showRhsThreshold = 820;
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' }); dis.dispatch({ action: 'hide_left_panel' });
} }
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) { if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
dis.dispatch({ action: 'show_left_panel' }); dis.dispatch({ action: 'show_left_panel' });
} }
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
dis.dispatch({ action: 'hide_right_panel' }); dis.dispatch({ action: 'hide_right_panel' });
} }
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) { if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
dis.dispatch({ action: 'show_right_panel' }); dis.dispatch({ action: 'show_right_panel' });
} }
this.setState({width: window.innerWidth}); this._windowWidth = window.innerWidth;
}, },
onRoomCreated: function(roomId) { onRoomCreated: function(roomId) {

View file

@ -101,6 +101,10 @@ const SETTINGS_LABELS = [
id: 'MessageComposerInput.autoReplaceEmoji', id: 'MessageComposerInput.autoReplaceEmoji',
label: 'Automatically replace plain text Emoji', label: 'Automatically replace plain text Emoji',
}, },
{
id: 'Pill.shouldHidePillAvatar',
label: 'Hide avatars in user and room mentions',
},
/* /*
{ {
id: 'useFixedWidthFont', id: 'useFixedWidthFont',

View file

@ -47,6 +47,8 @@ const Pill = React.createClass({
inMessage: PropTypes.bool, inMessage: PropTypes.bool,
// The room in which this pill is being rendered // The room in which this pill is being rendered
room: PropTypes.instanceOf(Room), room: PropTypes.instanceOf(Room),
// Whether to include an avatar in the pill
shouldShowPillAvatar: PropTypes.bool,
}, },
getInitialState() { getInitialState() {
@ -155,7 +157,9 @@ const Pill = React.createClass({
if (member) { if (member) {
userId = member.userId; userId = member.userId;
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16}/>; avatar = <MemberAvatar member={member} width={16} height={16}/>;
}
pillClass = 'mx_UserPill'; pillClass = 'mx_UserPill';
} }
} }
@ -164,7 +168,9 @@ const Pill = React.createClass({
const room = this.state.room; const room = this.state.room;
if (room) { if (room) {
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource; linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16}/>; avatar = <RoomAvatar room={room} width={16} height={16}/>;
}
pillClass = 'mx_RoomPill'; pillClass = 'mx_RoomPill';
} }
} }

View file

@ -170,6 +170,7 @@ module.exports = React.createClass({
}, },
pillifyLinks: function(nodes) { pillifyLinks: function(nodes) {
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; const node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) { if (node.tagName === "A" && node.getAttribute("href")) {
@ -181,7 +182,12 @@ module.exports = React.createClass({
const pillContainer = document.createElement('span'); const pillContainer = document.createElement('span');
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pill = <Pill url={href} inMessage={true} room={room}/>; const pill = <Pill
url={href}
inMessage={true}
room={room}
shouldShowPillAvatar={shouldShowPillAvatar}
/>;
ReactDOM.render(pill, pillContainer); ReactDOM.render(pill, pillContainer);
node.parentNode.replaceChild(pillContainer, node); node.parentNode.replaceChild(pillContainer, node);

View file

@ -172,7 +172,7 @@ export default class Autocomplete extends React.Component {
} }
hide() { hide() {
this.setState({hide: true, selectionOffset: 0}); this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
} }
forceComplete() { forceComplete() {

View file

@ -155,7 +155,9 @@ module.exports = withMatrixClient(React.createClass({
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
if (nextProps.mxEvent !== this.props.mxEvent) { // re-check the sender verification as outgoing events progress through
// the send process.
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
this._verifyEvent(nextProps.mxEvent); this._verifyEvent(nextProps.mxEvent);
} }
}, },
@ -386,6 +388,36 @@ module.exports = withMatrixClient(React.createClass({
}); });
}, },
_renderE2EPadlock: function() {
const ev = this.props.mxEvent;
const props = {onClick: this.onCryptoClicked};
if (ev.getContent().msgtype === 'm.bad.encrypted') {
return <E2ePadlockUndecryptable {...props}/>;
} else if (ev.isEncrypted()) {
if (this.state.verified) {
return <E2ePadlockVerified {...props}/>;
} else {
return <E2ePadlockUnverified {...props}/>;
}
} else {
// XXX: if the event is being encrypted (ie eventSendStatus ===
// encrypting), it might be nice to show something other than the
// open padlock?
// if the event is not encrypted, but it's an e2e room, show the
// open padlock
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
if (e2eEnabled) {
return <E2ePadlockUnencrypted {...props}/>;
}
}
// no padlock needed
return null;
},
render: function() { render: function() {
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('messages.SenderProfile'); var SenderProfile = sdk.getComponent('messages.SenderProfile');
@ -407,7 +439,6 @@ module.exports = withMatrixClient(React.createClass({
throw new Error("Event type not supported"); throw new Error("Event type not supported");
} }
var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted; const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
@ -485,26 +516,7 @@ module.exports = withMatrixClient(React.createClass({
const editButton = ( const editButton = (
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} /> <span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
); );
let e2e;
// cosmetic padlocks:
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12" />;
}
// real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Undecryptable")} src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
}
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12"/>;
}
else {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by an unverified device")} src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
}
}
else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Unencrypted message")} src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
const timestamp = this.props.mxEvent.getTs() ? const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null; <MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -572,7 +584,7 @@ module.exports = withMatrixClient(React.createClass({
<a href={ permalink } onClick={this.onPermalinkClicked}> <a href={ permalink } onClick={this.onPermalinkClicked}>
{ timestamp } { timestamp }
</a> </a>
{ e2e } { this._renderE2EPadlock() }
<EventTileType ref="tile" <EventTileType ref="tile"
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
@ -597,3 +609,39 @@ module.exports.haveTileForEvent = function(e) {
return true; return true;
} }
}; };
function E2ePadlockUndecryptable(props) {
return (
<E2ePadlock alt={_t("Undecryptable")}
src="img/e2e-blocked.svg" width="12" height="12"
style={{ marginLeft: "-1px" }} {...props} />
);
}
function E2ePadlockVerified(props) {
return (
<E2ePadlock alt={_t("Encrypted by a verified device")}
src="img/e2e-verified.svg" width="10" height="12"
{...props} />
);
}
function E2ePadlockUnverified(props) {
return (
<E2ePadlock alt={_t("Encrypted by an unverified device")}
src="img/e2e-warning.svg" width="15" height="12"
style={{ marginLeft: "-2px" }} {...props} />
);
}
function E2ePadlockUnencrypted(props) {
return (
<E2ePadlock alt={_t("Unencrypted message")}
src="img/e2e-unencrypted.svg" width="12" height="12"
{...props} />
);
}
function E2ePadlock(props) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
}

View file

@ -97,20 +97,39 @@ export default class MessageComposerInput extends React.Component {
onInputStateChanged: React.PropTypes.func, onInputStateChanged: React.PropTypes.func,
}; };
static getKeyBinding(e: SyntheticKeyboardEvent): string { static getKeyBinding(ev: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { let ctrlCmdOnly;
return 'toggle-mode'; if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
} }
// Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I // Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) { // importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
// When null is returned, draft-js will NOT preventDefault, allowing dev tools // handle this in `getDefaultKeyBinding` so we do it ourselves here.
// to be toggled when the editor is focussed //
// * if macOS, read second option
const ctrlCmdCommand = {
// C-m => Toggles between rich text and markdown modes
[KeyCode.KEY_M]: 'toggle-mode',
[KeyCode.KEY_B]: 'bold',
[KeyCode.KEY_I]: 'italic',
[KeyCode.KEY_U]: 'underline',
[KeyCode.KEY_J]: 'code',
[KeyCode.KEY_O]: 'split-block',
}[ev.keyCode];
if (ctrlCmdCommand) {
if (!ctrlCmdOnly) {
return null; return null;
} }
return ctrlCmdCommand;
}
return getDefaultKeyBinding(e); // Handle keys such as return, left and right arrows etc.
return getDefaultKeyBinding(ev);
} }
static getBlockStyle(block: ContentBlock): ?string { static getBlockStyle(block: ContentBlock): ?string {
@ -185,13 +204,19 @@ export default class MessageComposerInput extends React.Component {
createEditorState(richText: boolean, contentState: ?ContentState): EditorState { createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
const decorators = richText ? RichText.getScopedRTDecorators(this.props) : const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
RichText.getScopedMDDecorators(this.props); RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
decorators.push({ decorators.push({
strategy: this.findLinkEntities.bind(this), strategy: this.findLinkEntities.bind(this),
component: (entityProps) => { component: (entityProps) => {
const Pill = sdk.getComponent('elements.Pill'); const Pill = sdk.getComponent('elements.Pill');
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData(); const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
if (Pill.isPillUrl(url)) { if (Pill.isPillUrl(url)) {
return <Pill url={url} room={this.props.room} offsetKey={entityProps.offsetKey}/>; return <Pill
url={url}
room={this.props.room}
offsetKey={entityProps.offsetKey}
shouldShowPillAvatar={shouldShowPillAvatar}
/>;
} }
return ( return (
@ -244,7 +269,8 @@ export default class MessageComposerInput extends React.Component {
// paths for inserting a user pill is not fun // paths for inserting a user pill is not fun
const selection = this.state.editorState.getSelection(); const selection = this.state.editorState.getSelection();
const member = this.props.room.getMember(payload.user_id); const member = this.props.room.getMember(payload.user_id);
const completion = member ? member.name.replace(' (IRC)', '') : payload.user_id; const completion = member ?
member.rawDisplayName.replace(' (IRC)', '') : payload.user_id;
this.setDisplayedCompletion({ this.setDisplayedCompletion({
completion, completion,
selection, selection,
@ -254,10 +280,13 @@ export default class MessageComposerInput extends React.Component {
} }
break; break;
case 'quote': { case 'quote': {
let {body, formatted_body} = payload.event.getContent(); let {body} = payload.event.getContent();
formatted_body = formatted_body || escape(body); /// XXX: Not doing rich-text quoting from formatted-body because draft-js
if (formatted_body) { /// has regressed such that when links are quoted, errors are thrown. See
let content = RichText.htmlToContentState(`<blockquote>${formatted_body}</blockquote>`); /// https://github.com/vector-im/riot-web/issues/4756.
body = escape(body);
if (body) {
let content = RichText.htmlToContentState(`<blockquote>${body}</blockquote>`);
if (!this.state.isRichtextEnabled) { if (!this.state.isRichtextEnabled) {
content = ContentState.createFromText(RichText.stateToMarkdown(content)); content = ContentState.createFromText(RichText.stateToMarkdown(content));
} }
@ -516,7 +545,8 @@ export default class MessageComposerInput extends React.Component {
newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'); newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
} else if (shouldToggleBlockFormat) { } else if (shouldToggleBlockFormat) {
const currentStartOffset = this.state.editorState.getSelection().getStartOffset(); const currentStartOffset = this.state.editorState.getSelection().getStartOffset();
if (currentStartOffset === 0) { const currentEndOffset = this.state.editorState.getSelection().getEndOffset();
if (currentStartOffset === 0 && currentEndOffset === 0) {
// Toggle current block type (setting it to 'unstyled') // Toggle current block type (setting it to 'unstyled')
newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType); newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType);
} }

View file

@ -966,5 +966,6 @@
"Edit Group": "Edit Group", "Edit Group": "Edit Group",
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
"Failed to upload image": "Failed to upload image", "Failed to upload image": "Failed to upload image",
"Failed to update group": "Failed to update group" "Failed to update group": "Failed to update group",
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions"
} }