2022-12-27 23:46:13 +00:00
|
|
|
import "./ProfilePage.css";
|
2023-02-27 13:17:13 +00:00
|
|
|
import { useEffect, useState } from "react";
|
2023-02-08 21:10:26 +00:00
|
|
|
import { useIntl, FormattedMessage } from "react-intl";
|
2023-01-09 11:00:23 +00:00
|
|
|
import { useSelector } from "react-redux";
|
|
|
|
import { useNavigate, useParams } from "react-router-dom";
|
2023-03-25 22:55:34 +00:00
|
|
|
import { encodeTLV, NostrPrefix } from "@snort/nostr";
|
2022-12-29 22:23:41 +00:00
|
|
|
|
2023-03-25 22:55:34 +00:00
|
|
|
import { parseNostrLink, unwrap } from "Util";
|
2023-02-03 21:38:14 +00:00
|
|
|
import { formatShort } from "Number";
|
2023-02-13 15:49:19 +00:00
|
|
|
import Note from "Element/Note";
|
|
|
|
import Bookmarks from "Element/Bookmarks";
|
2023-02-10 19:23:52 +00:00
|
|
|
import RelaysMetadata from "Element/RelaysMetadata";
|
2023-02-06 21:42:47 +00:00
|
|
|
import { Tab, TabElement } from "Element/Tabs";
|
2023-03-02 17:47:02 +00:00
|
|
|
import Icon from "Icons/Icon";
|
2023-02-10 11:12:11 +00:00
|
|
|
import useMutedFeed from "Feed/MuteList";
|
2023-02-10 19:23:52 +00:00
|
|
|
import useRelaysFeed from "Feed/RelaysFeed";
|
2023-02-13 15:49:19 +00:00
|
|
|
import usePinnedFeed from "Feed/PinnedFeed";
|
|
|
|
import useBookmarkFeed from "Feed/BookmarkFeed";
|
2023-02-10 11:12:11 +00:00
|
|
|
import useFollowersFeed from "Feed/FollowersFeed";
|
|
|
|
import useFollowsFeed from "Feed/FollowsFeed";
|
2023-03-09 10:13:10 +00:00
|
|
|
import useProfileBadges from "Feed/BadgesFeed";
|
2023-03-03 14:30:31 +00:00
|
|
|
import { useUserProfile } from "Hooks/useUserProfile";
|
2023-02-10 11:12:11 +00:00
|
|
|
import useModeration from "Hooks/useModeration";
|
2023-02-03 21:38:14 +00:00
|
|
|
import useZapsFeed from "Feed/ZapsFeed";
|
2023-02-10 11:12:11 +00:00
|
|
|
import { default as ZapElement } from "Element/Zap";
|
2023-01-20 11:11:50 +00:00
|
|
|
import FollowButton from "Element/FollowButton";
|
|
|
|
import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
|
|
|
import Avatar from "Element/Avatar";
|
|
|
|
import Timeline from "Element/Timeline";
|
2023-02-07 20:04:50 +00:00
|
|
|
import Text from "Element/Text";
|
2023-02-07 13:32:32 +00:00
|
|
|
import SendSats from "Element/SendSats";
|
2023-01-20 11:11:50 +00:00
|
|
|
import Nip05 from "Element/Nip05";
|
|
|
|
import Copy from "Element/Copy";
|
2023-02-06 21:42:47 +00:00
|
|
|
import ProfileImage from "Element/ProfileImage";
|
2023-01-27 21:10:14 +00:00
|
|
|
import BlockList from "Element/BlockList";
|
2023-01-26 11:34:18 +00:00
|
|
|
import MutedList from "Element/MutedList";
|
2023-02-10 11:12:11 +00:00
|
|
|
import FollowsList from "Element/FollowListBase";
|
2023-01-29 22:11:04 +00:00
|
|
|
import IconButton from "Element/IconButton";
|
2023-01-20 11:11:50 +00:00
|
|
|
import { RootState } from "State/Store";
|
2023-02-07 20:04:50 +00:00
|
|
|
import FollowsYou from "Element/FollowsYou";
|
2023-01-29 22:11:04 +00:00
|
|
|
import QrCode from "Element/QrCode";
|
|
|
|
import Modal from "Element/Modal";
|
2023-03-09 10:13:10 +00:00
|
|
|
import BadgeList from "Element/BadgeList";
|
2023-02-07 20:04:50 +00:00
|
|
|
import { ProxyImg } from "Element/ProxyImg";
|
2023-02-07 13:32:32 +00:00
|
|
|
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
2023-02-08 21:10:26 +00:00
|
|
|
import messages from "./messages";
|
2023-02-27 13:17:13 +00:00
|
|
|
import { EmailRegex } from "Const";
|
|
|
|
import { getNip05PubKey } from "./Login";
|
2023-02-08 21:10:26 +00:00
|
|
|
|
|
|
|
const NOTES = 0;
|
|
|
|
const REACTIONS = 1;
|
|
|
|
const FOLLOWERS = 2;
|
|
|
|
const FOLLOWS = 3;
|
|
|
|
const ZAPS = 4;
|
|
|
|
const MUTED = 5;
|
|
|
|
const BLOCKED = 6;
|
2023-02-10 19:23:52 +00:00
|
|
|
const RELAYS = 7;
|
2023-02-13 15:49:19 +00:00
|
|
|
const BOOKMARKS = 8;
|
2022-12-18 14:51:32 +00:00
|
|
|
|
|
|
|
export default function ProfilePage() {
|
2023-02-08 21:10:26 +00:00
|
|
|
const { formatMessage } = useIntl();
|
2023-01-31 16:07:46 +00:00
|
|
|
const params = useParams();
|
|
|
|
const navigate = useNavigate();
|
2023-02-27 13:17:13 +00:00
|
|
|
const [id, setId] = useState<string>();
|
2023-01-31 16:07:46 +00:00
|
|
|
const user = useUserProfile(id);
|
2023-02-27 13:17:13 +00:00
|
|
|
const loginPubKey = useSelector((s: RootState) => s.login.publicKey);
|
2023-01-31 16:07:46 +00:00
|
|
|
const isMe = loginPubKey === id;
|
|
|
|
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
|
|
|
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
2023-02-07 20:04:50 +00:00
|
|
|
const aboutText = user?.about || "";
|
|
|
|
const about = Text({
|
|
|
|
content: aboutText,
|
|
|
|
tags: [],
|
|
|
|
creator: "",
|
|
|
|
});
|
2023-03-25 22:55:34 +00:00
|
|
|
const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id;
|
2023-02-16 17:19:40 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
|
2023-02-07 20:04:50 +00:00
|
|
|
const website_url =
|
2023-02-09 12:26:54 +00:00
|
|
|
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
|
2023-02-10 11:12:11 +00:00
|
|
|
// feeds
|
|
|
|
const { blocked } = useModeration();
|
|
|
|
const { notes: pinned, related: pinRelated } = usePinnedFeed(id);
|
|
|
|
const { notes: bookmarks, related: bookmarkRelated } = useBookmarkFeed(id);
|
2023-02-10 19:23:52 +00:00
|
|
|
const relays = useRelaysFeed(id);
|
2023-02-10 11:12:11 +00:00
|
|
|
const zaps = useZapsFeed(id);
|
2023-02-07 20:04:50 +00:00
|
|
|
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
2023-02-10 11:12:11 +00:00
|
|
|
const followers = useFollowersFeed(id);
|
|
|
|
const follows = useFollowsFeed(id);
|
|
|
|
const muted = useMutedFeed(id);
|
2023-03-09 10:13:10 +00:00
|
|
|
const badges = useProfileBadges(id);
|
2023-02-10 11:12:11 +00:00
|
|
|
// tabs
|
2023-02-08 21:10:26 +00:00
|
|
|
const ProfileTab = {
|
|
|
|
Notes: { text: formatMessage(messages.Notes), value: NOTES },
|
|
|
|
Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS },
|
2023-02-10 11:12:11 +00:00
|
|
|
Followers: { text: formatMessage(messages.FollowersCount, { n: followers.length }), value: FOLLOWERS },
|
|
|
|
Follows: { text: formatMessage(messages.FollowsCount, { n: follows.length }), value: FOLLOWS },
|
|
|
|
Zaps: { text: formatMessage(messages.ZapsCount, { n: zaps.length }), value: ZAPS },
|
|
|
|
Muted: { text: formatMessage(messages.MutedCount, { n: muted.length }), value: MUTED },
|
|
|
|
Blocked: { text: formatMessage(messages.BlockedCount, { n: blocked.length }), value: BLOCKED },
|
|
|
|
Relays: { text: formatMessage(messages.RelaysCount, { n: relays.length }), value: RELAYS },
|
|
|
|
Bookmarks: { text: formatMessage(messages.BookmarksCount, { n: bookmarks.length }), value: BOOKMARKS },
|
2023-02-08 21:10:26 +00:00
|
|
|
};
|
|
|
|
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
|
2023-02-13 15:49:19 +00:00
|
|
|
const optionalTabs = [
|
|
|
|
zapsTotal > 0 && ProfileTab.Zaps,
|
|
|
|
relays.length > 0 && ProfileTab.Relays,
|
|
|
|
bookmarks.length > 0 && ProfileTab.Bookmarks,
|
2023-02-10 11:12:11 +00:00
|
|
|
muted.length > 0 && ProfileTab.Muted,
|
2023-02-13 15:49:19 +00:00
|
|
|
].filter(a => unwrap(a)) as Tab[];
|
2023-02-10 11:12:11 +00:00
|
|
|
const horizontalScroll = useHorizontalScroll();
|
2023-01-10 10:30:33 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
useEffect(() => {
|
2023-02-27 13:17:13 +00:00
|
|
|
if (params.id?.match(EmailRegex)) {
|
|
|
|
getNip05PubKey(params.id).then(a => {
|
|
|
|
setId(a);
|
|
|
|
});
|
|
|
|
} else {
|
2023-03-25 22:55:34 +00:00
|
|
|
const nav = parseNostrLink(params.id ?? "");
|
|
|
|
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
|
|
|
|
// todo: use relays if any for nprofile
|
|
|
|
setId(nav.id);
|
|
|
|
} else {
|
|
|
|
setId(parseId(params.id ?? ""));
|
|
|
|
}
|
2023-02-27 13:17:13 +00:00
|
|
|
}
|
2023-01-31 16:07:46 +00:00
|
|
|
setTab(ProfileTab.Notes);
|
|
|
|
}, [params]);
|
2022-12-28 14:51:33 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
function username() {
|
|
|
|
return (
|
|
|
|
<div className="name">
|
|
|
|
<h2>
|
2023-02-07 20:04:50 +00:00
|
|
|
{user?.display_name || user?.name || "Nostrich"}
|
2023-02-24 19:28:29 +00:00
|
|
|
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
|
2023-01-31 16:07:46 +00:00
|
|
|
</h2>
|
|
|
|
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
2023-03-09 10:13:10 +00:00
|
|
|
<BadgeList badges={badges} />
|
2023-02-16 17:19:40 +00:00
|
|
|
<Copy text={npub} />
|
2023-01-31 16:07:46 +00:00
|
|
|
{links()}
|
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-01-19 18:00:56 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
function links() {
|
|
|
|
return (
|
|
|
|
<div className="links">
|
|
|
|
{user?.website && (
|
|
|
|
<div className="website f-ellipsis">
|
2023-03-02 18:39:29 +00:00
|
|
|
<Icon name="link" />
|
2023-02-07 20:04:50 +00:00
|
|
|
<a href={website_url} target="_blank" rel="noreferrer">
|
|
|
|
{user.website}
|
|
|
|
</a>
|
2023-01-31 16:07:46 +00:00
|
|
|
</div>
|
|
|
|
)}
|
2023-01-10 07:09:09 +00:00
|
|
|
|
2023-02-06 21:42:47 +00:00
|
|
|
{lnurl && (
|
|
|
|
<div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
|
2023-03-02 18:39:29 +00:00
|
|
|
<Icon name="zap" />
|
2023-02-06 21:42:47 +00:00
|
|
|
{lnurl}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2023-02-07 13:32:32 +00:00
|
|
|
<SendSats
|
2023-02-25 21:18:36 +00:00
|
|
|
lnurl={lnurl}
|
2023-02-07 13:32:32 +00:00
|
|
|
show={showLnQr}
|
|
|
|
onClose={() => setShowLnQr(false)}
|
|
|
|
author={id}
|
|
|
|
target={user?.display_name || user?.name}
|
|
|
|
/>
|
2023-01-31 16:07:46 +00:00
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-01-01 20:31:09 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
function bio() {
|
2023-02-07 20:04:50 +00:00
|
|
|
return (
|
2023-02-13 15:00:36 +00:00
|
|
|
aboutText.length > 0 && (
|
2023-02-13 10:47:52 +00:00
|
|
|
<div dir="auto" className="details">
|
|
|
|
{about}
|
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
)
|
|
|
|
);
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2022-12-28 23:28:28 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
function tabContent() {
|
2023-02-27 13:17:13 +00:00
|
|
|
if (!id) return null;
|
|
|
|
|
2023-02-08 21:10:26 +00:00
|
|
|
switch (tab.value) {
|
|
|
|
case NOTES:
|
2023-02-07 20:04:50 +00:00
|
|
|
return (
|
2023-02-13 15:49:19 +00:00
|
|
|
<>
|
|
|
|
<div className="main-content">
|
|
|
|
{pinned.map(n => {
|
|
|
|
return (
|
|
|
|
<Note
|
|
|
|
key={`pinned-${n.id}`}
|
|
|
|
data={n}
|
|
|
|
related={pinRelated}
|
|
|
|
options={{ showTime: false, showPinned: true, canUnpin: id === loginPubKey }}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<Timeline
|
|
|
|
key={id}
|
|
|
|
subject={{
|
|
|
|
type: "pubkey",
|
|
|
|
items: [id],
|
|
|
|
discriminator: id.slice(0, 12),
|
|
|
|
}}
|
|
|
|
postsOnly={false}
|
|
|
|
method={"TIME_RANGE"}
|
|
|
|
ignoreModeration={true}
|
2023-02-15 13:42:03 +00:00
|
|
|
window={60 * 60 * 6}
|
2023-02-13 15:49:19 +00:00
|
|
|
/>
|
|
|
|
</>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-02-08 21:10:26 +00:00
|
|
|
case ZAPS: {
|
2023-02-04 10:16:08 +00:00
|
|
|
return (
|
|
|
|
<div className="main-content">
|
2023-02-12 12:44:37 +00:00
|
|
|
<div className="zaps-total">
|
2023-02-09 12:26:54 +00:00
|
|
|
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
|
2023-02-12 12:44:37 +00:00
|
|
|
</div>
|
2023-02-09 12:26:54 +00:00
|
|
|
{zaps.map(z => (
|
2023-02-07 20:04:50 +00:00
|
|
|
<ZapElement showZapped={false} zap={z} />
|
|
|
|
))}
|
2023-02-04 10:16:08 +00:00
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-02-04 10:16:08 +00:00
|
|
|
}
|
|
|
|
|
2023-02-08 21:10:26 +00:00
|
|
|
case FOLLOWS: {
|
2023-02-25 21:27:01 +00:00
|
|
|
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={!isMe} />;
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-02-08 21:10:26 +00:00
|
|
|
case FOLLOWERS: {
|
2023-02-25 21:27:01 +00:00
|
|
|
return <FollowsList pubkeys={followers} showAbout={true} />;
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-02-08 21:10:26 +00:00
|
|
|
case MUTED: {
|
2023-02-10 11:12:11 +00:00
|
|
|
return <MutedList pubkeys={muted} />;
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-02-08 21:10:26 +00:00
|
|
|
case BLOCKED: {
|
2023-02-10 11:12:11 +00:00
|
|
|
return <BlockList />;
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-02-10 19:23:52 +00:00
|
|
|
case RELAYS: {
|
|
|
|
return <RelaysMetadata relays={relays} />;
|
|
|
|
}
|
2023-02-13 15:49:19 +00:00
|
|
|
case BOOKMARKS: {
|
|
|
|
return <Bookmarks pubkey={id} bookmarks={bookmarks} related={bookmarkRelated} />;
|
|
|
|
}
|
2023-01-10 10:30:33 +00:00
|
|
|
}
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-01-10 10:30:33 +00:00
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
function avatar() {
|
|
|
|
return (
|
|
|
|
<div className="avatar-wrapper">
|
|
|
|
<Avatar user={user} />
|
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
2023-01-14 13:28:22 +00:00
|
|
|
|
2023-01-29 22:11:04 +00:00
|
|
|
function renderIcons() {
|
2023-03-25 22:55:34 +00:00
|
|
|
if (!id) return;
|
|
|
|
|
|
|
|
const firstRelay = relays.find(a => a.settings.write)?.url;
|
|
|
|
const link = encodeTLV(id, NostrPrefix.Profile, firstRelay ? [firstRelay] : undefined);
|
2023-01-29 22:11:04 +00:00
|
|
|
return (
|
|
|
|
<div className="icon-actions">
|
|
|
|
<IconButton onClick={() => setShowProfileQr(true)}>
|
2023-03-02 17:47:02 +00:00
|
|
|
<Icon name="qr" size={16} />
|
2023-01-29 22:11:04 +00:00
|
|
|
</IconButton>
|
|
|
|
{showProfileQr && (
|
|
|
|
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
2023-03-25 22:55:34 +00:00
|
|
|
<ProfileImage pubkey={id} />
|
|
|
|
<QrCode data={link} className="m10 align-center" />
|
|
|
|
<Copy text={link} className="align-center" />
|
2023-01-29 22:11:04 +00:00
|
|
|
</Modal>
|
|
|
|
)}
|
|
|
|
{isMe ? (
|
|
|
|
<>
|
|
|
|
<button type="button" onClick={() => navigate("/settings")}>
|
2023-02-08 21:10:26 +00:00
|
|
|
<FormattedMessage {...messages.Settings} />
|
2023-01-29 22:11:04 +00:00
|
|
|
</button>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
2023-02-05 11:30:26 +00:00
|
|
|
{lnurl && (
|
|
|
|
<IconButton onClick={() => setShowLnQr(true)}>
|
2023-03-02 17:47:02 +00:00
|
|
|
<Icon name="zap" size={16} />
|
2023-02-05 11:30:26 +00:00
|
|
|
</IconButton>
|
|
|
|
)}
|
2023-02-27 13:17:13 +00:00
|
|
|
{loginPubKey && (
|
2023-01-29 22:11:04 +00:00
|
|
|
<>
|
2023-01-29 19:44:53 +00:00
|
|
|
<IconButton onClick={() => navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`)}>
|
2023-03-02 17:47:02 +00:00
|
|
|
<Icon name="envelope" size={16} />
|
2023-01-29 22:11:04 +00:00
|
|
|
</IconButton>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-29 22:11:04 +00:00
|
|
|
}
|
|
|
|
|
2023-01-31 16:07:46 +00:00
|
|
|
function userDetails() {
|
2023-03-25 22:55:34 +00:00
|
|
|
if (!id) return;
|
2022-12-18 14:51:32 +00:00
|
|
|
return (
|
2023-01-31 16:07:46 +00:00
|
|
|
<div className="details-wrapper">
|
|
|
|
{username()}
|
|
|
|
<div className="profile-actions">
|
|
|
|
{renderIcons()}
|
2023-03-25 22:55:34 +00:00
|
|
|
{!isMe && <FollowButton pubkey={id} />}
|
2023-01-31 16:07:46 +00:00
|
|
|
</div>
|
|
|
|
{bio()}
|
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
|
|
|
|
2023-02-06 21:42:47 +00:00
|
|
|
function renderTab(v: Tab) {
|
2023-02-14 10:13:19 +00:00
|
|
|
return <TabElement key={v.value} t={v} tab={tab} setTab={setTab} />;
|
2023-01-31 16:07:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const w = window.document.querySelector(".page")?.clientWidth;
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="profile flex">
|
2023-02-09 12:26:54 +00:00
|
|
|
{user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />}
|
2023-01-31 16:07:46 +00:00
|
|
|
<div className="profile-wrapper flex">
|
|
|
|
{avatar()}
|
|
|
|
{userDetails()}
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-02-10 11:12:11 +00:00
|
|
|
<div className="main-content">
|
|
|
|
<div className="tabs" ref={horizontalScroll}>
|
|
|
|
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows].map(renderTab)}
|
|
|
|
{optionalTabs.map(renderTab)}
|
|
|
|
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
|
|
|
|
</div>
|
2023-01-31 16:07:46 +00:00
|
|
|
</div>
|
|
|
|
{tabContent()}
|
|
|
|
</>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-10 09:18:46 +00:00
|
|
|
}
|