This commit is contained in:
2021-05-19 11:57:16 +07:00
parent 93c32a66a2
commit ab2830f449
124 changed files with 8061 additions and 0 deletions

146
src/lib/api.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
// 19-05-2021: For dev
export const uploadFile = (file: Blob) => {
// TODO:
}

67
src/lib/user.ts Normal file
View 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 doesnt 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
View 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
View 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
View 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
View 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) {
}