优化
This commit is contained in:
14
client/shared/utils/__tests__/array-helper.spec.ts
Normal file
14
client/shared/utils/__tests__/array-helper.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { joinArray } from '../array-helper';
|
||||
|
||||
describe('array-helper', () => {
|
||||
test('joinArray', () => {
|
||||
expect(joinArray([1, 2, 3], '5')).toMatchObject([1, '5', 2, '5', 3]);
|
||||
expect(joinArray([{ a: 1 }, { a: 2 }, { a: 3 }], '5')).toMatchObject([
|
||||
{ a: 1 },
|
||||
'5',
|
||||
{ a: 2 },
|
||||
'5',
|
||||
{ a: 3 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
14
client/shared/utils/__tests__/color-scheme-helper.spec.ts
Normal file
14
client/shared/utils/__tests__/color-scheme-helper.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { parseColorScheme } from '../color-scheme-helper';
|
||||
|
||||
describe('parseColorScheme', () => {
|
||||
test.each([
|
||||
['dark', { isDarkMode: true, extraSchemeName: null }],
|
||||
['light', { isDarkMode: false, extraSchemeName: null }],
|
||||
['auto', { isDarkMode: true, extraSchemeName: null }],
|
||||
['dark+miku', { isDarkMode: true, extraSchemeName: 'theme-miku' }],
|
||||
['light+miku', { isDarkMode: false, extraSchemeName: 'theme-miku' }],
|
||||
['miku', { isDarkMode: true, extraSchemeName: 'theme-miku' }],
|
||||
])('%s', (input, output) => {
|
||||
expect(parseColorScheme(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
21
client/shared/utils/__tests__/date-helper.spec.ts
Normal file
21
client/shared/utils/__tests__/date-helper.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { isToday, getMessageTimeDiff } from '../date-helper';
|
||||
|
||||
describe('isToday', () => {
|
||||
test.each([
|
||||
[new Date(), true],
|
||||
[new Date(new Date().setDate(new Date().getDate() - 1)), false],
|
||||
])('%s => %s', (input, should) => {
|
||||
expect(isToday(input)).toBe(should);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessageTimeDiff', () => {
|
||||
test.each([
|
||||
[new Date(), '几秒前'],
|
||||
[new Date(new Date().setMinutes(new Date().getMinutes() - 1)), '1 分钟前'],
|
||||
[new Date(new Date().setHours(new Date().getHours() - 1)), '1 小时前'],
|
||||
[new Date('2020-01-01T00:00:00Z'), '2020-01-01 08:00:00'],
|
||||
])('%s => %s', (input, should) => {
|
||||
expect(getMessageTimeDiff(input)).toBe(should);
|
||||
});
|
||||
});
|
||||
16
client/shared/utils/__tests__/is-promise.spec.ts
Normal file
16
client/shared/utils/__tests__/is-promise.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { isPromise } from '../is-promise';
|
||||
|
||||
describe('isPromise', () => {
|
||||
test.each([
|
||||
[Promise.resolve(), true],
|
||||
['str', false],
|
||||
[123, false],
|
||||
[[], false],
|
||||
[{}, false],
|
||||
[Symbol('sym'), false],
|
||||
[undefined, false],
|
||||
[null, false],
|
||||
])('%s => %s', (input, should) => {
|
||||
expect(isPromise(input)).toBe(should);
|
||||
});
|
||||
});
|
||||
18
client/shared/utils/__tests__/json-helper.spec.ts
Normal file
18
client/shared/utils/__tests__/json-helper.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isValidJson } from '../json-helper';
|
||||
|
||||
describe('isValidJson', () => {
|
||||
test.each([
|
||||
['foo', false],
|
||||
['[]', true],
|
||||
['{}', true],
|
||||
['{"foo": []}', true],
|
||||
['{"foo": [}', false],
|
||||
['{foo: bar}', false],
|
||||
['{"foo": "bar"}', true],
|
||||
[[], false],
|
||||
[null, false],
|
||||
[undefined, false],
|
||||
])('%s => %s', (input: any, should) => {
|
||||
expect(isValidJson(input)).toBe(should);
|
||||
});
|
||||
});
|
||||
41
client/shared/utils/__tests__/string-helper.spec.ts
Normal file
41
client/shared/utils/__tests__/string-helper.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { isAvailableString, isObjectId, isUrl } from '../string-helper';
|
||||
|
||||
describe('string-helper', () => {
|
||||
describe('isAvailableString', () => {
|
||||
test.each<[any, boolean]>([
|
||||
['any string', true],
|
||||
['', false],
|
||||
[1, false],
|
||||
[() => {}, false],
|
||||
[{}, false],
|
||||
[[], false],
|
||||
[undefined, false],
|
||||
[null, false],
|
||||
])('%p => %p', (url, res) => {
|
||||
expect(isAvailableString(url)).toBe(res);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUrl', () => {
|
||||
test.each<[string, boolean]>([
|
||||
['http://baidu.com', true],
|
||||
['https://baidu.com', true],
|
||||
['ws://baidu.com', true],
|
||||
['wss://baidu.com', true],
|
||||
['baidu.com', false],
|
||||
['baidu', false],
|
||||
])('%s => %p', (url, res) => {
|
||||
expect(isUrl(url)).toBe(res);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObjectId', () => {
|
||||
test.each<[string, boolean]>([
|
||||
['1', false],
|
||||
['unknown', false],
|
||||
['64b4a473a44c273805b25da5', true],
|
||||
])('%s => %p', (input, res) => {
|
||||
expect(isObjectId(input)).toBe(res);
|
||||
});
|
||||
});
|
||||
});
|
||||
21
client/shared/utils/array-helper.ts
Normal file
21
client/shared/utils/array-helper.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import _flatten from 'lodash/flatten';
|
||||
|
||||
/**
|
||||
* 类似于join,但是返回一个数组
|
||||
* join会将元素强制转化为字符串
|
||||
*
|
||||
* 改函数可以用于join ReactNode
|
||||
*
|
||||
* @example joinArray([1, 2, 3], '5') => [1, '5', 2, '5', 3]
|
||||
*/
|
||||
export function joinArray<T, K>(arr: T[], separator: K): (T | K)[] {
|
||||
return _flatten(
|
||||
arr.map((item, i) => {
|
||||
if (i === 0) {
|
||||
return [item];
|
||||
} else {
|
||||
return [separator, item];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
41
client/shared/utils/color-scheme-helper.ts
Normal file
41
client/shared/utils/color-scheme-helper.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { isValidStr } from './string-helper';
|
||||
|
||||
/**
|
||||
* 解析配色方案,优先dark模式
|
||||
*/
|
||||
export function parseColorScheme(colorScheme: string): {
|
||||
isDarkMode: boolean;
|
||||
extraSchemeName: string | null;
|
||||
} {
|
||||
if (colorScheme === 'dark') {
|
||||
return {
|
||||
isDarkMode: true,
|
||||
extraSchemeName: null,
|
||||
};
|
||||
} else if (colorScheme === 'light') {
|
||||
return {
|
||||
isDarkMode: false,
|
||||
extraSchemeName: null,
|
||||
};
|
||||
} else if (colorScheme === 'auto') {
|
||||
return {
|
||||
isDarkMode: window.matchMedia
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: true,
|
||||
extraSchemeName: null,
|
||||
};
|
||||
} else {
|
||||
// 可能是插件 for example: dark+miku
|
||||
let [base, name] = colorScheme.split('+');
|
||||
|
||||
if (!isValidStr(name)) {
|
||||
name = base;
|
||||
base = 'dark';
|
||||
}
|
||||
|
||||
return {
|
||||
isDarkMode: base === 'dark',
|
||||
extraSchemeName: `theme-${name}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
34
client/shared/utils/consts.ts
Normal file
34
client/shared/utils/consts.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { GlobalConfig } from '../model/config';
|
||||
|
||||
/**
|
||||
* 昵称合法性匹配
|
||||
* 最大八个汉字内容或者16字英文
|
||||
* 且中间不能有空格
|
||||
*/
|
||||
export const NAME_REGEXP =
|
||||
/^([0-9a-zA-Z]{1,2}|[\u4e00-\u9eff]|[\u3040-\u309Fー]|[\u30A0-\u30FF]){1,8}$/;
|
||||
|
||||
/**
|
||||
* 系统语言的常量
|
||||
*/
|
||||
export const LANGUAGE_KEY = 'i18n:language';
|
||||
|
||||
/**
|
||||
* 系统用户id
|
||||
*/
|
||||
export const SYSTEM_USERID = '000000000000000000000000';
|
||||
|
||||
export const defaultGlobalConfig: GlobalConfig = {
|
||||
tianji: {},
|
||||
uploadFileLimit: 1 * 1024 * 1024,
|
||||
emailVerification: false,
|
||||
serverName: 'Tailchat',
|
||||
disableMsgpack: false,
|
||||
disableUserRegister: false,
|
||||
disableGuestLogin: false,
|
||||
disableCreateGroup: false,
|
||||
disablePluginStore: false,
|
||||
disableAddFriend: false,
|
||||
disableTelemetry: false,
|
||||
announcement: false,
|
||||
};
|
||||
104
client/shared/utils/date-helper.ts
Normal file
104
client/shared/utils/date-helper.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'; // 导入插件
|
||||
import duration from 'dayjs/plugin/duration'; // 导入插件
|
||||
import 'dayjs/locale/zh-cn'; // 导入本地化语言
|
||||
import { onLanguageChanged } from '../i18n';
|
||||
|
||||
/**
|
||||
* Reference: https://day.js.org/
|
||||
*/
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(duration);
|
||||
dayjs.locale('zh-cn'); // 默认使用中文
|
||||
onLanguageChanged((lang) => {
|
||||
if (lang === 'en-US') {
|
||||
dayjs.locale('en');
|
||||
return;
|
||||
}
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否为当天
|
||||
*/
|
||||
export function isToday(date: dayjs.ConfigType): boolean {
|
||||
return dayjs(date).isSame(dayjs(), 'd');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息显示时间
|
||||
*/
|
||||
export function getMessageTimeDiff(input: Date): string {
|
||||
const date = dayjs(input);
|
||||
|
||||
if (isToday(date)) {
|
||||
return date.fromNow();
|
||||
} else {
|
||||
return date.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小时消息时间
|
||||
* 如果是当天则显示短时间,如果不是当天则显示完整时间
|
||||
*/
|
||||
export function showMessageTime(input: Date): string {
|
||||
const date = dayjs(input);
|
||||
|
||||
if (isToday(date)) {
|
||||
return formatShortTime(date);
|
||||
} else {
|
||||
return formatFullTime(date);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否应该显示消息时间
|
||||
* 间隔时间大于十五分钟则显示
|
||||
*/
|
||||
export function shouldShowMessageTime(date1: Date, date2: Date): boolean {
|
||||
return Math.abs(date1.valueOf() - date2.valueOf()) > 15 * 60 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为 小时:分钟
|
||||
*/
|
||||
export function formatShortTime(date: dayjs.ConfigType): string {
|
||||
return dayjs(date).format('HH:mm');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为完整时间 YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
export function formatFullTime(date: dayjs.ConfigType): string {
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前实例到现在的相对时间。
|
||||
* @example
|
||||
* dayjs('1999-01-01').toNow() // 22 年后
|
||||
*/
|
||||
export function datetimeToNow(input: dayjs.ConfigType): string {
|
||||
const date = dayjs(input);
|
||||
return date.toNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前实例到现在的相对时间。
|
||||
* @example
|
||||
* dayjs('1999-01-01').toNow() // 22 年前
|
||||
*/
|
||||
export function datetimeFromNow(input: dayjs.ConfigType): string {
|
||||
const date = dayjs(input);
|
||||
return date.fromNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将毫秒转换为易读的人类语言
|
||||
*/
|
||||
export function humanizeMsDuration(ms: number): string {
|
||||
return dayjs.duration(ms, 'ms').humanize();
|
||||
}
|
||||
9
client/shared/utils/environment.ts
Normal file
9
client/shared/utils/environment.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
export const isNavigator = typeof navigator !== 'undefined';
|
||||
|
||||
export const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export const version = process.env.VERSION || '0.0.0';
|
||||
7
client/shared/utils/is-promise.ts
Normal file
7
client/shared/utils/is-promise.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function isPromise(obj: any): obj is Promise<unknown> {
|
||||
return (
|
||||
!!obj &&
|
||||
(typeof obj === 'object' || typeof obj === 'function') &&
|
||||
typeof obj.then === 'function'
|
||||
);
|
||||
}
|
||||
16
client/shared/utils/json-helper.ts
Normal file
16
client/shared/utils/json-helper.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 判断是否是一个合法的json字符串
|
||||
* @param jsonStr json字符串
|
||||
*/
|
||||
export function isValidJson(jsonStr: string): boolean {
|
||||
if (typeof jsonStr !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(jsonStr);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
47
client/shared/utils/message-helper.ts
Normal file
47
client/shared/utils/message-helper.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
ChatMessage,
|
||||
SendMessagePayload,
|
||||
SimpleMessagePayload,
|
||||
} from '../model/message';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import _set from 'lodash/set';
|
||||
import _get from 'lodash/get';
|
||||
import _pick from 'lodash/pick';
|
||||
|
||||
const replyMsgFields = ['_id', 'content', 'author'] as const;
|
||||
export type ReplyMsgType = Pick<ChatMessage, typeof replyMsgFields[number]>;
|
||||
|
||||
export class MessageHelper {
|
||||
private payload: SendMessagePayload;
|
||||
|
||||
constructor(origin: SimpleMessagePayload) {
|
||||
this.payload = { ...origin };
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息体内是否有回复信息
|
||||
*/
|
||||
hasReply(): ReplyMsgType | false {
|
||||
const reply = _get(this.payload, ['meta', 'reply']);
|
||||
if (_isNil(reply)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
setReplyMsg(replyMsg: ReplyMsgType) {
|
||||
if (_isNil(replyMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_set(this.payload, ['meta', 'reply'], _pick(replyMsg, replyMsgFields));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成待发送的消息体
|
||||
*/
|
||||
generatePayload(): SendMessagePayload {
|
||||
return { ...this.payload };
|
||||
}
|
||||
}
|
||||
16
client/shared/utils/panel-helper.ts
Normal file
16
client/shared/utils/panel-helper.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GroupPanel, GroupPanelType } from '../model/group';
|
||||
|
||||
/**
|
||||
* 判断面板是否为会话面板
|
||||
*
|
||||
* 会话面板的属性是带有已读未读属性的(如默认的文本面板)
|
||||
*/
|
||||
export function isConversePanel(panel: GroupPanel) {
|
||||
// 目前只有文本面板
|
||||
|
||||
if (panel.type === GroupPanelType.TEXT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
89
client/shared/utils/request.ts
Normal file
89
client/shared/utils/request.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import _chunk from 'lodash/chunk';
|
||||
import _flatten from 'lodash/flatten';
|
||||
|
||||
interface QueueItem<T, R> {
|
||||
params: T;
|
||||
resolve: (r: R) => void;
|
||||
reject: (reason: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个自动合并请求的函数
|
||||
* 在一定窗口期内的所有请求都会被合并提交合并发送
|
||||
* @param fn 合并后的请求函数
|
||||
* @param windowMs 窗口期
|
||||
*/
|
||||
export function createAutoMergedRequest<T, R>(
|
||||
fn: (mergedParams: T[]) => Promise<R[]>,
|
||||
windowMs = 200
|
||||
): (params: T) => Promise<R> {
|
||||
let queue: QueueItem<T, R>[] = [];
|
||||
let timer: number | null = null;
|
||||
|
||||
async function submitQueue() {
|
||||
timer = null; // 清空计时器以接受后续请求
|
||||
const _queue = [...queue];
|
||||
queue = []; // 清空队列
|
||||
|
||||
try {
|
||||
const list = await fn(_queue.map((q) => q.params));
|
||||
_queue.forEach((q1, i) => {
|
||||
q1.resolve(list[i]);
|
||||
});
|
||||
} catch (err) {
|
||||
_queue.forEach((q2) => {
|
||||
q2.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (params: T): Promise<R> => {
|
||||
if (!timer) {
|
||||
// 如果没有开始窗口期,则创建
|
||||
timer = window.setTimeout(() => {
|
||||
submitQueue();
|
||||
}, windowMs);
|
||||
}
|
||||
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
queue.push({
|
||||
params,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个自动拆分请求参数的函数
|
||||
*/
|
||||
export function createAutoSplitRequest<Key, Item>(
|
||||
fn: (keys: Key[]) => Promise<Item[]>,
|
||||
type: 'serial' | 'parallel',
|
||||
limit = 100
|
||||
): (arr: Key[]) => Promise<Item[]> {
|
||||
return async (arr: Key[]): Promise<Item[]> => {
|
||||
const groups = _chunk(arr, limit);
|
||||
|
||||
if (type === 'serial') {
|
||||
const list: Item[] = [];
|
||||
for (const group of groups) {
|
||||
const res = await fn(group);
|
||||
if (Array.isArray(res)) {
|
||||
list.push(...res);
|
||||
} else {
|
||||
console.warn('[createAutoSplitRequest] fn should be return array');
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
} else if (type === 'parallel') {
|
||||
const res = await Promise.all(groups.map((group) => fn(group)));
|
||||
|
||||
return _flatten(res);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
}
|
||||
176
client/shared/utils/role-helper.ts
Normal file
176
client/shared/utils/role-helper.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { GroupPanelType } from 'tailchat-types';
|
||||
import { model, t } from '..';
|
||||
|
||||
/**
|
||||
* 所有人权限
|
||||
* 群组最低权限标识
|
||||
*/
|
||||
export const ALL_PERMISSION = Symbol('AllPermission');
|
||||
|
||||
export interface PermissionItemType {
|
||||
/**
|
||||
* 权限唯一key, 用于写入数据库
|
||||
* 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 权限点显示名称
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 权限描述
|
||||
*/
|
||||
desc: string;
|
||||
/**
|
||||
* 是否默认开启
|
||||
*/
|
||||
default: boolean;
|
||||
/**
|
||||
* 是否依赖其他权限点
|
||||
*/
|
||||
required?: string[];
|
||||
/**
|
||||
* 面板权限
|
||||
* 如果是内置类型(数字) 则仅会在规定的类型中展示
|
||||
* 如果是字符串数组则仅会在特定的插件面板中显示
|
||||
* 如果不传则视为不适用于面板
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
panel?: boolean | (string | GroupPanelType)[];
|
||||
}
|
||||
|
||||
export const PERMISSION = {
|
||||
/**
|
||||
* 非插件的权限点都叫core
|
||||
*/
|
||||
core: {
|
||||
viewPanel: 'core.viewPanel',
|
||||
message: 'core.message',
|
||||
invite: 'core.invite',
|
||||
unlimitedInvite: 'core.unlimitedInvite',
|
||||
editInvite: 'core.editInvite',
|
||||
groupDetail: 'core.groupDetail',
|
||||
groupBaseInfo: 'core.groupBaseInfo',
|
||||
groupConfig: 'core.groupConfig',
|
||||
manageUser: 'core.manageUser',
|
||||
managePanel: 'core.managePanel',
|
||||
manageInvite: 'core.manageInvite',
|
||||
manageRoles: 'core.manageRoles',
|
||||
deleteMessage: 'core.deleteMessage',
|
||||
},
|
||||
};
|
||||
|
||||
export const getPermissionList = (): PermissionItemType[] => [
|
||||
{
|
||||
key: PERMISSION.core.viewPanel,
|
||||
title: t('查看面板'),
|
||||
desc: t('允许成员查看面板'),
|
||||
default: true,
|
||||
panel: true,
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.message,
|
||||
title: t('发送消息'),
|
||||
desc: t('允许成员在文字频道发送消息'),
|
||||
default: true,
|
||||
panel: [GroupPanelType.TEXT],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.invite,
|
||||
title: t('邀请链接'),
|
||||
desc: t('允许成员创建邀请链接'),
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.unlimitedInvite,
|
||||
title: t('不限时邀请链接'),
|
||||
desc: t('允许成员创建不限时邀请链接'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.invite],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.editInvite,
|
||||
title: t('编辑邀请链接'),
|
||||
desc: t('允许成员编辑邀请链接'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.unlimitedInvite],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.groupDetail,
|
||||
title: t('查看群组详情'),
|
||||
desc: t('允许成员查看群组详情'),
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.groupBaseInfo,
|
||||
title: t('修改群组基本信息'),
|
||||
desc: t('允许成员修改群组基本信息'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.groupConfig,
|
||||
title: t('修改群组配置'),
|
||||
desc: t('允许成员修改群组配置'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.manageUser,
|
||||
title: t('允许管理用户'),
|
||||
desc: t('允许成员管理用户,如禁言、移除用户等操作'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.managePanel,
|
||||
title: t('允许管理频道'),
|
||||
desc: t('允许成员查看管理频道'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.manageInvite,
|
||||
title: t('允许管理邀请链接'),
|
||||
desc: t('允许成员管理邀请链接'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.manageRoles,
|
||||
title: t('允许管理身份组'),
|
||||
desc: t('允许成员管理身份组'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
{
|
||||
key: PERMISSION.core.deleteMessage,
|
||||
title: t('删除消息'),
|
||||
desc: t('允许删除用户信息'),
|
||||
default: false,
|
||||
required: [PERMISSION.core.groupDetail],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取默认权限列表
|
||||
*
|
||||
* @default ['core.message']
|
||||
*/
|
||||
export function getDefaultPermissionList(): string[] {
|
||||
return getPermissionList()
|
||||
.filter((p) => p.default === true)
|
||||
.map((p) => p.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认所有人身份组权限
|
||||
*/
|
||||
export async function applyDefaultFallbackGroupPermission(groupId: string) {
|
||||
await model.group.modifyGroupField(
|
||||
groupId,
|
||||
'fallbackPermissions',
|
||||
getDefaultPermissionList()
|
||||
);
|
||||
}
|
||||
72
client/shared/utils/string-helper.ts
Normal file
72
client/shared/utils/string-helper.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import _isString from 'lodash/isString';
|
||||
import urlRegex from 'url-regex';
|
||||
|
||||
/**
|
||||
* 判断一个字符串是否可用()
|
||||
* @param str 要判断的字符串
|
||||
*/
|
||||
export function isAvailableString(str: unknown): boolean {
|
||||
return typeof str === 'string' && str.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个字符串是否是url
|
||||
* @param str 要判断的字符串
|
||||
*/
|
||||
export function isUrl(str: string) {
|
||||
return urlRegex({ exact: true }).test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否是一个blobUrl
|
||||
* @param str url字符串
|
||||
*/
|
||||
export const isBlobUrl = (str: string) => {
|
||||
return _isString(str) && str.startsWith('blob:');
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取一段字符串中的所有url
|
||||
* @param str 字符串
|
||||
*/
|
||||
export const getUrls = (str: string): string[] => {
|
||||
return str.match(urlRegex()) ?? [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于判定环境变量的值
|
||||
*/
|
||||
export function is(it: string) {
|
||||
return !!it && it !== '0' && it !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否一个可用的字符串
|
||||
* 定义为有长度的字符串
|
||||
*/
|
||||
export function isValidStr(str: unknown): str is string {
|
||||
return typeof str == 'string' && str !== '';
|
||||
}
|
||||
|
||||
export function isLocalMessageId(str: unknown): boolean {
|
||||
if (typeof str !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str.startsWith('localMessage_');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是一个MongoDB的objectId
|
||||
*/
|
||||
export function isObjectId(str: any): boolean {
|
||||
if (typeof str === 'string' && str.length === 12) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof str === 'string' && /^[0-9A-Fa-f]{24}$/.test(str)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
62
client/shared/utils/upload-helper.ts
Normal file
62
client/shared/utils/upload-helper.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { showToasts, t } from '..';
|
||||
import { request } from '../api/request';
|
||||
import _get from 'lodash/get';
|
||||
import { getGlobalConfig } from '../model/config';
|
||||
import { showErrorToasts } from '../manager/ui';
|
||||
import filesize from 'filesize';
|
||||
|
||||
export type UploadFileUsage = 'chat' | 'group' | 'user' | 'unknown';
|
||||
|
||||
interface UploadFileOptions {
|
||||
usage?: UploadFileUsage;
|
||||
onProgress?: (percent: number, progressEvent: unknown) => void;
|
||||
}
|
||||
export interface UploadFileResult {
|
||||
etag: string;
|
||||
path: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
export async function uploadFile(
|
||||
file: File,
|
||||
options: UploadFileOptions = {}
|
||||
): Promise<UploadFileResult> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('usage', options.usage ?? 'unknown');
|
||||
|
||||
const uploadFileLimit = getGlobalConfig().uploadFileLimit;
|
||||
if (file.size > uploadFileLimit) {
|
||||
// 文件过大
|
||||
showErrorToasts(
|
||||
`${t('上传失败, 支持的文件最大大小为:')} ${filesize(uploadFileLimit, {
|
||||
base: 2,
|
||||
})}`
|
||||
);
|
||||
throw new Error('File Too Large');
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await request.post('/upload', form, {
|
||||
onUploadProgress(progressEvent) {
|
||||
if (progressEvent.lengthComputable) {
|
||||
if (typeof options.onProgress === 'function') {
|
||||
options.onProgress(
|
||||
progressEvent.loaded / progressEvent.total,
|
||||
progressEvent
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
showToasts(`${t('上传失败')}: ${t('可能是文件体积过大')}`, 'error');
|
||||
console.error(`${t('上传失败')}: ${_get(e, 'message')}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
12
client/shared/utils/url-helper.ts
Normal file
12
client/shared/utils/url-helper.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getServiceUrl } from '../manager/service';
|
||||
|
||||
/**
|
||||
* 解析url, 将中间的常量替换成变量
|
||||
* @param originUrl 原始Url
|
||||
* @returns 解析后的url
|
||||
*/
|
||||
export function parseUrlStr(originUrl: string): string {
|
||||
return String(originUrl)
|
||||
.replace('{BACKEND}', getServiceUrl())
|
||||
.replace('%7BBACKEND%7D', getServiceUrl());
|
||||
}
|
||||
10
client/shared/utils/utils.ts
Normal file
10
client/shared/utils/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* JavaScript 中的 sleep 函数
|
||||
* 参考 https://github.com/sqren/await-sleep/blob/master/index.js
|
||||
* @param milliseconds 阻塞毫秒
|
||||
*/
|
||||
export function sleep(milliseconds: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user