Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/markdown_allow_u

This commit is contained in:
Michael Telatynski 2017-07-06 13:56:20 +01:00
commit ff7ae5b995
No known key found for this signature in database
GPG key ID: 0435A1D4BBD34D64
46 changed files with 587 additions and 407 deletions

View file

@ -68,7 +68,7 @@ export default class Autocomplete extends React.Component {
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
// Don't debounce if we are already showing completions
if (this.state.completions.length > 0) {
if (this.state.completions.length > 0 || this.state.forceComplete) {
autocompleteDelay = 0;
}
@ -177,7 +177,7 @@ export default class Autocomplete extends React.Component {
hide: false,
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
done.resolve();
done.resolve(this.countCompletions());
});
});
return done.promise;

View file

@ -43,6 +43,8 @@ import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import {onSendMessageFailed} from './MessageComposerInputOld';
import MessageComposerStore from '../../../stores/MessageComposerStore';
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const ZWS_CODE = 8203;
@ -130,7 +132,10 @@ export default class MessageComposerInput extends React.Component {
isRichtextEnabled,
// the currently displayed editor state (note: this is always what is modified on input)
editorState: null,
editorState: this.createEditorState(
isRichtextEnabled,
MessageComposerStore.getContentState(this.props.room.roomId),
),
// the original editor state, before we started tabbing through completions
originalEditorState: null,
@ -138,11 +143,10 @@ export default class MessageComposerInput extends React.Component {
// the virtual state "above" the history stack, the message currently being composed that
// we want to persist whilst browsing history
currentlyComposedEditorState: null,
};
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
/* eslint react/no-direct-mutation-state:0 */
this.state.editorState = this.createEditorState();
// whether there were any completions
someCompletions: null,
};
this.client = MatrixClientPeg.get();
}
@ -336,6 +340,14 @@ export default class MessageComposerInput extends React.Component {
this.onFinishedTyping();
}
// Record the editor state for this room so that it can be retrieved after
// switching to another room and back
dis.dispatch({
action: 'content_state',
room_id: this.props.room.roomId,
content_state: state.editorState.getCurrentContent(),
});
if (!state.hasOwnProperty('originalEditorState')) {
state.originalEditorState = null;
}
@ -632,6 +644,10 @@ export default class MessageComposerInput extends React.Component {
};
onVerticalArrow = (e, up) => {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
return;
}
// Select history only if we are not currently auto-completing
if (this.autocomplete.state.completionList.length === 0) {
// Don't go back in history if we're in the middle of a multi-line message
@ -640,17 +656,16 @@ export default class MessageComposerInput extends React.Component {
const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock();
const lastBlock = this.state.editorState.getCurrentContent().getLastBlock();
const selectionOffset = selection.getAnchorOffset();
let canMoveUp = false;
let canMoveDown = false;
if (blockKey === firstBlock.getKey()) {
const textBeforeCursor = firstBlock.getText().slice(0, selectionOffset);
canMoveUp = textBeforeCursor.indexOf('\n') === -1;
canMoveUp = selection.getStartOffset() === selection.getEndOffset() &&
selection.getStartOffset() === 0;
}
if (blockKey === lastBlock.getKey()) {
const textAfterCursor = lastBlock.getText().slice(selectionOffset);
canMoveDown = textAfterCursor.indexOf('\n') === -1;
canMoveDown = selection.getStartOffset() === selection.getEndOffset() &&
selection.getStartOffset() === lastBlock.getText().length;
}
if ((up && !canMoveUp) || (!up && !canMoveDown)) return;
@ -707,10 +722,16 @@ export default class MessageComposerInput extends React.Component {
};
onTab = async (e) => {
this.setState({
someCompletions: null,
});
e.preventDefault();
if (this.autocomplete.state.completionList.length === 0) {
// Force completions to show for the text currently entered
await this.autocomplete.forceComplete();
const completionCount = await this.autocomplete.forceComplete();
this.setState({
someCompletions: completionCount > 0,
});
// Select the first item by moving "down"
await this.moveAutocompleteSelection(false);
} else {
@ -831,6 +852,7 @@ export default class MessageComposerInput extends React.Component {
const className = classNames('mx_MessageComposer_input', {
mx_MessageComposer_input_empty: hidePlaceholder,
mx_MessageComposer_input_error: this.state.someCompletions === false,
});
const content = activeEditorState.getCurrentContent();

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require('classnames');
var sdk = require('../../../index');
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal");
var dis = require("../../../dispatcher");
var rate_limited_func = require('../../../ratelimitedfunc');
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import dis from "../../../dispatcher";
import RateLimitedFunc from '../../../ratelimitedfunc';
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import {CancelButton} from './SimpleRoomHeader';
@ -58,7 +58,7 @@ module.exports = React.createClass({
},
componentDidMount: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
// When a room name occurs, RoomState.events is fired *before*
@ -79,14 +79,14 @@ module.exports = React.createClass({
if (this.props.room) {
this.props.room.removeListener("Room.name", this._onRoomNameChange);
}
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
}
},
_onRoomStateEvents: function(event, state) {
if (!this.props.room || event.getRoomId() != this.props.room.roomId) {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
@ -94,7 +94,8 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
_rateLimitedUpdate: new rate_limited_func(function() {
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500),
@ -109,15 +110,14 @@ module.exports = React.createClass({
},
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
}
changeAvatar.onFileSelected(ev).catch(function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const errMsg = (typeof err === "string") ? err : (err.error || "");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set avatar: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: _t("Error"),
@ -133,10 +133,10 @@ module.exports = React.createClass({
/**
* After editing the settings, get the new name for the room
*
* Returns undefined if we didn't let the user edit the room name
* @return {?string} newName or undefined if we didn't let the user edit the room name
*/
getEditedName: function() {
var newName;
let newName;
if (this.refs.nameEditor) {
newName = this.refs.nameEditor.getRoomName();
}
@ -146,10 +146,10 @@ module.exports = React.createClass({
/**
* After editing the settings, get the new topic for the room
*
* Returns undefined if we didn't let the user edit the room topic
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
*/
getEditedTopic: function() {
var newTopic;
let newTopic;
if (this.refs.topicEditor) {
newTopic = this.refs.topicEditor.getTopic();
}
@ -157,38 +157,31 @@ module.exports = React.createClass({
},
render: function() {
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
var header;
var name = null;
var searchStatus = null;
var topic_el = null;
var cancel_button = null;
var spinner = null;
var save_button = null;
var settings_button = null;
let name = null;
let searchStatus = null;
let topicElement = null;
let cancelButton = null;
let spinner = null;
let saveButton = null;
let settingsButton = null;
let canSetRoomName;
let canSetRoomAvatar;
let canSetRoomTopic;
if (this.props.editing) {
// calculate permissions. XXX: this should be done on mount or something
var user_id = MatrixClientPeg.get().credentials.userId;
const userId = MatrixClientPeg.get().credentials.userId;
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
'm.room.name', user_id
);
var can_set_room_avatar = this.props.room.currentState.maySendStateEvent(
'm.room.avatar', user_id
);
var can_set_room_topic = this.props.room.currentState.maySendStateEvent(
'm.room.topic', user_id
);
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
'm.room.name', user_id
);
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
save_button = (
saveButton = (
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
{_t("Save")}
</AccessibleButton>
@ -196,39 +189,41 @@ module.exports = React.createClass({
}
if (this.props.onCancelClick) {
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
}
if (this.props.saving) {
var Spinner = sdk.getComponent("elements.Spinner");
const Spinner = sdk.getComponent("elements.Spinner");
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
}
if (can_set_room_name) {
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
if (canSetRoomName) {
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
}
else {
var searchStatus;
} else {
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }</div>;
if (this.props.searchInfo &&
this.props.searchInfo.searchCount !== undefined &&
this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
</div>;
}
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
var settingsHint = false;
var members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
let settingsHint = false;
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
if (members) {
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!name || !name.getContent().name) {
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!nameEvent || !nameEvent.getContent().name) {
settingsHint = true;
}
}
}
var roomName = _t("Join Room");
let roomName = _t("Join Room");
if (this.props.oobData && this.props.oobData.name) {
roomName = this.props.oobData.name;
} else if (this.props.room) {
@ -243,24 +238,25 @@ module.exports = React.createClass({
</div>;
}
if (can_set_room_topic) {
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
if (canSetRoomTopic) {
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
} else {
var topic;
let topic;
if (this.props.room) {
var ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
if (ev) {
topic = ev.getContent().topic;
}
}
if (topic) {
topic_el = <EmojiText dir="auto" element="div" className="mx_RoomHeader_topic" ref="topic" title={topic}>{ topic }</EmojiText>;
topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
}
}
var roomAvatar = null;
if (can_set_room_avatar) {
let roomAvatar = null;
if (canSetRoomAvatar) {
roomAvatar = (
<div className="mx_RoomHeader_avatarPicker">
<div onClick={ this.onAvatarPickerClick }>
@ -276,8 +272,7 @@ module.exports = React.createClass({
</div>
</div>
);
}
else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
roomAvatar = (
<div onClick={this.props.onSettingsClick}>
<RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
@ -285,9 +280,8 @@ module.exports = React.createClass({
);
}
var settings_button;
if (this.props.onSettingsClick) {
settings_button =
settingsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</AccessibleButton>;
@ -301,61 +295,58 @@ module.exports = React.createClass({
// </div>;
// }
var forget_button;
let forgetButton;
if (this.props.onForgetClick) {
forget_button =
forgetButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</AccessibleButton>;
}
let search_button;
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
search_button =
searchButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>;
}
var rightPanel_buttons;
let rightPanelButtons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
rightPanelButtons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>;
}
var right_row;
let rightRow;
if (!this.props.editing) {
right_row =
rightRow =
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ forget_button }
{ search_button }
{ rightPanel_buttons }
{ settingsButton }
{ forgetButton }
{ searchButton }
{ rightPanelButtons }
</div>;
}
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topic_el }
</div>
</div>
{spinner}
{save_button}
{cancel_button}
{right_row}
</div>;
return (
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
{ header }
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topicElement }
</div>
</div>
{spinner}
{saveButton}
{cancelButton}
{rightRow}
</div>
</div>
);
},

View file

@ -39,6 +39,7 @@ function parseIntWithDefault(val, def) {
const BannedUser = React.createClass({
propTypes: {
canUnban: React.PropTypes.bool,
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
reason: React.PropTypes.string,
},
@ -67,13 +68,17 @@ const BannedUser = React.createClass({
},
render: function() {
let unbanButton;
if (this.props.canUnban) {
unbanButton = <AccessibleButton className="mx_RoomSettings_unbanButton" onClick={this._onUnbanClick}>
{ _t('Unban') }
</AccessibleButton>;
}
return (
<li>
<AccessibleButton className="mx_RoomSettings_unbanButton"
onClick={this._onUnbanClick}
>
{ _t('Unban') }
</AccessibleButton>
{ unbanButton }
<strong>{this.props.member.name}</strong> {this.props.member.userId}
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
</li>
@ -667,6 +672,7 @@ module.exports = React.createClass({
const banned = this.props.room.getMembersWithMembership("ban");
let bannedUsersSection;
if (banned.length) {
const canBanUsers = current_user_level >= ban_level;
bannedUsersSection =
<div>
<h3>{ _t('Banned users') }</h3>
@ -674,7 +680,7 @@ module.exports = React.createClass({
{banned.map(function(member) {
const banEvent = member.events.member.getContent();
return (
<BannedUser key={member.userId} member={member} reason={banEvent.reason} />
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
);
})}
</ul>