mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(visitors-list): Add to participant pane.
This commit is contained in:
@@ -909,6 +909,7 @@
|
||||
"visitorInQueue": " (waiting {{count}})",
|
||||
"visitorRequests": " (requests {{count}})",
|
||||
"visitors": "Viewers {{count}}",
|
||||
"visitorsList": "Viewers ({{count}})",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
},
|
||||
"search": "Search participants",
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -105,6 +105,7 @@
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
@@ -22298,6 +22299,16 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
@@ -41943,6 +41954,11 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A=="
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
|
||||
@@ -18,6 +18,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
|
||||
ROOM: 'room',
|
||||
SCREEN_SHARING: 'screen-sharing',
|
||||
SEND_GROUPCHAT: 'send-groupchat',
|
||||
LIST_VISITORS: 'list-visitors',
|
||||
SIP_INBOUND_CALL: 'sip-inbound-call',
|
||||
SIP_OUTBOUND_CALL: 'sip-outbound-call',
|
||||
TRANSCRIPTION: 'transcription'
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface IParticipantFeatures {
|
||||
'file-upload'?: boolean | string;
|
||||
'flip'?: boolean | string;
|
||||
'inbound-call'?: boolean | string;
|
||||
'list-visitors'?: boolean | string;
|
||||
'livestreaming'?: boolean | string;
|
||||
'lobby'?: boolean | string;
|
||||
'moderation'?: boolean | string;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { normalizeAccents } from '../../../base/util/strings.web';
|
||||
import { subscribeVisitorsList } from '../../../visitors/actions';
|
||||
import {
|
||||
getVisitorsCount,
|
||||
getVisitorsList,
|
||||
isVisitorsListEnabled,
|
||||
isVisitorsListSubscribed,
|
||||
shouldDisplayCurrentVisitorsList
|
||||
} from '../../../visitors/functions';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
|
||||
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
|
||||
/**
|
||||
* Props for the {@code CurrentVisitorsList} component.
|
||||
*/
|
||||
interface IProps {
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: theme.spacing(3),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
flexGrow: 1
|
||||
},
|
||||
heading: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.text02,
|
||||
flexShrink: 0
|
||||
},
|
||||
arrowContainer: {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '6px',
|
||||
marginLeft: theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
minHeight: '200px',
|
||||
maxHeight: '100%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the visitors list inside the participants pane.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React$Element<any>} The component.
|
||||
*/
|
||||
export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const visitors = useSelector(getVisitorsList);
|
||||
const featureEnabled = useSelector(isVisitorsListEnabled);
|
||||
const shouldDisplayList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const [ collapsed, setCollapsed ] = useState(true);
|
||||
const isSubscribed = useSelector(isVisitorsListSubscribed);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed(c => {
|
||||
const newCollapsed = !c;
|
||||
|
||||
if (featureEnabled && !newCollapsed && !isSubscribed) {
|
||||
dispatch(subscribeVisitorsList());
|
||||
}
|
||||
|
||||
return newCollapsed;
|
||||
});
|
||||
}, [ dispatch, isSubscribed, featureEnabled ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (featureEnabled && searchString) {
|
||||
setCollapsed(false);
|
||||
if (!isSubscribed) {
|
||||
dispatch(subscribeVisitorsList());
|
||||
}
|
||||
}
|
||||
}, [ searchString, dispatch, isSubscribed, featureEnabled ]);
|
||||
|
||||
if (!shouldDisplayList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = visitors.filter(v =>
|
||||
normalizeAccents(v.name).toLowerCase().includes(normalizeAccents(searchString).toLowerCase())
|
||||
);
|
||||
|
||||
// ListItem height is 56px including padding so the item size
|
||||
// for virtualization needs to match it exactly to avoid clipping.
|
||||
const itemSize = 56;
|
||||
|
||||
const Row = ({ index, style }: { index: number; style: any; }) => {
|
||||
const v = filtered[index];
|
||||
|
||||
return (
|
||||
<div style = { style }>
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { MEDIA_STATE.NONE }
|
||||
displayName = { v.name }
|
||||
participantID = { v.id }
|
||||
videoMediaState = { MEDIA_STATE.NONE } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const styles = {
|
||||
overflowX: 'hidden' as const,
|
||||
overflowY: 'auto' as const,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
className = { classes.heading }
|
||||
onClick = { toggleCollapsed }>
|
||||
<span>{ t('participantsPane.headings.visitorsList', { count: visitorsCount }) }</span>
|
||||
<span className = { classes.arrowContainer }>
|
||||
<Icon
|
||||
size = { 14 }
|
||||
src = { collapsed ? IconArrowDown : IconArrowUp } />
|
||||
</span>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className = { classes.listContainer }>
|
||||
<AutoSizer>
|
||||
{ ({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height = { Math.max(height, 200) }
|
||||
itemCount = { filtered.length }
|
||||
itemSize = { itemSize }
|
||||
style = { styles }
|
||||
width = { width }>
|
||||
{ Row }
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { findAncestorByClass } from '../../../base/ui/functions.web';
|
||||
import { isAddBreakoutRoomButtonVisible } from '../../../breakout-rooms/functions';
|
||||
import MuteEveryoneDialog from '../../../video-menu/components/web/MuteEveryoneDialog';
|
||||
import { shouldDisplayCurrentVisitorsList } from '../../../visitors/functions';
|
||||
import { close } from '../../actions.web';
|
||||
import {
|
||||
getParticipantsPaneOpen,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
|
||||
import { RoomList } from '../breakout-rooms/components/web/RoomList';
|
||||
|
||||
import CurrentVisitorsList from './CurrentVisitorsList';
|
||||
import { FooterContextMenu } from './FooterContextMenu';
|
||||
import LobbyParticipants from './LobbyParticipants';
|
||||
import MeetingParticipants from './MeetingParticipants';
|
||||
@@ -73,6 +75,8 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
padding: `0 ${participantsPaneTheme.panePadding}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none'
|
||||
@@ -129,6 +133,7 @@ const ParticipantsPane = () => {
|
||||
const paneOpen = useSelector(getParticipantsPaneOpen);
|
||||
const isBreakoutRoomsSupported = useSelector((state: IReduxState) => state['features/base/conference'])
|
||||
.conference?.getBreakoutRooms()?.isSupported();
|
||||
const showCurrentVisitorsList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const showAddRoomButton = useSelector(isAddBreakoutRoomButtonVisible);
|
||||
const showFooter = useSelector(isLocalParticipantModerator);
|
||||
const showMuteAllButton = useSelector(isMuteAllVisible);
|
||||
@@ -193,6 +198,7 @@ const ParticipantsPane = () => {
|
||||
setSearchString = { setSearchString } />
|
||||
{isBreakoutRoomsSupported && <RoomList searchString = { searchString } />}
|
||||
{showAddRoomButton && <AddBreakoutRoomButton />}
|
||||
{showCurrentVisitorsList && <CurrentVisitorsList searchString = { searchString } />}
|
||||
</div>
|
||||
{showFooter && (
|
||||
<div className = { classes.footer }>
|
||||
|
||||
@@ -68,3 +68,14 @@ export const SET_VISITOR_DEMOTE_ACTOR = 'SET_VISITOR_DEMOTE_ACTOR';
|
||||
* }
|
||||
*/
|
||||
export const SET_VISITORS_SUPPORTED = 'SET_VISITORS_SUPPORTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current visitors list.
|
||||
*/
|
||||
export const UPDATE_VISITORS_LIST = 'UPDATE_VISITORS_LIST';
|
||||
|
||||
/**
|
||||
* Action dispatched when the visitors list is expanded for the first time
|
||||
* and the client should subscribe for updates.
|
||||
*/
|
||||
export const SUBSCRIBE_VISITORS_LIST = 'SUBSCRIBE_VISITORS_LIST';
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
SUBSCRIBE_VISITORS_LIST,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import logger from './logger';
|
||||
@@ -216,6 +218,32 @@ export function updateVisitorsInQueueCount(count: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current list of visitors.
|
||||
*
|
||||
* @param {Array<Object>} visitors - The visitors list.
|
||||
* @returns {{
|
||||
* type: UPDATE_VISITORS_LIST,
|
||||
* }}
|
||||
*/
|
||||
export function updateVisitorsList(visitors: Array<{ id: string; name: string; }>) {
|
||||
return {
|
||||
type: UPDATE_VISITORS_LIST,
|
||||
visitors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the start of the visitors list websocket subscription.
|
||||
*
|
||||
* @returns {{ type: SUBSCRIBE_VISITORS_LIST }}
|
||||
*/
|
||||
export function subscribeVisitorsList() {
|
||||
return {
|
||||
type: SUBSCRIBE_VISITORS_LIST
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the overflow menu if opened.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
/**
|
||||
@@ -68,6 +70,26 @@ export function isVisitorsSupported(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].supported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current visitor list.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getVisitorsList(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].visitors ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the visitors list websocket subscription has been requested.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVisitorsListSubscribed(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].visitorsListSubscribed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is live.
|
||||
*
|
||||
@@ -89,3 +111,31 @@ export function isVisitorsLive(stateful: IStateful) {
|
||||
export function showVisitorsQueue(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the visitors list feature is enabled based on JWT and config.js.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} Whether the feature is allowed.
|
||||
*/
|
||||
export function isVisitorsListEnabled(state: IReduxState): boolean {
|
||||
const { visitors: visitorsConfig } = state['features/base/config'];
|
||||
|
||||
if (!visitorsConfig?.queueService) { // if the queue service is not configured, we can't retrieve the visitors list
|
||||
return false;
|
||||
}
|
||||
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.LIST_VISITORS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current visitors list should be displayed.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean} Whether the visitors list should be shown.
|
||||
*/
|
||||
export function shouldDisplayCurrentVisitorsList(stateful: IStateful): boolean {
|
||||
const state = toState(stateful);
|
||||
|
||||
return isVisitorsListEnabled(state) && getVisitorsCount(state) > 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from '../base/conference/actionTypes';
|
||||
@@ -35,7 +36,7 @@ import { INotificationProps } from '../notifications/types';
|
||||
import { open as openParticipantsPane } from '../participants-pane/actions';
|
||||
import { joinConference } from '../prejoin/actions';
|
||||
|
||||
import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import { SUBSCRIBE_VISITORS_LIST, UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import {
|
||||
approveRequest,
|
||||
clearPromotionRequest,
|
||||
@@ -45,10 +46,11 @@ import {
|
||||
setInVisitorsQueue,
|
||||
setVisitorDemoteActor,
|
||||
setVisitorsSupported,
|
||||
updateVisitorsInQueueCount
|
||||
updateVisitorsInQueueCount,
|
||||
updateVisitorsList
|
||||
} from './actions';
|
||||
import { JoinMeetingDialog } from './components';
|
||||
import { getPromotionRequests, getVisitorsInQueueCount } from './functions';
|
||||
import { getPromotionRequests, getVisitorsInQueueCount, isVisitorsListEnabled } from './functions';
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
@@ -135,6 +137,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
break;
|
||||
}
|
||||
case SUBSCRIBE_VISITORS_LIST: {
|
||||
if (isVisitorsListEnabled(getState()) && !WebsocketClient.getInstance().isActive()) {
|
||||
_subscribeVisitorsList(getState, dispatch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { data } = action;
|
||||
|
||||
@@ -314,6 +326,63 @@ function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch'])
|
||||
toState(stateful)['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
function _subscribeVisitorsList(getState: IStore['getState'], dispatch: IStore['dispatch']) {
|
||||
const state = getState();
|
||||
const { visitors: visitorsConfig } = state['features/base/config'];
|
||||
const conference = state['features/base/conference'].conference;
|
||||
const meetingId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const participantId = localParticipant?.id;
|
||||
|
||||
if (!visitorsConfig?.queueService || !meetingId || !participantId) {
|
||||
logger.warn(`Missing required data for visitors list subscription', ${JSON.stringify({
|
||||
queueService: visitorsConfig?.queueService,
|
||||
meetingId,
|
||||
participantId: participantId ? 'participantId present' : 'participantId missing'
|
||||
})}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queueEndpoint = `/secured/conference/visitors-list/queue/${meetingId}/${participantId}`;
|
||||
const topicEndpoint = `/secured/conference/visitors-list/topic/${meetingId}`;
|
||||
|
||||
logger.debug('Starting visitors list subscription');
|
||||
|
||||
WebsocketClient.getInstance()
|
||||
.connectVisitorsListWithInitial(
|
||||
`wss://${visitorsConfig.queueService}/visitors-list/websocket`,
|
||||
queueEndpoint,
|
||||
topicEndpoint,
|
||||
// Initial list callback - replace entire list
|
||||
initialVisitors => {
|
||||
const visitors = initialVisitors.map(v => ({ id: v.r, name: v.n }));
|
||||
|
||||
dispatch(updateVisitorsList(visitors));
|
||||
},
|
||||
// Delta updates callback - apply incremental changes
|
||||
updates => {
|
||||
let visitors = [ ...(getState()['features/visitors'].visitors ?? []) ];
|
||||
|
||||
updates.forEach(u => {
|
||||
if (u.s === 'j') {
|
||||
const index = visitors.findIndex(v => v.id === u.r);
|
||||
|
||||
if (index === -1) {
|
||||
visitors.push({ id: u.r, name: u.n });
|
||||
} else {
|
||||
visitors[index] = { id: u.r, name: u.n };
|
||||
}
|
||||
} else if (u.s === 'l') {
|
||||
visitors = visitors.filter(v => v.id !== u.r);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(updateVisitorsList(visitors));
|
||||
},
|
||||
getState()['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle the promotion notification.
|
||||
*
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
SUBSCRIBE_VISITORS_LIST,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
import { IPromotionRequest, IVisitor } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
count: 0,
|
||||
@@ -19,7 +21,9 @@ const DEFAULT_STATE = {
|
||||
inQueueCount: 0,
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: []
|
||||
promotionRequests: [],
|
||||
visitors: [] as IVisitor[],
|
||||
visitorsListSubscribed: false
|
||||
};
|
||||
|
||||
export interface IVisitorsState {
|
||||
@@ -30,6 +34,8 @@ export interface IVisitorsState {
|
||||
inQueueCount?: number;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
visitors: IVisitor[];
|
||||
visitorsListSubscribed: boolean;
|
||||
}
|
||||
ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_STATE, action): IVisitorsState => {
|
||||
switch (action.type) {
|
||||
@@ -90,6 +96,18 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
supported: action.value
|
||||
};
|
||||
}
|
||||
case SUBSCRIBE_VISITORS_LIST: {
|
||||
return {
|
||||
...state,
|
||||
visitorsListSubscribed: true
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_LIST: {
|
||||
return {
|
||||
...state,
|
||||
visitors: action.visitors
|
||||
};
|
||||
}
|
||||
case VISITOR_PROMOTION_REQUEST: {
|
||||
const currentRequests = state.promotionRequests || [];
|
||||
|
||||
|
||||
@@ -2,3 +2,8 @@ export interface IPromotionRequest {
|
||||
from: string;
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export interface IVisitor {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -147,4 +147,84 @@ export class WebsocketClient {
|
||||
get connectCount(): number {
|
||||
return this._connectCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the visitors list with initial queue subscription, then switches to topic deltas.
|
||||
*
|
||||
* @param {string} queueServiceURL - The service URL to use.
|
||||
* @param {string} queueEndpoint - The queue endpoint for initial list.
|
||||
* @param {string} topicEndpoint - The topic endpoint for deltas.
|
||||
* @param {Function} initialCallback - Callback executed with initial visitors list.
|
||||
* @param {Function} deltaCallback - Callback executed with delta updates.
|
||||
* @param {string} token - The token to be used for authorization.
|
||||
* @param {Function?} connectCallback - Callback executed when connected.
|
||||
* @returns {void}
|
||||
*/
|
||||
connectVisitorsListWithInitial(queueServiceURL: string,
|
||||
queueEndpoint: string,
|
||||
topicEndpoint: string,
|
||||
initialCallback: (visitors: Array<{ n: string; r: string; }>) => void,
|
||||
deltaCallback: (updates: Array<{ n: string; r: string; s: string; }>) => void,
|
||||
token: string | undefined,
|
||||
connectCallback?: () => void) {
|
||||
this.stompClient = new Client({
|
||||
brokerURL: queueServiceURL,
|
||||
forceBinaryWSFrames: true,
|
||||
appendMissingNULLonIncoming: true
|
||||
});
|
||||
|
||||
const errorConnecting = (error: any) => {
|
||||
logger.error(`Error connecting to ${queueServiceURL} ${JSON.stringify(error)}`);
|
||||
};
|
||||
|
||||
this.stompClient.onWebSocketError = errorConnecting;
|
||||
|
||||
this.stompClient.onStompError = frame => {
|
||||
logger.error('STOMP error received', frame);
|
||||
errorConnecting(frame.headers.message);
|
||||
};
|
||||
|
||||
if (token) {
|
||||
this.stompClient.connectHeaders = {
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
this.stompClient.onConnect = () => {
|
||||
if (!this.stompClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Connected to visitors list websocket');
|
||||
connectCallback?.();
|
||||
|
||||
// First subscribe to queue for initial list
|
||||
const queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
try {
|
||||
const visitors: Array<{ n: string; r: string; }> = JSON.parse(message.body);
|
||||
|
||||
logger.debug(`Received initial visitors list with ${visitors.length} visitors`);
|
||||
initialCallback(visitors);
|
||||
|
||||
// Unsubscribe from queue and subscribe to topic for deltas
|
||||
queueSubscription.unsubscribe();
|
||||
this.stompClient?.subscribe(topicEndpoint, deltaMessage => {
|
||||
try {
|
||||
const updates: Array<{ n: string; r: string; s: string; }> = JSON.parse(deltaMessage.body);
|
||||
|
||||
deltaCallback(updates);
|
||||
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing visitors delta response: ${deltaMessage}`, e);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing initial visitors response: ${message}`, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user