@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.
- package/lib/@types/index.d.ts +1 -1
- package/lib/Footer/links.js +26 -33
- package/lib/UserCenter/components/fallback.d.ts +8 -0
- package/lib/UserCenter/components/fallback.js +21 -0
- package/lib/UserCenter/components/user-center.js +205 -200
- package/lib/UserCenter/components/user-info/social-actions/chat.d.ts +5 -0
- package/lib/UserCenter/components/user-info/social-actions/chat.js +24 -0
- package/lib/UserCenter/components/user-info/social-actions/follow.d.ts +2 -0
- package/lib/UserCenter/components/user-info/social-actions/follow.js +19 -0
- package/lib/UserCenter/components/user-info/social-actions/index.d.ts +5 -0
- package/lib/UserCenter/components/user-info/social-actions/index.js +13 -0
- package/lib/UserCenter/components/user-info/user-basic-info.js +37 -35
- package/lib/UserCenter/libs/locales.d.ts +14 -0
- package/lib/UserCenter/libs/locales.js +16 -2
- package/lib/blocklets.js +56 -54
- package/lib/contexts/user-followers.d.ts +13 -0
- package/lib/contexts/user-followers.js +40 -0
- package/lib/hooks/use-follow.d.ts +12 -0
- package/lib/hooks/use-follow.js +47 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +37 -36
- package/package.json +6 -6
- package/src/@types/index.ts +1 -1
- package/src/Footer/links.jsx +10 -7
- package/src/UserCenter/components/fallback.tsx +51 -0
- package/src/UserCenter/components/user-center.tsx +22 -12
- package/src/UserCenter/components/user-info/social-actions/chat.tsx +42 -0
- package/src/UserCenter/components/user-info/social-actions/follow.tsx +30 -0
- package/src/UserCenter/components/user-info/social-actions/index.tsx +17 -0
- package/src/UserCenter/components/user-info/user-basic-info.tsx +6 -0
- package/src/UserCenter/libs/locales.ts +14 -0
- package/src/blocklets.js +5 -1
- package/src/contexts/user-followers.tsx +54 -0
- package/src/hooks/use-follow.tsx +74 -0
- package/src/utils.js +5 -0
package/lib/utils.js
CHANGED
|
@@ -1,61 +1,62 @@
|
|
|
1
1
|
import H from "semver";
|
|
2
|
-
const d = (
|
|
2
|
+
const d = (e, r, n = "children") => e.map((t) => Array.isArray(t[n]) ? r({
|
|
3
3
|
...t,
|
|
4
|
-
[
|
|
5
|
-
}) : r(t)), C = (
|
|
6
|
-
const
|
|
7
|
-
return d(
|
|
8
|
-
},
|
|
9
|
-
let
|
|
10
|
-
return d(
|
|
11
|
-
}, R = (
|
|
12
|
-
const s = t[
|
|
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,
|
|
15
|
-
t[
|
|
14
|
+
const o = R(s, r, n);
|
|
15
|
+
t[n] = o?.length ? o : void 0;
|
|
16
16
|
}
|
|
17
|
-
const c = { filteredChildren: t[
|
|
17
|
+
const c = { filteredChildren: t[n], isLeaf: !s?.length };
|
|
18
18
|
return r(t, c);
|
|
19
|
-
}),
|
|
20
|
-
if (!
|
|
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}/`,
|
|
23
|
-
return
|
|
24
|
-
},
|
|
25
|
-
const r =
|
|
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
|
-
},
|
|
28
|
-
const { columns:
|
|
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 =
|
|
31
|
-
return
|
|
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,
|
|
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,
|
|
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
|
-
}),
|
|
43
|
-
},
|
|
44
|
-
const
|
|
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
|
|
48
|
-
return 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
|
-
|
|
52
|
-
|
|
51
|
+
E as compareVersions,
|
|
52
|
+
P as countRecursive,
|
|
53
53
|
R as filterRecursive,
|
|
54
54
|
C as flatRecursive,
|
|
55
|
-
|
|
56
|
-
x as
|
|
55
|
+
W as isIconifyString,
|
|
56
|
+
x as isMailProtocol,
|
|
57
|
+
S as isUrl,
|
|
57
58
|
d as mapRecursive,
|
|
58
59
|
V as matchPath,
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
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.
|
|
39
|
-
"@arcblock/icons": "3.1.
|
|
40
|
-
"@arcblock/react-hooks": "3.1.
|
|
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.
|
|
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": "
|
|
95
|
+
"gitHead": "8a62be8796c0ab9129b24a08ecc051d1ac23e0de"
|
|
96
96
|
}
|
package/src/@types/index.ts
CHANGED
package/src/Footer/links.jsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
435
|
-
<Box>
|
|
436
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|