Merge pull request #1152 from matrix-org/rxl881/apps

Add support for apps
This commit is contained in:
Richard Lewis 2017-06-28 16:06:20 +01:00 committed by GitHub
commit d61525e420
9 changed files with 1565 additions and 973 deletions

View File

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -109,6 +110,76 @@ Example:
response: 78 response: 78
} }
set_widget
----------
Set a new widget in the room. Clobbers based on the ID.
Request:
- `room_id` (String) is the room to set the widget in.
- `widget_id` (String) is the ID of the widget to add (or replace if it already exists).
It can be an arbitrary UTF8 string and is purely for distinguishing between widgets.
- `url` (String) is the URL that clients should load in an iframe to run the widget.
All widgets must have a valid URL. If the URL is `null` (not `undefined`), the
widget will be removed from the room.
- `type` (String) is the type of widget, which is provided as a hint for matrix clients so they
can configure/lay out the widget in different ways. All widgets must have a type.
- `name` (String) is an optional human-readable string about the widget.
- `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs.
Response:
{
success: true
}
Example:
{
action: "set_widget",
room_id: "!foo:bar",
widget_id: "abc123",
url: "http://widget.url",
type: "example",
response: {
success: true
}
}
get_widgets
-----------
Get a list of all widgets in the room. The response is the `content` field
of the state event.
Request:
- `room_id` (String) is the room to get the widgets in.
Response:
{
$widget_id: {
type: "example",
url: "http://widget.url",
name: "Example Widget",
data: {
key: "val"
}
},
$widget_id: { ... }
}
Example:
{
action: "get_widgets",
room_id: "!foo:bar",
widget_id: "abc123",
url: "http://widget.url",
type: "example",
response: {
$widget_id: {
type: "example",
url: "http://widget.url",
name: "Example Widget",
data: {
key: "val"
}
},
$widget_id: { ... }
}
}
membership_state AND bot_options membership_state AND bot_options
-------------------------------- --------------------------------
@ -191,6 +262,84 @@ function inviteUser(event, roomId, userId) {
}); });
} }
function setWidget(event, roomId) {
const widgetId = event.data.widget_id;
const widgetType = event.data.type;
const widgetUrl = event.data.url;
const widgetName = event.data.name; // optional
const widgetData = event.data.data; // optional
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
return;
}
// both adding/removing widgets need these checks
if (!widgetId || widgetUrl === undefined) {
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
return;
}
if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc
// check types of fields
if (widgetName !== undefined && typeof widgetName !== 'string') {
sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string."));
return;
}
if (widgetData !== undefined && !(widgetData instanceof Object)) {
sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object."));
return;
}
if (typeof widgetType !== 'string') {
sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string."));
return;
}
if (typeof widgetUrl !== 'string') {
sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null."));
return;
}
}
// TODO: same dance we do for power levels. It'd be nice if the JS SDK had helper methods to do this.
client.getStateEvent(roomId, "im.vector.modular.widgets", "").then((widgets) => {
if (widgetUrl === null) {
delete widgets[widgetId];
}
else {
widgets[widgetId] = {
type: widgetType,
url: widgetUrl,
name: widgetName,
data: widgetData,
};
}
return client.sendStateEvent(roomId, "im.vector.modular.widgets", widgets);
}, (err) => {
if (err.errcode === "M_NOT_FOUND") {
return client.sendStateEvent(roomId, "im.vector.modular.widgets", {
[widgetId]: {
type: widgetType,
url: widgetUrl,
name: widgetName,
data: widgetData,
}
});
}
throw err;
}).done(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, _t('Failed to send request.'), err);
});
}
function getWidgets(event, roomId) {
returnStateEvent(event, roomId, "im.vector.modular.widgets", "");
}
function setPlumbingState(event, roomId, status) { function setPlumbingState(event, roomId, status) {
if (typeof status !== 'string') { if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string'); throw new Error('Plumbing state status should be a string');
@ -367,7 +516,7 @@ const onMessage = function(event) {
return; return;
} }
// Getting join rules does not require userId // These APIs don't require userId
if (event.data.action === "join_rules_state") { if (event.data.action === "join_rules_state") {
getJoinRules(event, roomId); getJoinRules(event, roomId);
return; return;
@ -377,6 +526,12 @@ const onMessage = function(event) {
} else if (event.data.action === "get_membership_count") { } else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId); getMembershipCount(event, roomId);
return; return;
} else if (event.data.action === "set_widget") {
setWidget(event, roomId);
return;
} else if (event.data.action === "get_widgets") {
getWidgets(event, roomId);
return;
} }
if (!userId) { if (!userId) {

View File

@ -30,11 +30,17 @@ export default {
id: 'rich_text_editor', id: 'rich_text_editor',
default: false, default: false,
}, },
{
name: "-",
id: 'matrix_apps',
default: false,
},
], ],
// horrible but it works. The locality makes this somewhat more palatable. // horrible but it works. The locality makes this somewhat more palatable.
doTranslations: function() { doTranslations: function() {
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete"); this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
this.LABS_FEATURES[1].name = _t("Matrix Apps");
}, },
loadProfileInfo: function() { loadProfileInfo: function() {

View File

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

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

View File

@ -0,0 +1,223 @@
/*
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,
onFinished: ()=>{
if (e) {
this.props.onCancelClick(e);
}
},
}, "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

@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index'; import sdk from '../../../index';
import dis from "../../../dispatcher"; import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils'; 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({ module.exports = React.createClass({
@ -28,6 +30,8 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// js-sdk room object // js-sdk room object
room: React.PropTypes.object.isRequired, room: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
showApps: React.PropTypes.bool,
// Conference Handler implementation // Conference Handler implementation
conferenceHandler: React.PropTypes.object, conferenceHandler: React.PropTypes.object,
@ -70,10 +74,10 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var CallView = sdk.getComponent("voip.CallView"); const CallView = sdk.getComponent("voip.CallView");
var TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
var fileDropTarget = null; let fileDropTarget = null;
if (this.props.draggingFile) { if (this.props.draggingFile) {
fileDropTarget = ( fileDropTarget = (
<div className="mx_RoomView_fileDropTarget"> <div className="mx_RoomView_fileDropTarget">
@ -87,14 +91,13 @@ module.exports = React.createClass({
); );
} }
var conferenceCallNotification = null; let conferenceCallNotification = null;
if (this.props.displayConfCallNotification) { if (this.props.displayConfCallNotification) {
let supportedText = ''; let supportedText = '';
let joinNode; let joinNode;
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = _t(" (unsupported)"); supportedText = _t(" (unsupported)");
} } else {
else {
joinNode = (<span> joinNode = (<span>
{_tJsx( {_tJsx(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.", "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
@ -105,7 +108,6 @@ module.exports = React.createClass({
] ]
)} )}
</span>); </span>);
} }
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages, // 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. // 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} <CallView ref="callView" room={this.props.room}
ConferenceHandler={this.props.conferenceHandler} ConferenceHandler={this.props.conferenceHandler}
onResize={this.props.onResize} 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 ( return (
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} > <div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
{ appsDrawer }
{ fileDropTarget } { fileDropTarget }
{ callView } { callView }
{ conferenceCallNotification } { conferenceCallNotification }

View File

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
var CallHandler = require('../../../CallHandler'); import CallHandler from '../../../CallHandler';
var MatrixClientPeg = require('../../../MatrixClientPeg'); import MatrixClientPeg from '../../../MatrixClientPeg';
var Modal = require('../../../Modal'); import Modal from '../../../Modal';
var sdk = require('../../../index'); import sdk from '../../../index';
var dis = require('../../../dispatcher'); import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete'; import Autocomplete from './Autocomplete';
import classNames from 'classnames'; import classNames from 'classnames';
import UserSettingsStore from '../../../UserSettingsStore'; import UserSettingsStore from '../../../UserSettingsStore';
@ -32,6 +31,8 @@ export default class MessageComposer extends React.Component {
this.onCallClick = this.onCallClick.bind(this); this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this); this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.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.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this); this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.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), showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
}; };
} }
componentDidMount() { componentDidMount() {
@ -127,7 +127,7 @@ export default class MessageComposer extends React.Component {
if(shouldUpload) { if(shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (files) { if (files) {
for(var i=0; i<files.length; i++) { for(let i=0; i<files.length; i++) {
this.props.uploadFile(files[i]); this.props.uploadFile(files[i]);
} }
} }
@ -139,7 +139,7 @@ export default class MessageComposer extends React.Component {
} }
onHangupClick() { onHangupClick() {
var call = CallHandler.getCallForRoom(this.props.room.roomId); const call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall(); //var call = CallHandler.getAnyActiveCall();
if (!call) { if (!call) {
return; 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) { onCallClick(ev) {
// NOTE -- Will be replaced by Jitsi code (currently commented)
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video", type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId, room_id: this.props.room.roomId,
}); });
// this._startCallApp(false);
} }
onVoiceCallClick(ev) { onVoiceCallClick(ev) {
// NOTE -- Will be replaced by Jitsi code (currently commented)
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
type: 'voice', type: "voice",
room_id: this.props.room.roomId, 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}) { onInputContentChanged(content: string, selection: {start: number, end: number}) {
@ -216,19 +264,19 @@ export default class MessageComposer extends React.Component {
} }
render() { render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'}; const uploadInputStyle = {display: 'none'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" + const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old")); (UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
var controls = []; const controls = [];
controls.push( controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar"> <div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} /> <MemberAvatar member={me} width={24} height={24} />
</div> </div>,
); );
let e2eImg, e2eTitle, e2eClass; let e2eImg, e2eTitle, e2eClass;
@ -247,16 +295,15 @@ export default class MessageComposer extends React.Component {
controls.push( controls.push(
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12" <img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
alt={e2eTitle} title={e2eTitle} alt={e2eTitle} title={e2eTitle}
/> />,
); );
var callButton, videoCallButton, hangupButton; let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
if (this.props.callState && this.props.callState !== 'ended') { if (this.props.callState && this.props.callState !== 'ended') {
hangupButton = hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}> <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"/> <img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
</div>; </div>;
} } else {
else {
callButton = callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }> <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"/> <TintableSvg src="img/icon-call.svg" width="35" height="35"/>
@ -267,14 +314,29 @@ export default class MessageComposer extends React.Component {
</div>; </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); MatrixClientPeg.get().credentials.userId);
if (canSendMessages) { if (canSendMessages) {
// This also currently includes the call buttons. Really we should // This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly // check separately for whether we can call, but this is slightly
// complex because of conference calls. // complex because of conference calls.
var uploadButton = ( const uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload" <div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title={ _t('Upload file') }> onClick={this.onUploadClick} title={ _t('Upload file') }>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/> <TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
@ -300,7 +362,7 @@ export default class MessageComposer extends React.Component {
controls.push( controls.push(
<MessageComposerInput <MessageComposerInput
ref={c => this.messageComposerInput = c} ref={(c) => this.messageComposerInput = c}
key="controls_input" key="controls_input"
onResize={this.props.onResize} onResize={this.props.onResize}
room={this.props.room} room={this.props.room}
@ -316,13 +378,15 @@ export default class MessageComposer extends React.Component {
uploadButton, uploadButton,
hangupButton, hangupButton,
callButton, callButton,
videoCallButton videoCallButton,
showAppsButton,
hideAppsButton,
); );
} else { } else {
controls.push( controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error"> <div key="controls_error" className="mx_MessageComposer_noperm_error">
{ _t('You do not have permission to post to this room') } { _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 {style, blockType} = this.state.inputState;
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map( const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
name => { (name) => {
const active = style.includes(name) || blockType === name; const active = style.includes(name) || blockType === name;
const suffix = active ? '-o-n' : ''; const suffix = active ? '-o-n' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
@ -403,5 +467,8 @@ MessageComposer.propTypes = {
uploadFile: React.PropTypes.func.isRequired, uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects // 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,
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
{ {
"Add a widget": "Add a widget",
"af": "Afrikaans", "af": "Afrikaans",
"ar-ae": "Arabic (U.A.E.)", "ar-ae": "Arabic (U.A.E.)",
"ar-bh": "Arabic (Bahrain)", "ar-bh": "Arabic (Bahrain)",
@ -311,6 +312,7 @@
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
"had": "had", "had": "had",
"Hangup": "Hangup", "Hangup": "Hangup",
"Hide Apps": "Hide Apps",
"Hide read receipts": "Hide read receipts", "Hide read receipts": "Hide read receipts",
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
"Historical": "Historical", "Historical": "Historical",
@ -362,6 +364,7 @@
"Markdown is disabled": "Markdown is disabled", "Markdown is disabled": "Markdown is disabled",
"Markdown is enabled": "Markdown is enabled", "Markdown is enabled": "Markdown is enabled",
"matrix-react-sdk version:": "matrix-react-sdk version:", "matrix-react-sdk version:": "matrix-react-sdk version:",
"Matrix Apps": "Matrix Apps",
"Members only": "Members only", "Members only": "Members only",
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
"Missing room_id in request": "Missing room_id in request", "Missing room_id in request": "Missing room_id in request",
@ -464,6 +467,7 @@
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.", "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
"Settings": "Settings", "Settings": "Settings",
"Show Apps": "Show Apps",
"Show panel": "Show panel", "Show panel": "Show panel",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
"Signed Out": "Signed Out", "Signed Out": "Signed Out",