(Date.now());
+
+ const restartCollaboration = () => {
+ setCollaborationStartTime(Date.now());
+ };
+
+ useEffect(() => {
+ if (!connecting && collaborating) {
+ setCollaborationStoppedNoticeOpen(false);
+ }
+ }, [connecting, collaborating]);
+
useEffect(() => {
- if (excalidrawApi && whiteboard?.id) {
+ if (excalidrawApi && whiteboard?.id && collaborationStartTime !== null) {
return initializeCollab({
excalidrawApi,
roomId: whiteboard.id,
});
}
- }, [excalidrawApi, whiteboard?.id]);
+ }, [excalidrawApi, whiteboard?.id, collaborationStartTime]);
const handleInitializeApi = useCallback(
(excalidrawApi: ExcalidrawImperativeAPI) => {
@@ -170,29 +180,68 @@ const CollaborativeExcalidrawWrapper = ({
[actions.onInitApi]
);
+ const [collaborationStoppedNoticeOpen, setCollaborationStoppedNoticeOpen] = useState(false);
+
+ const { t } = useTranslation();
+
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
+
+ useEffect(() => {
+ const handleOnlineChange = () => setIsOnline(navigator.onLine);
+ window.addEventListener('online', handleOnlineChange);
+ window.addEventListener('offline', handleOnlineChange);
+ setIsOnline(navigator.onLine);
+ return () => {
+ window.removeEventListener('online', handleOnlineChange);
+ window.removeEventListener('offline', handleOnlineChange);
+ };
+ }, []);
+
return (
-
- {whiteboard && (
- {
- return ;
- }}*/
- {...restOptions}
- />
- )}
-
+ <>
+
+ {whiteboard && (
+ {
+ return ;
+ }}*/
+ {...restOptions}
+ />
+ )}
+
+
+ >
);
};
diff --git a/src/domain/common/whiteboard/excalidraw/collab/Collab.ts b/src/domain/common/whiteboard/excalidraw/collab/Collab.ts
index 71cc8a81b0..dcae184fbf 100644
--- a/src/domain/common/whiteboard/excalidraw/collab/Collab.ts
+++ b/src/domain/common/whiteboard/excalidraw/collab/Collab.ts
@@ -5,11 +5,9 @@ import {
CURSOR_SYNC_TIMEOUT,
EVENT,
IDLE_THRESHOLD,
- INITIAL_SCENE_UPDATE_TIMEOUT,
SYNC_FULL_SCENE_INTERVAL_MS,
WS_SCENE_EVENT_TYPES,
} from './excalidrawAppConstants';
-import { ImportedDataState } from '@alkemio/excalidraw/types/data/types';
import { ExcalidrawElement } from '@alkemio/excalidraw/types/element/types';
import { getSceneVersion, newElementWith, restoreElements } from '@alkemio/excalidraw';
import { isImageElement, UserIdleState } from './utils';
@@ -24,17 +22,6 @@ interface CollabState {
activeRoomLink: string;
}
-type CollabInstance = InstanceType;
-
-export interface CollabAPI {
- /** function so that we can access the latest value from stale callbacks */
- onPointerUpdate: CollabInstance['onPointerUpdate'];
- startCollaboration: CollabInstance['startCollaboration'];
- stopCollaboration: CollabInstance['stopCollaboration'];
- syncScene: CollabInstance['syncScene'];
- notifySavedToDatabase: () => void; // Notify rest of the members in the room that I have saved the whiteboard
-}
-
export interface CollabProps {
excalidrawApi: ExcalidrawImperativeAPI;
username: string;
@@ -77,16 +64,8 @@ class Collab {
this.onSavedToDatabase = props.onSavedToDatabase;
}
- init(): CollabAPI {
+ init() {
window.addEventListener(EVENT.UNLOAD, this.onUnload);
-
- return {
- onPointerUpdate: this.onPointerUpdate,
- startCollaboration: this.startCollaboration,
- syncScene: this.syncScene,
- stopCollaboration: this.stopCollaboration,
- notifySavedToDatabase: this.notifySavedToDatabase,
- };
}
destroy() {
@@ -117,10 +96,6 @@ class Collab {
stopCollaboration = () => {
this.queueBroadcastAllElements.cancel();
- if (this.portal.socket && this.fallbackInitializationHandler) {
- this.portal.socket.off('connect_error', this.fallbackInitializationHandler);
- }
-
this.destroySocketClient();
const elements = this.excalidrawAPI.getSceneElementsIncludingDeleted().map(element => {
@@ -136,6 +111,10 @@ class Collab {
});
};
+ public isCollaborating = () => {
+ return this.portal.isOpen();
+ };
+
private destroySocketClient = (opts?: { isUnload: boolean }) => {
this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close();
@@ -149,149 +128,100 @@ class Collab {
}
};
- private fallbackInitializationHandler: null | (() => unknown) = null;
-
- startCollaboration = async (existingRoomLinkData: { roomId: string }): Promise =>
- new Promise(async resolve => {
- if (this.portal.socket) {
- return null;
- }
-
+ startCollaboration = async (existingRoomLinkData: { roomId: string }): Promise =>
+ new Promise(async (resolve, reject) => {
const { roomId } = existingRoomLinkData;
- const { default: socketIOClient } = await import('socket.io-client');
-
- const fallbackInitializationHandler = () => {
- this.initializeRoom({
- roomLinkData: existingRoomLinkData,
- fetchScene: true,
- }).then(scene => {
- resolve(scene);
- });
- };
-
- this.fallbackInitializationHandler = fallbackInitializationHandler;
-
try {
const socketServerData = await getCollabServer();
- this.portal.socket = this.portal.open(
- socketIOClient(socketServerData.url, {
- transports: socketServerData.polling ? ['websocket', 'polling'] : ['websocket'],
- path: '/api/private/ws/socket.io',
- }),
- roomId
- );
+ await this.portal.open(
+ {
+ ...socketServerData,
+ roomId,
+ },
+ {
+ 'client-broadcast': async (encryptedData: ArrayBuffer) => {
+ const decodedData = new TextDecoder().decode(encryptedData);
+ const decryptedData = JSON.parse(decodedData);
+
+ switch (decryptedData.type) {
+ case 'INVALID_RESPONSE':
+ return;
+ case WS_SCENE_EVENT_TYPES.INIT: {
+ if (!this.portal.socketInitialized) {
+ this.initializeRoom({ fetchScene: false });
+ const remoteElements = decryptedData.payload.elements;
+ const remoteFiles = decryptedData.payload.files;
+ this.handleRemoteSceneUpdate(this.reconcileElements(remoteElements, remoteFiles), {
+ init: true,
+ });
+ }
+ resolve();
+ break;
+ }
+ case WS_SCENE_EVENT_TYPES.SCENE_UPDATE: {
+ const remoteElements = decryptedData.payload.elements;
+ const remoteFiles = decryptedData.payload.files;
+ this.handleRemoteSceneUpdate(this.reconcileElements(remoteElements, remoteFiles));
+ break;
+ }
+
+ case WS_SCENE_EVENT_TYPES.MOUSE_LOCATION: {
+ const { pointer, button, username, selectedElementIds } = decryptedData.payload;
+ const socketId: SocketUpdateDataSource['MOUSE_LOCATION']['payload']['socketId'] =
+ decryptedData.payload.socketId ||
+ // @ts-ignore legacy, see #2094 (#2097)
+ decryptedData.payload.socketID;
+
+ const collaborators = new Map(this.collaborators);
+ const user = collaborators.get(socketId) || {}!;
+ user.pointer = pointer;
+ user.button = button;
+ user.selectedElementIds = selectedElementIds;
+ user.username = username;
+ collaborators.set(socketId, user);
+ this.excalidrawAPI.updateScene({
+ collaborators,
+ });
+ break;
+ }
+
+ case WS_SCENE_EVENT_TYPES.IDLE_STATUS: {
+ const { userState, socketId, username } = decryptedData.payload;
+ const collaborators = new Map(this.collaborators);
+ const user = collaborators.get(socketId) || {}!;
+ user.userState = userState;
+ user.username = username;
+ this.excalidrawAPI.updateScene({
+ collaborators,
+ });
+ break;
+ }
+
+ case WS_SCENE_EVENT_TYPES.SAVED: {
+ this.onSavedToDatabase?.();
+ break;
+ }
+ }
+ },
+ 'first-in-room': async () => {
+ await this.initializeRoom({
+ fetchScene: true,
+ roomLinkData: existingRoomLinkData,
+ });
- this.portal.socket.once('connect_error', fallbackInitializationHandler);
+ resolve();
+ },
+ }
+ );
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
this.state.errorMessage = (error as { message: string } | undefined)?.message ?? '';
- return null;
+ reject(error);
}
- if (!existingRoomLinkData) {
- const elements = this.excalidrawAPI.getSceneElements().map(element => {
- if (isImageElement(element) && element.status === 'saved') {
- return newElementWith(element, { status: 'pending' });
- }
-
- return element;
- });
- // remove deleted elements from elements array & history to ensure we don't
- // expose potentially sensitive user data in case user manually deletes
- // existing elements (or clears scene), which would otherwise be persisted
- // to database even if deleted before creating the room.
- this.excalidrawAPI.history.clear();
- this.excalidrawAPI.updateScene({
- elements,
- commitToHistory: true,
- });
- }
-
- // fallback in case you're not alone in the room but still don't receive
- // initial SCENE_INIT message
- this.socketInitializationTimer = window.setTimeout(fallbackInitializationHandler, INITIAL_SCENE_UPDATE_TIMEOUT);
-
- // All socket listeners are moving to Portal
- this.portal.socket.on('client-broadcast', async (encryptedData: ArrayBuffer) => {
- const decodedData = new TextDecoder().decode(encryptedData);
- const decryptedData = JSON.parse(decodedData);
-
- switch (decryptedData.type) {
- case 'INVALID_RESPONSE':
- return;
- case WS_SCENE_EVENT_TYPES.INIT: {
- if (!this.portal.socketInitialized) {
- this.initializeRoom({ fetchScene: false });
- const remoteElements = decryptedData.payload.elements;
- const remoteFiles = decryptedData.payload.files;
- this.handleRemoteSceneUpdate(this.reconcileElements(remoteElements, remoteFiles), {
- init: true,
- });
- }
- break;
- }
-
- case WS_SCENE_EVENT_TYPES.SCENE_UPDATE: {
- const remoteElements = decryptedData.payload.elements;
- const remoteFiles = decryptedData.payload.files;
- this.handleRemoteSceneUpdate(this.reconcileElements(remoteElements, remoteFiles));
- break;
- }
-
- case WS_SCENE_EVENT_TYPES.MOUSE_LOCATION: {
- const { pointer, button, username, selectedElementIds } = decryptedData.payload;
- const socketId: SocketUpdateDataSource['MOUSE_LOCATION']['payload']['socketId'] =
- decryptedData.payload.socketId ||
- // @ts-ignore legacy, see #2094 (#2097)
- decryptedData.payload.socketID;
-
- const collaborators = new Map(this.collaborators);
- const user = collaborators.get(socketId) || {}!;
- user.pointer = pointer;
- user.button = button;
- user.selectedElementIds = selectedElementIds;
- user.username = username;
- collaborators.set(socketId, user);
- this.excalidrawAPI.updateScene({
- collaborators,
- });
- break;
- }
-
- case WS_SCENE_EVENT_TYPES.IDLE_STATUS: {
- const { userState, socketId, username } = decryptedData.payload;
- const collaborators = new Map(this.collaborators);
- const user = collaborators.get(socketId) || {}!;
- user.userState = userState;
- user.username = username;
- this.excalidrawAPI.updateScene({
- collaborators,
- });
- break;
- }
-
- case WS_SCENE_EVENT_TYPES.SAVED: {
- this.onSavedToDatabase?.();
- break;
- }
- }
- });
-
- this.portal.socket.on('first-in-room', async () => {
- if (this.portal.socket) {
- this.portal.socket.off('first-in-room');
- }
-
- const sceneData = await this.initializeRoom({
- fetchScene: true,
- roomLinkData: existingRoomLinkData,
- });
- resolve(sceneData);
- });
-
this.initializeIdleDetector();
this.state.activeRoomLink = window.location.href;
@@ -308,11 +238,7 @@ class Collab {
| { fetchScene: false; roomLinkData?: null }) => {
clearTimeout(this.socketInitializationTimer!);
- if (this.portal.socket && this.fallbackInitializationHandler) {
- this.portal.socket.off('connect_error', this.fallbackInitializationHandler);
- }
-
- if (fetchScene && roomLinkData && this.portal.socket) {
+ if (fetchScene && roomLinkData) {
try {
this.queueBroadcastAllElements();
} catch (error: unknown) {
@@ -325,8 +251,6 @@ class Collab {
} else {
this.portal.socketInitialized = true;
}
-
- return null;
};
private reconcileElements = (
@@ -419,7 +343,7 @@ class Collab {
};
private setCollaborators = (sockets: string[]) => {
- const collaborators: InstanceType['collaborators'] = new Map();
+ const collaborators = new Map();
for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
@@ -449,7 +373,7 @@ class Collab {
return this.filesManager.getUploadedFiles(this.excalidrawAPI.getFiles());
};
- private onPointerUpdate = throttle(
+ public onPointerUpdate = throttle(
(payload: {
pointer: SocketUpdateDataSource['MOUSE_LOCATION']['payload']['pointer'];
button: SocketUpdateDataSource['MOUSE_LOCATION']['payload']['button'];
@@ -470,7 +394,7 @@ class Collab {
this.portal.broadcastIdleChange(userState, this.state.username);
};
- private syncScene = async (elements: readonly ExcalidrawElement[], files: BinaryFilesWithUrl) => {
+ public syncScene = async (elements: readonly ExcalidrawElement[], files: BinaryFilesWithUrl) => {
if (getSceneVersion(elements) > this.getLastBroadcastedOrReceivedSceneVersion()) {
this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.SCENE_UPDATE, elements, files, { syncAll: false });
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
@@ -478,7 +402,7 @@ class Collab {
}
};
- private notifySavedToDatabase = () => {
+ public notifySavedToDatabase = () => {
this.portal.broadcastSavedEvent(this.state.username);
};
diff --git a/src/domain/common/whiteboard/excalidraw/collab/Portal.ts b/src/domain/common/whiteboard/excalidraw/collab/Portal.ts
index a366663483..46fdae0189 100644
--- a/src/domain/common/whiteboard/excalidraw/collab/Portal.ts
+++ b/src/domain/common/whiteboard/excalidraw/collab/Portal.ts
@@ -23,6 +23,17 @@ interface BroadcastSceneOptions {
syncAll?: boolean;
}
+interface ConnectionOptions {
+ url: string;
+ roomId: string;
+ polling?: boolean;
+}
+
+interface SocketEventHandlers {
+ 'client-broadcast': (encryptedData: ArrayBuffer) => void;
+ 'first-in-room': () => void;
+}
+
class Portal {
onSaveRequest: () => Promise<{ success: boolean; errors?: string[] }>;
onCloseConnection: () => void;
@@ -43,39 +54,74 @@ class Portal {
this.onCloseConnection = onCloseConnection;
}
- open(socket: Socket, id: string) {
- this.socket = socket;
- this.roomId = id;
+ open(connectionOptions: ConnectionOptions, eventHandlers: SocketEventHandlers) {
+ if (this.socket) {
+ throw new Error('Socket already open');
+ }
- // Initialize socket listeners
- this.socket.on('init-room', () => {
- if (this.socket) {
- this.socket.emit('join-room', this.roomId);
- }
- });
+ return new Promise(async (resolve, reject) => {
+ const { default: socketIOClient } = await import('socket.io-client');
- this.socket.on('new-user', async (_socketId: string) => {
- this.broadcastScene(WS_SCENE_EVENT_TYPES.INIT, this.getSceneElements(), await this.getFiles(), { syncAll: true });
- });
+ const socket = socketIOClient(connectionOptions.url, {
+ transports: connectionOptions.polling ? ['websocket', 'polling'] : ['websocket'],
+ path: '/api/private/ws/socket.io',
+ retries: 0,
+ reconnection: false,
+ });
- this.socket.on('room-user-change', (clients: string[]) => {
- this.onRoomUserChange(clients);
- });
+ this.socket = socket;
+ this.roomId = connectionOptions.roomId;
- this.socket.on('save-request', async callback => {
- try {
- callback(await this.onSaveRequest());
- } catch (ex) {
- callback({ success: false, errors: [(ex as { message?: string })?.message ?? ex] });
- }
- });
+ // Initialize socket listeners
+ this.socket.on('init-room', () => {
+ if (this.socket) {
+ this.socket.emit('join-room', this.roomId);
+ }
+ });
- this.socket.on('disconnect', () => {
- this.close();
- this.onCloseConnection();
- });
+ this.socket.on('new-user', async (_socketId: string) => {
+ this.broadcastScene(WS_SCENE_EVENT_TYPES.INIT, this.getSceneElements(), await this.getFiles(), { syncAll: true });
+ });
+
+ this.socket.on('room-user-change', (clients: string[]) => {
+ this.onRoomUserChange(clients);
+ });
+
+ this.socket.on('save-request', async callback => {
+ try {
+ callback(await this.onSaveRequest());
+ } catch (ex) {
+ callback({ success: false, errors: [(ex as { message?: string })?.message ?? ex] });
+ }
+ });
+
+ this.socket.on('client-broadcast', eventHandlers['client-broadcast']);
- return socket;
+ this.socket.on('first-in-room', () => {
+ socket.off('first-in-room');
+ eventHandlers['first-in-room']();
+ });
+
+ this.socket.on('connect', () => {
+ resolve(socket);
+ });
+
+ this.socket.on('connect_error', () => {
+ reject(new Error('Socket could not connect'));
+ this.close();
+ this.onCloseConnection();
+ });
+
+ this.socket.on('disconnect', reason => {
+ if (reason === 'io client disconnect') {
+ // disconnected intentionally
+ return;
+ }
+ reject(new Error('Socket disconnected'));
+ this.close();
+ this.onCloseConnection();
+ });
+ });
}
close() {
diff --git a/src/domain/common/whiteboard/excalidraw/collab/excalidrawAppConstants.ts b/src/domain/common/whiteboard/excalidraw/collab/excalidrawAppConstants.ts
index baf115a1f6..89d7b6df6d 100644
--- a/src/domain/common/whiteboard/excalidraw/collab/excalidrawAppConstants.ts
+++ b/src/domain/common/whiteboard/excalidraw/collab/excalidrawAppConstants.ts
@@ -39,7 +39,6 @@ export const ACTIVE_THRESHOLD = 3_000;
// time constants (ms)
export const SAVE_TO_LOCAL_STORAGE_TIMEOUT = 300;
-export const INITIAL_SCENE_UPDATE_TIMEOUT = 5000;
export const FILE_UPLOAD_TIMEOUT = 300;
export const LOAD_IMAGES_TIMEOUT = 500;
export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
diff --git a/src/domain/common/whiteboard/excalidraw/collab/useCollab.ts b/src/domain/common/whiteboard/excalidraw/collab/useCollab.ts
index 998d50666d..6f70a2e2a6 100644
--- a/src/domain/common/whiteboard/excalidraw/collab/useCollab.ts
+++ b/src/domain/common/whiteboard/excalidraw/collab/useCollab.ts
@@ -1,7 +1,17 @@
-import { useRef } from 'react';
-import Collab, { CollabAPI, CollabProps } from './Collab';
+import { useRef, useState } from 'react';
+import Collab, { CollabProps } from './Collab';
-type UseCollabProvided = [CollabAPI | null, (initProps: InitProps) => void];
+type CollabInstance = InstanceType;
+
+export interface CollabAPI {
+ /** function so that we can access the latest value from stale callbacks */
+ onPointerUpdate: CollabInstance['onPointerUpdate'];
+ syncScene: CollabInstance['syncScene'];
+ notifySavedToDatabase: () => void; // Notify rest of the members in the room that I have saved the whiteboard
+ isCollaborating: () => boolean;
+}
+
+type UseCollabProvided = [CollabAPI | null, (initProps: InitProps) => void, CollabState];
interface UseCollabProps extends Omit {
onInitialize?: (collabApi: CollabAPI) => void;
@@ -11,28 +21,66 @@ interface InitProps extends Pick {
roomId: string;
}
-const useCollab = ({ onInitialize, ...collabProps }: UseCollabProps): UseCollabProvided => {
+interface CollabState {
+ collaborating: boolean;
+ connecting: boolean;
+}
+
+const useCollab = ({ onInitialize, onCloseConnection, ...collabProps }: UseCollabProps): UseCollabProvided => {
const collabRef = useRef(null);
const collabApiRef = useRef(null);
+ const [isConnecting, setIsConnecting] = useState(false);
+
+ const [isCollaborating, setIsCollaborating] = useState(false);
+
+ const handleCloseConnection = () => {
+ try {
+ onCloseConnection();
+ } finally {
+ setIsCollaborating(false);
+ }
+ };
+
const initialize = ({ excalidrawApi, roomId }: InitProps) => {
collabRef.current = new Collab({
...collabProps,
excalidrawApi,
+ onCloseConnection: handleCloseConnection,
});
- const collabApi = collabRef.current.init();
- collabApi.startCollaboration({ roomId });
+
+ collabRef.current.init();
+
+ const collabApi: CollabAPI = {
+ onPointerUpdate: collabRef.current.onPointerUpdate,
+ syncScene: collabRef.current.syncScene,
+ notifySavedToDatabase: collabRef.current.notifySavedToDatabase,
+ isCollaborating: collabRef.current.isCollaborating,
+ };
+
+ (async () => {
+ setIsConnecting(true);
+ try {
+ await collabRef.current?.startCollaboration({ roomId });
+ setIsCollaborating(true);
+ } finally {
+ setIsConnecting(false);
+ }
+ })();
+
collabApiRef.current = collabApi;
+
onInitialize?.(collabApi);
return () => {
+ collabRef.current?.stopCollaboration();
collabRef.current?.destroy();
collabApiRef.current = null;
};
};
- return [collabApiRef.current, initialize];
+ return [collabApiRef.current, initialize, { connecting: isConnecting, collaborating: isCollaborating }];
};
export default useCollab;
diff --git a/src/domain/community/application/applicationButton/OpportunityApplicationButton.tsx b/src/domain/community/application/applicationButton/OpportunityApplicationButton.tsx
index a009636f5c..1211f75078 100644
--- a/src/domain/community/application/applicationButton/OpportunityApplicationButton.tsx
+++ b/src/domain/community/application/applicationButton/OpportunityApplicationButton.tsx
@@ -1,17 +1,17 @@
import { Button as MuiButton, CircularProgress } from '@mui/material';
-import React, { forwardRef, Ref } from 'react';
+import React, { forwardRef, Ref, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link as RouterLink } from 'react-router-dom';
import { buildLoginUrl } from '../../../../main/routing/urlBuilders';
import { AddOutlined } from '@mui/icons-material';
-import RootThemeProvider from '../../../../core/ui/themes/RootThemeProvider';
-import useDirectMessageDialog from '../../../communication/messaging/DirectMessaging/useDirectMessageDialog';
+import { DirectMessageDialog } from '../../../communication/messaging/DirectMessaging/DirectMessageDialog';
export interface OpportunityApplicationButtonProps {
isAuthenticated?: boolean;
isMember: boolean;
isParentMember?: boolean;
parentUrl?: string;
+ sendMessageToCommunityLeads: (message: string) => Promise;
leadUsers: {
id: string;
displayName?: string;
@@ -19,13 +19,6 @@ export interface OpportunityApplicationButtonProps {
country?: string;
avatarUri?: string;
}[];
- adminUsers: {
- id: string;
- displayName?: string;
- city?: string;
- country?: string;
- avatarUri?: string;
- }[];
loading: boolean;
component?: typeof MuiButton;
extended?: boolean;
@@ -41,8 +34,8 @@ export const OpportunityApplicationButton = forwardRef<
isMember = false,
isParentMember = false,
parentUrl,
+ sendMessageToCommunityLeads,
leadUsers,
- adminUsers,
loading = false,
component: Button = MuiButton,
extended = false,
@@ -50,14 +43,12 @@ export const OpportunityApplicationButton = forwardRef<
ref
) => {
const { t } = useTranslation();
- const { sendMessage, directMessageDialog } = useDirectMessageDialog({
- dialogTitle: t('send-message-dialog.direct-message-title'),
- });
-
- const contactUsers = leadUsers.length > 0 ? leadUsers : adminUsers;
-
- const handleSendMessageToParentLeads = () => {
- sendMessage('user', ...contactUsers);
+ const [isContactLeadUsersDialogOpen, setIsContactLeadUsersDialogOpen] = useState(false);
+ const openContactLeadsDialog = () => {
+ setIsContactLeadUsersDialogOpen(true);
+ };
+ const closeContactLeadsDialog = () => {
+ setIsContactLeadUsersDialogOpen(false);
};
const renderApplicationButton = () => {
@@ -99,29 +90,33 @@ export const OpportunityApplicationButton = forwardRef<
);
}
- if (contactUsers.length === 0) {
+ if (leadUsers.length === 0) {
return null;
}
return (
-
+ <>
+
+
+ >
);
};
- return (
- <>
- {renderApplicationButton()}
- {directMessageDialog}
- >
- );
+ return <>{renderApplicationButton()}>;
}
);
diff --git a/src/domain/community/application/containers/OpportunityApplicationButtonContainer.tsx b/src/domain/community/application/containers/OpportunityApplicationButtonContainer.tsx
index 9afcc5073f..e46618ffdc 100644
--- a/src/domain/community/application/containers/OpportunityApplicationButtonContainer.tsx
+++ b/src/domain/community/application/containers/OpportunityApplicationButtonContainer.tsx
@@ -8,6 +8,7 @@ import { CommunityMembershipStatus } from '../../../../core/apollo/generated/gra
import { useCommunityContext } from '../../community/CommunityContext';
import { useAuthenticationContext } from '../../../../core/auth/authentication/hooks/useAuthenticationContext';
import { SimpleContainerProps } from '../../../../core/container/SimpleContainer';
+import useSendMessageToCommunityLeads from '../../CommunityLeads/useSendMessageToCommunityLeads';
interface ApplicationContainerState {
loading: boolean;
@@ -48,7 +49,6 @@ export const OpportunityApplicationButtonContainer: FC ({
id: user.id,
displayName: user.profile.displayName,
@@ -56,13 +56,8 @@ export const OpportunityApplicationButtonContainer: FC ({
- id: user.id,
- displayName: user.profile.displayName,
- country: user.profile.location?.country,
- city: user.profile.location?.city,
- avatarUri: user.profile.avatar?.uri,
- }));
+ const communityId = _communityPrivileges?.space.opportunity?.community?.id;
+ const sendMessageToCommunityLeads = useSendMessageToCommunityLeads(communityId);
const loading = communityPrivilegesLoading;
@@ -70,8 +65,8 @@ export const OpportunityApplicationButtonContainer: FC = ({ entities: { userMetadata } }) => {
const { t } = useTranslation();
const { user, keywords, skills } = userMetadata;
const references = user.profile.references;
const bio = user.profile.description;
-
- const nonSocialReferences = useMemo(() => {
- return references?.filter(x => !isSocialNetworkSupported(x.name));
+ const links = useMemo(() => {
+ return groupBy(references, reference =>
+ isSocialNetworkSupported(reference.name) ? SOCIAL_LINK_GROUP : OTHER_LINK_GROUP
+ );
}, [references]);
+ const socialLinks = links[SOCIAL_LINK_GROUP].map(s => ({
+ type: s.name as SocialNetworkEnum,
+ url: s.uri,
+ }));
+
return (
@@ -50,10 +64,13 @@ export const UserProfileView: FC = ({ entities: { userMeta
{t('components.profile.fields.links.title')}
{t('common.no-references')}}
/>
+
+
+
);
};
diff --git a/src/domain/shared/components/SocialLinks/SocialLinks.tsx b/src/domain/shared/components/SocialLinks/SocialLinks.tsx
index e3d090048c..21be5529ba 100644
--- a/src/domain/shared/components/SocialLinks/SocialLinks.tsx
+++ b/src/domain/shared/components/SocialLinks/SocialLinks.tsx
@@ -6,15 +6,12 @@ import WrapperTypography from '../../../../core/ui/typography/deprecated/Wrapper
import GitHub from './icons/GitHub';
import LinkedIn from './icons/LinkedIn';
import Twitter from './icons/Twitter';
-import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined';
import * as yup from 'yup';
interface SocialLinksProps {
title?: string;
items?: SocialLinkItem[];
iconSize?: SvgIconProps['fontSize'];
- isContactable: boolean;
- onContact: () => void;
}
const getSocialIcon = (type: SocialNetworkEnum, fontSize: SvgIconProps['fontSize'] = 'large') => {
@@ -52,7 +49,7 @@ export interface SocialLinkItem {
const schema = yup.string().url();
-export const SocialLinks: FC = ({ title, items, iconSize, isContactable, onContact }) => {
+export const SocialLinks: FC = ({ title, items, iconSize }) => {
const filteredSortedItems = useMemo(
() =>
items
@@ -79,17 +76,6 @@ export const SocialLinks: FC = ({ title, items, iconSize, isCo
{getSocialIcon(item.type, iconSize)}
))}
-
- {isContactable && (
-
-
-
- )}
);
};
diff --git a/src/domain/shared/components/SocialLinks/icons/Twitter.tsx b/src/domain/shared/components/SocialLinks/icons/Twitter.tsx
index b428fcab39..5f81599152 100644
--- a/src/domain/shared/components/SocialLinks/icons/Twitter.tsx
+++ b/src/domain/shared/components/SocialLinks/icons/Twitter.tsx
@@ -1,9 +1,9 @@
-import { Twitter as MUITwitter } from '@mui/icons-material';
+import { X as MUIX } from '@mui/icons-material';
import { SvgIconProps } from '@mui/material';
import React, { FC } from 'react';
const Twitter: FC = props => {
- return ;
+ return ;
};
export default Twitter;