@blocklet/ui-react 2.12.61 → 2.12.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/@types/index.d.ts +5 -0
- package/lib/@types/shims.d.ts +1 -0
- package/lib/Notifications/Snackbar.d.ts +14 -0
- package/lib/Notifications/Snackbar.js +210 -0
- package/lib/Notifications/hooks/use-title.d.ts +48 -0
- package/lib/Notifications/hooks/use-title.js +159 -0
- package/lib/Notifications/hooks/use-width.d.ts +2 -0
- package/lib/Notifications/hooks/use-width.js +11 -0
- package/lib/Notifications/utils.d.ts +70 -0
- package/lib/Notifications/utils.js +130 -0
- package/lib/UserCenter/components/danger-zone.d.ts +1 -0
- package/lib/UserCenter/components/danger-zone.js +125 -0
- package/lib/UserCenter/components/settings.js +21 -8
- package/lib/UserCenter/components/user-info/user-basic-info.js +1 -1
- package/lib/UserCenter/libs/locales.d.ts +32 -0
- package/lib/UserCenter/libs/locales.js +34 -2
- package/lib/common/notification-addon.js +31 -8
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +26 -0
- package/package.json +13 -6
- package/src/@types/index.ts +5 -0
- package/src/@types/shims.d.ts +1 -0
- package/src/Notifications/Snackbar.tsx +270 -0
- package/src/Notifications/hooks/use-title.tsx +248 -0
- package/src/Notifications/hooks/use-width.tsx +16 -0
- package/src/Notifications/utils.ts +245 -0
- package/src/UserCenter/components/danger-zone.tsx +136 -0
- package/src/UserCenter/components/settings.tsx +20 -7
- package/src/UserCenter/components/user-info/user-basic-info.tsx +1 -1
- package/src/UserCenter/libs/locales.ts +33 -0
- package/src/common/notification-addon.jsx +36 -9
- package/src/utils.js +26 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import isEmpty from 'lodash/isEmpty';
|
|
2
|
+
import DOMPurify from 'dompurify';
|
|
3
|
+
import { Link, toTextList, getLink as getLinkUtil } from '@abtnode/util/lib/notification-preview/highlight';
|
|
4
|
+
import { isSameAddr } from '@abtnode/util/lib/notification-preview/func';
|
|
5
|
+
import { client } from '../libs/client';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 通知对象的活动目标接口
|
|
9
|
+
*/
|
|
10
|
+
interface ActivityTarget {
|
|
11
|
+
type?: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 通知对象的活动元数据接口
|
|
17
|
+
*/
|
|
18
|
+
interface ActivityMeta {
|
|
19
|
+
id?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 活动对象接口
|
|
24
|
+
*/
|
|
25
|
+
interface Activity {
|
|
26
|
+
type?: string;
|
|
27
|
+
actor?: string;
|
|
28
|
+
target?: ActivityTarget;
|
|
29
|
+
meta?: ActivityMeta;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 通知对象接口
|
|
34
|
+
*/
|
|
35
|
+
interface Notification {
|
|
36
|
+
activity?: Activity;
|
|
37
|
+
actorInfo?: any;
|
|
38
|
+
severity?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
items?: Notification[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 合并相邻的通知数据
|
|
46
|
+
* 合并条件:
|
|
47
|
+
* 1. 数据必须是相邻的
|
|
48
|
+
* 2. activity.type 必须相同且不为 null 或 undefined
|
|
49
|
+
* 3. 如果存在target对象,activity.target.type和activity.target.id都必须相同
|
|
50
|
+
* 4. 如果相邻数据的 activity.type 相同但没有 activity.target 对象,则需要合并
|
|
51
|
+
*
|
|
52
|
+
* @param {Notification[]} notifications - 需要处理的通知数据
|
|
53
|
+
* @returns {Notification[]} - 合并后的通知数组
|
|
54
|
+
*/
|
|
55
|
+
export const mergeAdjacentNotifications = (notifications: Notification[]): Notification[] => {
|
|
56
|
+
if (!notifications || !notifications.length) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result: Notification[] = [];
|
|
61
|
+
let currentGroup: Notification | null = null;
|
|
62
|
+
let groupItems: Notification[] = [];
|
|
63
|
+
|
|
64
|
+
notifications.forEach((notification: Notification) => {
|
|
65
|
+
// 如果没有activity或activity.type为null/undefined或者这是第一条记录
|
|
66
|
+
if (!notification.activity || !notification.activity.type || !currentGroup) {
|
|
67
|
+
// 处理前一个分组(如果存在)
|
|
68
|
+
if (currentGroup) {
|
|
69
|
+
// 如果只有一个通知,直接添加原通知
|
|
70
|
+
if (groupItems.length === 1) {
|
|
71
|
+
result.push(groupItems[0]);
|
|
72
|
+
} else {
|
|
73
|
+
// 将items添加到第一个通知对象,并将其添加到结果中
|
|
74
|
+
currentGroup.items = groupItems;
|
|
75
|
+
result.push(currentGroup);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 处理当前通知
|
|
80
|
+
if (notification.activity && notification.activity.type) {
|
|
81
|
+
// 将第一个通知对象作为组的基础,不创建新对象
|
|
82
|
+
currentGroup = notification;
|
|
83
|
+
groupItems = [notification];
|
|
84
|
+
} else {
|
|
85
|
+
// 无activity或activity.type的记录直接添加到结果中
|
|
86
|
+
result.push(notification);
|
|
87
|
+
// 重置当前组
|
|
88
|
+
currentGroup = null;
|
|
89
|
+
groupItems = [];
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 获取当前组和当前通知的activity信息
|
|
95
|
+
const currentActivity = groupItems[0].activity;
|
|
96
|
+
const currentType = currentActivity?.type;
|
|
97
|
+
const currentTargetType = currentActivity?.target?.type;
|
|
98
|
+
const currentTargetId = currentActivity?.target?.id;
|
|
99
|
+
|
|
100
|
+
const newType = notification.activity.type;
|
|
101
|
+
const newTargetType = notification.activity.target?.type;
|
|
102
|
+
const newTargetId = notification.activity.target?.id;
|
|
103
|
+
|
|
104
|
+
// 判断是否需要合并
|
|
105
|
+
const shouldMerge =
|
|
106
|
+
// activity.type 必须相同且不为null或undefined
|
|
107
|
+
currentType &&
|
|
108
|
+
newType &&
|
|
109
|
+
currentType === newType &&
|
|
110
|
+
// 如果都没有target,可以合并
|
|
111
|
+
((!currentActivity?.target && !notification.activity.target) ||
|
|
112
|
+
// 如果都有target,则target.type和target.id都必须相同
|
|
113
|
+
(currentActivity?.target &&
|
|
114
|
+
notification.activity.target &&
|
|
115
|
+
currentTargetType === newTargetType &&
|
|
116
|
+
currentTargetId === newTargetId));
|
|
117
|
+
|
|
118
|
+
if (shouldMerge) {
|
|
119
|
+
// 合并到当前组
|
|
120
|
+
groupItems.push(notification);
|
|
121
|
+
} else {
|
|
122
|
+
// 不满足合并条件,处理当前组
|
|
123
|
+
// 如果只有一个通知,直接添加原通知
|
|
124
|
+
if (groupItems.length === 1) {
|
|
125
|
+
result.push(groupItems[0]);
|
|
126
|
+
} else {
|
|
127
|
+
// 将items添加到第一个通知对象,并将其添加到结果中
|
|
128
|
+
// 使用Object.assign创建浅拷贝,避免修改原对象
|
|
129
|
+
const groupToAdd = Object.assign({}, currentGroup, { items: groupItems });
|
|
130
|
+
result.push(groupToAdd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 开始新的分组
|
|
134
|
+
currentGroup = notification;
|
|
135
|
+
groupItems = [notification];
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 处理最后一个分组(如果有)
|
|
140
|
+
if (currentGroup) {
|
|
141
|
+
// 如果只有一个通知,直接添加原通知
|
|
142
|
+
if (groupItems.length === 1) {
|
|
143
|
+
result.push(groupItems[0]);
|
|
144
|
+
} else {
|
|
145
|
+
// 将items添加到第一个通知对象,并将其添加到结果中
|
|
146
|
+
// 使用Object.assign创建浅拷贝,避免修改原对象
|
|
147
|
+
const groupToAdd = Object.assign({}, currentGroup, { items: groupItems });
|
|
148
|
+
result.push(groupToAdd);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 判断通知是否包含activity
|
|
157
|
+
* @param {Notification} notification - 通知对象
|
|
158
|
+
* @returns {boolean} - 是否包含activity
|
|
159
|
+
*/
|
|
160
|
+
export const isIncludeActivity = (notification: Notification): boolean => {
|
|
161
|
+
return !isEmpty(notification.activity) && !!notification.activity?.type && !!notification.activity?.actor;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 是否可以自动已读
|
|
166
|
+
*/
|
|
167
|
+
export const canAutoRead = (notification: Notification | null | undefined): boolean => {
|
|
168
|
+
const { severity } = notification || {};
|
|
169
|
+
return !!severity && ['normal', 'success', 'info'].includes(severity);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 获取 activity 的链接
|
|
174
|
+
* 链接的来源有两种
|
|
175
|
+
* 1. activity.meta.id
|
|
176
|
+
* 2. activity.target.id
|
|
177
|
+
* @param {Activity} activity - 活动对象
|
|
178
|
+
* @returns {Object | null} - 活动的链接
|
|
179
|
+
*/
|
|
180
|
+
export const getActivityLink = (
|
|
181
|
+
activity: Activity | null | undefined
|
|
182
|
+
): { metaLink?: string | null; targetLink?: string | null } | null => {
|
|
183
|
+
if (!activity) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const { meta, target } = activity;
|
|
187
|
+
return {
|
|
188
|
+
metaLink: meta?.id,
|
|
189
|
+
targetLink: target?.id,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getUserProfileUrl = async (did: string, locale: string) => {
|
|
194
|
+
try {
|
|
195
|
+
const profileUrl = await client.user.getProfileUrl({ did, locale });
|
|
196
|
+
return profileUrl;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(error);
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const getLink = async (item: any, locale: string) => {
|
|
204
|
+
const { type, did } = item;
|
|
205
|
+
if (isSameAddr(type, 'did')) {
|
|
206
|
+
const profileUrl = await getUserProfileUrl(did, locale);
|
|
207
|
+
return profileUrl || getLinkUtil(item);
|
|
208
|
+
}
|
|
209
|
+
return getLinkUtil(item);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const toClickableSpan = async (str: string, locale: string, isHighLight = true) => {
|
|
213
|
+
const textList = toTextList(str);
|
|
214
|
+
const result = await Promise.all(
|
|
215
|
+
textList.map(async (item: any) => {
|
|
216
|
+
if (item instanceof Link) {
|
|
217
|
+
if (isHighLight) {
|
|
218
|
+
const url = await getLink(item, locale);
|
|
219
|
+
const { type, chainId, did } = item;
|
|
220
|
+
|
|
221
|
+
// HACK: 邮件中无法支持 dapp 的展示,缺少 dapp 链接,只能作为加粗展示
|
|
222
|
+
if (isSameAddr(type, 'dapp')) {
|
|
223
|
+
return `<em class="dapp" data-type="${type}" data-chain-id="${chainId}" data-did="${did}">${item.text}</em>`;
|
|
224
|
+
}
|
|
225
|
+
if (url) {
|
|
226
|
+
return `<a target="_blank" rel="noopener noreferrer" class="link" href="${url}">${item.text}</a>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 默认展示为加粗
|
|
230
|
+
return `<em class="common" data-type="${type}" data-chain-id="${chainId}" data-did="${did}">${item.text}</em>`;
|
|
231
|
+
}
|
|
232
|
+
return item.text;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return item;
|
|
236
|
+
})
|
|
237
|
+
).then((results) => results.join(''));
|
|
238
|
+
return result;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const sanitize = (innerHtml: string) => {
|
|
242
|
+
return DOMPurify.sanitize(innerHtml, {
|
|
243
|
+
ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'id', 'style'],
|
|
244
|
+
});
|
|
245
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { Box, Button, Typography } from '@mui/material';
|
|
3
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
5
|
+
import { useCreation, useMemoizedFn } from 'ahooks';
|
|
6
|
+
import { useConfirm } from '@arcblock/ux/lib/Dialog';
|
|
7
|
+
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
8
|
+
import { LOGIN_PROVIDER } from '@blocklet/constant';
|
|
9
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
10
|
+
|
|
11
|
+
import { translations } from '../libs/locales';
|
|
12
|
+
import { client } from '../../libs/client';
|
|
13
|
+
import type { SessionContext as TSessionContext } from '../../@types';
|
|
14
|
+
|
|
15
|
+
export default function DangerZone() {
|
|
16
|
+
const { confirmApi, confirmHolder } = useConfirm();
|
|
17
|
+
|
|
18
|
+
const { locale } = useLocaleContext();
|
|
19
|
+
const { session, connectApi } = useContext<TSessionContext>(SessionContext);
|
|
20
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
21
|
+
return translate(translations, key, locale, 'en', data);
|
|
22
|
+
});
|
|
23
|
+
const isNeedVerify = useCreation(() => {
|
|
24
|
+
if (['true', true].includes(window?.blocklet?.ALLOW_SKIP_DESTROY_MYSELF_VERIFY)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const connectedAccounts = session?.user?.connectedAccounts || [];
|
|
28
|
+
const ALLOW_VERIFY_PROVIDERS = [LOGIN_PROVIDER.WALLET];
|
|
29
|
+
if (connectedAccounts.some((x) => ALLOW_VERIFY_PROVIDERS.includes(x.provider))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}, [session?.user]);
|
|
35
|
+
|
|
36
|
+
const handleVerify = useMemoizedFn(() => {
|
|
37
|
+
return new Promise<{ did: string }>((resolve, reject) => {
|
|
38
|
+
const userDid = session?.user?.did;
|
|
39
|
+
connectApi.open({
|
|
40
|
+
locale,
|
|
41
|
+
action: 'destroy-myself',
|
|
42
|
+
forceConnected: true,
|
|
43
|
+
saveConnect: false,
|
|
44
|
+
autoConnect: false,
|
|
45
|
+
// 暂不允许使用 passkey 进行验证
|
|
46
|
+
passkeyBehavior: 'none',
|
|
47
|
+
extraParams: {
|
|
48
|
+
removeUserDid: userDid,
|
|
49
|
+
},
|
|
50
|
+
messages: {
|
|
51
|
+
title: t('destroyMyself.title'),
|
|
52
|
+
scan: t('destroyMyself.scan'),
|
|
53
|
+
confirm: t('destroyMyself.confirm'),
|
|
54
|
+
success: t('destroyMyself.success'),
|
|
55
|
+
},
|
|
56
|
+
onSuccess: ({ result }: { result: string }, decrypt = (x: string) => x) => {
|
|
57
|
+
const decryptResult = decrypt(result) as unknown as { did: string };
|
|
58
|
+
resolve(decryptResult);
|
|
59
|
+
},
|
|
60
|
+
onClose: () => {
|
|
61
|
+
connectApi.close();
|
|
62
|
+
reject(new Error(t('destroyMyself.abort')));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const handleDeleteAccount = useMemoizedFn(() => {
|
|
69
|
+
confirmApi.open({
|
|
70
|
+
title: t('dangerZone.deleteAccount'),
|
|
71
|
+
content: t('dangerZone.deleteAccountDescription'),
|
|
72
|
+
confirmButtonText: t('common.confirm'),
|
|
73
|
+
confirmButtonProps: {
|
|
74
|
+
color: 'error',
|
|
75
|
+
},
|
|
76
|
+
cancelButtonText: t('common.cancel'),
|
|
77
|
+
async onConfirm(close: () => void) {
|
|
78
|
+
let result;
|
|
79
|
+
try {
|
|
80
|
+
if (isNeedVerify) {
|
|
81
|
+
result = await handleVerify();
|
|
82
|
+
// TODO: 等 js-sdk 新版后,就有这个方法,可以删除注释
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
} else if (client?.user?.destroyMyself instanceof Function) {
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
result = await client.user.destroyMyself();
|
|
87
|
+
} else {
|
|
88
|
+
Toast.error(t('notImplemented'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (result?.did === session?.user?.did) {
|
|
92
|
+
// TODO: 前端执行退出等清理操作
|
|
93
|
+
session.logout(close);
|
|
94
|
+
} else {
|
|
95
|
+
Toast.error(t('destroyMyself.error'));
|
|
96
|
+
}
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
const errorMessage = error?.response?.data.error || error?.message || t('destroyMyself.error');
|
|
99
|
+
Toast.error(errorMessage);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<Box>
|
|
108
|
+
<Box
|
|
109
|
+
sx={{
|
|
110
|
+
display: 'flex',
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
gap: 1,
|
|
113
|
+
justifyContent: 'space-between',
|
|
114
|
+
}}>
|
|
115
|
+
<Box>
|
|
116
|
+
<Typography
|
|
117
|
+
variant="h6"
|
|
118
|
+
sx={{
|
|
119
|
+
fontSize: '0.875rem !important',
|
|
120
|
+
fontWeight: 'bold',
|
|
121
|
+
}}>
|
|
122
|
+
{t('dangerZone.deleteAccount')}
|
|
123
|
+
</Typography>
|
|
124
|
+
<Typography variant="caption" color="text.secondary">
|
|
125
|
+
{t('dangerZone.deleteAccountDescription')}
|
|
126
|
+
</Typography>
|
|
127
|
+
</Box>
|
|
128
|
+
<Button variant="contained" color="error" size="small" onClick={handleDeleteAccount}>
|
|
129
|
+
{t('dangerZone.delete')}
|
|
130
|
+
</Button>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
{confirmHolder}
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { BoxProps } from '@mui/material';
|
|
|
4
4
|
import { useCreation, useMemoizedFn } from 'ahooks';
|
|
5
5
|
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
6
6
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
7
|
+
import { mergeSx } from '@arcblock/ux/lib/Util/style';
|
|
7
8
|
|
|
8
9
|
import colors from '@arcblock/ux/lib/Colors/themes/temp';
|
|
9
10
|
import { translations } from '../libs/locales';
|
|
@@ -13,6 +14,7 @@ import { User, UserCenterTab } from '../../@types';
|
|
|
13
14
|
import { UserSessions } from '../../UserSessions';
|
|
14
15
|
import ThirdPartyLogin from './third-party-login';
|
|
15
16
|
import ConfigProfile from './config-profile';
|
|
17
|
+
import DangerZone from './danger-zone';
|
|
16
18
|
|
|
17
19
|
export default function Settings({
|
|
18
20
|
user,
|
|
@@ -68,6 +70,14 @@ export default function Settings({
|
|
|
68
70
|
value: 'session',
|
|
69
71
|
content: <UserSessions user={user} showUser={false} />,
|
|
70
72
|
},
|
|
73
|
+
{
|
|
74
|
+
label: t('dangerZone.title'),
|
|
75
|
+
value: 'dangerZone',
|
|
76
|
+
content: <DangerZone />,
|
|
77
|
+
sx: {
|
|
78
|
+
borderColor: 'error.main',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
71
81
|
].filter(Boolean);
|
|
72
82
|
}, [user, privacyConfigList]);
|
|
73
83
|
|
|
@@ -97,14 +107,17 @@ export default function Settings({
|
|
|
97
107
|
<Box
|
|
98
108
|
id={tab.value}
|
|
99
109
|
key={tab.value}
|
|
100
|
-
sx={
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
sx={mergeSx(
|
|
111
|
+
{
|
|
112
|
+
border: `1px solid ${colors.dividerColor}`,
|
|
113
|
+
borderRadius: 2,
|
|
114
|
+
p: 2,
|
|
115
|
+
'&:last-child': {
|
|
116
|
+
mb: 5,
|
|
117
|
+
},
|
|
106
118
|
},
|
|
107
|
-
|
|
119
|
+
tab.sx
|
|
120
|
+
)}>
|
|
108
121
|
<Typography
|
|
109
122
|
sx={{
|
|
110
123
|
color: colors.foregroundsFgBase,
|
|
@@ -99,6 +99,12 @@ export const translations = {
|
|
|
99
99
|
title: '通用设置',
|
|
100
100
|
locale: '偏好语言',
|
|
101
101
|
},
|
|
102
|
+
dangerZone: {
|
|
103
|
+
title: '危险操作',
|
|
104
|
+
deleteAccount: '删除账户',
|
|
105
|
+
deleteAccountDescription: '删除账户后,您将无法使用该账户登录,您的数据将从平台中完全删除,不可恢复。',
|
|
106
|
+
delete: '删除',
|
|
107
|
+
},
|
|
102
108
|
userStatus: {
|
|
103
109
|
Online: '在线',
|
|
104
110
|
Meeting: '开会中',
|
|
@@ -154,6 +160,16 @@ export const translations = {
|
|
|
154
160
|
invalidPostalCode: '邮政编码格式不正确',
|
|
155
161
|
},
|
|
156
162
|
},
|
|
163
|
+
destroyMyself: {
|
|
164
|
+
title: '删除账户',
|
|
165
|
+
scan: '删除账户后,您将无法使用该账户登录,您的数据将从平台中完全删除,不可恢复。',
|
|
166
|
+
confirm: '确认删除',
|
|
167
|
+
cancel: '取消',
|
|
168
|
+
success: '删除账户成功',
|
|
169
|
+
abort: '取消删除账户',
|
|
170
|
+
error: '删除账户失败',
|
|
171
|
+
},
|
|
172
|
+
notImplemented: '操作未实现',
|
|
157
173
|
},
|
|
158
174
|
en: {
|
|
159
175
|
settings: 'Settings',
|
|
@@ -255,6 +271,13 @@ export const translations = {
|
|
|
255
271
|
title: 'Common Settings',
|
|
256
272
|
locale: 'Preferred language',
|
|
257
273
|
},
|
|
274
|
+
dangerZone: {
|
|
275
|
+
title: 'Danger Zone',
|
|
276
|
+
deleteAccount: 'Delete Account',
|
|
277
|
+
deleteAccountDescription:
|
|
278
|
+
'Delete account will make you unable to login with this account, your data will be completely deleted from the platform and cannot be recovered.',
|
|
279
|
+
delete: 'Delete',
|
|
280
|
+
},
|
|
258
281
|
userStatus: {
|
|
259
282
|
Online: 'Online',
|
|
260
283
|
Meeting: 'In a Meeting',
|
|
@@ -311,5 +334,15 @@ export const translations = {
|
|
|
311
334
|
invalidPostalCode: 'Postal code is invalid',
|
|
312
335
|
},
|
|
313
336
|
},
|
|
337
|
+
destroyMyself: {
|
|
338
|
+
title: 'Delete Account',
|
|
339
|
+
scan: 'Delete account will make you unable to login with this account, your data will be completely deleted from the platform and cannot be recovered.',
|
|
340
|
+
confirm: 'Confirm delete',
|
|
341
|
+
cancel: 'Cancel',
|
|
342
|
+
success: 'Delete account successfully',
|
|
343
|
+
abort: 'Delete account aborted',
|
|
344
|
+
error: 'Delete account failed',
|
|
345
|
+
},
|
|
346
|
+
notImplemented: 'This action is not implemented',
|
|
314
347
|
},
|
|
315
348
|
};
|
|
@@ -3,22 +3,35 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import { useCallback, useEffect } from 'react';
|
|
4
4
|
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
5
5
|
import { IconButton } from '@mui/material';
|
|
6
|
-
|
|
6
|
+
import { useSnackbar } from 'notistack';
|
|
7
7
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
8
8
|
import NotificationsOutlinedIcon from '@arcblock/icons/lib/Notification';
|
|
9
9
|
import { useCreation } from 'ahooks';
|
|
10
|
-
import { EVENTS } from '@abtnode/constant';
|
|
10
|
+
import { EVENTS, WELLKNOWN_SERVICE_PATH_PREFIX } from '@abtnode/constant';
|
|
11
|
+
import { joinURL, withQuery } from 'ufo';
|
|
11
12
|
import { useListenWsClient } from './ws';
|
|
13
|
+
import NotificationSnackbar from '../Notifications/Snackbar';
|
|
14
|
+
import { compareVersions } from '../utils';
|
|
15
|
+
|
|
16
|
+
const viewAllUrl = joinURL(WELLKNOWN_SERVICE_PATH_PREFIX, 'user', 'notifications');
|
|
17
|
+
|
|
18
|
+
const getNotificationLink = (notification) => {
|
|
19
|
+
return withQuery(viewAllUrl, {
|
|
20
|
+
id: notification.id,
|
|
21
|
+
severity: notification.severity || 'all',
|
|
22
|
+
componentDid: notification.source === 'system' ? 'system' : notification.componentDid || 'all',
|
|
23
|
+
});
|
|
24
|
+
};
|
|
12
25
|
|
|
13
26
|
export default function NotificationAddon({ session = {} }) {
|
|
14
27
|
const { unReadCount, user, setUnReadCount } = session;
|
|
15
28
|
const userDid = useCreation(() => user?.did, [user]);
|
|
16
29
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
});
|
|
30
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
31
|
+
|
|
32
|
+
const serverVersion = useCreation(() => {
|
|
33
|
+
return window.blocklet?.serverVersion;
|
|
34
|
+
}, []);
|
|
22
35
|
|
|
23
36
|
const wsClient = useListenWsClient('user');
|
|
24
37
|
|
|
@@ -39,9 +52,23 @@ export default function NotificationAddon({ session = {} }) {
|
|
|
39
52
|
|
|
40
53
|
if (notificationReceiver === userDid) {
|
|
41
54
|
setUnReadCount((x) => x + 1);
|
|
55
|
+
// 显示通知, 如果是系统通知则不需要显示
|
|
56
|
+
// 兼容代码,如果 server 没有升级那么不需要提示
|
|
57
|
+
const isCompatible = compareVersions(serverVersion, '1.16.42-beta-20250407');
|
|
58
|
+
if (notification.source === 'component' && isCompatible) {
|
|
59
|
+
const link = getNotificationLink(notification);
|
|
60
|
+
const { severity, description } = notification || {};
|
|
61
|
+
const disableAutoHide = ['error', 'warning'].includes(severity) || notification.sticky;
|
|
62
|
+
enqueueSnackbar(description, {
|
|
63
|
+
variant: severity,
|
|
64
|
+
autoHideDuration: disableAutoHide ? null : 5000,
|
|
65
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
66
|
+
content: (key) => <NotificationSnackbar viewAllUrl={link} keyId={key} notification={notification} />,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
42
69
|
}
|
|
43
70
|
},
|
|
44
|
-
[userDid, setUnReadCount]
|
|
71
|
+
[userDid, setUnReadCount, enqueueSnackbar, serverVersion]
|
|
45
72
|
);
|
|
46
73
|
|
|
47
74
|
const readListenCallback = useCallback(
|
|
@@ -49,7 +76,7 @@ export default function NotificationAddon({ session = {} }) {
|
|
|
49
76
|
const { receiver, readCount } = data ?? {};
|
|
50
77
|
|
|
51
78
|
if (receiver === userDid) {
|
|
52
|
-
setUnReadCount((x) => (x - readCount
|
|
79
|
+
setUnReadCount((x) => Math.max(x - readCount, 0));
|
|
53
80
|
}
|
|
54
81
|
},
|
|
55
82
|
[userDid, setUnReadCount]
|
package/src/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
1
3
|
export const mapRecursive = (array, fn, childrenKey = 'children') => {
|
|
2
4
|
return array.map((item) => {
|
|
3
5
|
if (Array.isArray(item[childrenKey])) {
|
|
@@ -151,3 +153,27 @@ export const splitNavColumns = (items, options = {}) => {
|
|
|
151
153
|
|
|
152
154
|
return result;
|
|
153
155
|
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 比较两个版本号,用于判断版本号是否兼容
|
|
159
|
+
* @param {*} version1
|
|
160
|
+
* @param {*} version2
|
|
161
|
+
* @returns 0: 版本相同, -1: version1 < version2, 1: version1 > version2
|
|
162
|
+
*/
|
|
163
|
+
export const compareVersions = (version1, version2) => {
|
|
164
|
+
const getDateVersion = (version) => {
|
|
165
|
+
const match = version.match(/^(\d+\.\d+\.\d+(?:-[^-]+?-\d{8}))/);
|
|
166
|
+
return match ? match[1] : version;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const dateVersion1 = getDateVersion(version1);
|
|
170
|
+
const dateVersion2 = getDateVersion(version2);
|
|
171
|
+
|
|
172
|
+
// 如果基础版本相同,但完整版本不同(意味着有额外部分),返回false
|
|
173
|
+
if (dateVersion1 === dateVersion2 && version1 !== version2) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 其他情况正常比较
|
|
178
|
+
return semver.gte(dateVersion1, dateVersion2);
|
|
179
|
+
};
|