feat: init

This commit is contained in:
montaghanmy
2023-03-23 11:03:16 +01:00
commit 10fe6f78d1
11518 changed files with 509786 additions and 0 deletions
@@ -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();
@@ -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;