Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/delintify
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> # Conflicts: # src/components/views/rooms/RoomHeader.js
This commit is contained in:
commit
34c9bbfd86
35 changed files with 2562 additions and 1287 deletions
102
src/components/views/elements/AppTile.js
Normal file
102
src/components/views/elements/AppTile.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AppTile',
|
||||
|
||||
propTypes: {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
url: React.PropTypes.string.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
url: "",
|
||||
};
|
||||
},
|
||||
|
||||
_onEditClick: function() {
|
||||
console.log("Edit widget %s", this.props.id);
|
||||
},
|
||||
|
||||
_onDeleteClick: function() {
|
||||
console.log("Delete widget %s", this.props.id);
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
if (!appsStateEvents) {
|
||||
return;
|
||||
}
|
||||
const appsStateEvent = appsStateEvents.getContent();
|
||||
if (appsStateEvent[this.props.id]) {
|
||||
delete appsStateEvent[this.props.id];
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
appsStateEvent,
|
||||
'',
|
||||
).then(() => {
|
||||
console.log('Deleted widget');
|
||||
}, (e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
formatAppTileName: function() {
|
||||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className="mx_AppTileMenuBar">
|
||||
{this.formatAppTileName()}
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{/* Edit widget */}
|
||||
{/* <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8" alt="Edit"
|
||||
onClick={this._onEditClick}
|
||||
/> */}
|
||||
|
||||
{/* Delete widget */}
|
||||
<img src="img/cancel.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
|
||||
width="8" height="8" alt={_t("Cancel")}
|
||||
onClick={this._onDeleteClick}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx_AppTileBody">
|
||||
<iframe ref="appFrame" src={this.props.url} allowFullScreen="true"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -69,10 +69,19 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||
this.props.onSubmit(
|
||||
'', // XXX: Synapse breaks if you send null here:
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
null,
|
||||
null,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
|||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
} else if (content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.thumbnail_url) {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
||||
} else {
|
||||
return null;
|
||||
|
|
|
@ -29,6 +29,7 @@ import Modal from '../../../Modal';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -90,7 +91,18 @@ module.exports = React.createClass({
|
|||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||
highlight.highlightBlock(blocks[i])
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
|
||||
return cl.startsWith('language-');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
|
218
src/components/views/rooms/AppsDrawer.js
Normal file
218
src/components/views/rooms/AppsDrawer.js
Normal file
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AppsDrawer',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
apps: this._getApps(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
if (this.state.apps && this.state.apps.length < 1) {
|
||||
this.onClickAddWidget();
|
||||
}
|
||||
// TODO -- Handle Scalar errors
|
||||
// },
|
||||
// (err) => {
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
// });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
ScalarMessaging.stopListening();
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
},
|
||||
|
||||
_initAppConfig: function(appId, app) {
|
||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||
const params = {
|
||||
'$matrix_user_id': this.props.userId,
|
||||
'$matrix_room_id': this.props.room.roomId,
|
||||
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
};
|
||||
|
||||
if(app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
}
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
|
||||
// switch(app.type) {
|
||||
// case 'etherpad':
|
||||
// app.queryParams = '?userName=' + this.props.userId +
|
||||
// '&padId=' + this.props.room.roomId;
|
||||
// break;
|
||||
// case 'jitsi': {
|
||||
//
|
||||
// app.queryParams = '?confId=' + app.data.confId +
|
||||
// '&displayName=' + encodeURIComponent(user.displayName) +
|
||||
// '&avatarUrl=' + encodeURIComponent(MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl)) +
|
||||
// '&email=' + encodeURIComponent(this.props.userId) +
|
||||
// '&isAudioConf=' + app.data.isAudioConf;
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// case 'vrdemo':
|
||||
// app.queryParams = '?roomAlias=' + encodeURIComponent(app.data.roomAlias);
|
||||
// break;
|
||||
// }
|
||||
|
||||
return app;
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
}
|
||||
this._updateApps();
|
||||
},
|
||||
|
||||
_getApps: function() {
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
}
|
||||
const appsStateEvent = appsStateEvents.getContent();
|
||||
if (Object.keys(appsStateEvent).length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(appsStateEvent).map((appId) => {
|
||||
return this._initAppConfig(appId, appsStateEvent[appId]);
|
||||
});
|
||||
},
|
||||
|
||||
_updateApps: function() {
|
||||
const apps = this._getApps();
|
||||
if (apps.length < 1) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
apps: apps,
|
||||
});
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||
null;
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const apps = this.state.apps.map(
|
||||
(app, index, arr) => {
|
||||
return <AppTile
|
||||
key={app.name}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
/>;
|
||||
});
|
||||
|
||||
const addWidget = this.state.apps && this.state.apps.length < 2 &&
|
||||
(<div onClick={this.onClickAddWidget}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="mx_AddWidget_button"
|
||||
title={_t('Add a widget')}>
|
||||
[+] {_t('Add a widget')}
|
||||
</div>);
|
||||
|
||||
return (
|
||||
<div className="mx_AppsDrawer">
|
||||
<div id="apps" className="mx_AppsContainer">
|
||||
{apps}
|
||||
</div>
|
||||
{addWidget}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -6,6 +6,7 @@ import isEqual from 'lodash/isEqual';
|
|||
import sdk from '../../../index';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import Q from 'q';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
|
@ -39,26 +40,52 @@ export default class Autocomplete extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(props, state) {
|
||||
if (props.query === this.props.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.complete(props.query, props.selection);
|
||||
}
|
||||
|
||||
async complete(query, selection) {
|
||||
let forceComplete = this.state.forceComplete;
|
||||
const completionPromise = getCompletions(query, selection, forceComplete);
|
||||
this.completionPromise = completionPromise;
|
||||
const completions = await this.completionPromise;
|
||||
|
||||
// There's a newer completion request, so ignore results.
|
||||
if (completionPromise !== this.completionPromise) {
|
||||
componentWillReceiveProps(newProps, state) {
|
||||
// Query hasn't changed so don't try to complete it
|
||||
if (newProps.query === this.props.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const completionList = flatMap(completions, provider => provider.completions);
|
||||
this.complete(newProps.query, newProps.selection);
|
||||
}
|
||||
|
||||
complete(query, selection) {
|
||||
if (this.debounceCompletionsRequest) {
|
||||
clearTimeout(this.debounceCompletionsRequest);
|
||||
}
|
||||
if (query === "") {
|
||||
this.setState({
|
||||
// Clear displayed completions
|
||||
completions: [],
|
||||
completionList: [],
|
||||
// Reset selected completion
|
||||
selectionOffset: COMPOSER_SELECTED,
|
||||
// Hide the autocomplete box
|
||||
hide: true,
|
||||
});
|
||||
return Q(null);
|
||||
}
|
||||
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
|
||||
|
||||
// Don't debounce if we are already showing completions
|
||||
if (this.state.completions.length > 0) {
|
||||
autocompleteDelay = 0;
|
||||
}
|
||||
|
||||
const deferred = Q.defer();
|
||||
this.debounceCompletionsRequest = setTimeout(() => {
|
||||
getCompletions(
|
||||
query, selection, this.state.forceComplete,
|
||||
).then((completions) => {
|
||||
this.processCompletions(completions);
|
||||
deferred.resolve();
|
||||
});
|
||||
}, autocompleteDelay);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
processCompletions(completions) {
|
||||
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||
|
||||
// Reset selection when completion list becomes empty.
|
||||
let selectionOffset = COMPOSER_SELECTED;
|
||||
|
@ -69,21 +96,18 @@ export default class Autocomplete extends React.Component {
|
|||
const currentSelection = this.state.selectionOffset === 0 ? null :
|
||||
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||
selectionOffset = completionList.findIndex(
|
||||
completion => completion.completion === currentSelection);
|
||||
(completion) => completion.completion === currentSelection);
|
||||
if (selectionOffset === -1) {
|
||||
selectionOffset = COMPOSER_SELECTED;
|
||||
} else {
|
||||
selectionOffset++; // selectionOffset is 1-indexed!
|
||||
}
|
||||
} else {
|
||||
// If no completions were returned, we should turn off force completion.
|
||||
forceComplete = false;
|
||||
}
|
||||
|
||||
let hide = this.state.hide;
|
||||
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
|
||||
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
|
||||
newMatches = completions.map(completion => !!completion.command.command);
|
||||
const oldMatches = this.state.completions.map((completion) => !!completion.command.command),
|
||||
newMatches = completions.map((completion) => !!completion.command.command);
|
||||
|
||||
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
|
||||
if (!isEqual(oldMatches, newMatches)) {
|
||||
|
@ -95,7 +119,8 @@ export default class Autocomplete extends React.Component {
|
|||
completionList,
|
||||
selectionOffset,
|
||||
hide,
|
||||
forceComplete,
|
||||
// Force complete is turned off each time since we can't edit the query in that case
|
||||
forceComplete: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -149,6 +174,7 @@ export default class Autocomplete extends React.Component {
|
|||
const done = Q.defer();
|
||||
this.setState({
|
||||
forceComplete: true,
|
||||
hide: false,
|
||||
}, () => {
|
||||
this.complete(this.props.query, this.props.selection).then(() => {
|
||||
done.resolve();
|
||||
|
@ -169,7 +195,7 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset});
|
||||
this.setState({selectionOffset, hide: false});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -185,21 +211,24 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
setState(state, func) {
|
||||
super.setState(state, func);
|
||||
}
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let position = 1;
|
||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
let completions = completionResult.completions.map((completion, i) => {
|
||||
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, i) => {
|
||||
const className = classNames('mx_Autocomplete_Completion', {
|
||||
'selected': position === this.state.selectionOffset,
|
||||
});
|
||||
let componentPosition = position;
|
||||
const componentPosition = position;
|
||||
position++;
|
||||
|
||||
let onMouseOver = () => this.setSelection(componentPosition);
|
||||
let onClick = () => {
|
||||
const onMouseOver = () => this.setSelection(componentPosition);
|
||||
const onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onCompletionClicked();
|
||||
};
|
||||
|
@ -220,7 +249,7 @@ export default class Autocomplete extends React.Component {
|
|||
{completionResult.provider.renderCompletions(completions)}
|
||||
</div>
|
||||
) : null;
|
||||
}).filter(completion => !!completion);
|
||||
}).filter((completion) => !!completion);
|
||||
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
|
|
|
@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
|||
import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -28,6 +30,8 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// js-sdk room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
showApps: React.PropTypes.bool,
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: React.PropTypes.object,
|
||||
|
@ -70,10 +74,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var CallView = sdk.getComponent("voip.CallView");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const CallView = sdk.getComponent("voip.CallView");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var fileDropTarget = null;
|
||||
let fileDropTarget = null;
|
||||
if (this.props.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
|
@ -87,14 +91,13 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var conferenceCallNotification = null;
|
||||
let conferenceCallNotification = null;
|
||||
if (this.props.displayConfCallNotification) {
|
||||
let supportedText = '';
|
||||
let joinNode;
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
supportedText = _t(" (unsupported)");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
joinNode = (<span>
|
||||
{_tJsx(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
|
@ -105,7 +108,6 @@ module.exports = React.createClass({
|
|||
]
|
||||
)}
|
||||
</span>);
|
||||
|
||||
}
|
||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
||||
|
@ -118,7 +120,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var callView = (
|
||||
const callView = (
|
||||
<CallView ref="callView" room={this.props.room}
|
||||
ConferenceHandler={this.props.conferenceHandler}
|
||||
onResize={this.props.onResize}
|
||||
|
@ -126,8 +128,17 @@ module.exports = React.createClass({
|
|||
/>
|
||||
);
|
||||
|
||||
let appsDrawer = null;
|
||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
|
||||
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ conferenceCallNotification }
|
||||
|
|
|
@ -13,16 +13,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
|
@ -32,6 +31,8 @@ export default class MessageComposer extends React.Component {
|
|||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
|
@ -57,7 +58,6 @@ export default class MessageComposer extends React.Component {
|
|||
},
|
||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -127,7 +127,7 @@ export default class MessageComposer extends React.Component {
|
|||
if(shouldUpload) {
|
||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||
if (files) {
|
||||
for(var i=0; i<files.length; i++) {
|
||||
for(let i=0; i<files.length; i++) {
|
||||
this.props.uploadFile(files[i]);
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
onHangupClick() {
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
const call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
if (!call) {
|
||||
return;
|
||||
|
@ -152,20 +152,68 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// _startCallApp(isAudioConf) {
|
||||
// dis.dispatch({
|
||||
// action: 'appsDrawer',
|
||||
// show: true,
|
||||
// });
|
||||
|
||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
// let appsStateEvent = {};
|
||||
// if (appsStateEvents) {
|
||||
// appsStateEvent = appsStateEvents.getContent();
|
||||
// }
|
||||
// if (!appsStateEvent.videoConf) {
|
||||
// appsStateEvent.videoConf = {
|
||||
// type: 'jitsi',
|
||||
// // FIXME -- This should not be localhost
|
||||
// url: 'http://localhost:8000/jitsi.html',
|
||||
// data: {
|
||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||
// isAudioConf: isAudioConf,
|
||||
// },
|
||||
// };
|
||||
// MatrixClientPeg.get().sendStateEvent(
|
||||
// this.props.room.roomId,
|
||||
// 'im.vector.modular.widgets',
|
||||
// appsStateEvent,
|
||||
// '',
|
||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||
// }
|
||||
// }
|
||||
|
||||
onCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(false);
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: 'voice',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onShowAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
|
||||
onHideAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
|
@ -216,19 +264,19 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
||||
|
||||
var controls = [];
|
||||
const controls = [];
|
||||
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
|
@ -247,16 +295,15 @@ export default class MessageComposer extends React.Component {
|
|||
controls.push(
|
||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||
alt={e2eTitle} title={e2eTitle}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
var callButton, videoCallButton, hangupButton;
|
||||
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||
|
@ -267,14 +314,29 @@ export default class MessageComposer extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
// Apps
|
||||
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||
if (this.props.showApps) {
|
||||
hideAppsButton =
|
||||
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
} else {
|
||||
showAppsButton =
|
||||
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
MatrixClientPeg.get().credentials.userId);
|
||||
|
||||
if (canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
var uploadButton = (
|
||||
const uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||
|
@ -300,7 +362,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
|
@ -316,13 +378,15 @@ export default class MessageComposer extends React.Component {
|
|||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
videoCallButton
|
||||
videoCallButton,
|
||||
showAppsButton,
|
||||
hideAppsButton,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -340,7 +404,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
const {style, blockType} = this.state.inputState;
|
||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||
name => {
|
||||
(name) => {
|
||||
const active = style.includes(name) || blockType === name;
|
||||
const suffix = active ? '-o-n' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
|
@ -403,5 +467,8 @@ MessageComposer.propTypes = {
|
|||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number
|
||||
opacity: React.PropTypes.number,
|
||||
|
||||
// string representing the current room app drawer state
|
||||
showApps: React.PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -20,7 +20,6 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
|||
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
||||
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';
|
||||
|
||||
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
||||
import classNames from 'classnames';
|
||||
import escape from 'lodash/escape';
|
||||
import Q from 'q';
|
||||
|
@ -41,6 +40,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
|||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
import Markdown from '../../../Markdown';
|
||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||
import {onSendMessageFailed} from './MessageComposerInputOld';
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
@ -58,12 +58,42 @@ function stateToMarkdown(state) {
|
|||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
export default class MessageComposerInput extends React.Component {
|
||||
static propTypes = {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// called with current plaintext content (as a string) whenever it changes
|
||||
onContentChanged: React.PropTypes.func,
|
||||
|
||||
onUpArrow: React.PropTypes.func,
|
||||
|
||||
onDownArrow: React.PropTypes.func,
|
||||
|
||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||
tryComplete: React.PropTypes.func,
|
||||
|
||||
onInputStateChanged: React.PropTypes.func,
|
||||
};
|
||||
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
// Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
|
||||
if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
|
||||
// When null is returned, draft-js will NOT preventDefault, allowing dev tools
|
||||
// to be toggled when the editor is focussed
|
||||
return null;
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
|
@ -77,6 +107,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
historyManager: ComposerHistoryManager;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -84,13 +115,13 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.handleReturn = this.handleReturn.bind(this);
|
||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||
this.setEditorState = this.setEditorState.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this.onTab = this.onTab.bind(this);
|
||||
this.onEscape = this.onEscape.bind(this);
|
||||
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||
this.onTextPasted = this.onTextPasted.bind(this);
|
||||
|
||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
||||
|
||||
|
@ -103,6 +134,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
// the original editor state, before we started tabbing through completions
|
||||
originalEditorState: null,
|
||||
|
||||
// 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
|
||||
|
@ -119,7 +154,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
*/
|
||||
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
||||
let decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
||||
RichText.getScopedMDDecorators(this.props),
|
||||
RichText.getScopedMDDecorators(this.props),
|
||||
compositeDecorator = new CompositeDecorator(decorators);
|
||||
|
||||
let editorState = null;
|
||||
|
@ -132,110 +167,13 @@ export default class MessageComposerInput extends React.Component {
|
|||
return EditorState.moveFocusToEnd(editorState);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const component = this;
|
||||
this.sentHistory = {
|
||||
// The list of typed messages. Index 0 is more recent
|
||||
data: [],
|
||||
// The position in data currently displayed
|
||||
position: -1,
|
||||
// The room the history is for.
|
||||
roomId: null,
|
||||
// The original text before they hit UP
|
||||
originalText: null,
|
||||
// The textarea element to set text to.
|
||||
element: null,
|
||||
|
||||
init: function(element, roomId) {
|
||||
this.roomId = roomId;
|
||||
this.element = element;
|
||||
this.position = -1;
|
||||
var storedData = window.sessionStorage.getItem(
|
||||
"mx_messagecomposer_history_" + roomId
|
||||
);
|
||||
if (storedData) {
|
||||
this.data = JSON.parse(storedData);
|
||||
}
|
||||
if (this.roomId) {
|
||||
this.setLastTextEntry();
|
||||
}
|
||||
},
|
||||
|
||||
push: function(text) {
|
||||
// store a message in the sent history
|
||||
this.data.unshift(text);
|
||||
window.sessionStorage.setItem(
|
||||
"mx_messagecomposer_history_" + this.roomId,
|
||||
JSON.stringify(this.data)
|
||||
);
|
||||
// reset history position
|
||||
this.position = -1;
|
||||
this.originalText = null;
|
||||
},
|
||||
|
||||
// move in the history. Returns true if we managed to move.
|
||||
next: function(offset) {
|
||||
if (this.position === -1) {
|
||||
// user is going into the history, save the current line.
|
||||
this.originalText = this.element.value;
|
||||
}
|
||||
else {
|
||||
// user may have modified this line in the history; remember it.
|
||||
this.data[this.position] = this.element.value;
|
||||
}
|
||||
|
||||
if (offset > 0 && this.position === (this.data.length - 1)) {
|
||||
// we've run out of history
|
||||
return false;
|
||||
}
|
||||
|
||||
// retrieve the next item (bounded).
|
||||
var newPosition = this.position + offset;
|
||||
newPosition = Math.max(-1, newPosition);
|
||||
newPosition = Math.min(newPosition, this.data.length - 1);
|
||||
this.position = newPosition;
|
||||
|
||||
if (this.position !== -1) {
|
||||
// show the message
|
||||
this.element.value = this.data[this.position];
|
||||
}
|
||||
else if (this.originalText !== undefined) {
|
||||
// restore the original text the user was typing.
|
||||
this.element.value = this.originalText;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
saveLastTextEntry: function() {
|
||||
// save the currently entered text in order to restore it later.
|
||||
// NB: This isn't 'originalText' because we want to restore
|
||||
// sent history items too!
|
||||
let contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent()));
|
||||
window.sessionStorage.setItem("mx_messagecomposer_input_" + this.roomId, contentJSON);
|
||||
},
|
||||
|
||||
setLastTextEntry: function() {
|
||||
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
|
||||
if (contentJSON) {
|
||||
let content = convertFromRaw(JSON.parse(contentJSON));
|
||||
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.sentHistory.init(
|
||||
this.refs.editor,
|
||||
this.props.room.roomId
|
||||
);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.sentHistory.saveLastTextEntry();
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
|
@ -247,8 +185,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
let editor = this.refs.editor;
|
||||
onAction = (payload) => {
|
||||
const editor = this.refs.editor;
|
||||
let contentState = this.state.editorState.getCurrentContent();
|
||||
|
||||
switch (payload.action) {
|
||||
|
@ -262,22 +200,22 @@ export default class MessageComposerInput extends React.Component {
|
|||
contentState = Modifier.replaceText(
|
||||
contentState,
|
||||
this.state.editorState.getSelection(),
|
||||
`${payload.displayname}: `
|
||||
`${payload.displayname}: `,
|
||||
);
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||
this.onEditorContentChanged(editorState);
|
||||
editor.focus();
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'quote': {
|
||||
let {body, formatted_body} = payload.event.getContent();
|
||||
formatted_body = formatted_body || escape(body);
|
||||
if (formatted_body) {
|
||||
let content = RichText.HTMLtoContentState(`<blockquote>${formatted_body}</blockquote>`);
|
||||
let content = RichText.htmlToContentState(`<blockquote>${formatted_body}</blockquote>`);
|
||||
if (!this.state.isRichtextEnabled) {
|
||||
content = ContentState.createFromText(stateToMarkdown(content));
|
||||
content = ContentState.createFromText(RichText.stateToMarkdown(content));
|
||||
}
|
||||
|
||||
const blockMap = content.getBlockMap();
|
||||
|
@ -292,13 +230,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
||||
}
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
editorState = EditorState.moveSelectionToEnd(editorState);
|
||||
this.onEditorContentChanged(editorState);
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onTypingActivity() {
|
||||
this.isTyping = true;
|
||||
|
@ -318,7 +257,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
startUserTypingTimer() {
|
||||
this.stopUserTypingTimer();
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.userTypingTimer = setTimeout(function() {
|
||||
self.isTyping = false;
|
||||
self.sendTyping(self.isTyping);
|
||||
|
@ -335,7 +274,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
startServerTypingTimer() {
|
||||
if (!this.serverTypingTimer) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.serverTypingTimer = setTimeout(function() {
|
||||
if (self.isTyping) {
|
||||
self.sendTyping(self.isTyping);
|
||||
|
@ -356,7 +295,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||
).done();
|
||||
}
|
||||
|
||||
|
@ -367,60 +306,80 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// Called by Draft to change editor contents, and by setEditorState
|
||||
onEditorContentChanged(editorState: EditorState, didRespondToUserInput: boolean = true) {
|
||||
// Called by Draft to change editor contents
|
||||
onEditorContentChanged = (editorState: EditorState) => {
|
||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||
|
||||
const contentChanged = Q.defer();
|
||||
/* If a modification was made, set originalEditorState to null, since newState is now our original */
|
||||
/* Since a modification was made, set originalEditorState to null, since newState is now our original */
|
||||
this.setState({
|
||||
editorState,
|
||||
originalEditorState: didRespondToUserInput ? null : this.state.originalEditorState,
|
||||
}, () => contentChanged.resolve());
|
||||
originalEditorState: null,
|
||||
});
|
||||
};
|
||||
|
||||
if (editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity();
|
||||
} else {
|
||||
this.onFinishedTyping();
|
||||
/**
|
||||
* We're overriding setState here because it's the most convenient way to monitor changes to the editorState.
|
||||
* Doing it using a separate function that calls setState is a possibility (and was the old approach), but that
|
||||
* approach requires a callback and an extra setState whenever trying to set multiple state properties.
|
||||
*
|
||||
* @param state
|
||||
* @param callback
|
||||
*/
|
||||
setState(state, callback) {
|
||||
if (state.editorState != null) {
|
||||
state.editorState = RichText.attachImmutableEntitiesToEmoji(
|
||||
state.editorState);
|
||||
|
||||
if (state.editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity();
|
||||
} else {
|
||||
this.onFinishedTyping();
|
||||
}
|
||||
|
||||
if (!state.hasOwnProperty('originalEditorState')) {
|
||||
state.originalEditorState = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.onContentChanged) {
|
||||
const textContent = editorState.getCurrentContent().getPlainText();
|
||||
const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||
editorState.getCurrentContent().getBlocksAsArray());
|
||||
super.setState(state, () => {
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
|
||||
this.props.onContentChanged(textContent, selection);
|
||||
}
|
||||
return contentChanged.promise;
|
||||
}
|
||||
|
||||
setEditorState(editorState: EditorState) {
|
||||
return this.onEditorContentChanged(editorState, false);
|
||||
if (this.props.onContentChanged) {
|
||||
const textContent = this.state.editorState
|
||||
.getCurrentContent().getPlainText();
|
||||
const selection = RichText.selectionStateToTextOffsets(
|
||||
this.state.editorState.getSelection(),
|
||||
this.state.editorState.getCurrentContent().getBlocksAsArray());
|
||||
this.props.onContentChanged(textContent, selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableRichtext(enabled: boolean) {
|
||||
if (enabled === this.state.isRichtextEnabled) return;
|
||||
|
||||
let contentState = null;
|
||||
if (enabled) {
|
||||
const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText());
|
||||
contentState = RichText.HTMLtoContentState(md.toHTML());
|
||||
contentState = RichText.htmlToContentState(md.toHTML());
|
||||
} else {
|
||||
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent());
|
||||
let markdown = RichText.stateToMarkdown(this.state.editorState.getCurrentContent());
|
||||
if (markdown[markdown.length - 1] === '\n') {
|
||||
markdown = markdown.substring(0, markdown.length - 1); // stateToMarkdown tacks on an extra newline (?!?)
|
||||
}
|
||||
contentState = ContentState.createFromText(markdown);
|
||||
}
|
||||
|
||||
this.setEditorState(this.createEditorState(enabled, contentState)).then(() => {
|
||||
this.setState({
|
||||
isRichtextEnabled: enabled,
|
||||
});
|
||||
|
||||
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, contentState),
|
||||
isRichtextEnabled: enabled,
|
||||
});
|
||||
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
||||
}
|
||||
|
||||
handleKeyCommand(command: string): boolean {
|
||||
handleKeyCommand = (command: string): boolean => {
|
||||
if (command === 'toggle-mode') {
|
||||
this.enableRichtext(!this.state.isRichtextEnabled);
|
||||
return true;
|
||||
|
@ -434,32 +393,69 @@ export default class MessageComposerInput extends React.Component {
|
|||
const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'];
|
||||
|
||||
if (blockCommands.includes(command)) {
|
||||
this.setEditorState(RichUtils.toggleBlockType(this.state.editorState, command));
|
||||
this.setState({
|
||||
editorState: RichUtils.toggleBlockType(this.state.editorState, command),
|
||||
});
|
||||
} else if (command === 'strike') {
|
||||
// this is the only inline style not handled by Draft by default
|
||||
this.setEditorState(RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'));
|
||||
this.setState({
|
||||
editorState: RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let contentState = this.state.editorState.getCurrentContent(),
|
||||
selection = this.state.editorState.getSelection();
|
||||
let contentState = this.state.editorState.getCurrentContent();
|
||||
|
||||
let modifyFn = {
|
||||
'bold': text => `**${text}**`,
|
||||
'italic': text => `*${text}*`,
|
||||
'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||
'strike': text => `~~${text}~~`,
|
||||
'code': text => `\`${text}\``,
|
||||
'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
||||
'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''),
|
||||
'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''),
|
||||
const modifyFn = {
|
||||
'bold': (text) => `**${text}**`,
|
||||
'italic': (text) => `*${text}*`,
|
||||
'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||
'strike': (text) => `<del>${text}</del>`,
|
||||
'code-block': (text) => `\`\`\`\n${text}\n\`\`\`\n`,
|
||||
'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join('') + '\n',
|
||||
'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''),
|
||||
'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''),
|
||||
}[command];
|
||||
|
||||
const selectionAfterOffset = {
|
||||
'bold': -2,
|
||||
'italic': -1,
|
||||
'underline': -1,
|
||||
'strike': -6,
|
||||
'code-block': -5,
|
||||
'blockquote': -2,
|
||||
}[command];
|
||||
|
||||
// Returns a function that collapses a selectionState to its end and moves it by offset
|
||||
const collapseAndOffsetSelection = (selectionState, offset) => {
|
||||
const key = selectionState.getEndKey();
|
||||
return new SelectionState({
|
||||
anchorKey: key, anchorOffset: offset,
|
||||
focusKey: key, focusOffset: offset,
|
||||
});
|
||||
};
|
||||
|
||||
if (modifyFn) {
|
||||
const previousSelection = this.state.editorState.getSelection();
|
||||
const newContentState = RichText.modifyText(contentState, previousSelection, modifyFn);
|
||||
newState = EditorState.push(
|
||||
this.state.editorState,
|
||||
RichText.modifyText(contentState, selection, modifyFn),
|
||||
'insert-characters'
|
||||
newContentState,
|
||||
'insert-characters',
|
||||
);
|
||||
|
||||
let newSelection = newContentState.getSelectionAfter();
|
||||
// If the selection range is 0, move the cursor inside the formatted body
|
||||
if (previousSelection.getStartOffset() === previousSelection.getEndOffset() &&
|
||||
previousSelection.getStartKey() === previousSelection.getEndKey() &&
|
||||
selectionAfterOffset !== undefined
|
||||
) {
|
||||
const selectedBlock = newContentState.getBlockForKey(previousSelection.getAnchorKey());
|
||||
const blockLength = selectedBlock.getText().length;
|
||||
const newOffset = blockLength + selectionAfterOffset;
|
||||
newSelection = collapseAndOffsetSelection(newSelection, newOffset);
|
||||
}
|
||||
|
||||
newState = EditorState.forceSelection(newState, newSelection);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -468,19 +464,49 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
if (newState != null) {
|
||||
this.setEditorState(newState);
|
||||
this.setState({editorState: newState});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onTextPasted(text: string, html?: string) {
|
||||
const currentSelection = this.state.editorState.getSelection();
|
||||
const currentContent = this.state.editorState.getCurrentContent();
|
||||
|
||||
let contentState = null;
|
||||
|
||||
if (html) {
|
||||
contentState = Modifier.replaceWithFragment(
|
||||
currentContent,
|
||||
currentSelection,
|
||||
RichText.htmlToContentState(html).getBlockMap(),
|
||||
);
|
||||
} else {
|
||||
contentState = Modifier.replaceText(currentContent, currentSelection, text);
|
||||
}
|
||||
|
||||
let newEditorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
|
||||
newEditorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
|
||||
this.onEditorContentChanged(newEditorState);
|
||||
return true;
|
||||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if (ev.shiftKey) {
|
||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
|
||||
// If we're in any of these three types of blocks, shift enter should insert soft newlines
|
||||
// And just enter should end the block
|
||||
if(['blockquote', 'unordered-list-item', 'ordered-list-item'].includes(currentBlockType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentState = this.state.editorState.getCurrentContent();
|
||||
if (!contentState.hasText()) {
|
||||
return true;
|
||||
|
@ -489,11 +515,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
let contentText = contentState.getPlainText(), contentHTML;
|
||||
|
||||
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||
const cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||
if (cmd) {
|
||||
if (!cmd.error) {
|
||||
this.setState({
|
||||
editorState: this.createEditorState()
|
||||
editorState: this.createEditorState(),
|
||||
});
|
||||
}
|
||||
if (cmd.promise) {
|
||||
|
@ -501,16 +527,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
console.log("Command success.");
|
||||
}, function(err) {
|
||||
console.error("Command failure: %s", err);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Server error"),
|
||||
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (cmd.error) {
|
||||
} else if (cmd.error) {
|
||||
console.error(cmd.error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Command error"),
|
||||
description: cmd.error,
|
||||
|
@ -520,9 +545,30 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
if (this.state.isRichtextEnabled) {
|
||||
contentHTML = HtmlUtils.stripParagraphs(
|
||||
RichText.contentStateToHTML(contentState)
|
||||
);
|
||||
// We should only send HTML if any block is styled or contains inline style
|
||||
let shouldSendHTML = false;
|
||||
const blocks = contentState.getBlocksAsArray();
|
||||
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
||||
shouldSendHTML = true;
|
||||
} else {
|
||||
const characterLists = blocks.map((block) => block.getCharacterList());
|
||||
// For each block of characters, determine if any inline styles are applied
|
||||
// and if yes, send HTML
|
||||
characterLists.forEach((characters) => {
|
||||
const numberOfStylesForCharacters = characters.map(
|
||||
(character) => character.getStyle().toArray().length,
|
||||
).toArray();
|
||||
// If any character has more than 0 inline styles applied, send HTML
|
||||
if (numberOfStylesForCharacters.some((styles) => styles > 0)) {
|
||||
shouldSendHTML = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (shouldSendHTML) {
|
||||
contentHTML = HtmlUtils.processHtmlForSending(
|
||||
RichText.contentStateToHTML(contentState),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const md = new Markdown(contentText);
|
||||
if (md.isPlainText()) {
|
||||
|
@ -535,6 +581,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendHtmlFn = this.client.sendHtmlMessage;
|
||||
let sendTextFn = this.client.sendTextMessage;
|
||||
|
||||
if (this.state.isRichtextEnabled) {
|
||||
this.historyManager.addItem(
|
||||
contentHTML ? contentHTML : contentText,
|
||||
contentHTML ? 'html' : 'markdown',
|
||||
);
|
||||
} else {
|
||||
// Always store MD input as input history
|
||||
this.historyManager.addItem(contentText, 'markdown');
|
||||
}
|
||||
|
||||
if (contentText.startsWith('/me')) {
|
||||
contentText = contentText.substring(4);
|
||||
// bit of a hack, but the alternative would be quite complicated
|
||||
|
@ -543,12 +599,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
sendTextFn = this.client.sendEmoteMessage;
|
||||
}
|
||||
|
||||
// XXX: We don't actually seem to use this history?
|
||||
this.sentHistory.push(contentHTML || contentText);
|
||||
let sendMessagePromise;
|
||||
if (contentHTML) {
|
||||
sendMessagePromise = sendHtmlFn.call(
|
||||
this.client, this.props.room.roomId, contentText, contentHTML
|
||||
this.client, this.props.room.roomId, contentText, contentHTML,
|
||||
);
|
||||
} else {
|
||||
sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText);
|
||||
|
@ -569,85 +623,163 @@ export default class MessageComposerInput extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
async onUpArrow(e) {
|
||||
const completion = this.autocomplete.onUpArrow();
|
||||
if (completion != null) {
|
||||
e.preventDefault();
|
||||
onUpArrow = (e) => {
|
||||
this.onVerticalArrow(e, true);
|
||||
};
|
||||
|
||||
onDownArrow = (e) => {
|
||||
this.onVerticalArrow(e, false);
|
||||
};
|
||||
|
||||
onVerticalArrow = (e, up) => {
|
||||
// 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
|
||||
const selection = this.state.editorState.getSelection();
|
||||
const blockKey = selection.getStartKey();
|
||||
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;
|
||||
}
|
||||
|
||||
if (blockKey === lastBlock.getKey()) {
|
||||
const textAfterCursor = lastBlock.getText().slice(selectionOffset);
|
||||
canMoveDown = textAfterCursor.indexOf('\n') === -1;
|
||||
}
|
||||
|
||||
if ((up && !canMoveUp) || (!up && !canMoveDown)) return;
|
||||
|
||||
const selected = this.selectHistory(up);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
this.moveAutocompleteSelection(up);
|
||||
}
|
||||
return await this.setDisplayedCompletion(completion);
|
||||
}
|
||||
};
|
||||
|
||||
async onDownArrow(e) {
|
||||
const completion = this.autocomplete.onDownArrow();
|
||||
e.preventDefault();
|
||||
return await this.setDisplayedCompletion(completion);
|
||||
}
|
||||
selectHistory = async (up) => {
|
||||
const delta = up ? -1 : 1;
|
||||
|
||||
// tab and shift-tab are mapped to down and up arrow respectively
|
||||
async onTab(e) {
|
||||
e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes
|
||||
const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e);
|
||||
if (!didTab && this.autocomplete) {
|
||||
this.autocomplete.forceComplete().then(() => {
|
||||
this.onDownArrow(e);
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||
// We can't go any further - there isn't any more history, so nop.
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
currentlyComposedEditorState: this.state.editorState,
|
||||
});
|
||||
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||
// True when we return to the message being composed currently
|
||||
this.setState({
|
||||
editorState: this.state.currentlyComposedEditorState,
|
||||
});
|
||||
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onEscape(e) {
|
||||
const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
||||
if (!newContent) return false;
|
||||
let editorState = EditorState.push(
|
||||
this.state.editorState,
|
||||
newContent,
|
||||
'insert-characters',
|
||||
);
|
||||
|
||||
// Move selection to the end of the selected history
|
||||
let newSelection = SelectionState.createEmpty(newContent.getLastBlock().getKey());
|
||||
newSelection = newSelection.merge({
|
||||
focusOffset: newContent.getLastBlock().getLength(),
|
||||
anchorOffset: newContent.getLastBlock().getLength(),
|
||||
});
|
||||
editorState = EditorState.forceSelection(editorState, newSelection);
|
||||
|
||||
this.setState({editorState});
|
||||
return true;
|
||||
};
|
||||
|
||||
onTab = async (e) => {
|
||||
e.preventDefault();
|
||||
if (this.autocomplete.state.completionList.length === 0) {
|
||||
// Force completions to show for the text currently entered
|
||||
await this.autocomplete.forceComplete();
|
||||
// Select the first item by moving "down"
|
||||
await this.moveAutocompleteSelection(false);
|
||||
} else {
|
||||
await this.moveAutocompleteSelection(e.shiftKey);
|
||||
}
|
||||
};
|
||||
|
||||
moveAutocompleteSelection = (up) => {
|
||||
const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||
return this.setDisplayedCompletion(completion);
|
||||
};
|
||||
|
||||
onEscape = async (e) => {
|
||||
e.preventDefault();
|
||||
if (this.autocomplete) {
|
||||
this.autocomplete.onEscape(e);
|
||||
}
|
||||
this.setDisplayedCompletion(null); // restore originalEditorState
|
||||
}
|
||||
await this.setDisplayedCompletion(null); // restore originalEditorState
|
||||
};
|
||||
|
||||
/* If passed null, restores the original editor content from state.originalEditorState.
|
||||
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
|
||||
*/
|
||||
async setDisplayedCompletion(displayedCompletion: ?Completion): boolean {
|
||||
setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => {
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
||||
if (displayedCompletion == null) {
|
||||
if (this.state.originalEditorState) {
|
||||
this.setEditorState(this.state.originalEditorState);
|
||||
let editorState = this.state.originalEditorState;
|
||||
// This is a workaround from https://github.com/facebook/draft-js/issues/458
|
||||
// Due to the way we swap editorStates, Draft does not rerender at times
|
||||
editorState = EditorState.forceSelection(editorState,
|
||||
editorState.getSelection());
|
||||
this.setState({editorState});
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const {range = {}, completion = ''} = displayedCompletion;
|
||||
|
||||
let contentState = Modifier.replaceText(
|
||||
const contentState = Modifier.replaceText(
|
||||
activeEditorState.getCurrentContent(),
|
||||
RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()),
|
||||
completion
|
||||
completion,
|
||||
);
|
||||
|
||||
let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters');
|
||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||
const originalEditorState = activeEditorState;
|
||||
|
||||
await this.setEditorState(editorState);
|
||||
this.setState({originalEditorState});
|
||||
this.setState({editorState, originalEditorState: activeEditorState});
|
||||
|
||||
// for some reason, doing this right away does not update the editor :(
|
||||
setTimeout(() => this.refs.editor.focus(), 50);
|
||||
// setTimeout(() => this.refs.editor.focus(), 50);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
||||
e.preventDefault(); // don't steal focus from the editor!
|
||||
const command = {
|
||||
code: 'code-block',
|
||||
quote: 'blockquote',
|
||||
bullet: 'unordered-list-item',
|
||||
numbullet: 'ordered-list-item',
|
||||
}[name] || name;
|
||||
code: 'code-block',
|
||||
quote: 'blockquote',
|
||||
bullet: 'unordered-list-item',
|
||||
numbullet: 'ordered-list-item',
|
||||
}[name] || name;
|
||||
this.handleKeyCommand(command);
|
||||
}
|
||||
|
||||
/* returns inline style and block type of current SelectionState so MessageComposer can render formatting
|
||||
buttons. */
|
||||
buttons. */
|
||||
getSelectionInfo(editorState: EditorState) {
|
||||
const styleName = {
|
||||
BOLD: 'bold',
|
||||
|
@ -658,8 +790,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
||||
const style = originalStyle
|
||||
.map(style => styleName[style] || null)
|
||||
.filter(styleName => !!styleName);
|
||||
.map((style) => styleName[style] || null)
|
||||
.filter((styleName) => !!styleName);
|
||||
|
||||
const blockName = {
|
||||
'code-block': 'code',
|
||||
|
@ -678,10 +810,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onMarkdownToggleClicked(e) {
|
||||
onMarkdownToggleClicked = (e) => {
|
||||
e.preventDefault(); // don't steal focus from the editor!
|
||||
this.handleKeyCommand('toggle-mode');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
@ -698,7 +830,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
const className = classNames('mx_MessageComposer_input', {
|
||||
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||
});
|
||||
|
||||
const content = activeEditorState.getCurrentContent();
|
||||
|
@ -713,7 +845,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
ref={(e) => this.autocomplete = e}
|
||||
onConfirm={this.setDisplayedCompletion}
|
||||
query={contentText}
|
||||
selection={selection} />
|
||||
selection={selection}/>
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||
|
@ -729,13 +861,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
handlePastedText={this.onTextPasted}
|
||||
handlePastedFiles={this.props.onFilesPasted}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
onEscape={this.onEscape}
|
||||
spellCheck={true} />
|
||||
spellCheck={true}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -251,7 +251,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
if (topic) {
|
||||
topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
||||
<EmojiText dir="auto" element="div" className="mx_RoomHeader_topic" ref="topic" title={topic}>{ topic }</EmojiText>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue