mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-17 14:05:04 +08:00
Merge pull request #1152 from matrix-org/rxl881/apps
Add support for apps
This commit is contained in:
commit
d61525e420
@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
@ -109,6 +110,76 @@ Example:
|
||||
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
|
||||
--------------------------------
|
||||
@ -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) {
|
||||
if (typeof status !== 'string') {
|
||||
throw new Error('Plumbing state status should be a string');
|
||||
@ -367,7 +516,7 @@ const onMessage = function(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting join rules does not require userId
|
||||
// These APIs don't require userId
|
||||
if (event.data.action === "join_rules_state") {
|
||||
getJoinRules(event, roomId);
|
||||
return;
|
||||
@ -377,6 +526,12 @@ const onMessage = function(event) {
|
||||
} else if (event.data.action === "get_membership_count") {
|
||||
getMembershipCount(event, roomId);
|
||||
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) {
|
||||
|
@ -30,11 +30,17 @@ export default {
|
||||
id: 'rich_text_editor',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "-",
|
||||
id: 'matrix_apps',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
|
||||
// horrible but it works. The locality makes this somewhat more palatable.
|
||||
doTranslations: function() {
|
||||
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
|
||||
this.LABS_FEATURES[1].name = _t("Matrix Apps");
|
||||
},
|
||||
|
||||
loadProfileInfo: function() {
|
||||
|
@ -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
|
||||
|
102
src/components/views/elements/AppTile.js
Normal file
102
src/components/views/elements/AppTile.js
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AppTile',
|
||||
|
||||
propTypes: {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
url: React.PropTypes.string.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
url: "",
|
||||
};
|
||||
},
|
||||
|
||||
_onEditClick: function() {
|
||||
console.log("Edit widget %s", this.props.id);
|
||||
},
|
||||
|
||||
_onDeleteClick: function() {
|
||||
console.log("Delete widget %s", this.props.id);
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
if (!appsStateEvents) {
|
||||
return;
|
||||
}
|
||||
const appsStateEvent = appsStateEvents.getContent();
|
||||
if (appsStateEvent[this.props.id]) {
|
||||
delete appsStateEvent[this.props.id];
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
appsStateEvent,
|
||||
'',
|
||||
).then(() => {
|
||||
console.log('Deleted widget');
|
||||
}, (e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
formatAppTileName: function() {
|
||||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className="mx_AppTileMenuBar">
|
||||
{this.formatAppTileName()}
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{/* Edit widget */}
|
||||
{/* <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8" alt="Edit"
|
||||
onClick={this._onEditClick}
|
||||
/> */}
|
||||
|
||||
{/* Delete widget */}
|
||||
<img src="img/cancel.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
|
||||
width="8" height="8" alt={_t("Cancel")}
|
||||
onClick={this._onDeleteClick}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx_AppTileBody">
|
||||
<iframe ref="appFrame" src={this.props.url} allowFullScreen="true"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
223
src/components/views/rooms/AppsDrawer.js
Normal file
223
src/components/views/rooms/AppsDrawer.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -28,6 +30,8 @@ module.exports = React.createClass({
|
||||
propTypes: {
|
||||
// js-sdk room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
showApps: React.PropTypes.bool,
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: React.PropTypes.object,
|
||||
@ -70,10 +74,10 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var CallView = sdk.getComponent("voip.CallView");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const CallView = sdk.getComponent("voip.CallView");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var fileDropTarget = null;
|
||||
let fileDropTarget = null;
|
||||
if (this.props.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
@ -87,14 +91,13 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var conferenceCallNotification = null;
|
||||
let conferenceCallNotification = null;
|
||||
if (this.props.displayConfCallNotification) {
|
||||
let supportedText = '';
|
||||
let joinNode;
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
supportedText = _t(" (unsupported)");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
joinNode = (<span>
|
||||
{_tJsx(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
@ -105,7 +108,6 @@ module.exports = React.createClass({
|
||||
]
|
||||
)}
|
||||
</span>);
|
||||
|
||||
}
|
||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
||||
@ -118,7 +120,7 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var callView = (
|
||||
const callView = (
|
||||
<CallView ref="callView" room={this.props.room}
|
||||
ConferenceHandler={this.props.conferenceHandler}
|
||||
onResize={this.props.onResize}
|
||||
@ -126,8 +128,17 @@ module.exports = React.createClass({
|
||||
/>
|
||||
);
|
||||
|
||||
let appsDrawer = null;
|
||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
|
||||
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ conferenceCallNotification }
|
||||
|
@ -13,16 +13,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
@ -32,6 +31,8 @@ export default class MessageComposer extends React.Component {
|
||||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
@ -57,7 +58,6 @@ export default class MessageComposer extends React.Component {
|
||||
},
|
||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -127,7 +127,7 @@ export default class MessageComposer extends React.Component {
|
||||
if(shouldUpload) {
|
||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||
if (files) {
|
||||
for(var i=0; i<files.length; i++) {
|
||||
for(let i=0; i<files.length; i++) {
|
||||
this.props.uploadFile(files[i]);
|
||||
}
|
||||
}
|
||||
@ -139,7 +139,7 @@ export default class MessageComposer extends React.Component {
|
||||
}
|
||||
|
||||
onHangupClick() {
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
const call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
if (!call) {
|
||||
return;
|
||||
@ -152,20 +152,68 @@ export default class MessageComposer extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// _startCallApp(isAudioConf) {
|
||||
// dis.dispatch({
|
||||
// action: 'appsDrawer',
|
||||
// show: true,
|
||||
// });
|
||||
|
||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
// let appsStateEvent = {};
|
||||
// if (appsStateEvents) {
|
||||
// appsStateEvent = appsStateEvents.getContent();
|
||||
// }
|
||||
// if (!appsStateEvent.videoConf) {
|
||||
// appsStateEvent.videoConf = {
|
||||
// type: 'jitsi',
|
||||
// // FIXME -- This should not be localhost
|
||||
// url: 'http://localhost:8000/jitsi.html',
|
||||
// data: {
|
||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||
// isAudioConf: isAudioConf,
|
||||
// },
|
||||
// };
|
||||
// MatrixClientPeg.get().sendStateEvent(
|
||||
// this.props.room.roomId,
|
||||
// 'im.vector.modular.widgets',
|
||||
// appsStateEvent,
|
||||
// '',
|
||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||
// }
|
||||
// }
|
||||
|
||||
onCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(false);
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: 'voice',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onShowAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
|
||||
onHideAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
@ -216,19 +264,19 @@ export default class MessageComposer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
||||
|
||||
var controls = [];
|
||||
const controls = [];
|
||||
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
@ -247,16 +295,15 @@ export default class MessageComposer extends React.Component {
|
||||
controls.push(
|
||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||
alt={e2eTitle} title={e2eTitle}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
var callButton, videoCallButton, hangupButton;
|
||||
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||
@ -267,14 +314,29 @@ export default class MessageComposer extends React.Component {
|
||||
</div>;
|
||||
}
|
||||
|
||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
// Apps
|
||||
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||
if (this.props.showApps) {
|
||||
hideAppsButton =
|
||||
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
} else {
|
||||
showAppsButton =
|
||||
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
MatrixClientPeg.get().credentials.userId);
|
||||
|
||||
if (canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
var uploadButton = (
|
||||
const uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||
@ -300,7 +362,7 @@ export default class MessageComposer extends React.Component {
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
@ -316,13 +378,15 @@ export default class MessageComposer extends React.Component {
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
videoCallButton
|
||||
videoCallButton,
|
||||
showAppsButton,
|
||||
hideAppsButton,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
@ -340,7 +404,7 @@ export default class MessageComposer extends React.Component {
|
||||
|
||||
const {style, blockType} = this.state.inputState;
|
||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||
name => {
|
||||
(name) => {
|
||||
const active = style.includes(name) || blockType === name;
|
||||
const suffix = active ? '-o-n' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
@ -403,5 +467,8 @@ MessageComposer.propTypes = {
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number
|
||||
opacity: React.PropTypes.number,
|
||||
|
||||
// string representing the current room app drawer state
|
||||
showApps: React.PropTypes.bool,
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
{
|
||||
"Add a widget": "Add a widget",
|
||||
"af": "Afrikaans",
|
||||
"ar-ae": "Arabic (U.A.E.)",
|
||||
"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.",
|
||||
"had": "had",
|
||||
"Hangup": "Hangup",
|
||||
"Hide Apps": "Hide Apps",
|
||||
"Hide read receipts": "Hide read receipts",
|
||||
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
||||
"Historical": "Historical",
|
||||
@ -362,6 +364,7 @@
|
||||
"Markdown is disabled": "Markdown is disabled",
|
||||
"Markdown is enabled": "Markdown is enabled",
|
||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
||||
"Matrix Apps": "Matrix Apps",
|
||||
"Members only": "Members only",
|
||||
"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",
|
||||
@ -464,6 +467,7 @@
|
||||
"%(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.",
|
||||
"Settings": "Settings",
|
||||
"Show Apps": "Show Apps",
|
||||
"Show panel": "Show panel",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||
"Signed Out": "Signed Out",
|
||||
|
Loading…
Reference in New Issue
Block a user