Skip to content

Commit 3a29bde

Browse files
authored
Merge pull request #1040 from layer5io/activity
Create useRoomActivity hook for get collaboration info
2 parents ef62aa8 + 07568b9 commit 3a29bde

File tree

9 files changed

+252
-11
lines changed

9 files changed

+252
-11
lines changed

src/constants/constants.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export const KEPPEL_GREEN_FILL = '#00B39F';
66
export const CARIBBEAN_GREEN_FILL = '#00D3A9';
77
export const DEFAULT_STROKE = '#000';
88
export const DEFAULT_STROKE_WIDTH = '2';
9-
export const CLOUD_URL = 'https://cloud.layer5.io';
109

1110
export const KANVAS_MODE = {
1211
DESIGNER: 'design',
@@ -28,3 +27,43 @@ export const RESOURCE_TYPE = {
2827
} as const;
2928

3029
export type ResourceType = (typeof RESOURCE_TYPE)[keyof typeof RESOURCE_TYPE];
30+
export interface ICEServer {
31+
urls: string;
32+
username?: string;
33+
credential?: string;
34+
}
35+
36+
/**
37+
* ICE server configuration for WebRTC connections
38+
*/
39+
export const ICE_SERVERS: ICEServer[] = [
40+
{
41+
urls: 'stun:stun.l.google.com:19302'
42+
},
43+
{
44+
urls: 'stun:global.stun.twilio.com:3478'
45+
},
46+
{
47+
urls: 'stun:openrelay.metered.ca:80'
48+
},
49+
{
50+
urls: 'turn:openrelay.metered.ca:80',
51+
username: 'openrelayproject',
52+
credential: 'openrelayproject'
53+
},
54+
{
55+
urls: 'turn:openrelay.metered.ca:443',
56+
username: 'openrelayproject',
57+
credential: 'openrelayproject'
58+
},
59+
{
60+
urls: 'turn:openrelay.metered.ca:443?transport=tcp',
61+
username: 'openrelayproject',
62+
credential: 'openrelayproject'
63+
}
64+
];
65+
66+
export const MESHERY_CLOUD_PROD = 'https://cloud.layer5.io';
67+
export const MESHERY_CLOUD_STAGING = 'staging-cloud.layer5.io';
68+
export const MESHERY_CLOUD_WS_PROD = 'cloud-ws.layer5.io';
69+
export const MESHERY_CLOUD_WS_STAGING = 'staging-cloud-ws.layer5.io:6543';

src/custom/CatalogDesignTable/AuthorCell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { Avatar, Box, Grid, Typography } from '../../base';
3-
import { CLOUD_URL } from '../../constants/constants';
3+
import { MESHERY_CLOUD_PROD } from '../../constants/constants';
44
import { PersonIcon } from '../../icons';
55
import { CustomTooltip } from '../CustomTooltip';
66

@@ -44,7 +44,7 @@ const AuthorCell: React.FC<AuthorCellProps> = ({
4444
alt={displayName}
4545
src={avatarUrl}
4646
onClick={() => {
47-
window.open(`${CLOUD_URL}/user/${userId}`, '_blank');
47+
window.open(`${MESHERY_CLOUD_PROD}/user/${userId}`, '_blank');
4848
}}
4949
>
5050
{!avatarUrl && <PersonIcon />}

src/custom/CatalogDetail/ChallengesSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { Link, ListItemIcon } from '../../base';
3-
import { CLOUD_URL } from '../../constants/constants';
3+
import { MESHERY_CLOUD_PROD } from '../../constants/constants';
44
import { ChallengesIcon } from '../../icons';
55
import { useTheme } from '../../theme';
66
import CollapsibleSection from './CollapsibleSection';
@@ -30,7 +30,7 @@ const ChallengesSection: React.FC<ChallengesSectionProps> = ({ filteredAcademyDa
3030

3131
const renderChallengeItem = (item: string, index: number) => (
3232
<Link
33-
href={`${CLOUD_URL}/academy/challenges/${slugify('' + item)}`}
33+
href={`${MESHERY_CLOUD_PROD}/academy/challenges/${slugify('' + item)}`}
3434
target="_blank"
3535
rel="noopener noreferrer"
3636
style={{ textDecoration: 'none', color: 'inherit' }}

src/custom/CatalogDetail/LearningSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useState } from 'react';
22
import { Link, ListItemIcon } from '../../base';
3-
import { CLOUD_URL } from '../../constants/constants';
3+
import { MESHERY_CLOUD_PROD } from '../../constants/constants';
44
import { LearningIcon } from '../../icons';
55
import { useTheme } from '../../theme';
66
import CollapsibleSection from './CollapsibleSection';
@@ -30,7 +30,7 @@ const LearningSection: React.FC<LearningSectionProps> = ({ filteredAcademyData }
3030

3131
const renderLearningItem = (item: string, index: number) => (
3232
<Link
33-
href={`${CLOUD_URL}/academy/learning-paths/${slugify('' + item)}`}
33+
href={`${MESHERY_CLOUD_PROD}/academy/learning-paths/${slugify('' + item)}`}
3434
target="_blank"
3535
rel="noopener noreferrer"
3636
style={{ textDecoration: 'none', color: 'inherit' }}

src/custom/CatalogDetail/UserInfo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Avatar } from '../../base';
2-
import { CLOUD_URL } from '../../constants/constants';
2+
import { MESHERY_CLOUD_PROD } from '../../constants/constants';
33
import { LockIcon, PublicIcon } from '../../icons';
44
import { getFormatDate } from '../../utils';
55
import { Pattern } from '../CustomCatalog/CustomCard';
@@ -44,7 +44,7 @@ const UserInfo: React.FC<UserInfoProps> = ({
4444
}}
4545
/>
4646
<RedirectLink
47-
href={`${CLOUD_URL}/user/${details?.user_id}`}
47+
href={`${MESHERY_CLOUD_PROD}/user/${details?.user_id}`}
4848
target="_blank"
4949
rel="noopener noreferrer"
5050
>

src/custom/UsersTable/UserTableAvatarInfo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Avatar, Box, Grid, Typography } from '../../base';
2-
import { CLOUD_URL } from '../../constants/constants';
2+
import { MESHERY_CLOUD_PROD } from '../../constants/constants';
33
import { PersonIcon } from '../../icons';
44
import { useTheme } from '../../theme';
55

@@ -18,7 +18,7 @@ const UserTableAvatarInfo: React.FC<UserTableAvatarInfoProps> = ({
1818
}): JSX.Element => {
1919
const theme = useTheme();
2020
const handleProfileClick = (): void => {
21-
window.open(`${CLOUD_URL}/user/${userId}`);
21+
window.open(`${MESHERY_CLOUD_PROD}/user/${userId}`);
2222
};
2323

2424
return (

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useRoomActivity';

src/hooks/useRoomActivity.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import {
3+
ICE_SERVERS,
4+
ICEServer,
5+
MESHERY_CLOUD_PROD,
6+
MESHERY_CLOUD_STAGING,
7+
MESHERY_CLOUD_WS_PROD,
8+
MESHERY_CLOUD_WS_STAGING
9+
} from '../constants/constants';
10+
11+
interface UserProfile {
12+
[key: string]: unknown;
13+
}
14+
15+
interface CollaborationConfigParams {
16+
provider_url: string;
17+
getUserProfile: () => Promise<{ data: UserProfile }>;
18+
getUserAccessToken: () => Promise<{ data: string }>;
19+
}
20+
21+
interface CollaborationConfig {
22+
signaling: string[];
23+
user: UserProfile;
24+
authToken: string;
25+
refreshAuthToken: () => Promise<string>;
26+
websocketCallbacks: string[];
27+
peerOpts: {
28+
config: {
29+
iceServers: ICEServer[];
30+
};
31+
};
32+
}
33+
34+
interface SubscribeToRoomsActivityMessage {
35+
type: string;
36+
}
37+
38+
interface UserMapChangeMessage {
39+
type: string;
40+
user_map?: UserMapping;
41+
}
42+
43+
interface UserMapping {
44+
[roomId: string]: {
45+
[userId: string]: unknown;
46+
};
47+
}
48+
49+
interface UseRoomActivityParams {
50+
provider_url: string;
51+
getUserProfile: () => Promise<{ data: UserProfile }>;
52+
getUserAccessToken: () => Promise<{ data: string }>;
53+
}
54+
55+
const SUBSCRIBE_TO_ROOMS_ACTIVITY_MSG: SubscribeToRoomsActivityMessage = {
56+
type: 'subscribe_to_rooms_activity'
57+
};
58+
const USER_MAP_CHANGE_MSG = 'user_map';
59+
60+
/**
61+
* Determines the appropriate websocket host based on the provider host
62+
* @param {string} providerHost - The provider host
63+
* @returns {string} - The websocket host
64+
*/
65+
const getWebsocketHost = (providerHost: string): string => {
66+
if (providerHost === MESHERY_CLOUD_PROD) {
67+
return MESHERY_CLOUD_WS_PROD;
68+
} else if (providerHost === MESHERY_CLOUD_STAGING) {
69+
return MESHERY_CLOUD_WS_STAGING;
70+
}
71+
72+
return providerHost;
73+
};
74+
75+
/**
76+
* Determines the appropriate WebSocket protocol based on current page protocol
77+
* @returns {string} - WebSocket protocol ('ws://' or 'wss://')
78+
*/
79+
export function getWebSocketProtocol(): string {
80+
const isSecure = window.location.protocol === 'https:'; // https only accepts secure websockets
81+
return isSecure ? 'wss://' : 'ws://';
82+
}
83+
84+
/**
85+
* Constructs a signaling URL from a provider URL
86+
* @param {string} providerUrl - The provider URL
87+
* @returns {string} - The signaling URL
88+
*/
89+
const getSignalingUrlFromProviderUrl = (providerUrl: string): string => {
90+
const parsedUrl = new URL(providerUrl);
91+
const websocketHost = getWebsocketHost(parsedUrl.host);
92+
const protocol = websocketHost === MESHERY_CLOUD_WS_PROD ? 'wss://' : getWebSocketProtocol();
93+
return `${protocol}${websocketHost}/collaboration`;
94+
};
95+
96+
/**
97+
* Gets collaboration configuration for WebRTC
98+
*/
99+
export const getCollaborationConfig = async ({
100+
provider_url,
101+
getUserProfile,
102+
getUserAccessToken
103+
}: CollaborationConfigParams): Promise<CollaborationConfig> => {
104+
const { data: userProfile } = await getUserProfile();
105+
106+
const websocketCallbacks = ['user_info', 'user_left', 'user_joined', 'user_map'];
107+
108+
// Fetch token after fetching provider and user so that it
109+
// gets refreshed if necessary.
110+
const { data: accessToken } = await getUserAccessToken();
111+
const refreshToken = async () => {
112+
return (await getUserAccessToken()).data;
113+
};
114+
115+
return {
116+
signaling: [getSignalingUrlFromProviderUrl(provider_url)],
117+
user: userProfile,
118+
authToken: accessToken,
119+
refreshAuthToken: refreshToken,
120+
websocketCallbacks,
121+
peerOpts: {
122+
config: {
123+
iceServers: ICE_SERVERS
124+
}
125+
}
126+
};
127+
};
128+
129+
/**
130+
* Subscribes to room activity via WebSocket
131+
*/
132+
const subscribeToRoomActivity = async (
133+
wsRef: React.MutableRefObject<WebSocket | null>,
134+
onUserMapChange: (userMap: UserMapping) => void,
135+
provider_url: string,
136+
getUserProfile: () => Promise<{ data: UserProfile }>,
137+
getUserAccessToken: () => Promise<{ data: string }>
138+
): Promise<void> => {
139+
const config = await getCollaborationConfig({
140+
provider_url,
141+
getUserProfile,
142+
getUserAccessToken
143+
});
144+
145+
// Create the websocket connection with proper headers
146+
const ws = new WebSocket(config.signaling[0], ['auth', config.authToken]);
147+
wsRef.current = ws;
148+
149+
ws.addEventListener('open', () => {
150+
console.log('[RoomActivity] connected to room activity');
151+
ws.send(JSON.stringify(SUBSCRIBE_TO_ROOMS_ACTIVITY_MSG));
152+
});
153+
154+
ws.addEventListener('message', (event: MessageEvent) => {
155+
const data = JSON.parse(event.data) as UserMapChangeMessage;
156+
if (data.type === USER_MAP_CHANGE_MSG && data.user_map) {
157+
onUserMapChange(data.user_map);
158+
}
159+
});
160+
161+
ws.addEventListener('close', () => {
162+
console.log('[RoomActivity] subscription to room activity closed');
163+
});
164+
165+
ws.addEventListener('error', (err: Event) => {
166+
console.error('[RoomActivity] error in room activity subscription', err);
167+
});
168+
};
169+
170+
/**
171+
* Hook to subscribe to and get room activity data
172+
*/
173+
export const useRoomActivity = ({
174+
provider_url,
175+
getUserProfile,
176+
getUserAccessToken
177+
}: UseRoomActivityParams): UserMapping => {
178+
const [allRoomsUserMapping, setAllRoomsUserMapping] = useState<UserMapping>({});
179+
const wsRef = useRef<WebSocket | null>(null);
180+
181+
useEffect(() => {
182+
subscribeToRoomActivity(
183+
wsRef,
184+
setAllRoomsUserMapping,
185+
provider_url,
186+
getUserProfile,
187+
getUserAccessToken
188+
);
189+
190+
const ws = wsRef.current;
191+
192+
return () => {
193+
if (ws) {
194+
ws.close();
195+
}
196+
};
197+
}, [provider_url, getUserProfile, getUserAccessToken]);
198+
199+
return allRoomsUserMapping;
200+
};

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './actors';
22
export * from './base';
33
export * from './colors';
44
export * from './custom';
5+
export * from './hooks';
56
export * from './icons';
67
export * from './redux-persist';
78
export * from './schemas';

0 commit comments

Comments
 (0)