@blocklet/ui-react 3.1.26 → 3.1.28

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.
Files changed (35) hide show
  1. package/lib/@types/index.d.ts +1 -1
  2. package/lib/Footer/links.js +26 -33
  3. package/lib/UserCenter/components/fallback.d.ts +8 -0
  4. package/lib/UserCenter/components/fallback.js +21 -0
  5. package/lib/UserCenter/components/user-center.js +205 -200
  6. package/lib/UserCenter/components/user-info/social-actions/chat.d.ts +5 -0
  7. package/lib/UserCenter/components/user-info/social-actions/chat.js +24 -0
  8. package/lib/UserCenter/components/user-info/social-actions/follow.d.ts +2 -0
  9. package/lib/UserCenter/components/user-info/social-actions/follow.js +19 -0
  10. package/lib/UserCenter/components/user-info/social-actions/index.d.ts +5 -0
  11. package/lib/UserCenter/components/user-info/social-actions/index.js +13 -0
  12. package/lib/UserCenter/components/user-info/user-basic-info.js +37 -35
  13. package/lib/UserCenter/libs/locales.d.ts +14 -0
  14. package/lib/UserCenter/libs/locales.js +16 -2
  15. package/lib/blocklets.js +56 -54
  16. package/lib/contexts/user-followers.d.ts +13 -0
  17. package/lib/contexts/user-followers.js +40 -0
  18. package/lib/hooks/use-follow.d.ts +12 -0
  19. package/lib/hooks/use-follow.js +47 -0
  20. package/lib/utils.d.ts +1 -0
  21. package/lib/utils.js +37 -36
  22. package/package.json +6 -6
  23. package/src/@types/index.ts +1 -1
  24. package/src/Footer/links.jsx +10 -7
  25. package/src/UserCenter/components/fallback.tsx +51 -0
  26. package/src/UserCenter/components/user-center.tsx +22 -12
  27. package/src/UserCenter/components/user-info/social-actions/chat.tsx +42 -0
  28. package/src/UserCenter/components/user-info/social-actions/follow.tsx +30 -0
  29. package/src/UserCenter/components/user-info/social-actions/index.tsx +17 -0
  30. package/src/UserCenter/components/user-info/user-basic-info.tsx +6 -0
  31. package/src/UserCenter/libs/locales.ts +14 -0
  32. package/src/blocklets.js +5 -1
  33. package/src/contexts/user-followers.tsx +54 -0
  34. package/src/hooks/use-follow.tsx +74 -0
  35. package/src/utils.js +5 -0
package/lib/utils.js CHANGED
@@ -1,61 +1,62 @@
1
1
  import H from "semver";
2
- const d = (n, r, e = "children") => n.map((t) => Array.isArray(t[e]) ? r({
2
+ const d = (e, r, n = "children") => e.map((t) => Array.isArray(t[n]) ? r({
3
3
  ...t,
4
- [e]: d(t[e], r, e)
5
- }) : r(t)), C = (n, r = "children") => {
6
- const e = [];
7
- return d(n, (t) => e.push(t), r), e;
8
- }, S = (n, r = "children") => {
9
- let e = 0;
10
- return d(n, () => e++, r), e;
11
- }, R = (n, r, e = "children") => n.map((t) => ({ ...t })).filter((t) => {
12
- const s = t[e];
4
+ [n]: d(t[n], r, n)
5
+ }) : r(t)), C = (e, r = "children") => {
6
+ const n = [];
7
+ return d(e, (t) => n.push(t), r), n;
8
+ }, P = (e, r = "children") => {
9
+ let n = 0;
10
+ return d(e, () => n++, r), n;
11
+ }, R = (e, r, n = "children") => e.map((t) => ({ ...t })).filter((t) => {
12
+ const s = t[n];
13
13
  if (Array.isArray(s)) {
14
- const o = R(s, r, e);
15
- t[e] = o?.length ? o : void 0;
14
+ const o = R(s, r, n);
15
+ t[n] = o?.length ? o : void 0;
16
16
  }
17
- const c = { filteredChildren: t[e], isLeaf: !s?.length };
17
+ const c = { filteredChildren: t[n], isLeaf: !s?.length };
18
18
  return r(t, c);
19
- }), x = (n) => /^https?:\/\//.test(n), P = (n) => /^[\w-]+:[\w-]+$/.test(n), V = (n) => {
20
- if (!n || !n?.startsWith("/"))
19
+ }), S = (e) => /^https?:\/\//.test(e), x = (e) => /^mailto:/i.test(e.trim()), W = (e) => /^[\w-]+:[\w-]+$/.test(e), V = (e) => {
20
+ if (!e || !e?.startsWith("/"))
21
21
  return !1;
22
- const r = (s) => s.endsWith("/") ? s : `${s}/`, e = r(window.location.pathname), t = r(new URL(n, window.location.origin).pathname);
23
- return e.startsWith(t);
24
- }, W = (n = []) => {
25
- const r = n.map((t, s) => ({ path: t, index: s })).filter((t) => V(t.path));
22
+ const r = (s) => s.endsWith("/") ? s : `${s}/`, n = r(window.location.pathname), t = r(new URL(e, window.location.origin).pathname);
23
+ return n.startsWith(t);
24
+ }, k = (e = []) => {
25
+ const r = e.map((t, s) => ({ path: t, index: s })).filter((t) => V(t.path));
26
26
  return r?.length ? r.slice(1).reduce((t, s) => t.path.length >= s.path.length ? t : s, r[0]).index : -1;
27
- }, k = (n, r = {}) => {
28
- const { columns: e = 1, breakInside: t = !1, groupHeight: s = 48, itemHeight: c = 24, childrenKey: o = "items" } = r, p = n.reduce((u, g) => u + s + (g[o]?.length || 0) * c, 0), l = Math.ceil(p / e), h = [[]];
27
+ }, v = (e, r = {}) => {
28
+ const { columns: n = 1, breakInside: t = !1, groupHeight: s = 48, itemHeight: c = 24, childrenKey: o = "items" } = r, p = e.reduce((u, g) => u + s + (g[o]?.length || 0) * c, 0), h = Math.ceil(p / n), l = [[]];
29
29
  let i = 0, a = 0;
30
- const m = l * 0.2, f = (u) => a > l - m && i < e - 1 && a + u > l + m;
31
- return n.forEach((u) => {
30
+ const m = h * 0.2, f = (u) => a > h - m && i < n - 1 && a + u > h + m;
31
+ return e.forEach((u) => {
32
32
  const g = s + (u[o]?.length || 0) * c;
33
- t && f(s) && (i++, a = 0, h[i] = []), !t && a > 0 && f(g) && (i++, a = 0, h[i] = []), h[i].push({
33
+ t && f(s) && (i++, a = 0, l[i] = []), !t && a > 0 && f(g) && (i++, a = 0, l[i] = []), l[i].push({
34
34
  ...u,
35
35
  group: !0
36
36
  }), a += s, u[o] && u[o].forEach((w) => {
37
- t && f(c) && (i++, a = 0, h[i] = []), h[i].push({
37
+ t && f(c) && (i++, a = 0, l[i] = []), l[i].push({
38
38
  ...w,
39
39
  group: !1
40
40
  }), a += c;
41
41
  });
42
- }), h;
43
- }, v = (n, r) => {
44
- const e = (c) => {
42
+ }), l;
43
+ }, E = (e, r) => {
44
+ const n = (c) => {
45
45
  const o = c.match(/^(\d+\.\d+\.\d+(?:-[^-]+?-\d{8}))/);
46
46
  return o ? o[1] : c;
47
- }, t = e(n), s = e(r);
48
- return t === s && n !== r ? !1 : H.gte(t, s);
47
+ }, t = n(e), s = n(r);
48
+ return t === s && e !== r ? !1 : H.gte(t, s);
49
49
  };
50
50
  export {
51
- v as compareVersions,
52
- S as countRecursive,
51
+ E as compareVersions,
52
+ P as countRecursive,
53
53
  R as filterRecursive,
54
54
  C as flatRecursive,
55
- P as isIconifyString,
56
- x as isUrl,
55
+ W as isIconifyString,
56
+ x as isMailProtocol,
57
+ S as isUrl,
57
58
  d as mapRecursive,
58
59
  V as matchPath,
59
- W as matchPaths,
60
- k as splitNavColumns
60
+ k as matchPaths,
61
+ v as splitNavColumns
61
62
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "3.1.26",
3
+ "version": "3.1.28",
4
4
  "description": "Some useful front-end web components that can be used in Blocklets.",
5
5
  "keywords": [
6
6
  "react",
@@ -35,9 +35,9 @@
35
35
  "dependencies": {
36
36
  "@abtnode/constant": "^1.16.48",
37
37
  "@abtnode/util": "^1.16.48",
38
- "@arcblock/bridge": "3.1.26",
39
- "@arcblock/icons": "3.1.26",
40
- "@arcblock/react-hooks": "3.1.26",
38
+ "@arcblock/bridge": "3.1.28",
39
+ "@arcblock/icons": "3.1.28",
40
+ "@arcblock/react-hooks": "3.1.28",
41
41
  "@arcblock/ws": "^1.21.3",
42
42
  "@blocklet/constant": "^1.16.48",
43
43
  "@blocklet/did-space-react": "^1.1.16",
@@ -84,7 +84,7 @@
84
84
  "access": "public"
85
85
  },
86
86
  "devDependencies": {
87
- "@arcblock/did-connect-react": "3.1.26",
87
+ "@arcblock/did-connect-react": "3.1.28",
88
88
  "@types/dompurify": "^3.2.0",
89
89
  "@types/ua-parser-js": "^0.7.39",
90
90
  "@types/validator": "^13.15.2",
@@ -92,5 +92,5 @@
92
92
  "jest": "^29.7.0",
93
93
  "unbuild": "^2.0.0"
94
94
  },
95
- "gitHead": "2c9fa3385e138df1e652ef111353d1986e37b360"
95
+ "gitHead": "8a62be8796c0ab9129b24a08ecc051d1ac23e0de"
96
96
  }
@@ -130,7 +130,7 @@ export type UserCenterTab = {
130
130
  value: string;
131
131
  label: string;
132
132
  url: string;
133
- protected: boolean;
133
+ protected: boolean | string;
134
134
  icon?: string;
135
135
  isPrivate?: boolean; // 如果为 true 则不应该出现在隐私设置中
136
136
  };
@@ -9,7 +9,7 @@ import clsx from 'clsx';
9
9
  import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
10
10
  import Icon from '../Icon';
11
11
  import useMobile from '../hooks/use-mobile';
12
- import { splitNavColumns } from '../utils';
12
+ import { splitNavColumns, isMailProtocol } from '../utils';
13
13
 
14
14
  /**
15
15
  * footer 中的 links (支持分组, 最多支持 2 级)
@@ -28,14 +28,17 @@ export default function Links({ links = [], flowLayout = false, columns, ...rest
28
28
  result = render({ label, link, props });
29
29
  } else if (link && isString(link)) {
30
30
  const isExternal = link.startsWith('http') || link.startsWith('//');
31
+ const isMail = isMailProtocol(link);
32
+
33
+ const otherProps = isMail
34
+ ? {}
35
+ : {
36
+ target: isExternal ? '_blank' : '_self',
37
+ rel: isExternal ? 'noreferrer noopener' : undefined,
38
+ };
31
39
 
32
40
  result = (
33
- <a
34
- href={link}
35
- aria-label={`Footer link for ${label}`}
36
- target={isExternal ? '_blank' : '_self'}
37
- rel={isExternal ? 'noreferrer noopener' : undefined}
38
- {...props}>
41
+ <a href={link} aria-label={`Footer link for ${label}`} {...otherProps} {...props}>
39
42
  {label}
40
43
  </a>
41
44
  );
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 用户中心 fallback 组件,用于没有权限访问时展示的页面
3
+ */
4
+
5
+ import React from 'react';
6
+ import Empty from '@arcblock/ux/lib/Empty';
7
+ import { translate } from '@arcblock/ux/lib/Locale/util';
8
+ import { useMemoizedFn } from 'ahooks';
9
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
10
+
11
+ import { translations } from '../libs/locales';
12
+ import { UserCenterTab } from '../../@types';
13
+ import { useUserFollowersContext } from '../../contexts/user-followers';
14
+
15
+ function Fallback({
16
+ currentActiveTab,
17
+ isMyself,
18
+ children,
19
+ }: {
20
+ currentActiveTab: UserCenterTab;
21
+ isMyself: boolean;
22
+ children: React.ReactNode;
23
+ }) {
24
+ const { locale } = useLocaleContext();
25
+ const t = useMemoizedFn((key, data = {}) => {
26
+ return translate(translations, key, locale, 'en', data);
27
+ });
28
+ const { followed } = useUserFollowersContext();
29
+
30
+ // 自己的页面或公开内容,直接显示
31
+ if (isMyself || !currentActiveTab?.protected || currentActiveTab?.protected === 'all') {
32
+ return children;
33
+ }
34
+
35
+ const { protected: protection } = currentActiveTab;
36
+
37
+ // 私密内容(仅自己可见)
38
+ if (protection === true || protection === 'private') {
39
+ return <Empty>{t('underProtected')}</Empty>;
40
+ }
41
+
42
+ // 仅粉丝可见内容
43
+ if (protection === 'follower-only') {
44
+ return followed ? children : <Empty>{t('followersOnly')}</Empty>;
45
+ }
46
+
47
+ // 未知的保护类型,默认不显示
48
+ return <Empty>{t('underProtected')}</Empty>;
49
+ }
50
+
51
+ export default Fallback;
@@ -35,10 +35,13 @@ import useMobile from '../../hooks/use-mobile';
35
35
  import { ConfigUserSpaceProvider } from '../../contexts/config-user-space';
36
36
  import DidSpace from './storage';
37
37
  import Nft from './nft';
38
+ import { UserFollowersProvider } from '../../contexts/user-followers';
39
+ import Fallback from './fallback';
38
40
 
39
41
  const nftsLink = joinURL(PROFILE_URL, '/nfts');
40
42
  const settingsLink = joinURL(PROFILE_URL, '/settings');
41
43
  const didSpacesLink = joinURL(PROFILE_URL, '/did-spaces');
44
+ const userFollowersLink = joinURL(PROFILE_URL, '/user-followers');
42
45
 
43
46
  interface NavigationTabProps {
44
47
  label: string;
@@ -217,7 +220,14 @@ export default function UserCenter({
217
220
  value: nftsLink,
218
221
  url: getLink(nftsLink, locale),
219
222
  };
220
- let tabs: NavigationTabProps[] = [nftTab];
223
+ const userFollowersTab = {
224
+ label: t('userFollowers'),
225
+ protected: false,
226
+ isPrivate: false,
227
+ value: userFollowersLink,
228
+ url: getLink(userFollowersLink, locale),
229
+ };
230
+ let tabs: NavigationTabProps[] = [nftTab, userFollowersTab];
221
231
  if (isMyself) {
222
232
  tabs = [
223
233
  nftTab,
@@ -235,6 +245,7 @@ export default function UserCenter({
235
245
  value: didSpacesLink,
236
246
  url: getLink(didSpacesLink, locale),
237
247
  },
248
+ userFollowersTab,
238
249
  ];
239
250
  }
240
251
  return tabs;
@@ -254,6 +265,7 @@ export default function UserCenter({
254
265
  url: x.link || x.url,
255
266
  protected: privacyState?.data?.[value] ?? false,
256
267
  isPrivate: x.isPrivate || x.private || (x?._rawLink?.includes('/customer') ?? false), // FIXME: HACK: 隐藏 /customer 菜单, 需要一个通用的解决方案,在嵌入的时候就决定是否是私有的
268
+ followersOnly: x.component === 'did-comments', // 是否开启仅粉丝可查看的功能,目前只对 discuss kit 开启
257
269
  // icon: x.icon,
258
270
  };
259
271
  })
@@ -429,16 +441,10 @@ export default function UserCenter({
429
441
  }
430
442
 
431
443
  return (
432
- // eslint-disable-next-line react/jsx-no-useless-fragment
433
444
  <Box sx={{ flex: 1 }}>
434
- {currentActiveTab?.protected && !isMyself ? (
435
- <Box>
436
- <Empty>{t('underProtected')}</Empty>
437
- </Box>
438
- ) : (
439
- // eslint-disable-next-line react/jsx-no-useless-fragment
440
- <>{children ? <Box {...contentProps}>{renderChildrenWithProps(children)}</Box> : <>{renderDefaultTab}</>}</>
441
- )}
445
+ <Fallback currentActiveTab={currentActiveTab as UserCenterTab} isMyself={isMyself}>
446
+ {children ? <Box {...contentProps}>{renderChildrenWithProps(children)}</Box> : renderDefaultTab}
447
+ </Fallback>
442
448
  </Box>
443
449
  );
444
450
  }, [privacyState, currentActiveTab, isMyself, children, contentProps, renderDefaultTab, locale]);
@@ -681,7 +687,9 @@ export default function UserCenter({
681
687
  </Helmet>
682
688
  <Header style={{ display: 'none' }} />
683
689
  <Main>
684
- {content}
690
+ <UserFollowersProvider isMySelf={isMyself} userDid={userState.data?.did ?? ''}>
691
+ {content}
692
+ </UserFollowersProvider>
685
693
  {confirmHolder}
686
694
  </Main>
687
695
  </Box>
@@ -700,7 +708,9 @@ export default function UserCenter({
700
708
  </Helmet>
701
709
  <Header bordered {...headerProps} maxWidth="100%" />
702
710
  <Main>
703
- {content}
711
+ <UserFollowersProvider isMySelf={isMyself} userDid={userState.data?.did ?? ''}>
712
+ {content}
713
+ </UserFollowersProvider>
704
714
  {confirmHolder}
705
715
  </Main>
706
716
  {hideFooter ? null : (
@@ -0,0 +1,42 @@
1
+ import { useMemo } from 'react';
2
+ import { Button } from '@mui/material';
3
+ import { useMemoizedFn } from 'ahooks';
4
+ import { translate } from '@arcblock/ux/lib/Locale/util';
5
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
6
+
7
+ import { joinURL } from 'ufo';
8
+ import { User } from '../../../../@types';
9
+
10
+ import { translations } from '../../../libs/locales';
11
+
12
+ const getDiscussKitMountPoint = () => {
13
+ const { componentMountPoints = [] } = window.blocklet || {};
14
+ const component = componentMountPoints.find((c: any) => c.name === 'did-comments');
15
+ return component?.mountPoint;
16
+ };
17
+
18
+ function Chat({ user }: { user: User }) {
19
+ const { locale } = useLocaleContext();
20
+ const t = useMemoizedFn((key, data = {}) => {
21
+ return translate(translations, key, locale, 'en', data);
22
+ });
23
+ // 获取 discuss kit 的挂载点
24
+ const discussKitMountPoint = useMemo(() => getDiscussKitMountPoint(), []);
25
+
26
+ const onNavigateToChat = useMemoizedFn(() => {
27
+ window.open(joinURL(discussKitMountPoint, `/chat/dm/${user?.did}`), '_blank');
28
+ });
29
+
30
+ if (!discussKitMountPoint) {
31
+ return null;
32
+ }
33
+
34
+ return (
35
+ <Button fullWidth variant="outlined" color="inherit" onClick={onNavigateToChat}>
36
+ <i className="iconify" data-icon="mi:message-alt" style={{ fontSize: '15px', marginRight: '4px' }} />
37
+ {t('profile.chat')}
38
+ </Button>
39
+ );
40
+ }
41
+
42
+ export default Chat;
@@ -0,0 +1,30 @@
1
+ import { Button } from '@mui/material';
2
+ import { useMemoizedFn } from 'ahooks';
3
+ import { translate } from '@arcblock/ux/lib/Locale/util';
4
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
+ import NotificationsIcon from '@mui/icons-material/Notifications';
6
+ import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
7
+
8
+ import { translations } from '../../../libs/locales';
9
+ import { useUserFollowersContext } from '../../../../contexts/user-followers';
10
+
11
+ function Follow() {
12
+ const { locale } = useLocaleContext();
13
+ const t = useMemoizedFn((key, data = {}) => {
14
+ return translate(translations, key, locale, 'en', data);
15
+ });
16
+ const { followed, followUser, unfollowUser } = useUserFollowersContext();
17
+
18
+ return (
19
+ <Button variant="contained" onClick={() => (followed ? unfollowUser() : followUser())} fullWidth>
20
+ {followed ? (
21
+ <NotificationsOffIcon sx={{ fontSize: '14px', marginRight: '4px' }} />
22
+ ) : (
23
+ <NotificationsIcon sx={{ fontSize: '14px', marginRight: '4px' }} />
24
+ )}
25
+ {followed ? t('profile.unfollow') : t('profile.follow')}
26
+ </Button>
27
+ );
28
+ }
29
+
30
+ export default Follow;
@@ -0,0 +1,17 @@
1
+ import { Box } from '@mui/material';
2
+ import Chat from './chat';
3
+ import Follow from './follow';
4
+
5
+ import { User } from '../../../../@types';
6
+
7
+ // 进入这里肯定是他人的个人中心
8
+ function SocialActions({ user }: { user: User }) {
9
+ return (
10
+ <Box sx={{ display: 'flex', gap: 1 }}>
11
+ <Chat user={user} />
12
+ <Follow />
13
+ </Box>
14
+ );
15
+ }
16
+
17
+ export default SocialActions;
@@ -24,6 +24,7 @@ import UserMetadataComponent from './metadata';
24
24
  import UserStatus from './user-status';
25
25
  import UserInfo from './user-info';
26
26
  import { client } from '../../../libs/client';
27
+ import SocialActions from './social-actions';
27
28
 
28
29
  export default function UserBasicInfo({
29
30
  user,
@@ -225,6 +226,11 @@ export default function UserBasicInfo({
225
226
  <DID did={user.did} showQrcode copyable compact={!showFullDid} responsive={!showFullDid} locale={locale} />
226
227
  </Box>
227
228
  </Box>
229
+ {!isMyself ? (
230
+ <Box sx={{ mt: 2 }}>
231
+ <SocialActions user={user} />
232
+ </Box>
233
+ ) : null}
228
234
  <UserMetadataComponent isMobile={isMobile} isMyself={isMyself} user={user} onSave={onSave} />
229
235
  {isMyself ? (
230
236
  <>
@@ -25,10 +25,12 @@ export const translations = {
25
25
  emptyField: '未填写',
26
26
  emptyContent: '暂无内容',
27
27
  underProtected: '用户已设置隐私保护',
28
+ followersOnly: '用户已设置仅粉丝可见,关注用户后可查看',
28
29
  noUserFound: '未找到指定的用户',
29
30
  notificationManagement: '通知管理',
30
31
  privacyManagement: '隐私管理',
31
32
  storageManagement: 'DID Spaces',
33
+ userFollowers: '关注 & 粉丝',
32
34
  webhook: {
33
35
  url: '自定义URL',
34
36
  slack: 'Slack',
@@ -161,6 +163,11 @@ export const translations = {
161
163
  invalidPostalCode: '邮政编码格式不正确',
162
164
  },
163
165
  maxLinkCount: '最多可添加 {count} 个社交链接',
166
+ chat: '聊天',
167
+ follow: '关注',
168
+ unfollow: '取消关注',
169
+ follow_success: '关注成功',
170
+ unfollow_success: '取消关注成功',
164
171
  },
165
172
  destroyMyself: {
166
173
  title: '删除账户',
@@ -203,8 +210,10 @@ export const translations = {
203
210
  notificationManagement: 'Notifications',
204
211
  privacyManagement: 'Privacy',
205
212
  storageManagement: 'DID Spaces',
213
+ userFollowers: 'Following & Followers',
206
214
  emptyContent: 'Empty',
207
215
  underProtected: 'This page has protected privacy',
216
+ followersOnly: 'This page is only visible to followers, follow the user to view',
208
217
  noUserFound: 'No user found',
209
218
  webhook: {
210
219
  url: 'Custom url',
@@ -340,6 +349,11 @@ export const translations = {
340
349
  invalidPostalCode: 'Postal code is invalid',
341
350
  },
342
351
  maxLinkCount: 'Up to {count} social links can be added',
352
+ chat: 'Chat',
353
+ follow: 'Follow',
354
+ unfollow: 'Unfollow',
355
+ follow_success: 'Follow successfully',
356
+ unfollow_success: 'Unfollow successfully',
343
357
  },
344
358
  destroyMyself: {
345
359
  title: 'Delete Account',
package/src/blocklets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mapRecursive, filterRecursive, isUrl } from './utils';
1
+ import { mapRecursive, filterRecursive, isUrl, isMailProtocol } from './utils';
2
2
 
3
3
  export const publicPath = window?.blocklet?.groupPrefix || window?.blocklet?.prefix || '/';
4
4
 
@@ -28,6 +28,10 @@ export const getLink = (link, locale = 'en') => {
28
28
  url.searchParams.set('locale', locale);
29
29
  return url.href;
30
30
  }
31
+ // 如果是 mailto 协议, 直接返回
32
+ if (isMailProtocol(link)) {
33
+ return link;
34
+ }
31
35
  const url = new URL(link, window.location.origin);
32
36
  url.searchParams.set('locale', locale);
33
37
  return url.pathname + url.search;
@@ -0,0 +1,54 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { translate } from '@arcblock/ux/lib/Locale/util';
3
+ import { useMemoizedFn } from 'ahooks';
4
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
+
6
+ import useFollow from '../hooks/use-follow';
7
+ import { translations } from '../UserCenter/libs/locales';
8
+
9
+ const UserFollowersContext = createContext<UserFollowersContextType>({
10
+ followed: false,
11
+ followUser: () => {},
12
+ unfollowUser: () => {},
13
+ });
14
+ const { Provider } = UserFollowersContext;
15
+
16
+ type UserFollowersContextType = {
17
+ followed: boolean;
18
+ followUser: () => void;
19
+ unfollowUser: () => void;
20
+ };
21
+
22
+ function UserFollowersProvider({
23
+ isMySelf,
24
+ userDid,
25
+ children,
26
+ }: {
27
+ children: React.ReactNode;
28
+ isMySelf: boolean;
29
+ userDid: string;
30
+ }) {
31
+ const { locale } = useLocaleContext();
32
+ const t = useMemoizedFn((key, data = {}) => {
33
+ return translate(translations, key, locale, 'en', data);
34
+ });
35
+ const { followed, followUser, unfollowUser } = useFollow({ userDid, t, isMySelf });
36
+
37
+ return (
38
+ <Provider
39
+ value={{
40
+ followed,
41
+ followUser,
42
+ unfollowUser,
43
+ }}>
44
+ {children}
45
+ </Provider>
46
+ );
47
+ }
48
+
49
+ function useUserFollowersContext() {
50
+ const res = useContext(UserFollowersContext);
51
+ return res;
52
+ }
53
+
54
+ export { UserFollowersContext, useUserFollowersContext, UserFollowersProvider };
@@ -0,0 +1,74 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useMemoizedFn } from 'ahooks';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+
5
+ import type { AxiosError } from 'axios';
6
+ import { client } from '../libs/client';
7
+ import { formatAxiosError } from '../UserCenter/libs/utils';
8
+
9
+ /**
10
+ * 登录用户与当前用户(userDid)的关注关系
11
+ */
12
+ export default function useFollow({
13
+ userDid,
14
+ t,
15
+ isMySelf,
16
+ }: {
17
+ userDid: string;
18
+ t: (k: string) => string;
19
+ isMySelf: boolean;
20
+ }) {
21
+ const [followed, setFollowed] = useState(false);
22
+
23
+ const isFollowingUser = useMemoizedFn(async () => {
24
+ try {
25
+ if (isMySelf) {
26
+ setFollowed(true);
27
+ return;
28
+ }
29
+ const res = await client.user.isFollowingUser({ userDid });
30
+ setFollowed(res);
31
+ } catch (error) {
32
+ console.error(error);
33
+ }
34
+ });
35
+
36
+ const followUser = useMemoizedFn(async (followUserDid: string = userDid) => {
37
+ if (isMySelf && followUserDid === userDid) {
38
+ return;
39
+ }
40
+ try {
41
+ await client.user.followUser({ userDid: followUserDid });
42
+ Toast.success(t('profile.follow_success'));
43
+ isFollowingUser();
44
+ } catch (error) {
45
+ console.error(error);
46
+ Toast.error(formatAxiosError(error as AxiosError));
47
+ }
48
+ });
49
+ const unfollowUser = useMemoizedFn(async (unfollowUserDid: string = userDid) => {
50
+ if (isMySelf && unfollowUserDid === userDid) {
51
+ return;
52
+ }
53
+ try {
54
+ await client.user.unfollowUser({ userDid: unfollowUserDid });
55
+ Toast.success(t('profile.unfollow_success'));
56
+ isFollowingUser();
57
+ } catch (error) {
58
+ console.error(error);
59
+ Toast.error(formatAxiosError(error as AxiosError));
60
+ }
61
+ });
62
+
63
+ useEffect(() => {
64
+ if (userDid && !isMySelf) {
65
+ isFollowingUser();
66
+ }
67
+ }, [isFollowingUser, userDid, isMySelf]);
68
+
69
+ return {
70
+ followed,
71
+ followUser,
72
+ unfollowUser,
73
+ };
74
+ }
package/src/utils.js CHANGED
@@ -47,6 +47,11 @@ export const isUrl = (str) => {
47
47
  return /^https?:\/\//.test(str);
48
48
  };
49
49
 
50
+ // 链接是否是 mailto 协议
51
+ export const isMailProtocol = (str) => {
52
+ return /^mailto:/i.test(str.trim());
53
+ };
54
+
50
55
  /**
51
56
  * @description 检测是否是 Iconify 格式的字符串
52
57
  * @deprecated