feat: init
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user