Merge branch 'develop' into erikj/group_server

This commit is contained in:
Luke Barnard 2017-07-07 10:08:49 +01:00 committed by GitHub
commit 32a01b54b8
61 changed files with 2577 additions and 1482 deletions

View file

@ -47,13 +47,12 @@ import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore';
var DEBUG = false;
let DEBUG = false;
let debuglog = function() {};
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
debuglog = console.log.bind(console);
}
module.exports = React.createClass({
@ -113,6 +112,7 @@ module.exports = React.createClass({
callState: null,
guestsCanJoin: false,
canPeek: false,
showApps: false,
// error object, as from the matrix client/server API
// If we failed to load information about the room,
@ -236,6 +236,7 @@ module.exports = React.createClass({
if (room) {
this.setState({
unsentMessageError: this._getUnsentMessageError(room),
showApps: this._shouldShowApps(room),
});
this._onRoomLoaded(room);
}
@ -273,6 +274,11 @@ module.exports = React.createClass({
}
},
_shouldShowApps: function(room) {
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets', '');
return appsStateEvents && Object.keys(appsStateEvents.getContent()).length > 0;
},
componentDidMount: function() {
var call = this._getCallForRoom();
var callState = call ? call.call_state : "ended";
@ -453,9 +459,14 @@ module.exports = React.createClass({
this._updateConfCallNotification();
this.setState({
callState: callState
callState: callState,
});
break;
case 'appsDrawer':
this.setState({
showApps: payload.show,
});
break;
}
},
@ -1604,11 +1615,13 @@ module.exports = React.createClass({
var auxPanel = (
<AuxPanel ref="auxPanel" room={this.state.room}
userId={MatrixClientPeg.get().credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
draggingFile={this.state.draggingFile}
displayConfCallNotification={this.state.displayConfCallNotification}
maxHeight={this.state.auxPanelMaxHeight}
onResize={this.onChildResize} >
onResize={this.onChildResize}
showApps={this.state.showApps && !this.state.editingRoomSettings} >
{ aux }
</AuxPanel>
);
@ -1621,8 +1634,14 @@ module.exports = React.createClass({
if (canSpeak) {
messageComposer =
<MessageComposer
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
room={this.state.room}
onResize={this.onChildResize}
uploadFile={this.uploadFile}
callState={this.state.callState}
tabComplete={this.tabComplete}
opacity={ this.props.opacity }
showApps={ this.state.showApps }
/>;
}
// TODO: Why aren't we storing the term/scope/count in this format

View file

@ -93,6 +93,10 @@ const SETTINGS_LABELS = [
id: 'disableMarkdown',
label: 'Disable markdown formatting',
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: 'Enable automatic language detection for syntax highlighting',
},
/*
{
id: 'useFixedWidthFont',

View file

@ -0,0 +1,161 @@
/*
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 ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler';
import url from 'url';
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: "",
};
},
getInitialState: function() {
return {
loading: false,
widgetUrl: this.props.url,
error: null,
};
},
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
isScalarUrl: function() {
const scalarUrl = SdkConfig.get().integrations_rest_url;
return scalarUrl && this.props.url.startsWith(scalarUrl);
},
componentWillMount: function() {
if (!this.isScalarUrl()) {
return;
}
// Fetch the token before loading the iframe as we need to mangle the URL
this.setState({
loading: true,
});
this._scalarClient = new ScalarAuthClient();
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param
const u = url.parse(this.props.url);
if (!u.search) {
u.search = "?scalar_token=" + encodeURIComponent(token);
} else {
u.search += "&scalar_token=" + encodeURIComponent(token);
}
this.setState({
error: null,
widgetUrl: u.format(),
loading: false,
});
}, (err) => {
this.setState({
error: err.message,
loading: false,
});
});
},
_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() {
let appTileBody;
if (this.state.loading) {
appTileBody = (
<div> Loading... </div>
);
} else {
appTileBody = (
<div className="mx_AppTileBody">
<iframe ref="appFrame" src={this.state.widgetUrl} allowFullScreen="true"></iframe>
</div>
);
}
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>
{appTileBody}
</div>
);
},
});

View file

@ -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;

View file

@ -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);
}
@ -131,9 +143,15 @@ module.exports = React.createClass({
if (this.props.showUrlPreview && !this.state.links.length) {
var links = this.findLinks(this.refs.content.children);
if (links.length) {
this.setState({ links: links.map((link)=>{
return link.getAttribute("href");
})});
// de-dup the links (but preserve ordering)
const seen = new Set();
links = links.filter((link) => {
if (seen.has(link)) return false;
seen.add(link);
return true;
});
this.setState({ links: links });
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
@ -146,12 +164,13 @@ module.exports = React.createClass({
findLinks: function(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href"))
{
if (this.isLinkPreviewable(node)) {
links.push(node);
links.push(node.getAttribute("href"));
}
}
else if (node.tagName === "PRE" || node.tagName === "CODE" ||

View 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>
);
},
});

View file

@ -40,25 +40,51 @@ 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;
}
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 || this.state.forceComplete) {
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.
@ -88,23 +114,13 @@ export default class Autocomplete extends React.Component {
hide = false;
}
const autocompleteDelay = UserSettingsStore.getSyncedSetting('autocompleteDelay', 200);
// We had no completions before, but do now, so we should apply our display delay here
if (this.state.completionList.length === 0 && completionList.length > 0 &&
!forceComplete && autocompleteDelay > 0) {
await Q.delay(autocompleteDelay);
}
// Force complete is turned off each time since we can't edit the query in that case
forceComplete = false;
this.setState({
completions,
completionList,
selectionOffset,
hide,
forceComplete,
// Force complete is turned off each time since we can't edit the query in that case
forceComplete: false,
});
}
@ -161,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

@ -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 }

View file

@ -13,16 +13,14 @@ 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 +30,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 +57,6 @@ export default class MessageComposer extends React.Component {
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
};
}
componentDidMount() {
@ -127,7 +126,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 +138,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 +151,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 +263,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 +294,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 +313,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 +361,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 +377,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,18 +403,14 @@ 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);
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
const className = classNames("mx_MessageComposer_format_button", {
mx_MessageComposer_format_button_disabled: disabled,
mx_filterFlipColor: true,
});
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
title={ _t(name) }
onMouseDown={disabled ? null : onFormatButtonClicked}
onMouseDown={onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
@ -403,5 +462,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,
};

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;
@ -87,6 +89,13 @@ export default class MessageComposerInput extends React.Component {
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);
}
@ -114,6 +123,7 @@ export default class MessageComposerInput extends React.Component {
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);
@ -122,15 +132,21 @@ 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,
};
// 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();
// the virtual state "above" the history stack, the message currently being composed that
// we want to persist whilst browsing history
currentlyComposedEditorState: null,
// whether there were any completions
someCompletions: null,
};
this.client = MatrixClientPeg.get();
}
@ -217,7 +233,8 @@ export default class MessageComposerInput extends React.Component {
if (this.state.isRichtextEnabled) {
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
}
const editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
editorState = EditorState.moveSelectionToEnd(editorState);
this.onEditorContentChanged(editorState);
editor.focus();
}
@ -323,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;
}
@ -390,26 +415,59 @@ export default class MessageComposerInput extends React.Component {
});
}
} else {
let contentState = this.state.editorState.getCurrentContent(),
selection = this.state.editorState.getSelection();
let contentState = this.state.editorState.getCurrentContent();
const modifyFn = {
'bold': (text) => `**${text}**`,
'italic': (text) => `*${text}*`,
'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
'underline': (text) => `<u>${text}</u>`,
'strike': (text) => `<del>${text}</del>`,
'code-block': (text) => `\`\`\`\n${text}\n\`\`\``,
'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''),
'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': -4,
'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),
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);
}
}
@ -425,6 +483,28 @@ export default class MessageComposerInput extends React.Component {
return false;
}
onTextPasted(text: string, html?: string) {
const currentSelection = this.state.editorState.getSelection();
const currentContent = this.state.editorState.getCurrentContent();
let contentState = null;
if (html && this.state.isRichtextEnabled) {
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));
@ -476,9 +556,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()) {
@ -491,6 +592,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
@ -499,10 +610,6 @@ export default class MessageComposerInput extends React.Component {
sendTextFn = this.client.sendEmoteMessage;
}
this.historyManager.addItem(
this.state.isRichtextEnabled ? contentHTML : contentState.getPlainText(),
this.state.isRichtextEnabled ? 'html' : 'markdown');
let sendMessagePromise;
if (contentHTML) {
sendMessagePromise = sendHtmlFn.call(
@ -525,49 +632,117 @@ export default class MessageComposerInput extends React.Component {
this.autocomplete.hide();
return true;
}
onUpArrow = (e) => {
this.onVerticalArrow(e, true);
};
onUpArrow = async (e) => {
const completion = this.autocomplete.onUpArrow();
if (completion == null) {
const newContent = this.historyManager.getItem(-1, this.state.isRichtextEnabled ? 'html' : 'markdown');
if (!newContent) return false;
const editorState = EditorState.push(this.state.editorState,
newContent,
'insert-characters');
this.setState({editorState});
return true;
onDownArrow = (e) => {
this.onVerticalArrow(e, false);
};
onVerticalArrow = (e, up) => {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
return;
}
e.preventDefault();
return await this.setDisplayedCompletion(completion);
};
onDownArrow = async (e) => {
const completion = this.autocomplete.onDownArrow();
if (completion == null) {
const newContent = this.historyManager.getItem(+1, this.state.isRichtextEnabled ? 'html' : 'markdown');
if (!newContent) return false;
const editorState = EditorState.push(this.state.editorState,
newContent,
'insert-characters');
this.setState({editorState});
return true;
}
e.preventDefault();
return await this.setDisplayedCompletion(completion);
};
// tab and shift-tab are mapped to down and up arrow respectively
onTab = async (e) => {
e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes
// Select history only if we are not currently auto-completing
if (this.autocomplete.state.completionList.length === 0) {
await this.autocomplete.forceComplete();
this.onDownArrow(e);
// 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();
let canMoveUp = false;
let canMoveDown = false;
if (blockKey === firstBlock.getKey()) {
canMoveUp = selection.getStartOffset() === selection.getEndOffset() &&
selection.getStartOffset() === 0;
}
if (blockKey === lastBlock.getKey()) {
canMoveDown = selection.getStartOffset() === selection.getEndOffset() &&
selection.getStartOffset() === lastBlock.getText().length;
}
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 {
await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e);
this.moveAutocompleteSelection(up);
}
};
selectHistory = async (up) => {
const delta = up ? -1 : 1;
// 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;
}
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) => {
this.setState({
someCompletions: null,
});
e.preventDefault();
if (this.autocomplete.state.completionList.length === 0) {
// Force completions to show for the text currently entered
const completionCount = await this.autocomplete.forceComplete();
this.setState({
someCompletions: completionCount > 0,
});
// 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) {
@ -676,6 +851,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();
@ -706,6 +882,7 @@ 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}

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 = <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
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>

View file

@ -13,11 +13,11 @@ 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");
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
import React from 'react';
import dis from '../../../dispatcher';
import CallHandler from '../../../CallHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -73,10 +73,10 @@ module.exports = React.createClass({
},
showCall: function() {
var call;
let call;
if (this.props.room) {
var roomId = this.props.room.roomId;
const roomId = this.props.room.roomId;
call = CallHandler.getCallForRoom(roomId) ||
(this.props.ConferenceHandler ?
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
@ -86,9 +86,7 @@ module.exports = React.createClass({
if (this.call) {
this.setState({ call: call });
}
}
else {
} else {
call = CallHandler.getAnyActiveCall();
this.setState({ call: call });
}
@ -109,8 +107,7 @@ module.exports = React.createClass({
call.confUserId ? "none" : "block"
);
this.getVideoView().getRemoteVideoElement().style.display = "block";
}
else {
} else {
this.getVideoView().getLocalVideoElement().style.display = "none";
this.getVideoView().getRemoteVideoElement().style.display = "none";
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
@ -126,11 +123,11 @@ module.exports = React.createClass({
},
render: function() {
var VideoView = sdk.getComponent('voip.VideoView');
const VideoView = sdk.getComponent('voip.VideoView');
var voice;
let voice;
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
var callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
voice = (
<div className="mx_CallView_voice" onClick={ this.props.onClick }>
{_t("Active call (%(roomName)s)", {roomName: callRoom.name})}
@ -147,6 +144,6 @@ module.exports = React.createClass({
{ voice }
</div>
);
}
},
});

View file

@ -13,10 +13,9 @@ 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');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -29,34 +28,32 @@ module.exports = React.createClass({
onAnswerClick: function() {
dis.dispatch({
action: 'answer',
room_id: this.props.incomingCall.roomId
room_id: this.props.incomingCall.roomId,
});
},
onRejectClick: function() {
dis.dispatch({
action: 'hangup',
room_id: this.props.incomingCall.roomId
room_id: this.props.incomingCall.roomId,
});
},
render: function() {
var room = null;
let room = null;
if (this.props.incomingCall) {
room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId);
}
var caller = room ? room.name : _t("unknown caller");
const caller = room ? room.name : _t("unknown caller");
let incomingCallText = null;
if (this.props.incomingCall) {
if (this.props.incomingCall.type === "voice") {
incomingCallText = _t("Incoming voice call from %(name)s", {name: caller});
}
else if (this.props.incomingCall.type === "video") {
} else if (this.props.incomingCall.type === "video") {
incomingCallText = _t("Incoming video call from %(name)s", {name: caller});
}
else {
} else {
incomingCallText = _t("Incoming call from %(name)s", {name: caller});
}
}
@ -81,6 +78,6 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -16,7 +16,7 @@ limitations under the License.
'use strict';
var React = require('react');
import React from 'react';
module.exports = React.createClass({
displayName: 'VideoFeed',

View file

@ -16,11 +16,11 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
import React from 'react';
import ReactDOM from 'react-dom';
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import sdk from '../../../index';
import dis from '../../../dispatcher';
module.exports = React.createClass({
displayName: 'VideoView',
@ -53,9 +53,10 @@ module.exports = React.createClass({
// this needs to be somewhere at the top of the DOM which
// always exists to avoid audio interruptions.
// Might as well just use DOM.
var remoteAudioElement = document.getElementById("remoteAudio");
const remoteAudioElement = document.getElementById("remoteAudio");
if (!remoteAudioElement) {
console.error("Failed to find remoteAudio element - cannot play audio! You need to add an <audio/> to the DOM.");
console.error("Failed to find remoteAudio element - cannot play audio!"
+ "You need to add an <audio/> to the DOM.");
}
return remoteAudioElement;
},
@ -70,22 +71,21 @@ module.exports = React.createClass({
onAction: function(payload) {
switch (payload.action) {
case 'video_fullscreen':
case 'video_fullscreen': {
if (!this.container) {
return;
}
var element = this.container;
const element = this.container;
if (payload.fullscreen) {
var requestMethod = (
const requestMethod = (
element.requestFullScreen ||
element.webkitRequestFullScreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen
);
requestMethod.call(element);
}
else {
var exitMethod = (
} else {
const exitMethod = (
document.exitFullscreen ||
document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
@ -96,17 +96,18 @@ module.exports = React.createClass({
}
}
break;
}
}
},
render: function() {
var VideoFeed = sdk.getComponent('voip.VideoFeed');
const VideoFeed = sdk.getComponent('voip.VideoFeed');
// if we're fullscreen, we don't want to set a maxHeight on the video element.
var fullscreenElement = (document.fullscreenElement ||
const fullscreenElement = (document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement);
var maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
return (
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
@ -119,5 +120,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});