mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	Merge pull request #2035 from matrix-org/dbkr/widget_echo
Improve UX for Jitsi by adding local echo for widgets
This commit is contained in:
		
						commit
						10e4a4f288
					
				@ -62,6 +62,7 @@ import dis from './dispatcher';
 | 
			
		||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
 | 
			
		||||
import SettingsStore from "./settings/SettingsStore";
 | 
			
		||||
import WidgetUtils from './utils/WidgetUtils';
 | 
			
		||||
import WidgetEchoStore from './stores/WidgetEchoStore';
 | 
			
		||||
import ScalarAuthClient from './ScalarAuthClient';
 | 
			
		||||
 | 
			
		||||
global.mxCalls = {
 | 
			
		||||
@ -431,12 +432,19 @@ async function _startCallApp(roomId, type) {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const room = MatrixClientPeg.get().getRoom(roomId);
 | 
			
		||||
    if (!room) {
 | 
			
		||||
        console.error("Attempted to start conference call widget in unknown room: " + roomId);
 | 
			
		||||
    const currentRoomWidgets = WidgetUtils.getRoomWidgets(room);
 | 
			
		||||
 | 
			
		||||
    if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, 'jitsi')) {
 | 
			
		||||
        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
			
		||||
 | 
			
		||||
        Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
 | 
			
		||||
            title: _t('Call in Progress'),
 | 
			
		||||
            description: _t('A call is currently being placed!'),
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const currentJitsiWidgets = WidgetUtils.getRoomWidgets(room).filter((ev) => {
 | 
			
		||||
    const currentJitsiWidgets = currentRoomWidgets.filter((ev) => {
 | 
			
		||||
        return ev.getContent().type === 'jitsi';
 | 
			
		||||
    });
 | 
			
		||||
    if (currentJitsiWidgets.length > 0) {
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
 | 
			
		||||
 | 
			
		||||
import RoomViewStore from '../../stores/RoomViewStore';
 | 
			
		||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
 | 
			
		||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
 | 
			
		||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
 | 
			
		||||
import WidgetUtils from '../../utils/WidgetUtils';
 | 
			
		||||
 | 
			
		||||
@ -153,6 +154,8 @@ module.exports = React.createClass({
 | 
			
		||||
        // Start listening for RoomViewStore updates
 | 
			
		||||
        this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
 | 
			
		||||
        this._onRoomViewStoreUpdate(true);
 | 
			
		||||
 | 
			
		||||
        WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onRoomViewStoreUpdate: function(initial) {
 | 
			
		||||
@ -243,6 +246,12 @@ module.exports = React.createClass({
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onWidgetEchoStoreUpdate: function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            showApps: this._shouldShowApps(this.state.room),
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _setupRoom: function(room, roomId, joining, shouldPeek) {
 | 
			
		||||
        // if this is an unknown room then we're in one of three states:
 | 
			
		||||
        // - This is a room we can peek into (search engine) (we can /peek)
 | 
			
		||||
@ -319,7 +328,9 @@ module.exports = React.createClass({
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return WidgetUtils.getRoomWidgets(room).length > 0;
 | 
			
		||||
        const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
 | 
			
		||||
 | 
			
		||||
        return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    componentDidMount: function() {
 | 
			
		||||
@ -414,6 +425,8 @@ module.exports = React.createClass({
 | 
			
		||||
            this._roomStoreToken.remove();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate);
 | 
			
		||||
 | 
			
		||||
        // cancel any pending calls to the rate_limited_funcs
 | 
			
		||||
        this._updateRoomMembers.cancelPendingCall();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -325,6 +325,12 @@ export default class AppTile extends React.Component {
 | 
			
		||||
                            this.props.id,
 | 
			
		||||
                        ).catch((e) => {
 | 
			
		||||
                            console.error('Failed to delete widget', e);
 | 
			
		||||
                            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
			
		||||
 | 
			
		||||
                            Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
 | 
			
		||||
                                title: _t('Failed to remove widget'),
 | 
			
		||||
                                description: _t('An error ocurred whilst trying to remove the widget from the room'),
 | 
			
		||||
                            });
 | 
			
		||||
                        }).finally(() => {
 | 
			
		||||
                            this.setState({deleting: false});
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
 | 
			
		||||
import ScalarMessaging from '../../../ScalarMessaging';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
import WidgetUtils from '../../../utils/WidgetUtils';
 | 
			
		||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
 | 
			
		||||
 | 
			
		||||
// The maximum number of widgets that can be added in a room
 | 
			
		||||
const MAX_WIDGETS = 2;
 | 
			
		||||
@ -57,6 +58,7 @@ module.exports = React.createClass({
 | 
			
		||||
    componentWillMount: function() {
 | 
			
		||||
        ScalarMessaging.startListening();
 | 
			
		||||
        MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
 | 
			
		||||
        WidgetEchoStore.on('update', this._updateApps);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    componentDidMount: function() {
 | 
			
		||||
@ -82,6 +84,7 @@ module.exports = React.createClass({
 | 
			
		||||
        if (MatrixClientPeg.get()) {
 | 
			
		||||
            MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
 | 
			
		||||
        }
 | 
			
		||||
        WidgetEchoStore.removeListener('update', this._updateApps);
 | 
			
		||||
        dis.unregister(this.dispatcherRef);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -114,8 +117,11 @@ module.exports = React.createClass({
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _getApps: function() {
 | 
			
		||||
        return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => {
 | 
			
		||||
            return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender, this.props.room.roomId);
 | 
			
		||||
        const widgets = WidgetEchoStore.getEchoedRoomWidgets(
 | 
			
		||||
            this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
 | 
			
		||||
        );
 | 
			
		||||
        return widgets.map((ev) => {
 | 
			
		||||
            return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -200,10 +206,22 @@ module.exports = React.createClass({
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let spinner;
 | 
			
		||||
        if (
 | 
			
		||||
            apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
 | 
			
		||||
                this.props.room.roomId,
 | 
			
		||||
                WidgetUtils.getRoomWidgets(this.props.room),
 | 
			
		||||
            )
 | 
			
		||||
        ) {
 | 
			
		||||
            const Loader = sdk.getComponent("elements.Spinner");
 | 
			
		||||
            spinner = <Loader />;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
 | 
			
		||||
                <div id='apps' className='mx_AppsContainer'>
 | 
			
		||||
                    { apps }
 | 
			
		||||
                    { spinner }
 | 
			
		||||
                </div>
 | 
			
		||||
                { this._canUserModify() && addWidget }
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,7 @@
 | 
			
		||||
    "Could not connect to the integration server": "Could not connect to the integration server",
 | 
			
		||||
    "A conference call could not be started because the intgrations server is not available": "A conference call could not be started because the intgrations server is not available",
 | 
			
		||||
    "Call in Progress": "Call in Progress",
 | 
			
		||||
    "A call is currently being placed!": "A call is currently being placed!",
 | 
			
		||||
    "A call is already in progress!": "A call is already in progress!",
 | 
			
		||||
    "Permission Required": "Permission Required",
 | 
			
		||||
    "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
 | 
			
		||||
@ -695,6 +696,8 @@
 | 
			
		||||
    "Delete Widget": "Delete Widget",
 | 
			
		||||
    "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
 | 
			
		||||
    "Delete widget": "Delete widget",
 | 
			
		||||
    "Failed to remove widget": "Failed to remove widget",
 | 
			
		||||
    "An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
 | 
			
		||||
    "Revoke widget access": "Revoke widget access",
 | 
			
		||||
    "Minimize apps": "Minimize apps",
 | 
			
		||||
    "Reload widget": "Reload widget",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2017 Vector Creations Ltd
 | 
			
		||||
Copyright 2017 New Vector Ltd
 | 
			
		||||
Copyright 2017, 2018 New Vector Ltd
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										108
									
								
								src/stores/WidgetEchoStore.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/stores/WidgetEchoStore.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,108 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2018 New Vector 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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'events';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Acts as a place to get & set widget state, storing local echo state and
 | 
			
		||||
 * proxying through state from the js-sdk.
 | 
			
		||||
 */
 | 
			
		||||
class WidgetEchoStore extends EventEmitter {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this._roomWidgetEcho = {
 | 
			
		||||
            // Map as below. Object is the content of the widget state event,
 | 
			
		||||
            // so for widgets that have been deleted locally, the object is empty.
 | 
			
		||||
            // roomId: {
 | 
			
		||||
            //     widgetId: [object]
 | 
			
		||||
            // }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the widgets for a room, substracting those that are pending deletion.
 | 
			
		||||
     * Widgets that are pending addition are not included, since widgets are
 | 
			
		||||
     * represted as MatrixEvents, so to do this we'd have to create fake MatrixEvents,
 | 
			
		||||
     * and we don't really need the actual widget events anyway since we just want to
 | 
			
		||||
     * show a spinner / prevent widgets being added twice.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {Room} roomId The ID of the room to get widgets for
 | 
			
		||||
     * @param {MatrixEvent[]} currentRoomWidgets Current widgets for the room
 | 
			
		||||
     * @returns {MatrixEvent[]} List of widgets in the room, minus any pending removal
 | 
			
		||||
     */
 | 
			
		||||
    getEchoedRoomWidgets(roomId, currentRoomWidgets) {
 | 
			
		||||
        const echoedWidgets = [];
 | 
			
		||||
 | 
			
		||||
        const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]);
 | 
			
		||||
 | 
			
		||||
        for (const w of currentRoomWidgets) {
 | 
			
		||||
            const widgetId = w.getStateKey();
 | 
			
		||||
            // If there's no echo, or the echo still has a widget present, show the *old* widget
 | 
			
		||||
            // we don't include widgets that have changed for the same reason we don't include new ones,
 | 
			
		||||
            // ie. we'd need to fake matrix events to do so and therte's currently no need.
 | 
			
		||||
            if (!roomEchoState[widgetId] || Object.keys(roomEchoState[widgetId]).length !== 0) {
 | 
			
		||||
                echoedWidgets.push(w);
 | 
			
		||||
            }
 | 
			
		||||
            delete roomEchoState[widgetId];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return echoedWidgets;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type) {
 | 
			
		||||
        const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]);
 | 
			
		||||
 | 
			
		||||
        // any widget IDs that are already in the room are not pending, so
 | 
			
		||||
        // echoes for them don't count as pending.
 | 
			
		||||
        for (const w of currentRoomWidgets) {
 | 
			
		||||
            const widgetId = w.getStateKey();
 | 
			
		||||
            delete roomEchoState[widgetId];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if there's anything left then there are pending widgets.
 | 
			
		||||
        if (type === undefined) {
 | 
			
		||||
            return Object.keys(roomEchoState).length > 0;
 | 
			
		||||
        } else {
 | 
			
		||||
            return Object.values(roomEchoState).some((widget) => {
 | 
			
		||||
                return widget.type === type;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    roomHasPendingWidgets(roomId, currentRoomWidgets) {
 | 
			
		||||
        return this.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setRoomWidgetEcho(roomId, widgetId, state) {
 | 
			
		||||
        if (this._roomWidgetEcho[roomId] === undefined) this._roomWidgetEcho[roomId] = {};
 | 
			
		||||
 | 
			
		||||
        this._roomWidgetEcho[roomId][widgetId] = state;
 | 
			
		||||
        this.emit('update');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeRoomWidgetEcho(roomId, widgetId) {
 | 
			
		||||
        delete this._roomWidgetEcho[roomId][widgetId];
 | 
			
		||||
        if (Object.keys(this._roomWidgetEcho[roomId]).length === 0) delete this._roomWidgetEcho[roomId];
 | 
			
		||||
        this.emit('update');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let singletonWidgetEchoStore = null;
 | 
			
		||||
if (!singletonWidgetEchoStore) {
 | 
			
		||||
    singletonWidgetEchoStore = new WidgetEchoStore();
 | 
			
		||||
}
 | 
			
		||||
module.exports = singletonWidgetEchoStore;
 | 
			
		||||
@ -19,6 +19,11 @@ import MatrixClientPeg from '../MatrixClientPeg';
 | 
			
		||||
import SdkConfig from "../SdkConfig";
 | 
			
		||||
import dis from '../dispatcher';
 | 
			
		||||
import * as url from "url";
 | 
			
		||||
import WidgetEchoStore from '../stores/WidgetEchoStore';
 | 
			
		||||
 | 
			
		||||
// How long we wait for the state event echo to come back from the server
 | 
			
		||||
// before waitFor[Room/User]Widget rejects its promise
 | 
			
		||||
const WIDGET_WAIT_TIME = 20000;
 | 
			
		||||
import SettingsStore from "../settings/SettingsStore";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -155,7 +160,7 @@ export default class WidgetUtils {
 | 
			
		||||
            const timerId = setTimeout(() => {
 | 
			
		||||
                MatrixClientPeg.get().removeListener('accountData', onAccountData);
 | 
			
		||||
                reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
 | 
			
		||||
            }, 10000);
 | 
			
		||||
            }, WIDGET_WAIT_TIME);
 | 
			
		||||
            MatrixClientPeg.get().on('accountData', onAccountData);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@ -208,7 +213,7 @@ export default class WidgetUtils {
 | 
			
		||||
            const timerId = setTimeout(() => {
 | 
			
		||||
                MatrixClientPeg.get().removeListener('RoomState.events', onRoomStateEvents);
 | 
			
		||||
                reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
 | 
			
		||||
            }, 10000);
 | 
			
		||||
            }, WIDGET_WAIT_TIME);
 | 
			
		||||
            MatrixClientPeg.get().on('RoomState.events', onRoomStateEvents);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@ -271,11 +276,15 @@ export default class WidgetUtils {
 | 
			
		||||
            content = {};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content);
 | 
			
		||||
 | 
			
		||||
        const client = MatrixClientPeg.get();
 | 
			
		||||
        // TODO - Room widgets need to be moved to 'm.widget' state events
 | 
			
		||||
        // https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
 | 
			
		||||
        return client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).then(() => {
 | 
			
		||||
            return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget);
 | 
			
		||||
        }).finally(() => {
 | 
			
		||||
            WidgetEchoStore.removeRoomWidgetEcho(roomId, widgetId);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user