@blocklet/ui-react 2.12.61 → 2.12.62

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.
@@ -0,0 +1,16 @@
1
+ import { useTheme } from '@arcblock/ux/lib/Theme';
2
+ import useMediaQuery from '@mui/material/useMediaQuery';
3
+
4
+ function useWidth() {
5
+ const theme = useTheme();
6
+ const keys = [...theme.breakpoints.keys].reverse();
7
+ return (
8
+ keys.reduce<string | null>((output, key) => {
9
+ // eslint-disable-next-line
10
+ const matches = useMediaQuery(theme.breakpoints.up(key));
11
+ return !output && matches ? key : output;
12
+ }, null) || 'xs'
13
+ );
14
+ }
15
+
16
+ export default useWidth;
@@ -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
+ };
@@ -166,7 +166,7 @@ export default function UserBasicInfo({
166
166
  bottom: 0,
167
167
  left: 0,
168
168
  right: 0,
169
- height: '50%',
169
+ height: '25%',
170
170
  backgroundColor: 'rgba(0, 0, 0, 0.3)',
171
171
  display: 'flex',
172
172
  justifyContent: 'center',
@@ -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 viewAllUrl = useCreation(() => {
18
- const { navigation } = window.blocklet ?? {};
19
- const notification = navigation?.find((x) => x.id === '/userCenter/notification');
20
- return notification?.link;
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 <= 0 ? 0 : 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
+ };