feat: init
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
import Requests from 'app/features/global/framework/requests-api-service';
|
||||
|
||||
class GroupedQueryApi {
|
||||
private groupedQueryBuffer: any;
|
||||
private groupedQueryTimeout: any;
|
||||
|
||||
post(route: string, data: any, callback: any) {
|
||||
if (!this.groupedQueryBuffer) {
|
||||
this.groupedQueryBuffer = [];
|
||||
}
|
||||
this.groupedQueryBuffer.push({
|
||||
route: route,
|
||||
data: data,
|
||||
callback: callback,
|
||||
});
|
||||
if (this.groupedQueryTimeout) {
|
||||
clearTimeout(this.groupedQueryTimeout);
|
||||
}
|
||||
this.groupedQueryTimeout = setTimeout(() => {
|
||||
const queries = this.groupedQueryBuffer;
|
||||
this.groupedQueryBuffer = [];
|
||||
const request: any[] = [];
|
||||
queries.forEach((query: any) => {
|
||||
request.push(query.data);
|
||||
});
|
||||
Api.GroupedQueryApiPost(queries[0].route, { multiple: request }, (res: any) => {
|
||||
if (res.data && res.data.length) {
|
||||
res.data.forEach((result: any, i: string | number) => {
|
||||
queries[i] && queries[i].callback && queries[i].callback(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
const GroupedQueryApiInstance = new GroupedQueryApi();
|
||||
|
||||
export default class Api {
|
||||
static getWithParams<Response>(
|
||||
route: string,
|
||||
params: any,
|
||||
options: { disableJWTAuthentication?: boolean; withBlob?: boolean } = {},
|
||||
) {
|
||||
let query = '';
|
||||
|
||||
if (params) {
|
||||
for (const k of Object.keys(params)) {
|
||||
query += `&${k}=${params[k]}`;
|
||||
}
|
||||
if (!~route.indexOf('?')) {
|
||||
query = '?' + query.slice(1);
|
||||
}
|
||||
}
|
||||
return Api.get<Response>(route + query, () => {}, false, options);
|
||||
}
|
||||
|
||||
static get<Response>(
|
||||
route: string,
|
||||
callback?: (result: Response) => void,
|
||||
raw = false,
|
||||
options: { disableJWTAuthentication?: boolean; withBlob?: boolean } = {},
|
||||
): Promise<Response> {
|
||||
return new Promise((resolve, reject) => {
|
||||
route = Globals.api_root_url + route;
|
||||
|
||||
Requests.request(
|
||||
'get',
|
||||
route,
|
||||
'',
|
||||
(resp: any) => {
|
||||
const result = raw ? resp : JSON.parse(resp);
|
||||
if (resp.statusCode === 500) {
|
||||
callback && callback(result);
|
||||
return reject(result);
|
||||
}
|
||||
|
||||
resolve({ ...result, _statusCode: resp.statusCode });
|
||||
callback && callback({ ...result, _statusCode: resp.statusCode });
|
||||
},
|
||||
options,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static put<Request, Response>(
|
||||
route: string,
|
||||
data: Request,
|
||||
callback?: (result: Response) => void,
|
||||
raw = false,
|
||||
options: {
|
||||
disableJWTAuthentication?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
return Api.request<Request, Response>(route, data, callback, raw, {
|
||||
...options,
|
||||
requestType: 'put',
|
||||
});
|
||||
}
|
||||
|
||||
static post<Request, Response>(
|
||||
route: string,
|
||||
data: Request,
|
||||
callback?: (result: Response) => void,
|
||||
raw = false,
|
||||
options: {
|
||||
disableJWTAuthentication?: boolean;
|
||||
} = {},
|
||||
): Promise<Response> {
|
||||
return Api.request<Request, Response>(route, data, callback, raw, {
|
||||
...options,
|
||||
requestType: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
static GroupedQueryApiPost<
|
||||
Request extends { _grouped?: unknown; multiple?: unknown[] },
|
||||
Response,
|
||||
>(route: string, data: Request, callback?: (result: Response) => void): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (data && data._grouped && route === 'core/collections/init') {
|
||||
GroupedQueryApiInstance.post(route, data, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
static delete<Response>(
|
||||
route: string,
|
||||
callback?: (result: Response) => void,
|
||||
raw = false,
|
||||
options: {
|
||||
disableJWTAuthentication?: boolean;
|
||||
} = {},
|
||||
): Promise<Response> {
|
||||
return Api.request(route, null, callback, raw, { ...options, requestType: 'delete' });
|
||||
}
|
||||
|
||||
static request<Request, Response>(
|
||||
route: string,
|
||||
data: Request | null,
|
||||
callback: any = false,
|
||||
raw = false,
|
||||
options: {
|
||||
disableJWTAuthentication?: boolean;
|
||||
requestType?: 'post' | 'get' | 'put' | 'delete';
|
||||
} = {},
|
||||
): Promise<Response> {
|
||||
return new Promise(resolve => {
|
||||
Requests.request(
|
||||
options.requestType ? options.requestType : 'post',
|
||||
new URL(route, Globals.api_root_url).toString(),
|
||||
data === null ? '' : JSON.stringify(data),
|
||||
(resp: any) => {
|
||||
if (raw) {
|
||||
resolve(resp);
|
||||
if (callback) callback(resp);
|
||||
return;
|
||||
}
|
||||
let response: any = '';
|
||||
try {
|
||||
response = JSON.parse(resp);
|
||||
} catch (e) {
|
||||
console.log('Server internal error, bad JSON.');
|
||||
response = { errors: 'bad_json' };
|
||||
}
|
||||
resolve(response);
|
||||
if (callback) callback(response);
|
||||
},
|
||||
options,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static searching_last_query: any;
|
||||
static searching_http_timeout: any;
|
||||
static searching_http: any;
|
||||
static searching_javascript: any;
|
||||
|
||||
static search(source: any, _query: any, collectionService: any, callback: any) {
|
||||
if (!Api.searching_http_timeout) {
|
||||
Api.searching_http_timeout = {};
|
||||
}
|
||||
if (!Api.searching_javascript) {
|
||||
Api.searching_javascript = {};
|
||||
}
|
||||
if (!Api.searching_http) {
|
||||
Api.searching_http = {};
|
||||
}
|
||||
if (!this.searching_last_query) {
|
||||
Api.searching_last_query = {};
|
||||
}
|
||||
|
||||
const query = _query;
|
||||
|
||||
const http = source.http;
|
||||
const http_data = source.http_data || { query: query };
|
||||
|
||||
let collection: any = null,
|
||||
collection_filter: any = null,
|
||||
collection_find_by: any = null;
|
||||
if (source.collection && collectionService.get) {
|
||||
collection = collectionService.get(source.collection);
|
||||
collection_find_by = source.collection_find_by || {};
|
||||
collection_filter =
|
||||
source.collection_filter ||
|
||||
(() => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const search_key = source.http + '_' + source.collection;
|
||||
this.searching_last_query[search_key] = query;
|
||||
|
||||
//JavaScript search
|
||||
if (collection && !this.searching_javascript[search_key]) {
|
||||
this.searching_javascript[search_key] = true;
|
||||
const results = collection
|
||||
.findBy(collection_find_by)
|
||||
.filter((item: any) => collection_filter(item, query));
|
||||
callback(results);
|
||||
this.searching_javascript[search_key] = false;
|
||||
}
|
||||
|
||||
//HTTP Search
|
||||
if (http) {
|
||||
if (this.searching_http_timeout[search_key]) {
|
||||
clearTimeout(this.searching_http_timeout[search_key]);
|
||||
}
|
||||
|
||||
if (!this.searching_http[search_key]) {
|
||||
this.searching_http[search_key] = true;
|
||||
|
||||
try {
|
||||
Api.post(
|
||||
http,
|
||||
http_data,
|
||||
(res: any) => {
|
||||
if (res.data) {
|
||||
if (collection) {
|
||||
res.data.forEach((item: any) => {
|
||||
collection.completeObject(item, item.front_id);
|
||||
});
|
||||
collection.notify();
|
||||
|
||||
delete source.http;
|
||||
delete source.http_data;
|
||||
Api.search(
|
||||
source,
|
||||
this.searching_last_query[search_key],
|
||||
collectionService,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
callback && callback(res.data);
|
||||
this.searching_http[search_key] = false;
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
} catch (e) {
|
||||
this.searching_http[search_key] = false;
|
||||
}
|
||||
} else {
|
||||
this.searching_http_timeout[search_key] = setTimeout(() => {
|
||||
Api.search(source, query, collectionService, callback);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static route(route: string) {
|
||||
return Globals.api_root_url + route;
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).Api = Api;
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
class Electron {
|
||||
isElectron() {
|
||||
return (window as any).electron !== undefined;
|
||||
}
|
||||
|
||||
setBadge(value: any) {
|
||||
if (!this.isElectron()) return;
|
||||
try {
|
||||
if (
|
||||
(window as any).electron &&
|
||||
(window as any).electron.remote &&
|
||||
(window as any).electron.remote.app &&
|
||||
(window as any).electron.remote.app.dock
|
||||
) {
|
||||
(window as any).electron.remote.app.dock.setBadge(value);
|
||||
}
|
||||
if ((window as any).electron && (window as any).electron.ipcRenderer) {
|
||||
(window as any).electron.ipcRenderer.send('application:update_badge', value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('No electron app available for setting dock badge');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instanceElectron = new Electron();
|
||||
export default instanceElectron;
|
||||
@@ -0,0 +1,7 @@
|
||||
class Environment {
|
||||
isProduction() {
|
||||
return process.env?.NODE_ENV === "production";
|
||||
}
|
||||
}
|
||||
|
||||
export default new Environment();
|
||||
@@ -0,0 +1,43 @@
|
||||
export default class LocalStorage {
|
||||
static prefix = 'twake:';
|
||||
|
||||
static setItem(key: string, value: unknown) {
|
||||
window.localStorage.setItem(`${LocalStorage.prefix}${key}`, JSON.stringify(value));
|
||||
}
|
||||
|
||||
static getItem<T>(key: string): T | string | null {
|
||||
let value = window.localStorage.getItem(`${LocalStorage.prefix}${key}`);
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (e) {
|
||||
value = null;
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return value as unknown as T;
|
||||
}
|
||||
|
||||
static clear() {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
const depreciatedKeysRemove =
|
||||
[
|
||||
'twake-collections-db',
|
||||
'm_input',
|
||||
'language',
|
||||
'jwt',
|
||||
'autoload_workspaces',
|
||||
'oidc.',
|
||||
].some(m => key.indexOf(m) === 0) || key.indexOf(':channel') > 0;
|
||||
if (key.indexOf(LocalStorage.prefix) === 0 || depreciatedKeysRemove)
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import log from 'loglevel';
|
||||
import prefix from 'loglevel-plugin-prefix';
|
||||
import EnvironmentService from './environment-service';
|
||||
|
||||
const isProduction = EnvironmentService.isProduction();
|
||||
|
||||
isProduction ? log.setDefaultLevel(log.levels.WARN) : log.setDefaultLevel(log.levels.DEBUG);
|
||||
|
||||
prefix.reg(log);
|
||||
prefix.apply(log, {
|
||||
template: `${isProduction ? '' : '[%t] '}%l - %n -`,
|
||||
levelFormatter(level) {
|
||||
return level.toUpperCase();
|
||||
},
|
||||
nameFormatter(name) {
|
||||
return name || 'Twake';
|
||||
},
|
||||
timestampFormatter(date) {
|
||||
return date.toISOString();
|
||||
},
|
||||
});
|
||||
|
||||
export default log;
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ServiceRegistry from 'app/features/global/framework/registry-service';
|
||||
|
||||
const SERVICE_SUFFIX = 'ChannelServiceImpl';
|
||||
|
||||
export function TwakeService(name: string): ClassDecorator {
|
||||
return function DecoratedTwakeService(target: any): any {
|
||||
const originalConstrutor = target;
|
||||
|
||||
const decorated: any = function (...args: any) {
|
||||
const newService = new originalConstrutor(...args);
|
||||
|
||||
if (name) {
|
||||
const serviceName =
|
||||
name.endsWith(SERVICE_SUFFIX) || name.endsWith(SERVICE_SUFFIX.toLowerCase())
|
||||
? name
|
||||
: `${name}${SERVICE_SUFFIX}`;
|
||||
|
||||
ServiceRegistry.register(serviceName, newService);
|
||||
}
|
||||
|
||||
(window as any)[name] = newService;
|
||||
|
||||
return newService;
|
||||
};
|
||||
|
||||
decorated.prototype = originalConstrutor.prototype;
|
||||
Object.keys(originalConstrutor).forEach((name: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
decorated[name] = (<any>originalConstrutor)[name];
|
||||
});
|
||||
|
||||
return decorated;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
class ServiceRegistry {
|
||||
services: { [key: string]: unknown };
|
||||
|
||||
constructor() {
|
||||
this.services = {};
|
||||
}
|
||||
|
||||
register(name: string, service: unknown) {
|
||||
if (!service || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.services[name] = service;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServiceRegistry();
|
||||
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import JWTStorage from 'app/features/auth/jwt-storage-service';
|
||||
import Logger from './logger-service';
|
||||
|
||||
class Requests {
|
||||
logger: Logger.Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = Logger.getLogger('HTTPRequest');
|
||||
}
|
||||
|
||||
request(
|
||||
type: 'post' | 'get' | 'put' | 'delete',
|
||||
route: string,
|
||||
data: string,
|
||||
callback: (result: string | any) => void,
|
||||
options: { disableJWTAuthentication?: boolean; withBlob?: boolean } = {},
|
||||
) {
|
||||
this.logger.trace(`${type} ${route}`);
|
||||
if (options?.disableJWTAuthentication) {
|
||||
fetch(route, {
|
||||
credentials: 'same-origin',
|
||||
method: type,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(data ? { 'Content-Type': 'application/json' } : {}),
|
||||
Authorization: JWTStorage.getAutorizationHeader(),
|
||||
},
|
||||
body: type === 'post' ? data || '{}' : undefined,
|
||||
})
|
||||
.then(response => {
|
||||
if (options.withBlob) {
|
||||
response.blob().then(blob => {
|
||||
this.retrieveJWTToken(JSON.stringify(blob));
|
||||
callback && callback(blob);
|
||||
});
|
||||
} else {
|
||||
response.text().then(text => {
|
||||
if (text) this.retrieveJWTToken(text);
|
||||
callback && callback(text);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.logger.error('Error while sending HTTP request', err);
|
||||
callback && callback(JSON.stringify({ errors: [err] }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
JWTStorage.authenticateCall(() => {
|
||||
options = options || {};
|
||||
options.disableJWTAuthentication = true;
|
||||
this.request(type, route, data, callback, options);
|
||||
});
|
||||
}
|
||||
|
||||
retrieveJWTToken(rawBody: string) {
|
||||
try {
|
||||
const body = JSON.parse(rawBody);
|
||||
if (body.access_token) {
|
||||
JWTStorage.updateJWT(body.access_token);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error while reading jwt tokens from: ' + rawBody, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Requests();
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { isEqual } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const globalEffectDepsMap = new Map<string, ReadonlyArray<any>>();
|
||||
|
||||
export const flushGlobalEffects = () => {
|
||||
globalEffectDepsMap.clear();
|
||||
};
|
||||
|
||||
export const useGlobalEffect = (key: string, callback: () => void, deps: ReadonlyArray<any>) => {
|
||||
useEffect(() => {
|
||||
if (isEqual(globalEffectDepsMap.get(key), deps) === false) {
|
||||
globalEffectDepsMap.set(key, deps);
|
||||
callback();
|
||||
}
|
||||
}, deps);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Maybe } from 'app/features/global/types/global-types';
|
||||
import Globals from '../services/globals-twake-app-service';
|
||||
import JWTStorage from '../../auth/jwt-storage-service';
|
||||
import Logger from '../framework/logger-service';
|
||||
|
||||
const logger = Logger.getLogger('useHTTP');
|
||||
|
||||
const useGetHTTP = <T>(path: string): [Maybe<T>, Maybe<Error>] => {
|
||||
const url = `${Globals.api_root_url}/internal/services/${path}`;
|
||||
const [response, setResponse] = useState<T>();
|
||||
const [error, setError] = useState<Error>();
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: JWTStorage.getAutorizationHeader(),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
})
|
||||
.then(response => {
|
||||
logger.debug(url, response.status);
|
||||
return response;
|
||||
})
|
||||
.then(response => response.json() as unknown as T)
|
||||
.then(r => setResponse(r))
|
||||
.catch(err => setError(err));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
|
||||
return [response, error];
|
||||
};
|
||||
|
||||
export {
|
||||
useGetHTTP,
|
||||
// TODO: Other methods
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import Logger from 'app/features/global/framework/logger-service';
|
||||
|
||||
const logger = Logger.getLogger('useInterval');
|
||||
|
||||
const useInterval = (callback: () => void, delay: number | null) => {
|
||||
const savedCallback = useRef(callback);
|
||||
|
||||
// Remember the latest callback if it changes.
|
||||
useLayoutEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
// Don't schedule if no delay is specified.
|
||||
if (!delay) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setInterval(() => {
|
||||
logger.debug(`Running interval ${id}`);
|
||||
savedCallback.current();
|
||||
}, delay);
|
||||
logger.debug(`Created interval ${id} with delay ${delay}`);
|
||||
|
||||
return () => {
|
||||
logger.debug(`Clearing interval ${id}`);
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [delay]);
|
||||
};
|
||||
|
||||
export default useInterval;
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { RefObject, useEffect, useState } from 'react';
|
||||
|
||||
export default function useOnScreen(ref: RefObject<Element>) {
|
||||
const [isIntersecting, setIntersecting] = useState(false);
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting));
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
useEffect(() => {
|
||||
observer.observe(ref.current!);
|
||||
// Remove the observer as soon as the component is unmounted
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
return isIntersecting;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import useWebSocket from 'app/features/global/hooks/use-websocket';
|
||||
import Logger from 'app/features/global/framework/logger-service';
|
||||
import {
|
||||
RealtimeBaseAction,
|
||||
RealtimeBaseEvent,
|
||||
RealtimeResourceEvent,
|
||||
} from '../types/realtime-types';
|
||||
import { WebsocketRoom } from 'app/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 roomName
|
||||
* @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 };
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function useTimeout(callback: () => void, delay: number | null) {
|
||||
const savedCallback = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (delay === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setTimeout(() => savedCallback.current(), delay);
|
||||
|
||||
return () => clearTimeout(id);
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
export default useTimeout;
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
import Logger from 'app/features/global/framework/logger-service';
|
||||
|
||||
let initiatedService = false;
|
||||
const logger = Logger.getLogger(`app/features/global/use-usetiful`);
|
||||
export default function useUsetiful() {
|
||||
const { user } = useCurrentUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.id && !initiatedService) {
|
||||
initiatedService = true;
|
||||
|
||||
if (Globals.environment.usetiful_token?.length) {
|
||||
(window as any).usetifulTags = { userId: user.id };
|
||||
(function (w, d, s) {
|
||||
const a = d.getElementsByTagName('head')[0];
|
||||
const r = d.createElement('script');
|
||||
r.async = true;
|
||||
r.src = s;
|
||||
r.setAttribute('id', 'usetifulScript');
|
||||
r.dataset.token = Globals.environment.usetiful_token;
|
||||
a.appendChild(r);
|
||||
})(window, document, 'https://www.usetiful.com/dist/usetiful.js');
|
||||
}
|
||||
} else {
|
||||
if (!initiatedService) {
|
||||
logger.warn(`Usetiful not enabled`);
|
||||
}
|
||||
}
|
||||
}, [user]);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import WebSocketFactory, { WebsocketEvents } from 'app/features/global/types/websocket-types';
|
||||
import WebSocketService from 'app/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;
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { Modal } from 'antd';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { TwakeService } from '../framework/registry-decorator-service';
|
||||
|
||||
const { confirm, info } = Modal;
|
||||
|
||||
type Options = {
|
||||
title?: string;
|
||||
text?: string;
|
||||
};
|
||||
@TwakeService('Alert')
|
||||
class AlertServiceService {
|
||||
alert(onClose: () => void, options?: Options) {
|
||||
info({
|
||||
title: options?.title || options?.text || '',
|
||||
content: options?.text || '',
|
||||
onCancel: onClose,
|
||||
});
|
||||
}
|
||||
|
||||
confirm(onConfirm: () => void, onClose: (() => void) | false = () => {}, options?: Options) {
|
||||
confirm({
|
||||
title: options?.title || Languages.t('components.alert.confirm'),
|
||||
content: options?.text || Languages.t('components.alert.confirm_click'),
|
||||
onOk: onConfirm,
|
||||
onCancel: onClose || undefined,
|
||||
cancelButtonProps: onClose ? {} : { style: { display: 'none' } },
|
||||
});
|
||||
}
|
||||
}
|
||||
const AlertManager = new AlertServiceService();
|
||||
|
||||
export default AlertManager;
|
||||
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// Define feature names here
|
||||
export enum FeatureNames {
|
||||
GUESTS = 'chat:guests',
|
||||
MESSAGE_HISTORY = 'chat:message_history',
|
||||
MESSAGE_HISTORY_LIMIT = 'chat:message_history_limit',
|
||||
MULTIPLE_WORKSPACES = 'chat:multiple_workspaces',
|
||||
EDIT_FILES = 'chat:edit_files',
|
||||
UNLIMITED_STORAGE = 'chat:unlimited_storage', //Currently inactive
|
||||
COMPANY_INVITE_MEMBER = 'company:invite_member',
|
||||
}
|
||||
|
||||
export type FeatureValueType = boolean | number;
|
||||
|
||||
const availableFeaturesWithDefaults = new Map<FeatureNames, any>();
|
||||
|
||||
// Define available features with defaults here
|
||||
availableFeaturesWithDefaults.set(FeatureNames.GUESTS, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.MESSAGE_HISTORY, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.MESSAGE_HISTORY_LIMIT, 10000);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.MULTIPLE_WORKSPACES, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.EDIT_FILES, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.UNLIMITED_STORAGE, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_INVITE_MEMBER, true);
|
||||
|
||||
/**
|
||||
* ChannelServiceImpl that allow you to manage feature flipping in Twake using react feature toggles
|
||||
*/
|
||||
class FeatureTogglesService {
|
||||
public activeFeatureNames: FeatureNames[];
|
||||
private activeFeatureValues: Map<FeatureNames, FeatureValueType>;
|
||||
|
||||
constructor() {
|
||||
(window as any).FeatureTogglesService = this;
|
||||
this.activeFeatureNames = [];
|
||||
this.activeFeatureValues = new Map<FeatureNames, FeatureValueType>();
|
||||
|
||||
// We need to set with default features
|
||||
this.setFeaturesFromCompanyPlan({
|
||||
features: {},
|
||||
});
|
||||
}
|
||||
|
||||
public setFeaturesFromCompanyPlan(plan: { features: { [key: string]: FeatureValueType } }): void {
|
||||
for (const [featureName, defaultValue] of availableFeaturesWithDefaults) {
|
||||
this.setActiveFeatureName(
|
||||
featureName,
|
||||
plan.features[featureName] !== undefined ? plan.features[featureName] : defaultValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setActiveFeatureName(featureName: FeatureNames, value: FeatureValueType): void {
|
||||
if (typeof value === 'boolean') {
|
||||
this.activeFeatureNames = this.activeFeatureNames.filter(name => name !== featureName);
|
||||
if (value) this.activeFeatureNames.push(featureName);
|
||||
this.activeFeatureValues.set(featureName, !!value);
|
||||
} else {
|
||||
this.activeFeatureValues.set(featureName, value);
|
||||
}
|
||||
}
|
||||
|
||||
public isActiveFeatureName(featureName: FeatureNames) {
|
||||
return this.activeFeatureNames.includes(featureName);
|
||||
}
|
||||
|
||||
public getFeatureValue<T>(featureName: FeatureNames): T {
|
||||
return this.activeFeatureValues.get(featureName) as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
export default new FeatureTogglesService();
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import environment from 'environment/environment';
|
||||
import version from 'environment/version';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import LocalStorage from '../framework/local-storage-service';
|
||||
import { EnvironmentType, EnvironmentVersionType } from 'app/environment/types';
|
||||
import ServiceRegistry from '../framework/registry-service';
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && (window as any).sentry_dsn) {
|
||||
Sentry.init({
|
||||
dsn: (window as any).sentry_dsn,
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).getBoundingClientRect = (element: Element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
x: rect.x || rect.left,
|
||||
y: rect.y || rect.top,
|
||||
};
|
||||
};
|
||||
|
||||
document.body.addEventListener('dragover', e => e.preventDefault());
|
||||
document.body.addEventListener('dragenter', e => e.preventDefault());
|
||||
document.body.addEventListener('drop', e => e.preventDefault());
|
||||
|
||||
class TwakeApp {
|
||||
window: Window;
|
||||
environment: EnvironmentType;
|
||||
store_public_access_get_data: any;
|
||||
version: EnvironmentVersionType;
|
||||
services: any;
|
||||
api_root_url: string;
|
||||
|
||||
constructor() {
|
||||
this.services = ServiceRegistry;
|
||||
this.environment = environment as EnvironmentType;
|
||||
this.version = version;
|
||||
this.window = window;
|
||||
this.api_root_url = this.environment.api_root_url || '';
|
||||
|
||||
// FIXME: Deprecated, need to check all the places where the values are used
|
||||
Object.keys(environment).forEach(key => {
|
||||
if (!(this.window as any)[key]) {
|
||||
(this.window as any)[key] = (environment as any)[key];
|
||||
}
|
||||
});
|
||||
|
||||
const apiRootUrl = LocalStorage.getItem<string>('api_root_url');
|
||||
|
||||
if (apiRootUrl) {
|
||||
this.api_root_url = apiRootUrl;
|
||||
}
|
||||
|
||||
this.store_public_access_get_data = undefined;
|
||||
}
|
||||
|
||||
getDefaultLanguage(): string {
|
||||
return (navigator || {}).language || 'en';
|
||||
}
|
||||
}
|
||||
|
||||
const app = new TwakeApp();
|
||||
(window as any).TwakeApp = app;
|
||||
export default app;
|
||||
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Logger from 'app/features/global/framework/logger-service';
|
||||
import Observable from 'app/deprecated/Observable/Observable';
|
||||
import Api from 'app/features/global/framework/api-service';
|
||||
import { getCompany } from 'app/features/companies/state/companies';
|
||||
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
|
||||
export type ConsoleConfiguration = {
|
||||
authority: string;
|
||||
client_id: string;
|
||||
max_unverified_days: number;
|
||||
account_management_url: string;
|
||||
company_subscription_url: string;
|
||||
company_management_url: string;
|
||||
collaborators_management_url: string;
|
||||
};
|
||||
|
||||
export type InternalConfiguration = {
|
||||
disable_account_creation: boolean;
|
||||
disable_email_verification: boolean;
|
||||
};
|
||||
|
||||
export type ServerInfoType = null | {
|
||||
status: 'ready';
|
||||
version: {
|
||||
current: string;
|
||||
minimal: {
|
||||
web: string;
|
||||
mobile: string;
|
||||
};
|
||||
};
|
||||
branding?: {
|
||||
logo: string;
|
||||
};
|
||||
auth: Array<string>;
|
||||
configuration: {
|
||||
branding: any;
|
||||
help_url: string | null;
|
||||
pricing_plan_url: string | null;
|
||||
app_download_url: string | null;
|
||||
mobile: {
|
||||
mobile_redirect: string;
|
||||
mobile_appstore: string;
|
||||
mobile_googleplay: string;
|
||||
};
|
||||
accounts: {
|
||||
type: 'console' | 'internal';
|
||||
console?: ConsoleConfiguration;
|
||||
internal?: InternalConfiguration;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
class InitService extends Observable {
|
||||
public server_infos: ServerInfoType = null;
|
||||
public server_infos_loaded = false;
|
||||
public app_ready = false;
|
||||
private logger = Logger.getLogger('InitService');
|
||||
|
||||
getConsoleLink(
|
||||
link:
|
||||
| 'account_management_url'
|
||||
| 'company_management_url'
|
||||
| 'collaborators_management_url'
|
||||
| 'company_subscription_url',
|
||||
companyId?: string,
|
||||
) {
|
||||
companyId = companyId || WorkspaceService.currentGroupId;
|
||||
const identity_provider_id =
|
||||
getCompany(companyId || '')?.identity_provider_id || getCompany(companyId || '')?.id;
|
||||
return (this.server_infos?.configuration?.accounts?.console?.[link] || '').replace(
|
||||
/\{company_id\}/gm,
|
||||
identity_provider_id,
|
||||
);
|
||||
}
|
||||
|
||||
async getServer() {
|
||||
return await Api.get<ServerInfoType>('/internal/services/general/v1/server', undefined, false, {
|
||||
disableJWTAuthentication: true,
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.server_infos = await this.getServer();
|
||||
|
||||
if (this.server_infos?.status !== 'ready') {
|
||||
this.logger.debug('Server is not ready', this.server_infos);
|
||||
this.app_ready = false;
|
||||
this.notify();
|
||||
setTimeout(() => {
|
||||
this.init();
|
||||
}, 1000);
|
||||
} else {
|
||||
this.logger.debug('Server is ready', this.server_infos);
|
||||
this.server_infos_loaded = true;
|
||||
this.app_ready = true;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new InitService();
|
||||
@@ -0,0 +1,94 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import Observable from 'app/deprecated/CollectionsV1/observable';
|
||||
import DateTime from 'app/features/global/utils/datetime';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Version from 'app/environment/version';
|
||||
class LanguagesService extends Observable {
|
||||
private i18nt: Function | null = null;
|
||||
private language = '';
|
||||
private default = 'en';
|
||||
private available = [
|
||||
'de',
|
||||
'en',
|
||||
'eo',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'it',
|
||||
'ja',
|
||||
'nb_NO',
|
||||
'ru',
|
||||
'si',
|
||||
'tr',
|
||||
'vi',
|
||||
'zh_Hans',
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setObservableName('i18n');
|
||||
(window as any).languageService = this;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.i18nt = await i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next) // passes i18n down to react-i18next
|
||||
.init({
|
||||
fallbackLng: this.default,
|
||||
supportedLngs: this.available,
|
||||
backend: { loadPath: '/locales/{{lng}}.json' + '?v=' + Version.version_detail },
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setLanguage(language: string) {
|
||||
if (!language) language = this.default;
|
||||
|
||||
if (this.language === language) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.language = language;
|
||||
|
||||
if (!language) {
|
||||
language = this.default;
|
||||
}
|
||||
|
||||
await i18n.changeLanguage(language);
|
||||
DateTime.setCurrentLanguage(language);
|
||||
|
||||
this.notify();
|
||||
}
|
||||
|
||||
t(route: string, parameters: any[] = [], fallback?: string) {
|
||||
let replace: any = {};
|
||||
try {
|
||||
if (Array.isArray(parameters)) {
|
||||
(parameters || []).forEach((r, i) => {
|
||||
replace[`$${i + 1}`] = r;
|
||||
});
|
||||
} else if (typeof parameters === 'object') {
|
||||
replace = parameters;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
if (this.i18nt) {
|
||||
return this.i18nt(route, fallback, { replace });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
const Languages = new LanguagesService();
|
||||
export default Languages;
|
||||
@@ -0,0 +1,869 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import UserService from 'app/features/users/services/current-user-service';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import PseudoMarkdownDictionary from 'components/twacode/pseudo-markdown-dictionary';
|
||||
import anchorme from 'anchorme';
|
||||
import emojis_original_service from 'emojione';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
|
||||
class PseudoMarkdownCompiler {
|
||||
saved_messages: { [key: string]: any } = {};
|
||||
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
bullets: { [key: string]: any } = {
|
||||
'• ': () => '• ',
|
||||
'- ': () => '- ',
|
||||
'([0-9]+)\\. ': (match: any) => {
|
||||
const i = parseInt(match[1]);
|
||||
return i + 1 + '. ';
|
||||
},
|
||||
'([a-z])\\. ': (match: any) => {
|
||||
const i = this.alphabet.toLowerCase().indexOf(match[1]);
|
||||
return this.alphabet.toLowerCase()[i + 1] + '. ';
|
||||
},
|
||||
'([A-Z])\\. ': (match: any) => {
|
||||
const i = this.alphabet.indexOf(match[1]);
|
||||
return this.alphabet[i + 1] + '. ';
|
||||
},
|
||||
};
|
||||
|
||||
pseudo_markdown: { [key: string]: any } = {
|
||||
text_block_parent: {
|
||||
name: 'text',
|
||||
object: PseudoMarkdownDictionary.render_block.text_block_parent.object,
|
||||
simple_object: (child: any) => child,
|
||||
},
|
||||
text: {
|
||||
name: 'text',
|
||||
object: PseudoMarkdownDictionary.render_block.text.object,
|
||||
simple_object: (child: any) => child,
|
||||
},
|
||||
'\n': {
|
||||
name: 'br',
|
||||
end: '^',
|
||||
allowed_chars: '.',
|
||||
object: PseudoMarkdownDictionary.render_block.br.object,
|
||||
simple_object: (child: any) => child,
|
||||
},
|
||||
'[': {
|
||||
name: 'markdown_link',
|
||||
allowed_char_before: '', //"(^| )",
|
||||
allowed_chars: '.+?\\]\\([^ ]+',
|
||||
disable_recursion: true,
|
||||
end: '\\)',
|
||||
object: PseudoMarkdownDictionary.render_block.markdown_link.object,
|
||||
},
|
||||
':': {
|
||||
name: 'emoji',
|
||||
allowed_char_before: '', //"(^| )",
|
||||
allowed_chars: '[a-z_]+',
|
||||
disable_recursion: true,
|
||||
end: ':',
|
||||
object: PseudoMarkdownDictionary.render_block.emoji.object,
|
||||
},
|
||||
'@': {
|
||||
name: 'user',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '[a-z_.-A-Z0-9:]+',
|
||||
disable_recursion: true,
|
||||
after_end: ' |$|[^a-zA-Z0-9]',
|
||||
object: PseudoMarkdownDictionary.render_block.user.object,
|
||||
simple_object: (_child: any, obj: any) => '@' + (obj.content || '').split(':')[0] + ' ',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.user as any).text_transform,
|
||||
},
|
||||
'#': {
|
||||
name: 'channel',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '[a-z_.-A-Z0-9\u00C0-\u017F:]+',
|
||||
disable_recursion: true,
|
||||
after_end: ' |$[^a-zA-Z0-9]',
|
||||
object: PseudoMarkdownDictionary.render_block.channel.object,
|
||||
simple_object: (_child: any, obj: any) => '#' + (obj.content || '').split(':')[0] + ' ',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.channel as any).text_transform,
|
||||
},
|
||||
'```': {
|
||||
name: 'mcode',
|
||||
end: '```',
|
||||
after_end: '$|\n',
|
||||
view: true,
|
||||
allowed_chars: '(.|\n)',
|
||||
disable_recursion: true,
|
||||
object: PseudoMarkdownDictionary.render_block.mcode.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.mcode as any).text_transform,
|
||||
simple_object: (_child: any, obj: any) => {
|
||||
const str = (obj.content || '').trim();
|
||||
return str.length > 40 ? str.substr(0, 37) + '...' : str;
|
||||
},
|
||||
},
|
||||
'`': {
|
||||
name: 'icode',
|
||||
end: '`',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '.',
|
||||
disable_recursion: true,
|
||||
object: PseudoMarkdownDictionary.render_block.icode.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.icode as any).text_transform,
|
||||
},
|
||||
__: {
|
||||
name: 'underline',
|
||||
end: '__',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '.',
|
||||
object: PseudoMarkdownDictionary.render_block.underline.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.underline as any).text_transform,
|
||||
},
|
||||
'~~': {
|
||||
name: 'strikethrough',
|
||||
end: '~~',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '.',
|
||||
object: PseudoMarkdownDictionary.render_block.strikethrough.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.strikethrough as any).text_transform,
|
||||
},
|
||||
'**': {
|
||||
name: 'bold',
|
||||
end: '\\*\\*',
|
||||
allowed_char_before: '(^|\\B|.)',
|
||||
allowed_chars: '.',
|
||||
object: PseudoMarkdownDictionary.render_block.bold.object,
|
||||
text_transform: PseudoMarkdownDictionary.render_block.bold.text_transform,
|
||||
},
|
||||
'*': {
|
||||
name: 'italic',
|
||||
end: '\\*',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '.',
|
||||
object: PseudoMarkdownDictionary.render_block.italic.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.italic as any).text_transform,
|
||||
},
|
||||
_: {
|
||||
name: 'italic',
|
||||
end: '_',
|
||||
allowed_char_before: '(^|\\B)',
|
||||
allowed_chars: '.',
|
||||
object: PseudoMarkdownDictionary.render_block.italic.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.italic as any).text_transform,
|
||||
},
|
||||
'>>>': {
|
||||
name: 'mquote',
|
||||
allowed_char_before: '^',
|
||||
view: true,
|
||||
end: false,
|
||||
allowed_chars: '(.|\n)',
|
||||
object: PseudoMarkdownDictionary.render_block.mquote.object,
|
||||
simple_object: () => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.mquote as any).text_transform,
|
||||
},
|
||||
'>': {
|
||||
name: 'quote',
|
||||
view: true,
|
||||
allowed_char_before: '^',
|
||||
after_end: '$|\n',
|
||||
object: PseudoMarkdownDictionary.render_block.quote.object,
|
||||
simple_object: () => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.quote as any).text_transform,
|
||||
},
|
||||
};
|
||||
|
||||
pseudo_markdown_types: { [key: string]: any } = {
|
||||
nop: {
|
||||
object: PseudoMarkdownDictionary.render_block.nop.object,
|
||||
simple_object: (child: any) => child,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.nop as any).text_transform,
|
||||
},
|
||||
url: {
|
||||
object: PseudoMarkdownDictionary.render_block.url.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.url as any).text_transform,
|
||||
},
|
||||
email: {
|
||||
object: PseudoMarkdownDictionary.render_block.email.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.email as any).text_transform,
|
||||
},
|
||||
system: {
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.system.object,
|
||||
simple_object: (child: any) => child,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.system as any).text_transform,
|
||||
},
|
||||
file: {
|
||||
view: true,
|
||||
object: PseudoMarkdownDictionary.render_block.file.object,
|
||||
simple_object: () => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.file as any).text_transform,
|
||||
},
|
||||
iframe: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.iframe.object,
|
||||
simple_object: () => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.iframe as any).text_transform,
|
||||
},
|
||||
image: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.image.object,
|
||||
simple_object: () => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.image as any).text_transform,
|
||||
},
|
||||
icon: {
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.icon.object,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.icon as any).text_transform,
|
||||
},
|
||||
progress_bar: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.progress_bar.object,
|
||||
simple_object: (_child: any, object: any) => (object.progress || 0) + '%',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.progress_bar as any).text_transform,
|
||||
},
|
||||
attachment: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.attachment.object,
|
||||
simple_object: (child: any) => child,
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.attachment as any).text_transform,
|
||||
},
|
||||
button: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.button.object,
|
||||
simple_object: (_child: any) => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.button as any).text_transform,
|
||||
},
|
||||
copiable: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.copiable.object,
|
||||
simple_object: (_child: any) => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.copiable as any).text_transform,
|
||||
},
|
||||
input: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.input.object,
|
||||
simple_object: (_child: any) => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.input as any).text_transform,
|
||||
},
|
||||
select: {
|
||||
view: true,
|
||||
apps_only: true,
|
||||
object: PseudoMarkdownDictionary.render_block.select.object,
|
||||
simple_object: (_child: any) => '',
|
||||
text_transform: (PseudoMarkdownDictionary.render_block.select as any).text_transform,
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
Object.keys(this.pseudo_markdown).forEach(id => {
|
||||
const item = this.pseudo_markdown[id];
|
||||
this.pseudo_markdown_types[item.name] = item;
|
||||
});
|
||||
|
||||
(Globals.window as any).pmc = this;
|
||||
}
|
||||
|
||||
compileStringToLinkObject(string: string) {
|
||||
//Monkey hack for new markdown links, not the best place for this code
|
||||
const link_found = anchorme(string.replace(/\[.*?\]\(.*?\)/gm, ''), {
|
||||
list: true,
|
||||
ips: false,
|
||||
files: false,
|
||||
});
|
||||
|
||||
let result: any[] = [];
|
||||
|
||||
if (link_found.length === 0) {
|
||||
return [string];
|
||||
} else {
|
||||
const first_link = link_found[0];
|
||||
const pos = string.indexOf(first_link.raw);
|
||||
if (pos > 0) {
|
||||
result = result.concat(this.compileStringToLinkObject(string.slice(0, pos)));
|
||||
}
|
||||
result.push({
|
||||
type: first_link.reason,
|
||||
content: first_link.raw,
|
||||
});
|
||||
if (pos + first_link.raw.length < string.length) {
|
||||
result = result.concat(
|
||||
this.compileStringToLinkObject(string.slice(pos + first_link.raw.length)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
transformChannelsUsers(str: string) {
|
||||
//Users
|
||||
str = (str || '').replace(
|
||||
/(\B@)([a-z_.-A-Z0-9]*[a-z_A-Z0-9-])(( |$|([^a-zA-Z0-9]|$){2}))/g,
|
||||
(full_match, match1, username, match3) => {
|
||||
const values = username.split(':');
|
||||
if (values.length === 1) {
|
||||
if (username === 'me') {
|
||||
username = UserService.getCurrentUser().username;
|
||||
}
|
||||
let user_id = Collections.get('users').findBy({ username: username })[0];
|
||||
if (user_id && user_id.id) {
|
||||
user_id = user_id.id;
|
||||
return match1 + username + ':' + user_id + match3;
|
||||
} else {
|
||||
return full_match;
|
||||
}
|
||||
} else {
|
||||
return full_match;
|
||||
}
|
||||
},
|
||||
);
|
||||
//Channels
|
||||
str = str.replace(
|
||||
/(\B#)([a-z_.-A-Z0-9\u00C0-\u017F]*[a-z_A-Z0-9-])(( |$|([^a-zA-Z0-9]|$){2}))/g,
|
||||
(full_match, match1, channel, match3) => {
|
||||
const values = channel.split(':');
|
||||
if (values.length === 1) {
|
||||
let channel_id = Collections.get('channels')
|
||||
.findBy({})
|
||||
.filter(
|
||||
(item: { [key: string]: any }) =>
|
||||
(item.name || '').toLocaleLowerCase().replace(/[^a-z0-9_\-.\u00C0-\u017F]/g, '') ===
|
||||
channel,
|
||||
)[0];
|
||||
if (channel_id && channel_id.id) {
|
||||
channel_id = channel_id.id;
|
||||
return match1 + channel + ':' + channel_id + match3;
|
||||
} else {
|
||||
return full_match;
|
||||
}
|
||||
} else {
|
||||
return full_match;
|
||||
}
|
||||
},
|
||||
);
|
||||
return str;
|
||||
}
|
||||
|
||||
transformBackChannelsUsers(str: string) {
|
||||
//Users
|
||||
str = (str || '').replace(/\B(@[^\s]*?):.*?(( |$|[^a-zA-Z0-9-]))/g, '$1$2');
|
||||
//Channels
|
||||
str = str.replace(/\B(#[^\s]*?):.*?(( |$|[^a-zA-Z0-9-]))/g, '$1$2');
|
||||
return str;
|
||||
}
|
||||
|
||||
compileToJSON(str: string, recursive: any = false) {
|
||||
if (!recursive) {
|
||||
const result: any[] = [];
|
||||
const original_str = str;
|
||||
const _str = str.split('```'); //Priority to code
|
||||
_str.forEach((str, i) => {
|
||||
if (i % 2 === 0) {
|
||||
if (str) {
|
||||
str = this.transformChannelsUsers(str);
|
||||
|
||||
emojis_original_service.ascii = true;
|
||||
str = emojis_original_service.shortnameToUnicode(str);
|
||||
str = emojis_original_service.toShort(str);
|
||||
|
||||
const links = this.compileStringToLinkObject(str);
|
||||
links.forEach(item => {
|
||||
if (typeof item === 'string') {
|
||||
result.push(this.compileToJSON(item, true));
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const object = {
|
||||
start: '```',
|
||||
content: str,
|
||||
end: '\n```',
|
||||
};
|
||||
result.push(object);
|
||||
//original_str += "```\n"+str+"\n```";
|
||||
}
|
||||
});
|
||||
|
||||
const all = {
|
||||
original_str: original_str,
|
||||
fallback_string: original_str.substr(0, 280) + (original_str.length > 280 ? '...' : ''),
|
||||
prepared: result,
|
||||
};
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
const original_str = str;
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
let result: any = [];
|
||||
|
||||
let min_index_of = -1;
|
||||
let min_index_of_key: any = null;
|
||||
|
||||
let ret: any = [];
|
||||
Object.keys(this.pseudo_markdown)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.forEach(starting_value => {
|
||||
if (starting_value === 'text') {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed_char_before = this.pseudo_markdown[starting_value].allowed_char_before;
|
||||
let tmp = str;
|
||||
let offset = 0;
|
||||
const indexes = [];
|
||||
let did_match = -1;
|
||||
do {
|
||||
did_match = tmp.indexOf(starting_value);
|
||||
|
||||
let match_char_before =
|
||||
!allowed_char_before ||
|
||||
null !== tmp.slice(0, did_match).match(new RegExp(allowed_char_before + '$', 'gmi'));
|
||||
match_char_before = match_char_before && tmp[did_match - 1] !== '\\';
|
||||
|
||||
tmp = tmp.slice(did_match + 1);
|
||||
if (did_match >= 0 && match_char_before) indexes.push(did_match + offset);
|
||||
offset = offset + did_match + 1;
|
||||
} while (did_match >= 0);
|
||||
|
||||
if (indexes.length > 0) {
|
||||
const mini = Math.min(...indexes);
|
||||
if (min_index_of < 0 || mini < min_index_of) {
|
||||
min_index_of = mini;
|
||||
min_index_of_key = starting_value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
str = original_str;
|
||||
|
||||
if (min_index_of_key) {
|
||||
let str_left = str.substr(0, min_index_of);
|
||||
const char = min_index_of_key;
|
||||
let str_right = str.substr(min_index_of + char.length);
|
||||
|
||||
//Seach end of element in str_right
|
||||
let match: any = -1;
|
||||
let add_to_value = '';
|
||||
while (match < 0 || (match && match[1][match[1].length - 1] === '\\')) {
|
||||
if (match && match !== -1) {
|
||||
//It mean we found an antislashed element
|
||||
add_to_value += match[0];
|
||||
}
|
||||
const countManaged =
|
||||
(this.pseudo_markdown[char].allowed_chars || '').slice(-1) === '+' ||
|
||||
(this.pseudo_markdown[char].allowed_chars || '').slice(-1) === '}';
|
||||
const regex =
|
||||
'^(' +
|
||||
(this.pseudo_markdown[char].allowed_chars || '.') +
|
||||
(countManaged ? '' : '*') +
|
||||
(this.pseudo_markdown[char].end ? '?' : '') +
|
||||
')' +
|
||||
(this.pseudo_markdown[char].end ? '(' + this.pseudo_markdown[char].end + ')' : '');
|
||||
match = str_right.substr(add_to_value.length).match(new RegExp(regex, ''));
|
||||
}
|
||||
let completion_end_char = '';
|
||||
if (match) {
|
||||
match[0] = add_to_value + match[0];
|
||||
match[1] = add_to_value + match[1];
|
||||
completion_end_char = this.pseudo_markdown[char].after_end ? match[3] || '' : '';
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
str_left = str_left + char;
|
||||
result = result.concat(str_left);
|
||||
} else {
|
||||
if (str_left) {
|
||||
result = result.concat(str_left);
|
||||
}
|
||||
|
||||
//Generate object
|
||||
const object = {
|
||||
start: char,
|
||||
content: this.pseudo_markdown[char].disable_recursion
|
||||
? match[1]
|
||||
: this.compileToJSON(match[1], 1),
|
||||
end: match[2],
|
||||
};
|
||||
result.push(object);
|
||||
|
||||
str_right = completion_end_char + str_right.substr(match[0].length);
|
||||
}
|
||||
|
||||
result = result.concat(this.compileToJSON(str_right, 1));
|
||||
|
||||
ret = result;
|
||||
} else {
|
||||
if (original_str) {
|
||||
ret = original_str;
|
||||
} else {
|
||||
ret = [];
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
compileToHTML(
|
||||
json: any,
|
||||
is_app: any = false,
|
||||
event_container: any = undefined,
|
||||
text_transform: any = undefined,
|
||||
) {
|
||||
if (!text_transform) {
|
||||
text_transform = {};
|
||||
}
|
||||
|
||||
if (!json) {
|
||||
return this.pseudo_markdown['text'].object('');
|
||||
}
|
||||
|
||||
if (json.formatted || json.prepared) json = json.formatted || json.prepared;
|
||||
|
||||
if (typeof json === 'string') {
|
||||
json = [json];
|
||||
}
|
||||
|
||||
if (json.type || json.start) {
|
||||
json = [json];
|
||||
}
|
||||
|
||||
let el = null;
|
||||
let child_contain_view = false;
|
||||
const result: any = [];
|
||||
try {
|
||||
json.forEach((item: any) => {
|
||||
if (typeof item === 'string') {
|
||||
result.push(
|
||||
this.pseudo_markdown['text'].object(item, is_app, event_container, text_transform),
|
||||
);
|
||||
} else if (Array.isArray(item)) {
|
||||
el = this.compileToHTML(item, is_app, event_container, text_transform);
|
||||
child_contain_view = child_contain_view || el.child_contain_view;
|
||||
result.push(el);
|
||||
} else {
|
||||
let type = this.pseudo_markdown[item.start];
|
||||
if (item.type === 'compile' && is_app && typeof item.content === 'string') {
|
||||
el = this.compileToHTML(
|
||||
this.compileToJSON(item.content),
|
||||
is_app,
|
||||
event_container,
|
||||
text_transform,
|
||||
);
|
||||
child_contain_view = child_contain_view || el.child_contain_view;
|
||||
result.push(el);
|
||||
} else {
|
||||
if (item.type) {
|
||||
type = this.pseudo_markdown_types[item.type];
|
||||
}
|
||||
if (type) {
|
||||
if (!type.apps_only || is_app) {
|
||||
//If text transform do it
|
||||
const old_text_transform = JSON.parse(JSON.stringify(text_transform));
|
||||
text_transform = JSON.parse(JSON.stringify(text_transform));
|
||||
if (type.text_transform) {
|
||||
Object.keys(type.text_transform).forEach(key => {
|
||||
text_transform[key] = type.text_transform[key];
|
||||
});
|
||||
}
|
||||
|
||||
el = this.compileToHTML(
|
||||
item.content || '',
|
||||
is_app,
|
||||
event_container,
|
||||
text_transform,
|
||||
);
|
||||
if (type.view) {
|
||||
child_contain_view = true;
|
||||
}
|
||||
child_contain_view = child_contain_view || el.child_contain_view;
|
||||
|
||||
result.push(
|
||||
type.object(el, item, event_container, text_transform, child_contain_view),
|
||||
);
|
||||
|
||||
text_transform = old_text_transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return this.pseudo_markdown['text'].object('An error occured while showing this message.');
|
||||
}
|
||||
result.child_contain_view = child_contain_view;
|
||||
|
||||
result.forEach((item: any) => {
|
||||
if (!item.child_contain_view && child_contain_view) {
|
||||
item = this.pseudo_markdown['text_block_parent'].object(item);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
compileToSimpleHTML(
|
||||
json: any,
|
||||
is_app = false,
|
||||
text_transform: any = undefined,
|
||||
result_analysis: any = undefined,
|
||||
) {
|
||||
if (!text_transform) {
|
||||
text_transform = {};
|
||||
}
|
||||
|
||||
if (!json) {
|
||||
return this.pseudo_markdown['text'].object('');
|
||||
}
|
||||
|
||||
if (!result_analysis) {
|
||||
result_analysis = {
|
||||
has_string: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (json.formatted || json.prepared) json = json.formatted || json.prepared;
|
||||
|
||||
if (typeof json === 'string') {
|
||||
json = [json];
|
||||
}
|
||||
|
||||
if (json.type || json.start) {
|
||||
json = [json];
|
||||
}
|
||||
|
||||
const result: any = [];
|
||||
try {
|
||||
json.forEach((item: any) => {
|
||||
if (typeof item === 'string') {
|
||||
result_analysis.has_string = true;
|
||||
result.push(
|
||||
this.pseudo_markdown['text'].object(item, is_app, {}, text_transform, result_analysis),
|
||||
);
|
||||
} else if (Array.isArray(item)) {
|
||||
result.push(this.compileToSimpleHTML(item, is_app, text_transform, result_analysis));
|
||||
} else {
|
||||
let type = this.pseudo_markdown[item.start];
|
||||
if (item.type) {
|
||||
type = this.pseudo_markdown_types[item.type];
|
||||
}
|
||||
if (type) {
|
||||
if (item.type === 'compile' && is_app && typeof item.content === 'string') {
|
||||
result.push(
|
||||
this.compileToSimpleHTML(
|
||||
this.compileToJSON(item.content),
|
||||
is_app,
|
||||
text_transform,
|
||||
result_analysis,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (item.type) {
|
||||
type = this.pseudo_markdown_types[item.type];
|
||||
}
|
||||
if (type) {
|
||||
if (!type.apps_only || is_app) {
|
||||
//If text transform do it
|
||||
const old_text_transform = JSON.parse(JSON.stringify(text_transform));
|
||||
text_transform = JSON.parse(JSON.stringify(text_transform));
|
||||
if (type.text_transform) {
|
||||
Object.keys(type.text_transform).forEach(key => {
|
||||
text_transform[key] = type.text_transform[key];
|
||||
});
|
||||
}
|
||||
|
||||
result.push(
|
||||
(type.simple_object || type.object)(
|
||||
this.compileToSimpleHTML(
|
||||
item.content || '',
|
||||
is_app,
|
||||
text_transform,
|
||||
result_analysis,
|
||||
),
|
||||
item,
|
||||
{},
|
||||
),
|
||||
);
|
||||
|
||||
text_transform = old_text_transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return this.pseudo_markdown['text'].object('An error occured while showing this message.');
|
||||
}
|
||||
|
||||
if (!result_analysis.has_string) {
|
||||
return this.pseudo_markdown['text'].object('No text content to display.');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
compileToText(json: any) {
|
||||
if (!json) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof json === 'string') {
|
||||
json = [json];
|
||||
}
|
||||
|
||||
if (json.original_str) {
|
||||
return this.transformBackChannelsUsers(json.original_str);
|
||||
}
|
||||
|
||||
if (json.type || json.start) {
|
||||
json = [json];
|
||||
}
|
||||
|
||||
let result = '';
|
||||
try {
|
||||
json.forEach((item: any) => {
|
||||
if (typeof item === 'string') {
|
||||
let tmp = item;
|
||||
Object.keys(this.pseudo_markdown).forEach(starting_value => {
|
||||
let starting_value_reg = starting_value;
|
||||
const allowed_chars = this.pseudo_markdown[starting_value].allowed_chars;
|
||||
if (starting_value === '*') {
|
||||
starting_value_reg = '\\*';
|
||||
}
|
||||
if (allowed_chars) {
|
||||
const reg = new RegExp(starting_value_reg, 'gm');
|
||||
tmp = tmp.replace(reg + '(' + allowed_chars + ')', '\\' + starting_value + '$1');
|
||||
} else {
|
||||
// eslint-disable-next-line no-redeclare
|
||||
const reg = new RegExp(starting_value_reg, 'gm');
|
||||
tmp = tmp.replace(reg, '\\' + starting_value);
|
||||
}
|
||||
});
|
||||
|
||||
result += tmp;
|
||||
} else if (Array.isArray(item)) {
|
||||
result += this.compileToText(item);
|
||||
} else {
|
||||
result += item.start || '';
|
||||
result += this.compileToText(item.content);
|
||||
result += item.end || '';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.transformBackChannelsUsers(result);
|
||||
}
|
||||
|
||||
insertAtCursor(myField: any, myValue: any) {
|
||||
//IE support
|
||||
if ((document as any).selection) {
|
||||
myField.focus();
|
||||
const sel = (document as any).selection.createRange();
|
||||
sel.text = myValue;
|
||||
}
|
||||
//MOZILLA and others
|
||||
else if (myField.selectionStart || myField.selectionStart === '0') {
|
||||
const startPos = myField.selectionStart;
|
||||
const endPos = myField.selectionEnd;
|
||||
myField.value =
|
||||
myField.value.substring(0, startPos) +
|
||||
myValue +
|
||||
myField.value.substring(endPos, myField.value.length);
|
||||
} else {
|
||||
myField.value += myValue;
|
||||
}
|
||||
}
|
||||
|
||||
//Call this function after each line break
|
||||
autoCompleteBulletList(input: any, didEnter: any) {
|
||||
const getCursorPos = (input: any) => {
|
||||
if ('selectionStart' in input && document.activeElement === input) {
|
||||
return {
|
||||
start: input.selectionStart,
|
||||
end: input.selectionEnd,
|
||||
};
|
||||
} else if (input.createTextRange) {
|
||||
const sel = (document as any).selection.createRange();
|
||||
if (sel.parentElement() === input) {
|
||||
const rng = input.createTextRange();
|
||||
rng.moveToBookmark(sel.getBookmark());
|
||||
let len = 0;
|
||||
for (len; rng.compareEndPoints('EndToStart', rng) > 0; rng.moveEnd('character', -1)) {
|
||||
len++;
|
||||
rng.setEndPoint('StartToStart', input.createTextRange());
|
||||
}
|
||||
const pos = { start: 0, end: len };
|
||||
for (pos; rng.compareEndPoints('EndToStart', rng) > 0; rng.moveEnd('character', -1)) {
|
||||
pos.start++;
|
||||
pos.end++;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const setCaretPosition = (ctrl: any, pos: any) => {
|
||||
// Modern browsers
|
||||
if (ctrl.setSelectionRange) {
|
||||
ctrl.focus();
|
||||
ctrl.setSelectionRange(pos, pos);
|
||||
|
||||
// IE8 and below
|
||||
} else if (ctrl.createTextRange) {
|
||||
const range = ctrl.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd('character', pos);
|
||||
range.moveStart('character', pos);
|
||||
range.select();
|
||||
}
|
||||
};
|
||||
|
||||
if (didEnter) {
|
||||
//@ts-ignore
|
||||
const cursor_position = (getCursorPos(input) || {}).start;
|
||||
if (cursor_position === false || cursor_position < 0) {
|
||||
return;
|
||||
}
|
||||
const value = input.value;
|
||||
|
||||
const str_before = value.substr(0, cursor_position);
|
||||
const str_after = value.substr(cursor_position);
|
||||
|
||||
const src_line_before = str_before.split('\n').pop();
|
||||
let addon = '';
|
||||
let to_remove = 0;
|
||||
Object.keys(this.bullets).forEach(regex => {
|
||||
const match = src_line_before.match(new RegExp('^' + regex, ''));
|
||||
if (match) {
|
||||
if (src_line_before.length > match[0].length) {
|
||||
addon = this.bullets[regex](match);
|
||||
} else {
|
||||
to_remove = src_line_before.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (to_remove > 0) {
|
||||
input.value = str_before.substr(0, str_before.length - to_remove) + str_after;
|
||||
setCaretPosition(input, cursor_position - to_remove);
|
||||
} else {
|
||||
input.value = str_before + '\n' + addon + str_after;
|
||||
setCaretPosition(input, cursor_position + addon.length + 1);
|
||||
}
|
||||
|
||||
return input.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const service = new PseudoMarkdownCompiler();
|
||||
export default service;
|
||||
@@ -0,0 +1,194 @@
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import { UserType } from 'app/features/users/types/user';
|
||||
import Strings from 'app/features/global/utils/strings';
|
||||
import UsersService from 'app/features/users/services/current-user-service';
|
||||
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import RouterServices from 'app/features/router/services/router-service';
|
||||
import { getUserParts } from 'app/components/member/user-parts';
|
||||
import Observable from 'app/deprecated/Observable/Observable';
|
||||
import UserAPIClient from '../../users/api/user-api-client';
|
||||
import ChannelsReachableAPIClient from '../../channels/api/channels-reachable-api-client';
|
||||
import ChannelsMineAPIClient from '../../channels/api/channels-mine-api-client';
|
||||
|
||||
export type GenericChannel = {
|
||||
type: 'user' | 'direct' | 'workspace';
|
||||
sortString: string;
|
||||
filterString: string;
|
||||
lastActivity?: number;
|
||||
resource: UserType | ChannelType;
|
||||
};
|
||||
|
||||
class SearchListManager extends Observable {
|
||||
private workspaceChannels: GenericChannel[];
|
||||
private directChannels: GenericChannel[];
|
||||
private users: GenericChannel[];
|
||||
public list: GenericChannel[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.workspaceChannels = [];
|
||||
this.directChannels = [];
|
||||
this.users = [];
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
public async searchAll(
|
||||
search: string,
|
||||
opts?: {
|
||||
onlyChannel?: boolean;
|
||||
userListState?: UserType[];
|
||||
},
|
||||
): Promise<void> {
|
||||
const { workspaceId, companyId } = RouterServices.getStateFromRoute();
|
||||
|
||||
// Reachable
|
||||
let channels: ChannelType[] = [];
|
||||
|
||||
// Direct Channels
|
||||
let directChannels: ChannelType[] = [];
|
||||
|
||||
// Mine
|
||||
let mineWorkspaceChannels: ChannelType[] = [];
|
||||
|
||||
const usersSearched: UserType[] = opts?.userListState ? opts.userListState : [];
|
||||
if (companyId && workspaceId) {
|
||||
channels = await ChannelsReachableAPIClient.get(companyId, workspaceId);
|
||||
directChannels = await ChannelsMineAPIClient.get({ companyId, workspaceId: 'direct' });
|
||||
mineWorkspaceChannels = await ChannelsMineAPIClient.get({ companyId, workspaceId });
|
||||
}
|
||||
|
||||
// Filters
|
||||
this.workspaceChannels = this.filterWorkspaceChannels({
|
||||
channels,
|
||||
mineWorkspaceChannels,
|
||||
});
|
||||
|
||||
this.directChannels = opts?.onlyChannel
|
||||
? []
|
||||
: this.filterDirectChannels({ channels: directChannels });
|
||||
|
||||
this.users = this.filterUsers({ users: usersSearched });
|
||||
// Concat list
|
||||
this.list = [...this.workspaceChannels, ...this.directChannels, ...this.users];
|
||||
|
||||
this.removeDuplicate();
|
||||
|
||||
this.list = this.list
|
||||
.filter(({ filterString }) => {
|
||||
return filterString.toUpperCase().indexOf(search.toUpperCase()) > -1;
|
||||
})
|
||||
.sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0));
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private async searchUsers(text: string) {
|
||||
return UserAPIClient.search<UserType>(Strings.removeAccents(text), {
|
||||
scope: 'company',
|
||||
companyId: Workspaces.currentGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
private filterWorkspaceChannels({
|
||||
channels,
|
||||
mineWorkspaceChannels,
|
||||
}: {
|
||||
channels: ChannelType[];
|
||||
mineWorkspaceChannels: ChannelType[];
|
||||
}) {
|
||||
const workspaceChannels: GenericChannel[] = channels.map(channel => {
|
||||
return {
|
||||
sortString: channel.name || '',
|
||||
filterString: channel.name || '',
|
||||
type: 'workspace',
|
||||
lastActivity: channel.last_activity || 0,
|
||||
resource: channel,
|
||||
};
|
||||
});
|
||||
|
||||
return workspaceChannels.filter(channel => {
|
||||
if (channel.type === 'workspace') {
|
||||
const resource = channel.resource as ChannelType;
|
||||
const isNotAbleToSeeChannel =
|
||||
!this.isChannelMember(mineWorkspaceChannels, resource) &&
|
||||
resource.visibility === 'private';
|
||||
return !isNotAbleToSeeChannel;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private filterDirectChannels({ channels }: { channels: ChannelType[] }) {
|
||||
const directChannels: GenericChannel[] = channels.map((channel: ChannelType) => {
|
||||
const { name } = getUserParts({
|
||||
usersIds: channel.members || [],
|
||||
});
|
||||
|
||||
return {
|
||||
sortString: name,
|
||||
filterString: name,
|
||||
type: 'direct',
|
||||
lastActivity: channel.last_activity || 0,
|
||||
resource: channel,
|
||||
};
|
||||
});
|
||||
|
||||
return directChannels;
|
||||
}
|
||||
|
||||
private filterUsers({ users }: { users: UserType[] }) {
|
||||
const usersSearched: GenericChannel[] = users.map(user => {
|
||||
|
||||
return (
|
||||
user && {
|
||||
sortString: UsersService.getFullName(user),
|
||||
filterString: UsersService.getFullName(user),
|
||||
type: 'user',
|
||||
resource: user,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return usersSearched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicates between direct channels and users
|
||||
*/
|
||||
private removeDuplicate() {
|
||||
const existingUsersIdAsDirectChannels = this.list
|
||||
.filter(
|
||||
userOrChannel =>
|
||||
userOrChannel.type === 'direct' &&
|
||||
((userOrChannel.resource as ChannelType).members?.length || 0) <= 2,
|
||||
)
|
||||
.map(userOrChannel => {
|
||||
const channel = userOrChannel.resource as ChannelType;
|
||||
if (channel.members?.length === 1) {
|
||||
return channel.members[0];
|
||||
}
|
||||
if (channel.members?.length === 2) {
|
||||
const otherUserId = channel.members.filter(
|
||||
id => id !== UsersService.getCurrentUserId(),
|
||||
)[0];
|
||||
return otherUserId;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
this.list = this.list.filter(userOrChannel => {
|
||||
if (userOrChannel.type === 'user') {
|
||||
const user = userOrChannel.resource as UserType;
|
||||
return !existingUsersIdAsDirectChannels.includes(user.id);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private isChannelMember(mine: ChannelType[], resource: ChannelType) {
|
||||
return mine.some(channel => resource.id === channel.id && channel.user_member?.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
const SearchListManagerService = new SearchListManager();
|
||||
|
||||
export default SearchListManagerService;
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Shortcuts } from 'shortcuts';
|
||||
|
||||
const shortcuts = new Shortcuts({
|
||||
target: document,
|
||||
});
|
||||
|
||||
export type ShortcutType = {
|
||||
shortcut: string;
|
||||
handler?: (event: any) => any;
|
||||
};
|
||||
|
||||
export const defaultShortcutsMap = {
|
||||
SEARCH_CHANNEL: 'CmdOrCtrl+K',
|
||||
};
|
||||
|
||||
export const addShortcut = (shortcut: ShortcutType | ShortcutType[]) => {
|
||||
return shortcuts.add(shortcut);
|
||||
};
|
||||
|
||||
export const removeShortcut = (shortcut: ShortcutType | ShortcutType[]) => {
|
||||
return shortcuts.remove(shortcut);
|
||||
};
|
||||
|
||||
export default { addShortcut, removeShortcut };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { message } from 'antd';
|
||||
|
||||
export { message as ToasterService };
|
||||
@@ -0,0 +1,44 @@
|
||||
import Globals from './globals-twake-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();
|
||||
@@ -0,0 +1,261 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import io from 'socket.io-client';
|
||||
import { EventEmitter } from 'events';
|
||||
import Logger from 'app/features/global/framework/logger-service';
|
||||
import { WebsocketEvents, WebSocketListener, WebsocketRoomActions } from '../types/websocket-types';
|
||||
import { Maybe } from 'app/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;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const LoadingState = atomFamily<boolean, string>({
|
||||
key: 'LoadingState',
|
||||
default: () => false,
|
||||
});
|
||||
|
||||
export const LoadingStateInitTrue = atomFamily<boolean, string>({
|
||||
key: 'LoadingStateInitTrue',
|
||||
default: () => true,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const useWebState = atom<boolean>({
|
||||
key: 'useWebState',
|
||||
default: true,
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type ConfigurationType = {
|
||||
auth?: {
|
||||
internal?: {
|
||||
disable_account_creation?: any | null;
|
||||
disable_email_verification?: any | null;
|
||||
use?: boolean;
|
||||
};
|
||||
console?: {
|
||||
max_unverified_days: number;
|
||||
account_management_url: string;
|
||||
collaborators_management_url: string;
|
||||
company_subscription_url: string;
|
||||
company_management_url: string;
|
||||
use: boolean;
|
||||
};
|
||||
};
|
||||
auth_mode?: string[];
|
||||
elastic_search_available?: boolean;
|
||||
help_url?: string;
|
||||
ready?: boolean;
|
||||
version?: {
|
||||
current?: string;
|
||||
minimal?: {
|
||||
web?: string;
|
||||
mobile?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export type Maybe<T> = T | undefined;
|
||||
|
||||
export type Timeout = ReturnType<typeof setTimeout>;
|
||||
@@ -0,0 +1,41 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
@@ -0,0 +1,22 @@
|
||||
import Observable from 'app/deprecated/CollectionsV1/observable.js';
|
||||
|
||||
class Autocomplete extends Observable {
|
||||
constructor() {
|
||||
super();
|
||||
this.observableName = 'autocompleteService';
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
const autocompleteService = new Autocomplete();
|
||||
export default autocompleteService;
|
||||
@@ -0,0 +1,13 @@
|
||||
export async function copyToClipboard(url: string): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} catch (err) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = url;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
class DroppableManager {
|
||||
constructor() {
|
||||
this.drop = {};
|
||||
this.draggingData = {};
|
||||
this.dragging = false;
|
||||
}
|
||||
|
||||
over(key, callback, event) {
|
||||
this.drop[key] = {
|
||||
callback: callback,
|
||||
element: event.target,
|
||||
};
|
||||
}
|
||||
|
||||
out(key) {
|
||||
this.drop[key] = undefined;
|
||||
delete this.drop[key];
|
||||
}
|
||||
|
||||
up() {
|
||||
var that = this;
|
||||
if (!that.draggingData.type || !that.draggingData.data || that.draggingData.data.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (!this.dragging) {
|
||||
return;
|
||||
}
|
||||
this.drop.forEach(el => {
|
||||
if (el && el.callback) {
|
||||
el.callback(that.draggingData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const instanceDroppableManager = new DroppableManager();
|
||||
export default instanceDroppableManager;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
export default class Numbers {
|
||||
static unid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
static humanFileSize(bytes: number, si: boolean) {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
const units = si
|
||||
? ['kb', 'mb', 'gb', 'gb', 'pb', 'eb', 'zb', 'yb']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
let u = -1;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
||||
return `${bytes.toFixed(1)}${units[u]}`;
|
||||
}
|
||||
|
||||
static hexToBase64(str: string) {
|
||||
return btoa(
|
||||
String.fromCharCode.apply(
|
||||
null,
|
||||
str
|
||||
.replace(/\r|\n/g, '')
|
||||
.replace(/([\da-fA-F]{2}) ?/g, '0x$1 ')
|
||||
.replace(/ +$/, '')
|
||||
.split(' ')
|
||||
.map(s => parseInt(s)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static timeuuidToDate(time_str: string) {
|
||||
if (!time_str) {
|
||||
return 0;
|
||||
}
|
||||
const uuid_arr = time_str.split('-');
|
||||
// eslint-disable-next-line no-redeclare
|
||||
time_str = [uuid_arr[2].substring(1), uuid_arr[1], uuid_arr[0]].join('');
|
||||
return parseInt(time_str, 16);
|
||||
}
|
||||
|
||||
static compareTimeuuid(a?: string, b?: string) {
|
||||
return Numbers.timeuuidToDate(a || '') - Numbers.timeuuidToDate(b || '');
|
||||
}
|
||||
|
||||
static minTimeuuid(a?: string, b?: string) {
|
||||
if (!a) return b || '';
|
||||
if (!b) return a || '';
|
||||
return (Numbers.compareTimeuuid(a, b) > 0 ? b : a) || '';
|
||||
}
|
||||
|
||||
static maxTimeuuid(a?: string, b?: string) {
|
||||
if (!a) return b || '';
|
||||
if (!b) return a || '';
|
||||
return (Numbers.compareTimeuuid(a, b) > 0 ? a : b) || '';
|
||||
}
|
||||
|
||||
static convertBases(src: string, srcAlphabet: string, dstAlphabet: string) {
|
||||
// orion elenzil
|
||||
// 20080905
|
||||
|
||||
const getValueOfDigit = function (digit: string, alphabet: string) {
|
||||
const pos = alphabet.indexOf(digit);
|
||||
return pos;
|
||||
};
|
||||
|
||||
const srcBase = srcAlphabet.length;
|
||||
const dstBase = dstAlphabet.length;
|
||||
|
||||
let val = 0;
|
||||
let mlt = 1;
|
||||
|
||||
while (src.length > 0) {
|
||||
const digit = src.charAt(src.length - 1);
|
||||
val += mlt * getValueOfDigit(digit, srcAlphabet);
|
||||
src = src.substring(0, src.length - 1);
|
||||
mlt *= srcBase;
|
||||
}
|
||||
|
||||
let wetint = val;
|
||||
let ret = '';
|
||||
|
||||
while (wetint >= dstBase) {
|
||||
const digitVal = wetint % dstBase;
|
||||
// eslint-disable-next-line no-redeclare
|
||||
const digit = dstAlphabet.charAt(digitVal);
|
||||
ret = digit + ret;
|
||||
wetint /= dstBase;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
const digit = dstAlphabet.charAt(wetint);
|
||||
ret = digit + ret;
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
export const formatTime = (
|
||||
time: number | string,
|
||||
locale?: string,
|
||||
options: { keepTime?: boolean; keepSeconds?: boolean; keepDate?: boolean } = {
|
||||
keepTime: true,
|
||||
}
|
||||
) => {
|
||||
time = new Date(time).getTime();
|
||||
locale = locale || navigator.language;
|
||||
const now = Date.now();
|
||||
const year = new Date(time).getFullYear();
|
||||
const nowYear = new Date(now).getFullYear();
|
||||
const day = 24 * 60 * 60 * 1000;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: nowYear !== year || options?.keepDate ? "numeric" : undefined,
|
||||
month: now - time >= day || options?.keepDate ? "short" : undefined,
|
||||
day: now - time >= day || options?.keepDate ? "numeric" : undefined,
|
||||
hour: now - time < day || options?.keepTime ? "numeric" : undefined,
|
||||
minute: now - time < day || options?.keepTime ? "numeric" : undefined,
|
||||
second: options?.keepSeconds ? "numeric" : undefined,
|
||||
}).format(new Date(time));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import environment from 'app/environment/environment';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
|
||||
export function addApiUrlIfNeeded(url: string, asCssUrl?: boolean): string {
|
||||
function _wrap(url: string): string {
|
||||
return asCssUrl ? `url('${url}')` : url;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return _wrap(url);
|
||||
}
|
||||
|
||||
if (/^http/.test(url)) {
|
||||
return _wrap(url);
|
||||
}
|
||||
|
||||
return _wrap(`${Globals.api_root_url}/${url.replace(/^\//, '').replace(/\/+/g, '/')}`);
|
||||
}
|
||||
|
||||
export function getAsFrontUrl(path: string): string {
|
||||
return `${environment.front_root_url || ''}${path}`;
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
class AceModeList {
|
||||
constructor() {
|
||||
var supportedModes = {
|
||||
ABAP: ['abap'],
|
||||
ABC: ['abc'],
|
||||
ActionScript: ['as'],
|
||||
ADA: ['ada|adb'],
|
||||
Apache_Conf: ['^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd'],
|
||||
AsciiDoc: ['asciidoc|adoc'],
|
||||
Assembly_x86: ['asm|a'],
|
||||
AutoHotKey: ['ahk'],
|
||||
BatchFile: ['bat|cmd'],
|
||||
Bro: ['bro'],
|
||||
C_Cpp: ['cpp|c|cc|cxx|h|hh|hpp|ino'],
|
||||
C9Search: ['c9search_results'],
|
||||
Cirru: ['cirru|cr'],
|
||||
Clojure: ['clj|cljs'],
|
||||
Cobol: ['CBL|COB'],
|
||||
coffee: ['coffee|cf|cson|^Cakefile'],
|
||||
ColdFusion: ['cfm'],
|
||||
CSharp: ['cs'],
|
||||
Csound_Document: ['csd'],
|
||||
Csound_Orchestra: ['orc'],
|
||||
Csound_Score: ['sco'],
|
||||
CSS: ['css'],
|
||||
Curly: ['curly'],
|
||||
D: ['d|di'],
|
||||
Dart: ['dart'],
|
||||
Diff: ['diff|patch'],
|
||||
Dockerfile: ['^Dockerfile'],
|
||||
Dot: ['dot'],
|
||||
Drools: ['drl'],
|
||||
Edifact: ['edi'],
|
||||
Eiffel: ['e|ge'],
|
||||
EJS: ['ejs'],
|
||||
Elixir: ['ex|exs'],
|
||||
Elm: ['elm'],
|
||||
Erlang: ['erl|hrl'],
|
||||
Forth: ['frt|fs|ldr|fth|4th'],
|
||||
Fortran: ['f|f90'],
|
||||
FTL: ['ftl'],
|
||||
Gcode: ['gcode'],
|
||||
Gherkin: ['feature'],
|
||||
Gitignore: ['^.gitignore'],
|
||||
Glsl: ['glsl|frag|vert'],
|
||||
Gobstones: ['gbs'],
|
||||
golang: ['go'],
|
||||
GraphQLSchema: ['gql'],
|
||||
Groovy: ['groovy'],
|
||||
HAML: ['haml'],
|
||||
Handlebars: ['hbs|handlebars|tpl|mustache'],
|
||||
Haskell: ['hs'],
|
||||
Haskell_Cabal: ['cabal'],
|
||||
haXe: ['hx'],
|
||||
Hjson: ['hjson'],
|
||||
HTML: ['html|htm|xhtml|vue|we|wpy'],
|
||||
HTML_Elixir: ['eex|html.eex'],
|
||||
HTML_Ruby: ['erb|rhtml|html.erb'],
|
||||
INI: ['ini|conf|cfg|prefs'],
|
||||
Io: ['io'],
|
||||
Jack: ['jack'],
|
||||
Jade: ['jade|pug'],
|
||||
Java: ['java'],
|
||||
JavaScript: ['js|jsm|jsx'],
|
||||
JSON: ['json'],
|
||||
JSONiq: ['jq'],
|
||||
JSP: ['jsp'],
|
||||
JSSM: ['jssm|jssm_state'],
|
||||
JSX: ['jsx'],
|
||||
Julia: ['jl'],
|
||||
Kotlin: ['kt|kts'],
|
||||
LaTeX: ['tex|latex|ltx|bib'],
|
||||
LESS: ['less'],
|
||||
Liquid: ['liquid'],
|
||||
Lisp: ['lisp'],
|
||||
LiveScript: ['ls'],
|
||||
LogiQL: ['logic|lql'],
|
||||
LSL: ['lsl'],
|
||||
Lua: ['lua'],
|
||||
LuaPage: ['lp'],
|
||||
Lucene: ['lucene'],
|
||||
Makefile: ['^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make'],
|
||||
Markdown: ['md|markdown'],
|
||||
Mask: ['mask'],
|
||||
MATLAB: ['matlab'],
|
||||
Maze: ['mz'],
|
||||
MEL: ['mel'],
|
||||
MIXAL: ['mixal'],
|
||||
MUSHCode: ['mc|mush'],
|
||||
MySQL: ['mysql'],
|
||||
Nix: ['nix'],
|
||||
NSIS: ['nsi|nsh'],
|
||||
ObjectiveC: ['m|mm'],
|
||||
OCaml: ['ml|mli'],
|
||||
Pascal: ['pas|p'],
|
||||
Perl: ['pl|pm'],
|
||||
pgSQL: ['pgsql'],
|
||||
PHP: ['php|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module'],
|
||||
Pig: ['pig'],
|
||||
Powershell: ['ps1'],
|
||||
Praat: ['praat|praatscript|psc|proc'],
|
||||
Prolog: ['plg|prolog'],
|
||||
Properties: ['properties'],
|
||||
Protobuf: ['proto'],
|
||||
Python: ['py'],
|
||||
R: ['r'],
|
||||
Razor: ['cshtml|asp'],
|
||||
RDoc: ['Rd'],
|
||||
Red: ['red|reds'],
|
||||
RHTML: ['Rhtml'],
|
||||
RST: ['rst'],
|
||||
Ruby: ['rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile'],
|
||||
Rust: ['rs'],
|
||||
SASS: ['sass'],
|
||||
SCAD: ['scad'],
|
||||
Scala: ['scala'],
|
||||
Scheme: ['scm|sm|rkt|oak|scheme'],
|
||||
SCSS: ['scss'],
|
||||
SH: ['sh|bash|^.bashrc'],
|
||||
SJS: ['sjs'],
|
||||
Smarty: ['smarty|tpl'],
|
||||
snippets: ['snippets'],
|
||||
Soy_Template: ['soy'],
|
||||
Space: ['space'],
|
||||
SQL: ['sql'],
|
||||
SQLServer: ['sqlserver'],
|
||||
Stylus: ['styl|stylus'],
|
||||
SVG: ['svg'],
|
||||
Swift: ['swift'],
|
||||
Tcl: ['tcl'],
|
||||
Tex: ['tex'],
|
||||
Text: ['txt'],
|
||||
Textile: ['textile'],
|
||||
Toml: ['toml'],
|
||||
TSX: ['tsx'],
|
||||
Twig: ['twig|swig'],
|
||||
Typescript: ['ts|typescript|str'],
|
||||
Vala: ['vala'],
|
||||
VBScript: ['vbs|vb'],
|
||||
Velocity: ['vm'],
|
||||
Verilog: ['v|vh|sv|svh'],
|
||||
VHDL: ['vhd|vhdl'],
|
||||
Wollok: ['wlk|wpgm|wtest'],
|
||||
XML: ['xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml'],
|
||||
XQuery: ['xq'],
|
||||
YAML: ['yaml|yml'],
|
||||
// Add the missing mode "Django" to ext-modelist
|
||||
Django: ['html'],
|
||||
};
|
||||
|
||||
this.modes = [];
|
||||
var Mode = function (name, caption, extensions) {
|
||||
this.name = name;
|
||||
this.caption = caption;
|
||||
this.mode = 'ace/mode/' + name;
|
||||
this.extensions = extensions;
|
||||
var re;
|
||||
if (/\^/.test(extensions)) {
|
||||
re =
|
||||
extensions.replace(/\|(\^)?/g, function (a, b) {
|
||||
return '$|' + (b ? '^' : '^.*\\.');
|
||||
}) + '$';
|
||||
} else {
|
||||
re = '^.*\\.(' + extensions + ')$';
|
||||
}
|
||||
|
||||
this.extRe = new RegExp(re, 'gi');
|
||||
};
|
||||
|
||||
Mode.prototype.supportsFile = function (filename) {
|
||||
return filename.match(this.extRe);
|
||||
};
|
||||
|
||||
var nameOverrides = {
|
||||
ObjectiveC: 'Objective-C',
|
||||
CSharp: 'C#',
|
||||
golang: 'Go',
|
||||
C_Cpp: 'C and C++',
|
||||
Csound_Document: 'Csound Document',
|
||||
Csound_Orchestra: 'Csound',
|
||||
Csound_Score: 'Csound Score',
|
||||
coffee: 'CoffeeScript',
|
||||
HTML_Ruby: 'HTML (Ruby)',
|
||||
HTML_Elixir: 'HTML (Elixir)',
|
||||
FTL: 'FreeMarker',
|
||||
};
|
||||
this.modesByName = {};
|
||||
for (var name in supportedModes) {
|
||||
var data = supportedModes[name];
|
||||
var displayName = (nameOverrides[name] || name).replace(/_/g, ' ');
|
||||
var filename = name.toLowerCase();
|
||||
var mode = new Mode(filename, displayName, data[0]);
|
||||
this.modesByName[filename] = mode;
|
||||
this.modes.push(mode);
|
||||
}
|
||||
}
|
||||
|
||||
getMode(ext) {
|
||||
var mode = this.modesByName.text;
|
||||
for (var i = 0; i < this.modes.length; i++) {
|
||||
if (this.modes[i].supportsFile('.' + ext)) {
|
||||
mode = this.modes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mode.name;
|
||||
}
|
||||
}
|
||||
|
||||
const aceModeList = new AceModeList();
|
||||
export default aceModeList;
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/ru';
|
||||
import 'moment/locale/fr';
|
||||
import 'moment/locale/de';
|
||||
import 'moment/locale/ja';
|
||||
import 'moment/locale/es';
|
||||
import Observable from 'app/deprecated/CollectionsV1/observable.js';
|
||||
import UserService from 'app/features/users/services/current-user-service';
|
||||
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
|
||||
class DateTime extends Observable {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!Globals.window.navigator) {
|
||||
Globals.window.navigator = {};
|
||||
}
|
||||
|
||||
this.observableName = 'dateTimeService';
|
||||
this.locale = this.cleanLocal(
|
||||
Globals.window.navigator.userLanguage || Globals.window.navigator.language || 'en',
|
||||
);
|
||||
}
|
||||
getCurrentLanguage() {
|
||||
return this.locale;
|
||||
}
|
||||
setCurrentLanguage(lang) {
|
||||
this.locale = this.cleanLocal(lang);
|
||||
moment.locale(this.locale);
|
||||
this.notify();
|
||||
}
|
||||
cleanLocal(string) {
|
||||
if (string.split('-').length > 1) {
|
||||
return string.split('-')[0];
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
getDefaultTimeFormat() {
|
||||
var h24list = [
|
||||
'af',
|
||||
'ar-dz',
|
||||
'ar-kw',
|
||||
'ar-ly',
|
||||
'ar-ma',
|
||||
'ar-sa',
|
||||
'ar-tn',
|
||||
'ar',
|
||||
'az',
|
||||
'be',
|
||||
'bg',
|
||||
'bn',
|
||||
'bo',
|
||||
'br',
|
||||
'bs',
|
||||
'ca',
|
||||
'cs',
|
||||
'cv',
|
||||
'cy',
|
||||
'da',
|
||||
'de-at',
|
||||
'de-ch',
|
||||
'de',
|
||||
'dv',
|
||||
'el',
|
||||
'en-au',
|
||||
'en-ca',
|
||||
'en-gb',
|
||||
'en-ie',
|
||||
'en-nz',
|
||||
'eo',
|
||||
'es-do',
|
||||
'es',
|
||||
'et',
|
||||
'eu',
|
||||
'fa',
|
||||
'fi',
|
||||
'fo',
|
||||
'fr-ca',
|
||||
'fr-ch',
|
||||
'fr',
|
||||
'fy',
|
||||
'gd',
|
||||
'gl',
|
||||
'gom-latn',
|
||||
'he',
|
||||
'hi',
|
||||
'hr',
|
||||
'hu',
|
||||
'hy-am',
|
||||
'id',
|
||||
'is',
|
||||
'it',
|
||||
'ja',
|
||||
'jv',
|
||||
'ka',
|
||||
'kk',
|
||||
'km',
|
||||
'kn',
|
||||
'ko',
|
||||
'ky',
|
||||
'lb',
|
||||
'lo',
|
||||
'lt',
|
||||
'lv',
|
||||
'me',
|
||||
'mi',
|
||||
'mk',
|
||||
'ml',
|
||||
'mr',
|
||||
'ms-my',
|
||||
'ms',
|
||||
'my',
|
||||
'nb',
|
||||
'ne',
|
||||
'nl-be',
|
||||
'nl',
|
||||
'nn',
|
||||
'pa-in',
|
||||
'pl',
|
||||
'pt-br',
|
||||
'pt',
|
||||
'ro',
|
||||
'ru',
|
||||
'sd',
|
||||
'se',
|
||||
'si',
|
||||
'sk',
|
||||
'sl',
|
||||
'sq',
|
||||
'sr-cyrl',
|
||||
'sr',
|
||||
'ss',
|
||||
'sv',
|
||||
'sw',
|
||||
'ta',
|
||||
'te',
|
||||
'tet',
|
||||
'th',
|
||||
'tl-ph',
|
||||
'tlh',
|
||||
'tr',
|
||||
'tzl',
|
||||
'tzm-latn',
|
||||
'tzm',
|
||||
'uk',
|
||||
'ur',
|
||||
'uz-latn',
|
||||
'uz',
|
||||
'vi',
|
||||
'x-pseudo',
|
||||
'yo',
|
||||
'zh-cn',
|
||||
'zh-hk',
|
||||
'zh-tw',
|
||||
];
|
||||
if (
|
||||
h24list.indexOf(
|
||||
(UserService.getCurrentUser() || {}).language ||
|
||||
Globals.window.navigator.language ||
|
||||
Globals.window.navigator.userLanguage ||
|
||||
'en',
|
||||
) >= 0
|
||||
) {
|
||||
return 'H:mm';
|
||||
}
|
||||
return 'LT';
|
||||
}
|
||||
isDateFirstInFormat() {
|
||||
var numbers = moment().format('L').split('/');
|
||||
if (numbers[0] === new Date().getDate()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
getDefaultDateFormat() {
|
||||
/*var numbers = moment().format("L").split("/");
|
||||
if(numbers[0] == (new Date()).getDate()){
|
||||
return "DD/MM/YYYY";
|
||||
}else{
|
||||
return "MM/DD/YYYY";
|
||||
}*/
|
||||
return 'LL'; //Default format for country better but US is "May 11, 2019" instead of "11 may 2019"
|
||||
}
|
||||
}
|
||||
|
||||
var x = new DateTime();
|
||||
Globals.window.dateTimeService = x;
|
||||
export default x;
|
||||
@@ -0,0 +1,9 @@
|
||||
export function getDevice() {
|
||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
return 'ios';
|
||||
}
|
||||
if (/Android/i.test(navigator.userAgent)) {
|
||||
return 'android';
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const formatDate = (date?: number) => {
|
||||
return date
|
||||
? new Intl.DateTimeFormat(navigator.languages[0], {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
weekday: 'short',
|
||||
hour12: false,
|
||||
}).format(new Date(date))
|
||||
: '';
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const formatSize = (size?: number) => {
|
||||
if (size && size >= 0) {
|
||||
let pos = 0;
|
||||
while (size > 1024) {
|
||||
size = size / 1024;
|
||||
pos++;
|
||||
}
|
||||
return size.toFixed(2) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB', 'PB'][pos];
|
||||
}
|
||||
return 'Unknown size';
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
*
|
||||
* This helper will make sure of two things:
|
||||
* - we call request as soon as possible
|
||||
* - then we wait options.timeout before calling any new request
|
||||
* - we can avoid the initial instant callback with options.doInitialCallback
|
||||
*/
|
||||
|
||||
const delayedRequests: Map<string, () => Promise<void>> = new Map();
|
||||
const delayedRequestsHasTimout: Map<string, boolean> = new Map();
|
||||
|
||||
const requestIsInProgress: { [key: string]: boolean } = {};
|
||||
|
||||
export const delayRequest = async (
|
||||
key: string,
|
||||
request: () => Promise<void>,
|
||||
options: { timeout: number; doInitialCall: boolean } = { timeout: 1000, doInitialCall: true },
|
||||
) => {
|
||||
if (!delayedRequestsHasTimout.has(key)) {
|
||||
delayedRequestsHasTimout.set(key, true);
|
||||
|
||||
if (options.doInitialCall) {
|
||||
requestIsInProgress[key] = true;
|
||||
try {
|
||||
await request();
|
||||
} catch (e) {
|
||||
requestIsInProgress[key] = false;
|
||||
throw e;
|
||||
}
|
||||
requestIsInProgress[key] = false;
|
||||
} else delayedRequests.set(key, request);
|
||||
|
||||
setTimeout(() => {
|
||||
const request = delayedRequests.get(key);
|
||||
delayedRequestsHasTimout.delete(key);
|
||||
request &&
|
||||
delayRequest(key, request, { ...options, doInitialCall: !requestIsInProgress[key] });
|
||||
delayedRequests.delete(key);
|
||||
}, options.timeout);
|
||||
} else {
|
||||
delayedRequests.set(key, request);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Snapshot, useGotoRecoilSnapshot, useRecoilCallback, useRecoilSnapshot } from 'recoil';
|
||||
|
||||
export const DebugObserver = () => {
|
||||
const snapshot = useRecoilSnapshot();
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('The following atoms have been modified');
|
||||
|
||||
for (const node of snapshot.getNodes_UNSTABLE({ isModified: true })) {
|
||||
console.debug(node.key, snapshot.getLoadable(node));
|
||||
}
|
||||
}, [snapshot]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const DebugButton = () => {
|
||||
const onClick = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
console.debug('Atom values');
|
||||
for (const node of snapshot.getNodes_UNSTABLE()) {
|
||||
const value = await snapshot.getPromise(node);
|
||||
console.debug(node.key, value);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return <button onClick={onClick}>Dump State</button>;
|
||||
};
|
||||
|
||||
export const TimeTravelObserver = () => {
|
||||
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
|
||||
|
||||
const snapshot = useRecoilSnapshot();
|
||||
useEffect(() => {
|
||||
if (snapshots.every(s => s.getID() !== snapshot.getID())) {
|
||||
setSnapshots([...snapshots, snapshot]);
|
||||
}
|
||||
}, [snapshot, snapshots]);
|
||||
|
||||
const gotoSnapshot = useGotoRecoilSnapshot();
|
||||
|
||||
return (
|
||||
<ol>
|
||||
{snapshots.map((snapshot, i) => (
|
||||
<li key={i}>
|
||||
Snapshot {i}
|
||||
<button onClick={() => gotoSnapshot(snapshot)}>Restore</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
};
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export const getBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((result, fail) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function () {
|
||||
result(`${reader.result}`);
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
fail(error);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default class Strings {
|
||||
static verifyMail(email: string) {
|
||||
const re =
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email.toLowerCase());
|
||||
}
|
||||
|
||||
static removeAccents(str: string) {
|
||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
static autoSpaces(element: any, separator: any, size: any, max: any) {
|
||||
if (element.textAreaRef) {
|
||||
element = element.textAreaRef;
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (!separator) {
|
||||
separator = ' ';
|
||||
}
|
||||
if (!size) {
|
||||
size = 5;
|
||||
}
|
||||
if (!max) {
|
||||
size = (5 + 1) * 4;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static convertBase(src: any, srctable: any, desttable: any) {
|
||||
const srclen = srctable.length;
|
||||
const destlen = desttable.length;
|
||||
// first convert to base 10
|
||||
let val = 0;
|
||||
const numlen = src.length;
|
||||
for (let i = 0; i < numlen; i++) {
|
||||
val = val * srclen + srctable.indexOf(src.charAt(i));
|
||||
}
|
||||
if (val < 0) {
|
||||
return 0;
|
||||
}
|
||||
// then covert to any base
|
||||
let r = val % destlen;
|
||||
let res = desttable.charAt(r);
|
||||
let q = Math.floor(val / destlen);
|
||||
while (q) {
|
||||
r = q % destlen;
|
||||
q = Math.floor(q / destlen);
|
||||
res = desttable.charAt(r) + res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export const matchQuery = (query: string, candidate: string) => {
|
||||
return query
|
||||
.split(' ')
|
||||
.every(
|
||||
word =>
|
||||
Strings.removeAccents(candidate)
|
||||
.toLocaleLowerCase()
|
||||
.indexOf(Strings.removeAccents(word).toLocaleLowerCase()) > -1,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The goal of this score is to get closest match for a query and candidates based of
|
||||
* not only number of valid words but also how close words are.
|
||||
*
|
||||
* Example:
|
||||
* query: "a flower"
|
||||
* candidates: "amazing flowers", "flower", "Flower A", "a flower", "a bus", "a"
|
||||
* The result should be in this order:
|
||||
* "a flower"
|
||||
* "Flower A"
|
||||
* "amazing flowers" ("a" and "flower" included but "a" is only 15% of amazing)
|
||||
* "flower" (a not included but flower 100% match)
|
||||
* "a" (flower not included but a 100% match)
|
||||
* "a bus" (a 100% match but parasite words)
|
||||
*
|
||||
* Idea:
|
||||
* 1. Any non letter nor number is a separator
|
||||
* 2. For each words in query we get the percentage of match + We add this percentages multiplied by the word size relative to query
|
||||
* 3. We reduce score for each parasite words by 90%
|
||||
* 4. We add a bonus for full match in the query 110%
|
||||
* query: "a flower", a represent 15% of the query, flower represent 85%
|
||||
* candidates computed:
|
||||
* "a flower" a=1*0.15 flower=1*0.85 parasite=0 => 1 => full match bonus => 1.1
|
||||
* "Flower A" a=1*0.15 flower=1*0.85 parasite=0 => 1
|
||||
* "amazing flower" (a is 15% of amazing) a=0.15*0.15 flower=1*0.85 parasite=0 => 0.87
|
||||
* "flower" a=0 flower=1*0.85 parasite=0 => 0.85
|
||||
* "a" a=1*0.15 flower=0 parasite=0 => 0.15
|
||||
* "a bus" a=1*0.15 flower=0 parasite=1 => 0.15*(0.9^1parasite) => 0.14
|
||||
*/
|
||||
export const distanceFromQuery = (
|
||||
candidates: string | string[],
|
||||
query: string,
|
||||
options?: { booster: number[] },
|
||||
) => {
|
||||
let score = 0;
|
||||
let parasites = 0;
|
||||
|
||||
if (isString(candidates)) {
|
||||
candidates = [candidates];
|
||||
}
|
||||
|
||||
//Step 1
|
||||
Strings.removeAccents(query)
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^a-z0-9]/gm, ' ')
|
||||
.split(' ')
|
||||
|
||||
//Step 2
|
||||
.forEach(queryWord => {
|
||||
const queryWordImportance = queryWord.length / query.replace(/ /gm, '').length;
|
||||
let i = 0;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const boost = options?.booster[i] || 1;
|
||||
i++;
|
||||
Strings.removeAccents(candidate)
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^a-z0-9]/gm, ' ')
|
||||
.split(' ')
|
||||
.map(sanitizedField => {
|
||||
if (sanitizedField?.trim()) {
|
||||
const match =
|
||||
(sanitizedField.length - sanitizedField.replace(queryWord, '').length) /
|
||||
sanitizedField.length;
|
||||
if (match === 0) {
|
||||
parasites += 1;
|
||||
} else {
|
||||
let prefixBoost = 1;
|
||||
if (sanitizedField.indexOf(queryWord) === 0) {
|
||||
prefixBoost = 2;
|
||||
}
|
||||
score += match * queryWordImportance * boost * prefixBoost;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//Step 3
|
||||
score *= Math.pow(0.9, parasites);
|
||||
|
||||
//Step 4
|
||||
const candidateSanitized = Strings.removeAccents(candidates.join(' '))
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^a-z0-9]/gm, ' ');
|
||||
if (
|
||||
candidateSanitized.replace(/ /gm, '').replace(query, '').length !==
|
||||
candidateSanitized.replace(/ /gm, '').length
|
||||
)
|
||||
score *= 1.1;
|
||||
|
||||
return 0 - score;
|
||||
};
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { capitalize } from 'lodash';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
|
||||
class WindowState {
|
||||
public readonly app_name: string = 'Twake';
|
||||
public prefix = '';
|
||||
public suffix = '';
|
||||
|
||||
public allGetParameter() {
|
||||
const result: { [key: string]: string } = {};
|
||||
let tmp: string[] = [];
|
||||
Globals.window.location.search
|
||||
.substr(1)
|
||||
.split('&')
|
||||
.forEach(item => {
|
||||
tmp = item.split('=');
|
||||
result[tmp[0]] = tmp[1];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public findGetParameter(parameterName: string) {
|
||||
let result = null;
|
||||
let tmp = [];
|
||||
Globals.window.location.search
|
||||
.substr(1)
|
||||
.split('&')
|
||||
.forEach(function (item) {
|
||||
tmp = item.split('=');
|
||||
if (tmp[0] === parameterName) {
|
||||
result = decodeURIComponent(tmp[1]);
|
||||
if (tmp[1] === undefined) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public updateTitle(): string {
|
||||
return (document.title = `${this.prefix}${this.app_name}${this.suffix}`);
|
||||
}
|
||||
|
||||
public setSuffix(text?: string) {
|
||||
const separator = '-';
|
||||
this.suffix = text ? ` ${separator} ${capitalize(text)}` : '';
|
||||
|
||||
return this.updateTitle();
|
||||
}
|
||||
|
||||
public setPrefix(notifications_count = 0) {
|
||||
this.prefix = notifications_count > 0 ? `(${notifications_count}) ` : '';
|
||||
|
||||
return this.updateTitle();
|
||||
}
|
||||
|
||||
public nameToUrl(str: string) {
|
||||
return str
|
||||
.trim()
|
||||
.replace(/[ -/]+/g, '_')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^@a-zA-Z0-9_]/g, '');
|
||||
}
|
||||
|
||||
public reduceUUID4(id: string) {
|
||||
if (!id) return undefined;
|
||||
|
||||
return id
|
||||
.replace(/(.)\1{2,3}/g, '$1i')
|
||||
.replace(/(.)\1{1,2}/g, '$1h')
|
||||
.replace(/-/g, 'g');
|
||||
}
|
||||
|
||||
public expandUUID4(id: string) {
|
||||
if (!id) return undefined;
|
||||
|
||||
return (
|
||||
id
|
||||
.replace(/(.)i/g, '$1$1$1')
|
||||
.replace(/(.)h/g, '$1$1')
|
||||
.replace(/[^0-9a-g]/g, '')
|
||||
.replace(/g/g, '-') || undefined
|
||||
);
|
||||
}
|
||||
|
||||
public getInfoFromUrl() {
|
||||
let result: { [key: string]: any } = {};
|
||||
let url: string = document.location.pathname.replace(/^\/client/, '');
|
||||
|
||||
if (url) {
|
||||
if (url.indexOf('/private/') === 0) {
|
||||
url = url.split('/').pop() || '';
|
||||
|
||||
const list = url.split('-');
|
||||
|
||||
result.channel_id = this.expandUUID4(list[1]);
|
||||
result.message = list[2] ? this.expandUUID4(list[2]) : false;
|
||||
|
||||
if (!result.channel_id) result = {};
|
||||
} else {
|
||||
url = url.split('/').pop() || '';
|
||||
const list = url.split('-');
|
||||
const channel_id = list[2];
|
||||
const workspace_id = list[1];
|
||||
|
||||
result.message = list[3] ? this.expandUUID4(list[3]) : false;
|
||||
result.channel_id = this.expandUUID4(channel_id);
|
||||
result.workspace_id = this.expandUUID4(workspace_id);
|
||||
|
||||
if (!result.workspace_id || !result.channel_id) result = {};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.setPrefix();
|
||||
this.setSuffix();
|
||||
}
|
||||
}
|
||||
|
||||
export default new WindowState();
|
||||
@@ -0,0 +1,31 @@
|
||||
import Observable from 'app/deprecated/CollectionsV1/observable.js';
|
||||
import Api from 'app/features/global/framework/api-service';
|
||||
|
||||
class WorkspacePicker extends Observable {
|
||||
constructor() {
|
||||
super();
|
||||
this.observableName = 'workspacePicker';
|
||||
this.searchedWorkspace = null;
|
||||
this.refresh = false;
|
||||
this.searchInput = '';
|
||||
this.searchLabel = '';
|
||||
this.isSearching = false;
|
||||
}
|
||||
|
||||
getWorkspaceByName(name) {
|
||||
var data = {
|
||||
name: name,
|
||||
};
|
||||
this.isSearching = true;
|
||||
this.notify();
|
||||
var that = this;
|
||||
Api.post('/ajax/workspace/getByName', data, function (res) {
|
||||
that.searchedWorkspace = res.data.workspace;
|
||||
that.isSearching = false;
|
||||
that.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const wsPicker = new WorkspacePicker();
|
||||
export default wsPicker;
|
||||
Reference in New Issue
Block a user