bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
germanocaumo 9d059dc5e5 fix(whiteboard): avoid subscribing to annotations multiple times
The subscription to annotations sometimes could be ready and stopped before the component fully loads,
allowing it to be subscribed again (and then receiving all the annotations via websocket two times).

Moved the subscription to occur only after the base ones.
2022-10-04 09:35:06 +00:00

174 lines
6.0 KiB
JavaScript
Executable File

import { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import logger from '/imports/startup/client/logger';
import GroupChat from '/imports/api/group-chat';
import Annotations from '/imports/api/annotations';
import Users from '/imports/api/users';
import AnnotationsTextService from '/imports/ui/components/whiteboard/annotations/text/service';
import { Annotations as AnnotationsLocal } from '/imports/ui/components/whiteboard/service';
import {
localBreakoutsSync,
localBreakoutsHistorySync,
localGuestUsersSync,
localMeetingsSync,
localUsersSync,
} from '/client/collection-mirror-initializer';
import SubscriptionRegistry, { subscriptionReactivity } from '../../services/subscription-registry/subscriptionRegistry';
import { isChatEnabled } from '/imports/ui/services/features';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
const SUBSCRIPTIONS = [
'users', 'meetings', 'polls', 'presentations', 'slides', 'slide-positions', 'captions',
'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat',
'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'meeting-time-remaining',
'local-settings', 'users-typing', 'record-meetings', 'video-streams',
'connection-status', 'voice-call-states', 'external-video-meetings', 'breakouts', 'breakouts-history',
'pads', 'pads-sessions', 'pads-updates',
];
const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped';
const EVENT_NAME_SUBSCRIPTION_READY = 'bbb-group-chat-messages-subscriptions-ready';
let oldRole = '';
class Subscriptions extends Component {
componentDidUpdate() {
const { subscriptionsReady } = this.props;
if (subscriptionsReady) {
Session.set('subscriptionsReady', true);
const event = new Event(EVENT_NAME_SUBSCRIPTION_READY);
window.dispatchEvent(event);
Session.set('globalIgnoreDeletes', false);
}
}
render() {
const { children } = this.props;
return children;
}
}
export default withTracker(() => {
const { credentials } = Auth;
const { meetingId, requesterUserId } = credentials;
const userWillAuth = Session.get('userWillAuth');
// This if exist because when a unauth user try to subscribe to a publisher
// it returns a empty collection to the subscription
// and not rerun when the user is authenticated
if (userWillAuth) return {};
if (Session.get('codeError')) {
return {
subscriptionsReady: true,
};
}
const subscriptionErrorHandler = {
onError: (error) => {
logger.error({
logCode: 'startup_client_subscription_error',
extraInfo: { error },
}, 'Error while subscribing to collections');
Session.set('codeError', error.error);
},
};
const currentUser = Users.findOne({ intId: requesterUserId }, { fields: { role: 1 } });
let subscriptionsHandlers = SUBSCRIPTIONS.map((name) => {
let subscriptionHandlers = subscriptionErrorHandler;
if ((!TYPING_INDICATOR_ENABLED && name.indexOf('typing') !== -1)
|| (!isChatEnabled() && name.indexOf('chat') !== -1)) return null;
if (name === 'users') {
subscriptionHandlers = {
...subscriptionHandlers,
onStop: () => {
const event = new Event(EVENT_NAME);
window.dispatchEvent(event);
},
};
}
return SubscriptionRegistry.createSubscription(name, subscriptionHandlers);
});
if (currentUser && (oldRole !== currentUser?.role)) {
// stop subscription from the client-side as the server-side only watch moderators
if (oldRole === 'VIEWER' && currentUser?.role === 'MODERATOR') {
// let this withTracker re-execute when a subscription is stopped
subscriptionReactivity.depend();
localBreakoutsSync.setIgnoreDeletes(true);
localBreakoutsHistorySync.setIgnoreDeletes(true);
localGuestUsersSync.setIgnoreDeletes(true);
localMeetingsSync.setIgnoreDeletes(true);
localUsersSync.setIgnoreDeletes(true);
// Prevent data being removed by subscription stop
// stop role dependent subscriptions
[
SubscriptionRegistry.getSubscription('meetings'),
SubscriptionRegistry.getSubscription('users'),
SubscriptionRegistry.getSubscription('breakouts'),
SubscriptionRegistry.getSubscription('breakouts-history'),
SubscriptionRegistry.getSubscription('guestUser'),
].forEach((item) => {
if (item) item.stop();
});
}
oldRole = currentUser?.role;
}
subscriptionsHandlers = subscriptionsHandlers.filter(obj => obj);
const ready = subscriptionsHandlers.every(handler => handler.ready());
let groupChatMessageHandler = {};
if (isChatEnabled() && ready) {
const chatsCount = GroupChat.find({
$or: [
{
meetingId,
access: PUBLIC_CHAT_TYPE,
chatId: { $ne: PUBLIC_GROUP_CHAT_ID },
},
{ meetingId, users: { $all: [requesterUserId] } },
],
}).count();
const subHandler = {
...subscriptionErrorHandler,
};
groupChatMessageHandler = Meteor.subscribe('group-chat-msg', chatsCount, subHandler);
}
// TODO: Refactor all the late subscribers
let usersPersistentDataHandler = {};
if (ready) {
usersPersistentDataHandler = Meteor.subscribe('users-persistent-data');
const annotationsHandler = Meteor.subscribe('annotations', {
onReady: () => {
const activeTextShapeId = AnnotationsTextService.activeTextShapeId();
AnnotationsLocal.remove({ id: { $ne: `${activeTextShapeId}-fake` } });
Annotations.find({ id: { $ne: activeTextShapeId } }, { reactive: false }).forEach((a) => {
try {
AnnotationsLocal.insert(a);
} catch (e) {
// TODO
}
});
annotationsHandler.stop();
},
...subscriptionErrorHandler,
});
}
return {
subscriptionsReady: ready,
subscriptionsHandlers,
};
})(Subscriptions);