🚫 Disable all ws for frontend (#324)

This commit is contained in:
Montassar Ghanmy
2024-01-19 17:56:09 +01:00
committed by GitHub
parent 58b5a1fcc0
commit 4769a23480
25 changed files with 8 additions and 995 deletions
@@ -6,20 +6,11 @@ import CheckCircleIcon from '@material-ui/icons/CheckCircle';
import Languages from '@features/global/services/languages-service';
import { ConnectedState } from '@features/users/state/atoms/connected';
import { useRecoilState } from 'recoil';
import WebSocket, { WebsocketEvents } from '@features/global/types/websocket-types';
// import WebSocket, { WebsocketEvents } from '@features/global/types/websocket-types';
export default () => {
const [{ connected, reconnecting }, setState] = useRecoilState(ConnectedState);
useEffect(() => {
WebSocket.get().on(WebsocketEvents.Disconnected, () => {
setState({ connected: false, reconnecting: false });
});
WebSocket.get().on(WebsocketEvents.Connected, () => {
setState({ connected: true, reconnecting: false });
});
}, [setState]);
return (
<div className={'connection_indicator ' + (connected === false ? 'visible' : '')}>
{connected === false && reconnecting !== true && (
@@ -1,28 +1,20 @@
import Api from '../../global/framework/api-service';
import { TdriveService } from '../../global/framework/registry-decorator-service';
import { Application } from 'app/features/applications/types/application';
import { WebsocketRoom } from '../../global/types/websocket-types';
const PREFIX = '/internal/services/applications/v1/companies';
@TdriveService('CompanyApplicationsAPIClientService')
class CompanyApplicationsAPIClient {
private realtime: Map<string, WebsocketRoom[]> = new Map();
websockets(companyId: string): WebsocketRoom[] {
return this.realtime.get(companyId) || [];
}
/**
* Get all applications for a company
*
* @param companyId
*/
async list(companyId: string): Promise<Application[]> {
return Api.get<{ resources: Application[]; websockets: WebsocketRoom[] }>(
return Api.get<{ resources: Application[] }>(
`${PREFIX}/${companyId}/applications`,
).then(result => {
this.realtime.set(companyId, result.websockets);
return result.resources && result.resources.length ? result.resources : [];
});
}
@@ -9,7 +9,6 @@ import {
} from '../state/company-applications';
import CompanyApplicationsAPIClient from 'app/features/applications/api/company-applications-api-client';
import { useCurrentCompany } from '../../companies/hooks/use-companies';
import { useRealtimeRoom } from 'app/features/global/hooks/use-realtime';
import { Application } from 'app/features/applications/types/application';
import { LoadingState } from 'app/features/global/state/atoms/Loading';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
@@ -102,18 +101,6 @@ export function useCompanyApplications(companyId = '') {
};
}
export const useCompanyApplicationsRealtime = (companyId = '') => {
const { company } = useCurrentCompany();
companyId = companyId || company?.id || '';
const { refresh } = useCompanyApplications(companyId);
const room = CompanyApplicationsAPIClient.websockets(companyId || '')[0];
useRealtimeRoom<Application[]>(room, 'useCompanyApplications', () => {
refresh();
});
};
/**
* Use a single application
* @param applicationId
@@ -4,7 +4,6 @@ import AccessRightsService from 'app/features/workspace-members/services/workspa
import CurrentUser from 'app/deprecated/user/CurrentUser';
import JWT from 'app/features/auth/jwt-storage-service';
import LocalStorage from '../../global/framework/local-storage-service';
import WebSocket from '../../global/types/websocket-types';
class Application {
private logger: Logger.Logger;
@@ -25,9 +24,6 @@ class Application {
this.started = true;
this.logger.info('Starting application for user', user);
const ws = WebSocket.get();
ws.connect();
AccessRightsService.resetLevels();
CurrentUser.start();
}
@@ -2,7 +2,6 @@ import { CompanyType } from '@features/companies/types/company';
import { WorkspaceType } from '@features/workspaces/types/workspace';
import Api from '../../global/framework/api-service';
import { TdriveService } from '../../global/framework/registry-decorator-service';
import { WebsocketRoom } from '../../global/types/websocket-types';
const PREFIX = '/internal/services/users/v1';
@@ -14,12 +13,6 @@ export type UpdateWorkspaceBody = {
@TdriveService('CompanyAPIClientService')
class CompanyAPIClient {
private realtime: Map<string, WebsocketRoom> = new Map();
websocket(companyId: string): WebsocketRoom {
return this.realtime.get(companyId) || { room: '', token: '' };
}
/**
* Get a list of companies for a user, only common companies with current user are returned.
@@ -37,13 +30,12 @@ class CompanyAPIClient {
* @param companyId
*/
async get(companyId: string, disableJWTAuthentication = false): Promise<CompanyType> {
return Api.get<{ resource: CompanyType; websocket: WebsocketRoom }>(
return Api.get<{ resource: CompanyType; }>(
`${PREFIX}/companies/${companyId}`,
undefined,
false,
{ disableJWTAuthentication },
).then(result => {
result.resource?.id && this.realtime.set(result.resource.id, result.websocket);
return result.resource;
});
}
@@ -12,7 +12,6 @@ import WorkspacesService from '@deprecated/workspaces/workspaces.jsx';
import AccessRightsService from '@features/workspace-members/services/workspace-members-access-rights-service';
import LoginService from '@features/auth/login-service';
import UserAPIClient from '@features/users/api/user-api-client';
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import CompanyAPIClient from '@features/companies/api/company-api-client';
/**
@@ -86,16 +85,6 @@ export const useCurrentCompany = () => {
return { company: company as CompanyType, refresh };
};
export const useCurrentCompanyRealtime = () => {
const { company, refresh } = useCurrentCompany();
const room = CompanyAPIClient.websocket(company?.id || '');
useRealtimeRoom<CompanyType>(room, 'useCurrentCompany', () => {
refresh();
});
};
/**
* Company priority:
* 1. Router company id
@@ -1,17 +0,0 @@
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import { useDriveActions } from './use-drive-actions';
import { useDriveItem } from './use-drive-item';
export const useDriveRealtime = (id: string) => {
const { refresh } = useDriveActions();
const { websockets } = useDriveItem(id);
const room = websockets?.[0];
useRealtimeRoom(room as { room: string; token: string }, 'useDriveRealtime-' + id, () => {
refresh(id);
});
};
export const DriveRealtimeObject = ({ id }: { id: string }) => {
useDriveRealtime(id);
return <></>;
};
@@ -1,96 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback, useEffect, useRef, useState } from 'react';
import useWebSocket from '@features/global/hooks/use-websocket';
import Logger from '@features/global/framework/logger-service';
import {
RealtimeBaseAction,
RealtimeBaseEvent,
RealtimeResourceEvent,
} from '../types/realtime-types';
import { WebsocketRoom } from '@features/global/types/websocket-types';
const logger = Logger.getLogger('useRealtimeRoom');
export type RealtimeRoomService<T> = {
lastEvent: T;
send: (data: T) => void;
emit: (event: string, data: T) => void;
};
/**
* Subscribe to a room using websocket channel.
*
* Note: It will subscribe only once, even if the component using it re renders. If you need to unsubscribe and subscribe again, call unsubscribe on the returned object.
*
* @param roomConf
* @param tagName
* @param onEvent
* @returns
*/
const useRealtimeRoom = <T>(
roomConf: WebsocketRoom,
tagName: string,
onEvent: (action: RealtimeBaseAction, event: T) => void,
) => {
const { websocket } = useWebSocket();
const [lastEvent, setLastEvent] = useState<{ action: RealtimeBaseAction; payload: T }>();
const [room, setRoom] = useState(roomConf);
const [tag] = useState(tagName);
// subscribe once
const subscribed = useRef(false);
const newEvent = useCallback(
(event: { action: RealtimeBaseAction; payload: T }) => {
if (event) {
setLastEvent(event);
onEvent(event.action, event.payload);
}
},
[onEvent],
);
useEffect(() => {
if (room !== roomConf) {
setRoom({ ...roomConf });
if (room && subscribed.current && websocket) {
websocket.leave(room.room, tag);
subscribed.current = false;
}
}
}, [roomConf?.room, roomConf?.token, tagName]);
useEffect(() => {
if (room && room.room && websocket && !subscribed.current) {
websocket.join(room.room, room.token, tag, (type: string, event: RealtimeBaseEvent) => {
logger.debug('Received WebSocket event', type, event);
if (type === 'realtime:resource') {
newEvent({
action: (event as RealtimeResourceEvent<T>).action,
payload: {
...(event as RealtimeResourceEvent<T>).resource,
_type: (event as RealtimeResourceEvent<T>).type,
},
});
} else if (type === 'realtime:event') {
newEvent({ action: 'event', payload: event.data });
} else if (type === 'realtime:join:success') {
logger.debug(`Room ${room} has been joined`);
} else {
logger.debug('Event type is not supported', type);
}
});
subscribed.current = true;
}
}, [websocket, tag, room, onEvent]);
return {
lastEvent,
send: (data: any) => websocket?.send(room.room, room.token, data),
unsubscribe: () => {
subscribed.current = false;
websocket?.leave(room.room, tagName);
},
};
};
export { useRealtimeRoom };
@@ -1,34 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import WebSocketFactory, { WebsocketEvents } from '@features/global/types/websocket-types';
import WebSocketService from '@features/global/services/websocket-service';
const useWebSocket = () => {
const wsRef = useRef<WebSocketService>();
// having this will allow consumers to be updated with the io instance once connected
const [connected, setConnected] = useState(false);
useEffect(() => {
wsRef.current = WebSocketFactory.get();
if (wsRef.current.isConnected()) {
setConnected(() => true);
}
wsRef.current.on(WebsocketEvents.Connected, () => {
setConnected(() => true);
});
wsRef.current.on(WebsocketEvents.Disconnected, () => {
setConnected(() => false);
});
return () => undefined;
}, []);
return {
websocket: wsRef.current,
connected,
};
};
export default useWebSocket;
@@ -1,44 +0,0 @@
import Globals from './globals-tdrive-app-service';
import Logger from '../framework/logger-service';
import JWT from '../../auth/jwt-storage-service';
import WebSocketService, { WebSocketOptions } from './websocket-service';
class WebSocketFactory {
private logger: Logger.Logger;
private instance!: WebSocketService;
constructor() {
this.logger = Logger.getLogger('WebSocketFactory');
}
get(): WebSocketService {
if (!this.instance) {
this.instance = new WebSocketService(this.getOptions());
}
return this.instance;
}
private getOptions(): WebSocketOptions {
return {
url: Globals.environment.websocket_url,
authenticateHandler: async () => {
let token = JWT.getJWT();
if (JWT.isAccessExpired()) {
try {
token = (await JWT.renew()).value;
} catch (err) {
this.logger.error('Can not get a new JWT token for WS collection', err);
}
}
return {
token,
};
},
};
}
}
export default new WebSocketFactory();
@@ -1,261 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import io from 'socket.io-client';
import { EventEmitter } from 'events';
import Logger from '@features/global/framework/logger-service';
import { WebsocketEvents, WebSocketListener, WebsocketRoomActions } from '../types/websocket-types';
import { Maybe } from '@features/global/types/global-types';
export type WebSocketOptions = {
url: string;
authenticateHandler: () => Promise<any>;
};
const CONNECT_TIMEOUT = 30000;
class WebSocketService extends EventEmitter {
private logger: Logger.Logger;
private lastConnection = 0;
private wsListeners: {
[path: string]: { [tag: string]: WebSocketListener };
} = {};
private socket: SocketIOClient.Socket | null = null;
private connectTimeout?: ReturnType<typeof setTimeout>;
constructor(private options: WebSocketOptions) {
super();
this.logger = Logger.getLogger('WebsocketService');
this.addEventListeners();
}
private addEventListeners(): void {
const reconnectWhenNeeded = () => {
if (!this.isConnected()) {
this.connect();
}
};
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
reconnectWhenNeeded();
}
});
document.addEventListener('focus', () => {
reconnectWhenNeeded();
});
setInterval(() => {
if (new Date().getTime() - this.lastConnection > CONNECT_TIMEOUT) {
this.lastConnection = new Date().getTime();
reconnectWhenNeeded();
}
}, CONNECT_TIMEOUT);
}
isConnected(): boolean {
return this.socket?.connected || false;
}
async connect(): Promise<boolean> {
let connected: (value: boolean) => void;
const promise = new Promise<boolean>(resolve => {
connected = resolve;
});
if (!this.options.url) {
this.logger.info('Skipping connect to empty URL');
return false;
}
this.logger.debug('Connecting to websocket', this.options.url);
if (this.socket) {
if (this.socket.connected) {
this.logger.debug('Already connected to', this.options.url);
return false;
} else {
this.socket?.close();
this.socket = null;
}
}
if (!this.options.authenticateHandler) {
this.logger.error('Cannot connect without an authentication method');
return false;
}
this.socket = io.connect(this.options.url || '', {
path: '/socket',
reconnectionDelayMax: 10000,
reconnectionDelay: 2000,
});
this.socket.on('disconnect', () => {
this.logger.debug('Disconnected from websocket, socket.io will reconnect');
});
this.socket.on('connect', async () => {
this.logger.debug('Connected to websocket, authenticating...');
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
}
this.socket
?.emit('authenticate', (await this.options.authenticateHandler()) || {})
.on('authenticated', () => {
this.logger.debug('Authenticated');
this.rejoinAll(true);
connected(true);
this.emit(WebsocketEvents.Connected, { url: this.options.url });
})
.on('unauthorized', (err: any) => {
this.logger.warn('Websocket is not authorized', err);
this.socket?.close();
this.socket = null;
//Retry and expect new jwt
this.connectTimeout = setTimeout(async () => {
const isConnected = await this.connect();
if (isConnected) {
connected(true);
}
}, 1000);
});
this.socket?.on(WebsocketEvents.JoinSuccess, (event: any) => {
this.logger.debug('Websocket join success', event.name);
event.name && this.notify(event.name, WebsocketEvents.JoinSuccess, event);
});
this.socket?.on(WebsocketEvents.JoinError, (event: any) => {
this.logger.debug('Websocket join error', event.name);
event.name && this.notify(event.name, WebsocketEvents.JoinError, event);
});
this.socket?.on(WebsocketEvents.Resource, (event: any) => {
this.logger.debug('Received resource on room', event.room, event);
event.room && this.notify(event.room, WebsocketEvents.Resource, event);
});
this.socket?.on(WebsocketEvents.Event, (event: any) => {
this.logger.debug('New Websocket event', event.name);
event.name && this.notify(event.name, WebsocketEvents.Event, event);
});
this.socket?.on('disconnect', () => {
this.emit(WebsocketEvents.Disconnected, { url: this.options.url });
});
});
return promise;
}
private rejoinAll(newlyConnected = false) {
Object.keys(this.wsListeners).forEach(key => {
Object.keys(this.wsListeners[key]).forEach(tag => {
if (this.wsListeners[key][tag]) {
newlyConnected && this.wsListeners[key][tag].callback(WebsocketEvents.Connected, {});
this.join(
key,
this.wsListeners[key][tag].token,
tag,
this.wsListeners[key][tag].callback,
);
}
});
});
}
private notify(path: string, type: WebsocketEvents, event: any) {
if (this.wsListeners[path]) {
Object.values(this.wsListeners[path]).forEach(callback => callback.callback?.(type, event));
}
}
public getSocket(): SocketIOClient.Socket | null {
return this.socket;
}
/**
* Join a room. callback will be called when a message is received on the given room
*
* @param path
* @param tag
* @param callback
*/
public join(
path: string,
token: string,
tag: string,
callback: (type: WebsocketEvents, event: any) => void,
) {
const name = path.replace(/\/$/, '');
this.logger.debug(`Join room with name='${name}' and tag='${tag}'`);
if (this.socket) {
this.socket.emit(WebsocketRoomActions.Join, { name, token });
}
this.wsListeners[name] = this.wsListeners[name] || {};
this.wsListeners[name][tag] = { token, callback };
}
/**
* Leave a room
*
* @param path
* @param tag
*/
public leave(path: string, tag: string) {
const name = path.replace(/\/$/, '');
this.wsListeners[name] = this.wsListeners[name] || {};
delete this.wsListeners[name][tag];
if (Object.keys(this.wsListeners[name]).length === 0) {
if (this.socket) {
this.logger.debug(`Leave room with name='${name}' and tag='${tag}'`);
this.socket.emit(WebsocketRoomActions.Leave, { name });
}
delete this.wsListeners[name];
}
}
/**
* Send data as {name: path, data} in the realtime:event topic.
*
* @param path
* @param data
*/
public send<T>(path: string, token: string, data: T): void {
const name = path.replace(/\/$/, '');
this.logger.debug(`Send realtime:event with name='${name}'`);
if (this.socket) {
this.socket.emit('realtime:event', { name, data, token });
}
}
public async get<Request, Response>(
route: string,
request: Request,
callback?: (response: Response) => void,
): Promise<Maybe<Response>> {
this.logger.debug(`Get ${route}`);
return new Promise<Maybe<Response>>(resolve => {
if (this.socket) {
this.socket.emit(route, { data: request }, (response: { data: Response }) => {
this.logger.trace('Got a socket ack');
const result = response.data;
callback && callback(result);
resolve(result);
});
return;
}
});
}
}
export default WebSocketService;
@@ -1,41 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type WebSocketResource = {
room: string;
name?: string;
};
export type RealtimeResources<T> = {
resources: T[];
websockets: WebSocketResource[];
};
export type RealtimeApplicationEventAction = 'configure' | 'close_configure';
export type RealtimeEventAction = 'saved' | 'updated' | 'deleted' | 'event';
export type RealtimeBaseAction = RealtimeEventAction | RealtimeApplicationEventAction;
export interface RealtimeBaseEvent {
action: RealtimeEventAction;
data?: any;
}
export interface RealtimeResourceEvent<T> extends RealtimeBaseEvent {
resource: T;
type?: string;
}
export interface RealtimeEvent<T, U> extends RealtimeBaseEvent {
room: string;
path: string;
type: U;
resource: T;
}
export interface RealtimeApplicationEvent {
action: 'configure' | 'close_configure';
connection_id: string;
application: unknown;
form: unknown;
hidden_data: unknown;
configurator_id: string;
}
@@ -1,28 +0,0 @@
import WebSocketFactory from '../services/websocket-factory-service';
export type WebsocketRoom = {
room: string;
token: string;
};
export enum WebsocketRoomActions {
Join = 'realtime:join',
Leave = 'realtime:leave',
}
export enum WebsocketEvents {
Connected = 'connected',
Connecting = 'connecting',
Disconnected = 'disconnected',
JoinSuccess = 'realtime:join:success',
JoinError = 'realtime:join:error',
Resource = 'realtime:resource',
Event = 'realtime:event',
}
export type WebSocketListener = {
token: string;
callback: <T>(type: WebsocketEvents, event: T) => void;
};
export default WebSocketFactory;
@@ -1,47 +0,0 @@
import WebSocketService from '../../global/services/websocket-service';
import Logger from '../../global/framework/logger-service';
import UsersService from '@features/users/services/current-user-service';
const logger = Logger.getLogger('OnlineUsersRealtimeAPI');
export type GetUserRequestType = string;
export type GetUsersRequestType = Array<GetUserRequestType>;
export type GetUserResponseType = [string, boolean];
export type GetUsersResponseType = Array<GetUserResponseType>;
export type RealtimeUpdateMessageType = GetUsersResponseType;
export type OnlineUserRealtimeAPIType = {
getUserStatus: (id: GetUserRequestType) => Promise<GetUserResponseType>;
getUsersStatus: (ids: GetUsersRequestType) => Promise<GetUsersResponseType>;
setMyStatus: () => void;
};
export const ONLINE_ROOM = (companyId: string) => `/users/online/${companyId}`;
export const OnlineUserRealtimeAPI = (websocket: WebSocketService): OnlineUserRealtimeAPIType => {
const getUserStatus = (id: GetUserRequestType): Promise<GetUserResponseType> => {
logger.debug(`Get user online status ${id}`);
return getUsersStatus([id]).then(response => response?.[0] || [id, false]);
};
const getUsersStatus = (ids: GetUsersRequestType = []): Promise<GetUsersResponseType> => {
logger.debug(`Get users statuses ${ids.join(',')}`);
return websocket
.get<GetUsersRequestType, GetUsersResponseType>('online:get', ids)
.then(result => result || []);
};
const setMyStatus = (): void => {
const id = UsersService.getCurrentUserId();
logger.debug(`Set user online status ${id}`);
websocket
.get<GetUsersRequestType, GetUsersResponseType>('online:set', [id])
.then(result => result || []);
};
return {
getUserStatus,
getUsersStatus,
setMyStatus,
};
};
@@ -3,7 +3,6 @@ import { CompanyType } from '@features/companies/types/company';
import { UserPreferencesType, UserType } from '@features/users/types/user';
import Api from '../../global/framework/api-service';
import { TdriveService } from '../../global/framework/registry-decorator-service';
import { WebsocketRoom } from '../../global/types/websocket-types';
import WorkspaceAPIClient from '../../workspaces/api/workspace-api-client';
import CurrentUser from '../../../deprecated/user/CurrentUser';
import { setUserList } from '../hooks/use-user-list';
@@ -23,13 +22,8 @@ type SearchUserApiResponse<T> = {
@TdriveService('UserAPIClientService')
class UserAPIClientService {
private readonly prefixUrl: string = '/internal/services/users/v1';
private realtime: Map<string, WebsocketRoom> = new Map();
private logger = Logger.getLogger('UserAPIClientService');
websocket(userId: string): WebsocketRoom {
return this.realtime.get(userId) || { room: '', token: '' };
}
/**
* Get users from their ID
*
@@ -101,7 +95,6 @@ class UserAPIClientService {
companyId: string,
options?: { bufferize?: boolean; callback?: (res: UserType[]) => void },
): Promise<UserType[]> {
const res = await new Promise<UserType[]>(resolve =>
Api.post(
`/internal/services/users/v1/users/${companyId}/all`,
@@ -135,7 +128,7 @@ class UserAPIClientService {
}
async getCurrent(disableJWTAuthentication = false): Promise<UserType> {
return Api.get<{ resource: UserType; websocket: WebsocketRoom }>(
return Api.get<{ resource: UserType }>(
'/internal/services/users/v1/users/me',
undefined,
false,
@@ -144,7 +137,6 @@ class UserAPIClientService {
if (!result?.resource?.id) {
throw new Error('User not found');
}
result.resource.id && this.realtime.set(result.resource.id, result.websocket);
return result.resource;
});
}
@@ -1,29 +1,20 @@
import Api from '../../global/framework/api-service';
import { NotificationType } from '@features/users/types/notification-types';
import { TdriveService } from '../../global/framework/registry-decorator-service';
import { WebsocketRoom } from '@features/global/types/websocket-types';
@TdriveService('UserNotificationAPIClientService')
class UserNotificationAPIClient {
private realtime: WebsocketRoom = { room: '', token: '' };
websocket(): WebsocketRoom {
return this.realtime;
}
async getAllCompaniesBadges(): Promise<NotificationType[]> {
const response = await Api.get<{ resources: NotificationType[]; websockets: WebsocketRoom[] }>(
const response = await Api.get<{ resources: NotificationType[] }>(
'/internal/services/notifications/v1/badges?limit=1000&websockets=1&all_companies=true',
);
if (response.websockets) this.realtime = response.websockets[0];
return response.resources ? response.resources : [];
}
async getCompanyBadges(companyId: string): Promise<NotificationType[]> {
const response = await Api.get<{ resources: NotificationType[]; websockets: WebsocketRoom[] }>(
const response = await Api.get<{ resources: NotificationType[] }>(
'/internal/services/notifications/v1/badges?limit=1000&websockets=1&company_id=' + companyId,
);
if (response.websockets) this.realtime = response.websockets[0];
return response.resources ? response.resources : [];
}
@@ -4,7 +4,6 @@ import LoginService from '@features/auth/login-service';
import UserAPIClient from '@features/users/api/user-api-client';
import { useRecoilState } from 'recoil';
import { CurrentUserState } from '../state/atoms/current-user';
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import Languages from '@features/global/services/languages-service';
import { useSetUserList } from './use-user-list';
import { getPublicLinkToken } from 'app/features/drive/api-client/api-client';
@@ -46,23 +45,3 @@ export const useCurrentUser = () => {
return { user, refresh };
};
export const useCurrentUserRealtime = () => {
const { user, refresh } = useCurrentUser();
const room = UserAPIClient.websocket(user?.id || '');
const timeout = useRef(0);
useRealtimeRoom<any>(room, 'useCurrentUser', async (action, resource) => {
switch (resource._type) {
case 'user':
clearTimeout(timeout.current); //
timeout.current = setTimeout(() => {
refresh();
}, 1000) as any;
break;
default:
console.error('Unknown resource type');
}
});
};
@@ -1,193 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useGlobalEffect } from '@features/global/hooks/use-global-effect';
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import useRouterCompany from '@features/router/hooks/use-router-company';
import _ from 'lodash';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import userNotificationApiClient from '../api/user-notification-api-client';
import {
NotificationsBadgesState,
NotificationsChannelBadgesSelector,
NotificationsCompanyBadgesSelector,
NotificationsOtherCompanyBadgesSelector,
NotificationsWorkspaceBadgesSelector,
} from '../state/atoms/notifications';
import { NotificationType } from '../types/notification-types';
import ElectronService from '@features/global/framework/electron-service';
import windowState from '@features/global/utils/window';
import RouterService, { ClientStateType } from '../../router/services/router-service';
import { pushDesktopNotification } from '../services/push-desktop-notification';
import { RouterState } from '@features/router/state/atoms/router';
import { useCurrentUser } from '@features/users/hooks/use-current-user';
export let removeBadgesNow: (type: 'company' | 'workspace' | 'channel', id: string) => void = () =>
undefined;
const executeRequest = () => {
if ('Notification' in window && window.Notification.requestPermission) {
const request = window.Notification.requestPermission();
if (request && request.then) {
request
.then(() => undefined)
.finally(() => {
window.removeEventListener('click', executeRequest);
});
}
}
};
window.addEventListener('click', executeRequest);
executeRequest();
export const useNotifications = () => {
const [badges, setBadges] = useRecoilState(NotificationsBadgesState);
const companyId = useRouterCompany();
const addBadges = useRecoilCallback(
({ snapshot }) =>
(newBadges: NotificationType[]) => {
const badges = snapshot.getLoadable(NotificationsBadgesState).getValue();
const list = _.uniqBy([...badges, ...newBadges], 'id');
setBadges(list);
},
[setBadges, badges],
);
const removeBadges = useRecoilCallback(
({ snapshot }) =>
(removedBadges: NotificationType[]) => {
const badges = snapshot.getLoadable(NotificationsBadgesState).getValue();
const list = _.differenceBy(badges, removedBadges, 'id');
setBadges(list);
},
[setBadges, badges],
);
const removeObjectBadges = useRecoilCallback(
({ snapshot }) =>
(type: 'channel' | 'workspace' | 'company', id: string) => {
const badges = snapshot.getLoadable(NotificationsBadgesState).getValue();
const list = badges.filter(
b =>
(type === 'channel' && b.channel_id !== id) ||
(type === 'workspace' && b.workspace_id !== id) ||
(type === 'company' && b.company_id !== id),
);
setBadges(list);
},
[setBadges, badges],
);
removeBadgesNow = removeObjectBadges;
const setCompanyBadges = useRecoilCallback(
({ snapshot }) =>
(newBadges: NotificationType[], companyId: string) => {
const badges = snapshot.getLoadable(NotificationsBadgesState).getValue();
const list = _.uniqBy(
[...badges.filter(b => b.company_id !== companyId), ...newBadges],
'id',
);
setBadges(list);
},
[setBadges, badges],
);
useGlobalEffect(
'useCompanyNotifications',
async () => {
if (companyId) {
const updatedBadges = await userNotificationApiClient.getCompanyBadges(companyId);
const updatedOtherCompaniesBadges = await userNotificationApiClient.getAllCompaniesBadges();
setCompanyBadges([...updatedOtherCompaniesBadges, ...updatedBadges], companyId);
}
},
[companyId],
);
useGlobalEffect(
'useNotificationsSetWindowBadges',
() => {
let notifications = 0;
const state = RouterService.getStateFromRoute();
const ignore: any = [];
for (const notification of badges) {
if (
ignore.indexOf(notification.company_id) >= 0 ||
ignore.indexOf(notification.workspace_id) >= 0
)
return;
if (
notification.company_id !== state.companyId ||
(notification.workspace_id !== state.workspaceId &&
notification.workspace_id !== 'direct')
) {
notifications++;
ignore.push(notification.company_id);
ignore.push(notification.workspace_id);
} else {
notifications++;
}
}
windowState.setPrefix(notifications);
if (notifications > 0) {
ElectronService.setBadge('' + notifications);
} else {
ElectronService.setBadge('');
}
},
[badges],
);
const { user } = useCurrentUser();
const soundType =
user?.preference?.notifications?.[0]?.preferences?.notification_sound || 'default';
const realtimeEvent = useRecoilCallback(
({ snapshot }) =>
async (action: string, resource: any) => {
const routerState = snapshot.getLoadable(RouterState).valueMaybe() as ClientStateType;
if (action === 'event' && resource._type === 'notification:desktop') {
pushDesktopNotification({ ...resource, routerState }, soundType);
userNotificationApiClient.acknowledge(resource);
}
if (action === 'saved') addBadges([resource]);
if (action === 'deleted') removeBadges([resource]);
},
[addBadges, removeBadges],
);
const room = userNotificationApiClient.websocket();
useRealtimeRoom<any>(room, 'useNotifications', realtimeEvent);
return {
badges,
};
};
export const useCompanyNotifications = (id: string) => {
const badges = useRecoilValue(NotificationsCompanyBadgesSelector(id));
return {
badges,
};
};
export const useOtherCompanyNotifications = (id: string) => {
const badges = useRecoilValue(NotificationsOtherCompanyBadgesSelector(id));
return {
badges,
};
};
export const useWorkspaceNotifications = (id: string) => {
const badges = useRecoilValue(NotificationsWorkspaceBadgesSelector(id));
return {
badges,
};
};
export const useChannelNotifications = (id: string) => {
const badges = useRecoilValue(NotificationsChannelBadgesSelector(id));
return {
badges,
};
};
@@ -1,95 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-empty-function */
import { useCallback, useEffect } from 'react';
import { useRecoilState } from 'recoil';
import Logger from '@features/global/framework/logger-service';
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import { OnlineUsersState, OnlineUserType } from '../state/atoms/online-users';
import useWebSocket from '../../global/hooks/use-websocket';
import {
OnlineUserRealtimeAPI,
ONLINE_ROOM,
RealtimeUpdateMessageType,
} from '../../../features/users/api/online-user-realtime-api-client';
import JWTStorage from '../../auth/jwt-storage-service';
import useRouterCompany from '@features/router/hooks/use-router-company';
const logger = Logger.getLogger('useOnlineUsers');
const GET_INTERVAL = 30000; //30 seconds
const SET_INTERVAL = 600000; //10 minutes
let getIntervalId = setTimeout(() => {}, 0);
let setIntervalId = setTimeout(() => {}, 0);
export const useOnlineUsers = (): void => {
logger.trace('Running online hook');
const { websocket } = useWebSocket();
const companyId = useRouterCompany();
const [onlineUsers, setOnlineUsersState] = useRecoilState(OnlineUsersState);
const updateOnline = useCallback((statuses: Array<[string, boolean]> = []): void => {
logger.debug(`Update online status for ${statuses.length} users`, statuses);
const lastSeen = Date.now();
setOnlineUsersState(users => {
if (!statuses.length) {
return users;
}
const previousStateMap = new Map<string, OnlineUserType>(users.map(u => [u.id!, u]));
for (const statusTuple of statuses) {
previousStateMap.set(statusTuple[0], {
id: statusTuple[0],
connected: statusTuple[1],
lastSeen: statusTuple[1] ? lastSeen : previousStateMap.get(statusTuple[0])?.lastSeen || 0,
initialized: true,
});
}
return [...previousStateMap.entries()].map(entry => ({
id: entry[0],
connected: entry[1].connected,
lastSeen: entry[1].lastSeen,
initialized: true,
}));
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (websocket) {
// got some local users, ask if there are still online
const api = OnlineUserRealtimeAPI(websocket);
clearInterval(getIntervalId);
getIntervalId = setInterval(async () => {
const users = onlineUsers.map(u => u.id).filter(<T>(n?: T): n is T => Boolean(n));
const status = await api.getUsersStatus(users);
updateOnline(status);
}, GET_INTERVAL);
clearInterval(setIntervalId);
setIntervalId = setInterval(async () => await api.setMyStatus(), SET_INTERVAL);
}
return () => {
getIntervalId && clearInterval(getIntervalId);
setIntervalId && clearInterval(setIntervalId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [websocket, onlineUsers]);
// listen to room events in which online events are pushed
useRealtimeRoom<RealtimeUpdateMessageType>(
{ room: ONLINE_ROOM(companyId), token: JWTStorage.getJWT() },
'useOnlineUsers' + companyId,
(action, resource: any) => {
delete resource['_type'];
resource = Object.values(resource?.online || resource);
if (action === 'event' && resource?.length) {
updateOnline(resource.map((u: any) => [u.user_id, u.is_online]));
}
},
);
};
@@ -1,7 +1,6 @@
import { useRecoilState, useRecoilValue } from 'recoil';
import { useGlobalEffect } from '@features/global/hooks/use-global-effect';
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import { LoadingState } from '@features/global/state/atoms/Loading';
import { UserType } from '@features/users/types/user';
import UserAPIClient from '../api/user-api-client';
@@ -34,10 +33,5 @@ export const useUser = (userId: string): UserType | undefined => {
[],
);
const room = UserAPIClient.websocket(userId);
useRealtimeRoom<UserType>(room, 'useUser', () => {
refresh();
});
return user;
};
@@ -1,11 +1,9 @@
// eslint-disable-next-line @typescript-eslint/no-use-before-define
import React from 'react';
import { useOnlineUsers } from '@features/users/hooks/use-online-users';
/**
* This hook will be global to application
*/
export default (): JSX.Element => {
useOnlineUsers();
return <></>;
};
@@ -2,7 +2,6 @@ import Api from '../../global/framework/api-service';
import { CompanyType } from '@features/companies/types/company';
import { WorkspaceType } from '@features/workspaces/types/workspace';
import { TdriveService } from '../../global/framework/registry-decorator-service';
import { WebsocketRoom } from '../../global/types/websocket-types';
import _ from 'lodash';
const PREFIX = '/internal/services/workspaces/v1/companies';
@@ -23,22 +22,15 @@ type UpdateWorkspaceInviteDomainResponse = {
@TdriveService('WorkspaceAPIClientService')
class WorkspaceAPIClient {
private realtime: Map<string, WebsocketRoom[]> = new Map();
websockets(companyId: string): WebsocketRoom[] {
return this.realtime.get(companyId) || [];
}
/**
* Get all workspaces for a company
*
* @param companyId
*/
async list(companyId: string): Promise<WorkspaceType[]> {
return Api.get<{ resources: WorkspaceType[]; websockets: WebsocketRoom[] }>(
return Api.get<{ resources: WorkspaceType[] }>(
`${PREFIX}/${companyId}/workspaces?websockets=1`,
).then(result => {
this.realtime.set(companyId, result.websockets);
return result.resources && result.resources.length ? result.resources : [];
});
}
@@ -2,15 +2,10 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { WorkspaceType } from '@features/workspaces/types/workspace';
import { WorkspaceListStateFamily } from '../state/workspace-list';
import Collections from '@deprecated/CollectionsV1/Collections/Collections';
import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import useRouterWorkspace from '@features/router/hooks/use-router-workspace';
import RouterService from '@features/router/services/router-service';
import _ from 'lodash';
import WorkspacesService from '@deprecated/workspaces/workspaces.jsx';
import AccessRightsService, {
RightsOrNone,
} from '@features/workspace-members/services/workspace-members-access-rights-service';
import LocalStorage from '@features/global/framework/local-storage-service';
import useRouterCompany from '@features/router/hooks/use-router-company';
import WorkspaceAPIClient from '@features/workspaces/api/workspace-api-client';
@@ -47,22 +42,6 @@ export const useWorkspacesCommons = (companyId = '') => {
return { workspaces, loading, refresh };
};
export function useWorkspaces(companyId = '') {
const { workspaces, loading, refresh } = useWorkspacesCommons(companyId);
useRealtimeRoom<WorkspaceType>(
WorkspaceAPIClient.websockets(companyId)[0],
'useWorkspaces',
action => {
if (action === 'saved') {
refresh();
}
},
);
return { workspaces, loading, refresh };
}
export function useWorkspaceLoader(companyId: string) {
const loading = useRecoilValue(LoadingState(`workspaces-${companyId}`));
return { loading };
@@ -6,7 +6,6 @@ import { getFilesTree } from '@components/uploads/file-tree-utils';
import UploadZone from '@components/uploads/upload-zone';
import { setTdriveTabToken } from '@features/drive/api-client/api-client';
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
import { DriveRealtimeObject } from '@features/drive/hooks/use-drive-realtime';
import { useDriveUpload } from '@features/drive/hooks/use-drive-upload';
import { DriveItemSelectedList } from '@features/drive/state/store';
import { formatBytes } from '@features/drive/utils';
@@ -257,7 +256,6 @@ export default memo(
onDrop={handleDrop}
>
<DriveRealtimeObject id={parentId} key={parentId} />
{role == "admin" && <UsersModal />}
<VersionsModal />
<AccessModal />
@@ -9,7 +9,7 @@ import ModalComponent from '@components/modal/modal-component';
import PopupService from '@deprecated/popupManager/popupManager.js';
import useUsetiful from '@features/global/hooks/use-usetiful';
import Languages from '@features/global/services/languages-service';
import { useCurrentUser, useCurrentUserRealtime } from '@features/users/hooks/use-current-user';
import { useCurrentUser } from '@features/users/hooks/use-current-user';
import UserContext from '@features/users/state/integration/user-context';
import Viewer from '@views/client/viewer/viewer';
import ConnectionIndicator from 'components/connection-indicator/connection-indicator';
@@ -26,7 +26,6 @@ import SideBar from './side-bar';
export default React.memo((): JSX.Element => {
const [menuIsOpen, setMenuIsOpen] = useState(false);
const { user } = useCurrentUser();
useCurrentUserRealtime();
const { FeatureToggles, activeFeatureNames } = useFeatureToggles();
useUsetiful();