up
This commit is contained in:
146
src/lib/api.ts
Normal file
146
src/lib/api.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import axios, {AxiosResponse} from "axios";
|
||||
|
||||
import {SERVER_API} from "@/config";
|
||||
import {ChatboxTextMessage} from "@/typings/message.d";
|
||||
import {getAdminInfo} from "@/lib/user";
|
||||
import {APIResponse} from "@/typings/network";
|
||||
import {message} from "antd";
|
||||
|
||||
type APIResultType = {status: 'ok'|'error', data?: any, msg?: string};
|
||||
|
||||
const admin_info = getAdminInfo();
|
||||
|
||||
// reference: https://www.npmjs.com/package/axios#axios-api
|
||||
const axios_instance = axios.create({
|
||||
baseURL: SERVER_API,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: admin_info.jwt, // admin_info.jwt contains client_id & admin_id
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function formatAxiosResponse(res: AxiosResponse): APIResultType {
|
||||
if(res.status !== 200){
|
||||
return {
|
||||
status: 'error',
|
||||
msg: 'Server return status: '+ res.status,
|
||||
}
|
||||
}
|
||||
|
||||
let api_response: APIResponse = res.data;
|
||||
|
||||
if(api_response.errCode === 1) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: api_response.msg,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
data: api_response.data,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function notifyApiResult (result: APIResultType, successMsg?: string) : void{
|
||||
if(result.status === 'ok') {
|
||||
message.success(successMsg || 'Cập nhật thành công', 2)
|
||||
}else {
|
||||
message.error('Lỗi xảy ra: ' + result.msg, 20)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const get = async (endpoint: string, params?: object): Promise<APIResultType> => {
|
||||
try {
|
||||
let res: AxiosResponse = await axios_instance.get(endpoint, {
|
||||
params
|
||||
});
|
||||
|
||||
return formatAxiosResponse(res);
|
||||
}catch (e) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: e.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const post = async (endpoint: string, data?: object): Promise<APIResultType> => {
|
||||
try {
|
||||
let res: AxiosResponse = await axios_instance.post(endpoint, data);
|
||||
|
||||
return formatAxiosResponse(res);
|
||||
}catch (e) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: e.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const put = async (endpoint: string, data: object, params?: object): Promise<APIResultType> => {
|
||||
try {
|
||||
let res: AxiosResponse = await axios_instance.put(endpoint, data,{
|
||||
params
|
||||
});
|
||||
|
||||
return formatAxiosResponse(res);
|
||||
}catch (e) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: e.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const patch = async (endpoint: string, data: any, params?: object): Promise<APIResultType> => {
|
||||
try {
|
||||
let res: AxiosResponse = await axios_instance.patch(endpoint, data, {
|
||||
params: params
|
||||
});
|
||||
|
||||
return formatAxiosResponse(res);
|
||||
}catch (e) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: e.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const del = async (endpoint: string, params?: object): Promise<APIResultType> => {
|
||||
try {
|
||||
let res: AxiosResponse = await axios_instance.delete(endpoint, {
|
||||
params
|
||||
});
|
||||
|
||||
return formatAxiosResponse(res);
|
||||
}catch (e) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: e.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
get, post, patch, put, delete: del
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
||||
|
||||
export async function createSingleTag(payload: {tag: string, item_type?: string, item_id?: string|number}) : Promise<boolean> {
|
||||
let result = await api.post('tag/create', payload);
|
||||
return result.status === 'ok';
|
||||
}
|
||||
|
||||
|
||||
export async function getUserChatHistory(opts: {thread_id: string, last_fetch?: number}): Promise<ChatboxTextMessage[]>{
|
||||
let result = await get("chat/history", {tid: opts.thread_id, from: opts.last_fetch});
|
||||
// because old messages order from newest->oldest. we need them in reverse order: oldest->newsest
|
||||
return (result.status === 'ok') ? result.data.list.reverse() : [];
|
||||
}
|
||||
30
src/lib/chatngay.ts
Normal file
30
src/lib/chatngay.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
MAX_ALLOW_IDLE_TIME,
|
||||
} from '@/config';
|
||||
import {currentTimestamp} from "@/lib/utils";
|
||||
import {getUserLastActiveTime} from "@/lib/user";
|
||||
import * as networking from "@/lib/networking";
|
||||
|
||||
|
||||
async function _heartbeat() {
|
||||
// console.log("_heartbeat: yup yup ..." + Date.now());
|
||||
}
|
||||
|
||||
|
||||
// auto-disconnect ws connection to preserve servers' resource and convert to heartbeat
|
||||
export async function selfDisconnect() {
|
||||
let last_active_time = getUserLastActiveTime();
|
||||
let current_time = currentTimestamp();
|
||||
|
||||
if( current_time - last_active_time > MAX_ALLOW_IDLE_TIME ) {
|
||||
networking.disconnect();
|
||||
await _heartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const chatngay = {
|
||||
disconnect: networking.disconnect
|
||||
}
|
||||
|
||||
|
||||
18
src/lib/emitter.ts
Normal file
18
src/lib/emitter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// copied straight from https://stackoverflow.com/questions/62827419/event-driven-approach-in-react
|
||||
// reason: sometimes using Redux approach requires a number of code/boilplate
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {EventType} from "@/typings";
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
const Emitter = {
|
||||
on: (event: EventType, fn: (...args: any[]) => void) => eventEmitter.on(event, fn),
|
||||
once: (event: EventType, fn: (...args: any[]) => void) => eventEmitter.once(event, fn),
|
||||
off: (event: EventType, fn?: (...args: any[]) => void) => eventEmitter.off(event, fn),
|
||||
emit: (event: EventType, payload?: (...args: any[]) => void) => eventEmitter.emit(event, payload)
|
||||
}
|
||||
|
||||
Object.freeze(Emitter);
|
||||
|
||||
export default Emitter;
|
||||
38
src/lib/messaging.ts
Normal file
38
src/lib/messaging.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// 19-05-2021: For dev
|
||||
|
||||
import {ServerMessage, UserMessage} from "@/typings/message";
|
||||
|
||||
|
||||
export const sendTextMessageToServer = (to: string, text: string, local_sequence: number = 0) => {
|
||||
let payload = {
|
||||
type: 'text',
|
||||
content: {
|
||||
to,
|
||||
text,
|
||||
local_sequence
|
||||
}
|
||||
} as UserMessage;
|
||||
|
||||
return sendMessageToServer(payload);
|
||||
}
|
||||
|
||||
|
||||
export const sendMessageToServer = (payload: UserMessage) => {
|
||||
console.log('sendMessageToServer payload');
|
||||
console.log(payload);
|
||||
// TODO:
|
||||
}
|
||||
|
||||
export function handleMessageFromServer(server_message: ServerMessage) {
|
||||
|
||||
console.log('handleMessageFromServer');
|
||||
console.log(server_message);
|
||||
|
||||
// TODO:
|
||||
|
||||
}
|
||||
|
||||
function _checkUserInChatOrRequest(user_id: string|number) : boolean {
|
||||
// TODO:
|
||||
return true;
|
||||
}
|
||||
49
src/lib/networking.ts
Normal file
49
src/lib/networking.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// 19-05-2021: For dev
|
||||
|
||||
import {ServerMessage} from "@/typings/message.d";
|
||||
import {io, Socket} from "socket.io-client";
|
||||
|
||||
import {ConnectionToServerStatusType} from "@/typings/network";
|
||||
|
||||
|
||||
// single & private socket connection
|
||||
let _connected_socket: Socket | null;
|
||||
|
||||
|
||||
export function disconnect(destroy: boolean = false) {
|
||||
if( isConnected() && _connected_socket) {
|
||||
_connected_socket.close(); // close but can re-connect
|
||||
if(destroy) _connected_socket = null; // if destroy, cannot re-connect
|
||||
}else{
|
||||
console.info("Socket is not connected to be disconnected!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getSocket = (): Socket => {
|
||||
return _connected_socket as Socket;
|
||||
}
|
||||
|
||||
|
||||
export const openSocketConnection = () : boolean => {
|
||||
// TODO:
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export const isConnected = (): boolean => {
|
||||
// TODO:
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// only create a ready socket, not connection to the remote server yet. Use openSocketConnection() to connect when time come
|
||||
export function createSocket (
|
||||
endpoint_url: string,
|
||||
opts: {jwt_token: string, [key:string]: any},
|
||||
handleServerMessage: (msg: ServerMessage) => void ,
|
||||
handleNodeConnectionStatus: (status: ConnectionToServerStatusType, message: string) => void
|
||||
) {
|
||||
// TODO:
|
||||
handleNodeConnectionStatus('connect', "Connection succeeded!");
|
||||
}
|
||||
127
src/lib/notification.ts
Normal file
127
src/lib/notification.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {SERVER_STATIC, CHATNGAY_LOGO} from "@/config";
|
||||
import {isMobile} from "@/lib/utils";
|
||||
|
||||
const isDeviceMobile = isMobile();
|
||||
const MAX_SCROLL_TIME = 20;
|
||||
const SCROLLING_TITLE = "Bạn có tin nhắn mới ";
|
||||
|
||||
let _settings = {
|
||||
//sound alert
|
||||
sound_enable : true,//default for all app
|
||||
sound_file : SERVER_STATIC + "/ring_once.ogg",
|
||||
|
||||
//scrolling
|
||||
scroll_page_title_alert : "Bạn có tin nhắn mới ",
|
||||
old_web_page_title : '',
|
||||
is_scrolling : false,
|
||||
}
|
||||
|
||||
type SettingKeyType = keyof typeof _settings;
|
||||
|
||||
|
||||
//NOTE: The Notification permission may only be requested in a secure context.
|
||||
export async function askForBrowserNotificationPermit() {
|
||||
|
||||
// Let's check if the browser supports notifications
|
||||
if (!("Notification" in window)) {
|
||||
console.log("This browser does not support desktop notification");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we need to ask the user for permission
|
||||
if (Notification.permission !== "denied" && Notification.permission !== "granted") {
|
||||
try {
|
||||
let result = await Notification.requestPermission();
|
||||
console.log("requestPermission =" + result);
|
||||
|
||||
} catch (error) {
|
||||
// Safari doesn't return a promise for requestPermissions and it
|
||||
// throws a TypeError. It takes a callback as the first argument
|
||||
if (error instanceof TypeError) {
|
||||
console.log("requestPermission =" + error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function showBrowserNotification(title: string, content?: string) {
|
||||
if (Notification.permission === "granted") {
|
||||
let expire_in_second = (arguments[2]) ? parseInt(arguments[2]) : 10;
|
||||
let options = {
|
||||
body: content,
|
||||
icon: CHATNGAY_LOGO
|
||||
};
|
||||
let n = new Notification(title, options);
|
||||
console.log("Notify: " + content);
|
||||
// auto close after x seconds
|
||||
setTimeout(n.close.bind(n), expire_in_second * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const changeSettings = (new_settings: {[key in SettingKeyType]: any}) => {
|
||||
_settings = {..._settings, ...new_settings};
|
||||
}
|
||||
|
||||
|
||||
export const alertNewMessages = () => {
|
||||
playSound();
|
||||
showBrowserNotification("Bạn có tin nhắn mới");
|
||||
scrollPageTitleStart();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description: play sound if enable, this code supports html5 only
|
||||
* Play method will be blocked by browser: https://stackoverflow.com/questions/57504122/browser-denying-javascript-play
|
||||
*/
|
||||
function playSound() {
|
||||
|
||||
if(!_settings.sound_enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof Audio == 'function') {
|
||||
let audio = new Audio(_settings.sound_file);
|
||||
audio.play();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @author: Hieu
|
||||
* @description: alert new message by scrolling page title
|
||||
*/
|
||||
function scrollPageTitleStart() {
|
||||
|
||||
//stop scroll if on mobile
|
||||
//or user is on the page but the scrolling has not started. If has started, it should continue
|
||||
if(isDeviceMobile || _settings.is_scrolling ) {
|
||||
return ;
|
||||
}
|
||||
|
||||
_settings.is_scrolling = true;
|
||||
_settings.old_web_page_title = document.title;
|
||||
|
||||
let scroll_timeout_id;
|
||||
let track_scroll_count: number = 0;
|
||||
|
||||
(function titleScroller(text) {
|
||||
document.title = text;
|
||||
track_scroll_count += 1;
|
||||
if(track_scroll_count > MAX_SCROLL_TIME) {
|
||||
clearTimeout(scroll_timeout_id);
|
||||
_settings.is_scrolling = false;
|
||||
document.title = _settings.old_web_page_title;
|
||||
return;
|
||||
}
|
||||
|
||||
scroll_timeout_id = setTimeout(function () {
|
||||
titleScroller(text.substr(1) + text.substr(0, 1));
|
||||
}, 500);
|
||||
}(SCROLLING_TITLE));
|
||||
}
|
||||
79
src/lib/personalize.ts
Normal file
79
src/lib/personalize.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
let _settings = {
|
||||
//chat notification option
|
||||
notify : {
|
||||
sound_enable : true //default
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function getSettings() {
|
||||
return _settings;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @date: 22-02-2016
|
||||
* @author: Hieu
|
||||
* @description: set sound on or off
|
||||
*/
|
||||
export function setSound () {
|
||||
|
||||
let $sound_on_off: string = '';
|
||||
|
||||
if(_settings.notify.sound_enable) {
|
||||
//turn off
|
||||
_settings.notify.sound_enable = false;
|
||||
//saveLocalData('chatngay_sound', 'off');
|
||||
$sound_on_off = 'Bật âm thanh';
|
||||
|
||||
}else{
|
||||
//turn on
|
||||
_settings.notify.sound_enable = true;
|
||||
//deleteLocalData('chatngay_sound');
|
||||
$sound_on_off = 'Tắt âm thanh';
|
||||
}
|
||||
|
||||
return $sound_on_off;
|
||||
|
||||
// getIframeElement("chatngay-sound-txt").innerHTML = $sound_on_off;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @date: 22-02-2016
|
||||
* @author: Hieu
|
||||
* @description: build user_info when page loaded
|
||||
*/
|
||||
export function getUserInfo () {
|
||||
|
||||
/*//get saved name if provided previously
|
||||
user_info.id = getLocalData('chatngay_uid');
|
||||
user_info.name = getLocalData('chatngay_uname');
|
||||
user_info.token = getLocalData('chatngay_utoken');
|
||||
|
||||
//check if user has disabled sound before and update when page reloads
|
||||
if(getLocalData('chatngay_sound') == 'off') {
|
||||
_settings.notify.sound_enable = false;
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* @date: 28-02-2016
|
||||
* @author: Hieu
|
||||
* @description: build user_info when page loaded
|
||||
*/
|
||||
export function saveUserInfo (server_response: any) {
|
||||
|
||||
/*user_info.id = server_response.id;
|
||||
saveLocalData('chatngay_uid', server_response.id, 300);
|
||||
|
||||
if(server_response.token != '') {
|
||||
user_info.token = server_response.token;
|
||||
saveLocalData('chatngay_utoken', server_response.token, 300);
|
||||
}
|
||||
|
||||
if(server_response.name != '') {
|
||||
user_info.name = server_response.name;
|
||||
saveLocalData('chatngay_uname', server_response.name, 300);
|
||||
}*/
|
||||
}
|
||||
96
src/lib/public_ip.ts
Normal file
96
src/lib/public_ip.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// https://github.com/sindresorhus/public-ip/blob/master/browser.js
|
||||
|
||||
class CancelError extends Error {
|
||||
constructor() {
|
||||
super('Request was cancelled');
|
||||
this.name = 'CancelError';
|
||||
}
|
||||
|
||||
get isCanceled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
const urls = {
|
||||
v4: [
|
||||
'https://ipv4.icanhazip.com/',
|
||||
'https://api.ipify.org/'
|
||||
],
|
||||
v6: [
|
||||
'https://ipv6.icanhazip.com/',
|
||||
'https://api6.ipify.org/'
|
||||
]
|
||||
};
|
||||
|
||||
const sendXhr = (url: string, options: { timeout: number; }, version: string | number) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
let _reject: { (arg0: CancelError): void; (reason?: any): void; };
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
_reject = reject;
|
||||
xhr.addEventListener('error', reject, {once: true});
|
||||
xhr.addEventListener('timeout', reject, {once: true});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
const ip = xhr.responseText.trim();
|
||||
|
||||
if (!ip) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(ip);
|
||||
}, {once: true});
|
||||
|
||||
xhr.open('GET', url);
|
||||
xhr.timeout = options.timeout;
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
promise.cancel = () => {
|
||||
xhr.abort();
|
||||
_reject(new CancelError());
|
||||
};
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
const queryHttps = (version: string, options: any) => {
|
||||
let request: any;
|
||||
const promise = (async function () {
|
||||
// @ts-ignore
|
||||
const urls_ = [].concat.apply(urls[version], options.fallbackUrls || []);
|
||||
for (const url of urls_) {
|
||||
try {
|
||||
request = sendXhr(url, options, version);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
return await request;
|
||||
} catch (error) {
|
||||
if (error instanceof CancelError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Couldn\'t find your IP');
|
||||
})();
|
||||
|
||||
// @ts-ignore
|
||||
promise.cancel = () => {
|
||||
request.cancel();
|
||||
};
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
const public_ip = {
|
||||
v4: (options: any) => queryHttps('v4', {...defaults, ...options}),
|
||||
v6: (options: any) => queryHttps('v6', {...defaults, ...options}),
|
||||
};
|
||||
|
||||
export default public_ip;
|
||||
31
src/lib/registry.ts
Normal file
31
src/lib/registry.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// simple global objects
|
||||
let _registry: {[k: string]: any} = {};
|
||||
|
||||
function update(key: string, value: any) {
|
||||
_registry[key] = value;
|
||||
}
|
||||
|
||||
function get(key: string): any {
|
||||
return (_registry.hasOwnProperty(key)) ? _registry[key] : undefined;
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
if(!_registry.hasOwnProperty(key)) return ;
|
||||
|
||||
delete _registry[key];
|
||||
}
|
||||
|
||||
function clear() {
|
||||
for (let member in _registry) {
|
||||
delete _registry[member];
|
||||
}
|
||||
}
|
||||
|
||||
const registry = {
|
||||
update,
|
||||
get,
|
||||
remove,
|
||||
clear
|
||||
}
|
||||
|
||||
export default registry;
|
||||
57
src/lib/schedule.ts
Normal file
57
src/lib/schedule.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
class SimpleSchedule {
|
||||
|
||||
private task_is_running: boolean;
|
||||
private taskCaller: () => Promise<any>;
|
||||
private trackInterval: NodeJS.Timeout | null;
|
||||
private checkInterval: number; //default
|
||||
|
||||
constructor(taskFn: () => Promise<any>, checkInterval: number = 5) {
|
||||
this.taskCaller = taskFn;
|
||||
this.task_is_running = false;
|
||||
this.trackInterval = null;
|
||||
this.checkInterval = checkInterval;
|
||||
}
|
||||
|
||||
start = () => {
|
||||
console.log(`Schedule ${this.taskCaller.name} started at ${this.timeNow()}`);
|
||||
|
||||
this.trackInterval = setInterval(async () => {
|
||||
// flag for long-running task so another instance wont start
|
||||
if(this.task_is_running) {
|
||||
console.log(`Task ${this.taskCaller.name} is still running. Check time ${this.timeNow()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.task_is_running = true;
|
||||
console.log(`OK invoke ${this.taskCaller.name} at ${this.timeNow()}`);
|
||||
await this.taskCaller();
|
||||
this.task_is_running = false;
|
||||
}, this.checkInterval * 1000 );
|
||||
}
|
||||
|
||||
getInterval = (): number => this.checkInterval;
|
||||
|
||||
changeInterval = (new_interval: number) => {
|
||||
if(new_interval === this.checkInterval) {
|
||||
return; // nothing change!
|
||||
}
|
||||
|
||||
// remove running
|
||||
if(this.trackInterval) clearInterval(this.trackInterval);
|
||||
|
||||
// set and start
|
||||
this.checkInterval = new_interval;
|
||||
this.start();
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
console.log(`Schedule ${this.taskCaller.name} stoped at ${this.timeNow()}`);
|
||||
if(this.trackInterval) clearInterval(this.trackInterval);
|
||||
}
|
||||
|
||||
timeNow = () => {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleSchedule;
|
||||
72
src/lib/security.ts
Normal file
72
src/lib/security.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
export function createChecksum(content: string|object): number {
|
||||
let content_str = typeof content == 'string' ? content : JSON.stringify(content);
|
||||
return crc32(content_str);
|
||||
}
|
||||
|
||||
|
||||
// copy from: https://github.com/wbond/crc32-js-php
|
||||
// javascript: crc32(txt)
|
||||
// php backend: sprintf('%u', crc32(txt))
|
||||
function crc32(txt: string) : number {
|
||||
let table = [
|
||||
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
|
||||
0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
||||
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
|
||||
0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
|
||||
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
|
||||
0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
|
||||
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
|
||||
0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
|
||||
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
|
||||
0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
|
||||
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
|
||||
0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
|
||||
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
|
||||
0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
|
||||
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
|
||||
0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
|
||||
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
|
||||
0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
|
||||
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
|
||||
0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
|
||||
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
|
||||
0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
|
||||
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
|
||||
0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
|
||||
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
|
||||
0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
|
||||
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
|
||||
0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
|
||||
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
|
||||
0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
|
||||
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
|
||||
0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
|
||||
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
|
||||
0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
|
||||
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
|
||||
0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
|
||||
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
|
||||
0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
|
||||
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
|
||||
0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
|
||||
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
|
||||
0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
||||
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
||||
];
|
||||
|
||||
// This converts a unicode string to UTF-8 bytes
|
||||
txt = unescape(encodeURI(txt));
|
||||
let crc = 0 ^ (-1);
|
||||
let len = txt.length;
|
||||
for (let i=0; i < len; i++) {
|
||||
crc = (crc >>> 8) ^ table[(crc ^ txt.charCodeAt(i)) & 0xFF];
|
||||
}
|
||||
crc = crc ^ (-1);
|
||||
// Turns the signed integer into an unsigned integer
|
||||
if (crc < 0) {
|
||||
crc += 4294967296;
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
30
src/lib/storage.ts
Normal file
30
src/lib/storage.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// read: https://web.dev/storage-for-the-web/
|
||||
// https://www.npmjs.com/package/store2 for localStorage
|
||||
// https://www.npmjs.com/package/idb-keyval for IndexedDB
|
||||
|
||||
//import store from "store2";
|
||||
import {createStore, set, get, clear, del, UseStore} from 'idb-keyval';
|
||||
|
||||
const customStore: UseStore = createStore('chatngay', 'chatboard');
|
||||
|
||||
const storage = {
|
||||
async clear() {
|
||||
return await clear(customStore);
|
||||
},
|
||||
async save(key?: string, data?: any) {
|
||||
return (key) ? await set(key, data, customStore) : false;
|
||||
},
|
||||
async get(key?: string) {
|
||||
return (key) ? await get(key, customStore) : null;
|
||||
},
|
||||
async delete(key?: string) {
|
||||
return (key) ? await del(key, customStore) : false;
|
||||
}
|
||||
}
|
||||
|
||||
export default storage;
|
||||
|
||||
|
||||
export const userChatHistoryStorageKey = (user_id?: string|number) => {
|
||||
return user_id ? `chat-history-${user_id}` : undefined;
|
||||
}
|
||||
19
src/lib/theme.ts
Normal file
19
src/lib/theme.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {SERVER_STATIC} from "@/config";
|
||||
|
||||
const CSS_FILES: {[key: string]: string} = {
|
||||
set1: SERVER_STATIC + "/style_1.css",
|
||||
set2: SERVER_STATIC + "/style_2.css",
|
||||
set3: SERVER_STATIC + "/style_3.css"
|
||||
}
|
||||
|
||||
/**
|
||||
* @author: Hieu
|
||||
* @description: get current theme css file
|
||||
*/
|
||||
export function getCSSFile(id: number){
|
||||
if(!CSS_FILES.hasOwnProperty('set'+id)) {
|
||||
return CSS_FILES.set3; //default
|
||||
}
|
||||
|
||||
return CSS_FILES['set'+id];// + '?t=' + getCurrentTimestamp();
|
||||
}
|
||||
4
src/lib/upload.ts
Normal file
4
src/lib/upload.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// 19-05-2021: For dev
|
||||
export const uploadFile = (file: Blob) => {
|
||||
// TODO:
|
||||
}
|
||||
67
src/lib/user.ts
Normal file
67
src/lib/user.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {Dispatch} from "redux";
|
||||
|
||||
import {AdminInfo} from "@/typings/user";
|
||||
import {actions} from "@/store/actions";
|
||||
import {getConnectNode} from "@/config";
|
||||
|
||||
let _user_last_active_time: number = 0;
|
||||
|
||||
|
||||
/*declare global {
|
||||
interface Window {
|
||||
admin_info: AdminInfo
|
||||
}
|
||||
}*/
|
||||
|
||||
const getAdminInfo = () : AdminInfo => {
|
||||
//if(MODE === 'dev') return x as AdminInfo;
|
||||
return window.admin_info || { client_id:'', id: '', name:'', jwt: '', group_id: '', node: '' };
|
||||
}
|
||||
|
||||
|
||||
const getUserSocketConnectionProperty = () : { endpoint: string, token: string } => {
|
||||
const admin_info = getAdminInfo();
|
||||
|
||||
return {
|
||||
endpoint: admin_info.node ? getConnectNode(admin_info.node) : '',
|
||||
token: admin_info.jwt || '',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getUserLastActiveTime = (): number => {
|
||||
return _user_last_active_time;
|
||||
}
|
||||
|
||||
const setUserLastActiveTime = (time: number) => {
|
||||
_user_last_active_time = time;
|
||||
}
|
||||
|
||||
|
||||
// track users change the browser tab
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
|
||||
export function trackVisibilityChange(dispatch: Dispatch) {
|
||||
document.addEventListener("visibilitychange", function() {
|
||||
//console.log( 'document.visibilityState = ' + document.visibilityState );
|
||||
dispatch(actions.changeUserVisibilityState(document.visibilityState));
|
||||
});
|
||||
|
||||
// For safari: Safari doesn’t fire visibilitychange as expected when the value of the visibilityState property transitions to hidden; so for that case, you need to also include code to listen for the pagehide event.
|
||||
// console.log( 'navigator.userAgent = ' + navigator.userAgent );
|
||||
if(navigator.userAgent.indexOf("Safari") !== -1) {
|
||||
//console.log("Yes it;s Safari!");
|
||||
window.addEventListener("pagehide", event => {
|
||||
if (event.persisted) {
|
||||
/* the page isn't being discarded, so it can be reused later */
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
getAdminInfo,
|
||||
getUserSocketConnectionProperty,
|
||||
setUserLastActiveTime,
|
||||
getUserLastActiveTime
|
||||
}
|
||||
311
src/lib/utils.ts
Normal file
311
src/lib/utils.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import {REDIRECTOR_URL} from "@/config";
|
||||
import publicIp from "@/lib/public_ip";
|
||||
//import memoizeOne from 'memoize-one';
|
||||
import api from "@/lib/api";
|
||||
|
||||
|
||||
//format a number for readability
|
||||
//1000 => 1.000
|
||||
//-1000 => -1.000
|
||||
export function formatNumber (num?: number) : string{
|
||||
if(!num || num === 0) return '0';
|
||||
|
||||
const is_negative_number = (num < 0);
|
||||
|
||||
let str = (is_negative_number) ? (num * -1) + '' : num + ''; //convert to string
|
||||
let char_count = str.length;
|
||||
if(char_count <= 3) {
|
||||
return (is_negative_number) ? '-' + str : str;
|
||||
}
|
||||
|
||||
let first_part = str.substr(0, char_count % 3); // num = 10000 => this part = 10
|
||||
let remain_part = str.replace(first_part, "");
|
||||
let num_group = Math.round(remain_part.length/3);
|
||||
|
||||
let parts = [];
|
||||
if(first_part !== '') parts.push( first_part ); // num = 10000 => this part = 10
|
||||
|
||||
for (let i = 0; i < num_group; i++){
|
||||
parts.push( remain_part.substr( i*3, 3));
|
||||
}
|
||||
|
||||
return (is_negative_number) ? '-' + parts.join('.') : parts.join('.');
|
||||
}
|
||||
|
||||
|
||||
export function isBrowserSupport(): boolean {
|
||||
// check support for indexedDB to store various async data
|
||||
if (!window.indexedDB) return false;
|
||||
|
||||
// check localstorage support for redux store persist
|
||||
if ( ! _localStorageAvailable()) return false;
|
||||
|
||||
// other
|
||||
// ...
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
// shamelessly copied from https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
|
||||
function _localStorageAvailable() {
|
||||
let storage;
|
||||
try {
|
||||
// @ts-ignore
|
||||
storage = window['localStorage'];
|
||||
let x = '__storage_test__';
|
||||
storage.setItem(x, x);
|
||||
storage.removeItem(x);
|
||||
return true;
|
||||
}
|
||||
catch(e) {
|
||||
return e instanceof DOMException && (
|
||||
// everything except Firefox
|
||||
e.code === 22 ||
|
||||
// Firefox
|
||||
e.code === 1014 ||
|
||||
// test name field too, because code might not be present
|
||||
// everything except Firefox
|
||||
e.name === 'QuotaExceededError' ||
|
||||
// Firefox
|
||||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
|
||||
// acknowledge QuotaExceededError only if there's something already stored
|
||||
(storage && storage.length !== 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// runUpdateAdminStatus periodically
|
||||
export const runUpdateAdminStatus = (admin_id: string, interval: number = 10) => {
|
||||
|
||||
_run();
|
||||
|
||||
// then check periodically
|
||||
setTimeout(function () {
|
||||
runUpdateAdminStatus(admin_id, interval);
|
||||
}, interval * 1000);
|
||||
|
||||
function _run(){
|
||||
api.post("admin/update-status", {admin_id: admin_id, connected: true}) ;
|
||||
}
|
||||
}
|
||||
|
||||
// check user's internet connection periodically
|
||||
export const runCheckNetworkConnection = (interval: number = 10, cb: (isOnline: boolean) => void ) => {
|
||||
// check onload
|
||||
_runCheck();
|
||||
|
||||
// then check periodically
|
||||
setTimeout(function (){
|
||||
runCheckNetworkConnection(interval, cb);
|
||||
}, interval * 1000);
|
||||
|
||||
function _runCheck(){
|
||||
checkUserInternetConnection().then(cb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function confirmLeavePage() {
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
let confirmationMessage = "Thay đổi trang sẽ mất dữ liệu hiện tại";
|
||||
e.returnValue = confirmationMessage; // Gecko, Trident, Chrome 34+
|
||||
return confirmationMessage; // Gecko, WebKit, Chrome <34
|
||||
});
|
||||
}
|
||||
|
||||
export const showUnixTime = (timestamp: number|undefined) => {
|
||||
//(t) ? dayjs.unix(t).format('DD-MM-YYYY h:mma') : '';
|
||||
if(!timestamp) return '';
|
||||
|
||||
let a = new Date(timestamp * 1000);
|
||||
let months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
let year = a.getFullYear();
|
||||
let month = months[a.getMonth()];
|
||||
let date = a.getDate();
|
||||
let hour = a.getHours();
|
||||
let min = a.getMinutes();
|
||||
//let sec = a.getSeconds();
|
||||
return [date, month, year + " " + hour + ':' + min].join("-") ;
|
||||
}
|
||||
|
||||
|
||||
export function getRandomInt(min: number, max: number) : number {
|
||||
let floor_min = Math.ceil(min);
|
||||
let floor_max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (floor_max - floor_min + 1)) + floor_min;
|
||||
}
|
||||
|
||||
// check whether user's connection is ok
|
||||
// ref: https://github.com/sindresorhus/is-online
|
||||
// https://www.npmjs.com/package/public-ip
|
||||
export async function checkUserInternetConnection(){
|
||||
let options = {
|
||||
timeout: 5000,
|
||||
ipVersion: 4,
|
||||
};
|
||||
|
||||
if (navigator && !navigator.onLine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const publicIpFunctionName = options.ipVersion === 4 ? 'v4' : 'v6';
|
||||
|
||||
try {
|
||||
return await publicIp[publicIpFunctionName](options);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get current timestamp in UTC
|
||||
export function getCurrentUTCTimestamp(microtime: boolean = false) : number {
|
||||
let x = new Date();
|
||||
let micro_time = x.getTime() + x.getTimezoneOffset() * 60 * 1000;
|
||||
return (microtime) ? micro_time : Math.floor(micro_time / 1000);
|
||||
}
|
||||
|
||||
|
||||
export function maskExternalUrl(url: string) {
|
||||
return `${REDIRECTOR_URL}?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
|
||||
// given an array, keep max_size latest items and discard other. return new array
|
||||
export function keepMaxArraySize(arr: any[], max_size: number) {
|
||||
let arr_size = arr.length;
|
||||
|
||||
// no change
|
||||
if(arr_size <= max_size) return arr;
|
||||
|
||||
let copied_arr = [...arr];
|
||||
copied_arr.splice(0, copied_arr.length - max_size);
|
||||
|
||||
return copied_arr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function findObjectInArray(arr: { [key: string]: any }[], key: string, value: any) : {index: number, item: object} {
|
||||
let item: object = {};
|
||||
let match_index = -1;
|
||||
for ( let index = 0; index < arr.length; index ++) {
|
||||
if(arr[index][key] === value) {
|
||||
item = {...arr[index]};
|
||||
match_index = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
index: match_index,
|
||||
item: item
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function createUserId(): string {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
export function currentTimestamp() : number {
|
||||
return Date.now() / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @date 22-02-2016
|
||||
* @author Hieu
|
||||
* @description: replace console.log()
|
||||
* @usage example
|
||||
console(obj)
|
||||
*/
|
||||
export function log(obj: any) {
|
||||
console.log(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @date 26-02-2016
|
||||
* @author Hieu
|
||||
* @description: count number of items in object
|
||||
* @usage example
|
||||
*/
|
||||
export function objectSize( content: object ) {
|
||||
let length = 0;
|
||||
for( let key in content ) {
|
||||
if( content.hasOwnProperty(key) ) {
|
||||
length ++;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @date 09-03-2016
|
||||
* @author http://stackoverflow.com/questions/1500260/detect-urls-in-text-with-javascript
|
||||
* @description: find url in text and replace with clickable a
|
||||
* @usage
|
||||
*/
|
||||
export function formatUrl(text: string) {
|
||||
let urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
return text.replace(urlRegex, '<a href="//www.chatngay.com/redirect.php?url=$1" target="_blank">$1</a>')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @date 03-03-2016
|
||||
* @author http://stackoverflow.com/questions/4959975/generate-random-value-between-two-numbers-in-javascript
|
||||
* @description: get a random number between min-max
|
||||
* @usage example
|
||||
*/
|
||||
export function randomBetween(min: number, max: number) {
|
||||
return Math.floor( Math.random() * ( max - min + 1) + min);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @date 21-02-2016
|
||||
* @author http://youmightnotneedjquery.com/
|
||||
* @description: trim a string, support IE8+
|
||||
* @param str
|
||||
* @return string
|
||||
* @usage example
|
||||
trim(str);
|
||||
*/
|
||||
export function trim(str: string){
|
||||
if (!String.prototype.trim) {
|
||||
//in case of IE 8 or lower
|
||||
return str.replace(/^\s+|\s+$/g, '') ;
|
||||
}else{
|
||||
return str.trim();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @date 22-02-2016
|
||||
* @author Hieu
|
||||
* @description: shorten a string by char count
|
||||
* @usage example
|
||||
subStr(str, char_count)
|
||||
*/
|
||||
export function subStr (str: string | undefined, char_count: number = 30): string {
|
||||
if(!str) return '';
|
||||
let padding = ' ...';
|
||||
let result = '';
|
||||
let cut = str.indexOf(' ', char_count);
|
||||
if(cut === -1) result = str;
|
||||
else result = str.substring(0, cut);
|
||||
return (result.length <= char_count) ? result : result.substring(0, char_count) + padding;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* https://coderwall.com/p/i817wa/one-line-function-to-detect-mobile-devices-with-javascript
|
||||
*/
|
||||
export function isMobile(){
|
||||
return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
|
||||
}
|
||||
48
src/lib/validation.ts
Normal file
48
src/lib/validation.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export function validURL(str: string) : boolean {
|
||||
let pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
|
||||
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
|
||||
|
||||
return pattern.test(str.trim());
|
||||
}
|
||||
|
||||
|
||||
export function isUrlImage(str: string) : boolean {
|
||||
if(!validURL(str)) return false;
|
||||
|
||||
let text_ext = str.substr(str.lastIndexOf("."));
|
||||
let acceptable_exts = ['.jpg', '.jpeg', '.png', '.gif'];
|
||||
|
||||
return (text_ext !== '' && acceptable_exts.includes(text_ext.toLowerCase()));
|
||||
}
|
||||
|
||||
|
||||
// accept tel or mobile number in different format: 0912.123.123
|
||||
// we remove all non-number and validate the length
|
||||
// need to validate prefix as well but it seems unnecessary because we dont know for sure if the phone is actually true even if all the formats pass the test
|
||||
export function validatePhone(txt: string) : boolean {
|
||||
let all_numbers = txt.replace(/[^0-9]/g, '') + '';
|
||||
|
||||
return validateLength(all_numbers, 8, 14);
|
||||
}
|
||||
|
||||
|
||||
export function validateLength(txt: string, min_length: number=1, max_length: number = 1000) : boolean {
|
||||
let txt_length = txt.trim().length;
|
||||
return (max_length > txt_length && txt_length >= min_length );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @author Hieu
|
||||
* @description: validate an email address
|
||||
* ref: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
* @usage example
|
||||
*/
|
||||
export function validateEmail(email: string) : boolean {
|
||||
let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
}
|
||||
257
src/lib/vietnamese.ts
Normal file
257
src/lib/vietnamese.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
//enhanced function to isStringFound
|
||||
//which search for vietnamese and non-vietnamese in main_str
|
||||
//does not care about cases as well
|
||||
export function isFound(sub_str: string, main_str: string) {
|
||||
const sub_str_unique = unvietnamese(sub_str).toLowerCase();
|
||||
const main_str_unique = getUniqueWords(unvietnamese(main_str) +" " + chuyenKhongdau(main_str)).toLowerCase();
|
||||
|
||||
return isStringFound(sub_str_unique, main_str_unique);
|
||||
}
|
||||
|
||||
|
||||
function isStringFound(sub_str: string, main_str: string) : boolean{
|
||||
const test_sub_str = sub_str.trim();
|
||||
|
||||
//empty str should fail
|
||||
if( test_sub_str.length === 0) return false;
|
||||
|
||||
//start
|
||||
const sub_str_parts = test_sub_str.split(" ");
|
||||
let is_all_parts_found = true;
|
||||
let test_part;
|
||||
for (let i = 0, total_part = sub_str_parts.length; i < total_part ; i++) {
|
||||
test_part = sub_str_parts[i].trim();
|
||||
//test if part in the main_str, if not then we dont need further test
|
||||
if(test_part.length > 0 && main_str.indexOf(test_part) === -1 ) {
|
||||
is_all_parts_found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return is_all_parts_found;
|
||||
}
|
||||
|
||||
|
||||
function unvietnamese(str: string){
|
||||
let replacer = getVietnameseEnglishEquivalent();
|
||||
return replaceAll(str, replacer);
|
||||
}
|
||||
|
||||
|
||||
//credit: stackoverflow
|
||||
function replaceAll(str: string, mapObj: any) {
|
||||
let re = new RegExp(Object.keys(mapObj).join("|"), "gi");
|
||||
|
||||
return (str+'').replace(re, function (matched) {
|
||||
return mapObj[matched]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//28-10-2015
|
||||
//matching Vietnamese special characters to English equivalent
|
||||
//used in some functions around the system like Search->sanitizeVietnamese($txt), ListView::buildSqlEquation
|
||||
function getVietnameseEnglishEquivalent(){
|
||||
return {
|
||||
"đ" : "dd",
|
||||
"Đ" : "DD",
|
||||
|
||||
"ó" : 'os',
|
||||
"ỏ" : 'or',
|
||||
"ò" : 'of',
|
||||
"ọ" : 'oj',
|
||||
"õ" : 'ox',
|
||||
|
||||
"ô" : 'oo',
|
||||
"ỗ" : 'oox',
|
||||
"ổ" : 'oor',
|
||||
"ồ" : 'oof',
|
||||
"ố" : 'oos',
|
||||
"ộ" : 'ooj',
|
||||
|
||||
"ơ" : 'ow',
|
||||
"ỡ" : 'owx',
|
||||
"ớ" : 'ows',
|
||||
"ờ" : 'owf',
|
||||
"ở" : 'owr',
|
||||
"ợ" : 'owj',
|
||||
|
||||
"Ó" : 'OS',
|
||||
"Ỏ" : 'OR',
|
||||
"Ò" : 'OF',
|
||||
"Ọ" : 'OJ',
|
||||
"Õ" : 'OX',
|
||||
|
||||
"Ô" : 'OO',
|
||||
"Ỗ" : 'OOX',
|
||||
"Ổ" : 'OOR',
|
||||
"Ồ" : 'OOF',
|
||||
"Ố" : 'OOS',
|
||||
"Ộ" : 'OOJ',
|
||||
|
||||
"Ơ" : 'OW',
|
||||
"Ỡ" : 'OWX',
|
||||
"Ớ" : 'OWS',
|
||||
"Ờ" : 'OWF',
|
||||
"Ở" : 'OWR',
|
||||
"Ợ" : 'OWJ',
|
||||
|
||||
"ì" : 'if',
|
||||
"í" : 'is',
|
||||
"ỉ" : 'ir',
|
||||
"ĩ" : 'ix',
|
||||
"ị" : 'ij',
|
||||
|
||||
"Ì" : 'IF',
|
||||
"Í" : 'IS',
|
||||
"Ỉ" : 'IR',
|
||||
"Ĩ" : 'IX',
|
||||
"Ị" : 'IJ',
|
||||
|
||||
"ê" : 'ee',
|
||||
"ệ" : 'eej',
|
||||
"ế" : 'ees',
|
||||
"ể" : 'eer',
|
||||
"ễ" : 'eex',
|
||||
"ề" : 'eef',
|
||||
|
||||
"é" : 'es',
|
||||
"ẹ" : 'ej',
|
||||
"ẽ" : 'ex',
|
||||
"è" : 'ef',
|
||||
"ẻ" : 'er',
|
||||
|
||||
"Ê" : 'EE',
|
||||
"Ệ" : 'EEJ',
|
||||
"Ế" : 'EES',
|
||||
"Ể" : 'EER',
|
||||
"Ễ" : 'EEX',
|
||||
"Ề" : 'EEF',
|
||||
|
||||
"É" : 'ES',
|
||||
"Ẹ" : 'EJ',
|
||||
"Ẽ" : 'EX',
|
||||
"È" : 'EF',
|
||||
"Ẻ" : 'ER',
|
||||
|
||||
"ả" : 'ar',
|
||||
"á" : 'as',
|
||||
"ạ" : 'aj',
|
||||
"ã" : 'ax',
|
||||
"à" : 'af',
|
||||
|
||||
"â" : 'aa',
|
||||
"ẩ" : 'aar',
|
||||
"ấ" : 'aas',
|
||||
"ầ" : 'aaf',
|
||||
"ậ" : 'aaj',
|
||||
"ẫ" : 'aax',
|
||||
|
||||
"ă" : 'aw',
|
||||
"ẳ" : 'awr',
|
||||
"ắ" : 'aws',
|
||||
"ằ" : 'awf',
|
||||
"ặ" : 'awj',
|
||||
"ẵ" : 'awx',
|
||||
|
||||
"Ả" : 'AR',
|
||||
"Á" : 'AS',
|
||||
"Ạ" : 'AJ',
|
||||
"Ã" : 'AX',
|
||||
"À" : 'AF',
|
||||
|
||||
"Â" : 'AA',
|
||||
"Ẩ" : 'AAR',
|
||||
"Ấ" : 'AAS',
|
||||
"Ầ" : 'AAF',
|
||||
"Ậ" : 'AAJ',
|
||||
"Ẫ" : 'AAX',
|
||||
|
||||
"Ă" : 'AW',
|
||||
"Ẳ" : 'AWR',
|
||||
"Ắ" : 'AWS',
|
||||
"Ằ" : 'AWF',
|
||||
"Ặ" : 'AWJ',
|
||||
"Ẵ" : 'AWX',
|
||||
|
||||
"ũ" : 'ux',
|
||||
"ụ" : 'uj',
|
||||
"ú" : 'us',
|
||||
"ủ" : 'ur',
|
||||
"ù" : 'uf',
|
||||
|
||||
"ư" : 'uw',
|
||||
"ữ" : 'uwx',
|
||||
"ự" : 'uwj',
|
||||
"ứ" : 'uws',
|
||||
"ử" : 'uwr',
|
||||
"ừ" : 'uwf',
|
||||
|
||||
"Ũ" : 'UX',
|
||||
"Ụ" : 'UJ',
|
||||
"Ú" : 'US',
|
||||
"Ủ" : 'UR',
|
||||
"Ù" : 'UF',
|
||||
|
||||
"Ư" : 'UW',
|
||||
"Ữ" : 'UWX',
|
||||
"Ự" : 'UWJ',
|
||||
"Ứ" : 'UWS',
|
||||
"Ử" : 'UWR',
|
||||
"Ừ" : 'UWF',
|
||||
|
||||
"ỹ" : 'yx',
|
||||
"ỵ" : 'yj',
|
||||
"ý" : 'ys',
|
||||
"ỷ" : 'yr',
|
||||
"ỳ" : 'yf',
|
||||
|
||||
"Ỹ" : 'YX',
|
||||
"Ỵ" : 'YJ',
|
||||
"Ý" : 'YS',
|
||||
"Ỷ" : 'YR',
|
||||
"Ỳ" : 'YF',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function chuyenKhongdau(txt: string){
|
||||
const arraychar = [
|
||||
["đ"],
|
||||
["Đ"],
|
||||
["ó","ỏ","ò","ọ","õ","ô","ỗ","ổ","ồ","ố","ộ","ơ","ỡ","ớ","ờ","ở","ợ"],
|
||||
["Ó","Ỏ","Ò","Ọ","Õ","Ô","Ỗ","Ổ","Ồ","Ố","Ộ","Ơ","Ỡ","Ớ","Ờ","Ở","Ợ"],
|
||||
["ì","í","ỉ","ì","ĩ","ị",],
|
||||
["Ì","Í","Ỉ","Ì","Ĩ","Ị"],
|
||||
["ê","ệ","ế","ể","ễ","ề","é","ẹ","ẽ","è","ẻ",],
|
||||
["Ê","Ệ","Ế","Ể","Ễ","Ề","É","Ẹ","Ẽ","È","Ẻ"],
|
||||
["ả","á","ạ","ã","à","â","ẩ","ấ","ầ","ậ","ẫ","ă","ẳ","ắ","ằ","ặ","ẵ",],
|
||||
["Ả","Á","Ạ","Ã","À","Â","Ẩ","Ấ","Ầ","Ậ","Ẫ","Ă","Ẳ","Ắ","Ằ","Ặ","Ẵ"],
|
||||
["ũ","ụ","ú","ủ","ù","ư","ữ","ự","ứ","ử","ừ",],
|
||||
["Ũ","Ụ","Ú","Ủ","Ù","Ư","Ũ","Ự","Ứ","Ử","Ừ"],
|
||||
["ỹ","ỵ","ý","ỷ","ỳ",],
|
||||
["Ỹ","Ỵ","Ý","Ỷ","Ỳ"]
|
||||
];
|
||||
const arrayconvert = ["d","D","o","O","i","I","e","E","a","A","u","U","y","Y"];
|
||||
|
||||
let mappings: any = {};
|
||||
for ( let i = 0, count = arrayconvert.length; i < count; i++){
|
||||
for ( let j = 0, total = arraychar[i].length; j < total ; j++){
|
||||
mappings[arraychar[i][j]] = arrayconvert[i];
|
||||
}
|
||||
}
|
||||
|
||||
return replaceAll(txt, mappings);
|
||||
}
|
||||
|
||||
|
||||
function getUniqueWords(str: string) {
|
||||
const sub_str_parts = str.trim().split(" ");
|
||||
const unique_values = sub_str_parts.filter( _onlyUnique );
|
||||
|
||||
return unique_values.join(" ").trim();
|
||||
|
||||
function _onlyUnique(value: any, index: any, self: string | any[]) {
|
||||
return self.indexOf(value) === index;
|
||||
}
|
||||
}
|
||||
9
src/lib/webworker.ts
Normal file
9
src/lib/webworker.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// 19-05-2021: For dev
|
||||
|
||||
let registered_callbacks: {[key: string] : any} = {};
|
||||
let _web_worker: { sendTask: (payload: {type: string, task_id: string, [key:string]: any}, callback: Function) => void }|undefined = undefined;
|
||||
|
||||
|
||||
export function createWebWorker(url: string) {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user