mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-09 12:23:37 +02:00
show lists the user is in
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonProps,
|
ButtonProps,
|
||||||
@@ -9,30 +10,69 @@ import {
|
|||||||
MenuGroup,
|
MenuGroup,
|
||||||
MenuOptionGroup,
|
MenuOptionGroup,
|
||||||
MenuDivider,
|
MenuDivider,
|
||||||
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import useSubject from "../hooks/use-subject";
|
import useSubject from "../hooks/use-subject";
|
||||||
import clientFollowingService from "../services/client-following";
|
import clientFollowingService from "../services/client-following";
|
||||||
import { useUserContacts } from "../hooks/use-user-contacts";
|
import { useUserContacts } from "../hooks/use-user-contacts";
|
||||||
import "../services/lists";
|
import "../services/lists";
|
||||||
import { ArrowDownSIcon, FollowIcon, PlusCircleIcon, TrashIcon, UnfollowIcon } from "./icons";
|
import { ArrowDownSIcon, FollowIcon, PlusCircleIcon, UnfollowIcon } from "./icons";
|
||||||
import useUserLists from "../hooks/use-user-lists";
|
import useUserLists from "../hooks/use-user-lists";
|
||||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||||
import { useAdditionalRelayContext } from "../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../providers/additional-relay-context";
|
||||||
|
|
||||||
function UsersLists() {
|
function UsersLists({ pubkey }: { pubkey: string }) {
|
||||||
|
const toast = useToast();
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
|
const [isLoading, setLoading] = useState(false);
|
||||||
|
|
||||||
const readRelays = useReadRelayUrls(useAdditionalRelayContext());
|
const readRelays = useReadRelayUrls(useAdditionalRelayContext());
|
||||||
const lists = useUserLists(account.pubkey, readRelays);
|
const lists = useUserLists(account.pubkey, readRelays);
|
||||||
|
|
||||||
|
const listsArray = Array.from(Object.values(lists));
|
||||||
|
const inLists = listsArray.filter((list) => list.people.value.some((p) => p.pubkey === pubkey));
|
||||||
|
|
||||||
|
const handleChange = useCallback(async (names: string | string[]) => {
|
||||||
|
if (!Array.isArray(names)) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const addToList = listsArray.find((list) => !inLists.includes(list) && names.includes(list.name));
|
||||||
|
const removeFromList = listsArray.find((list) => inLists.includes(list) && !names.includes(list.name));
|
||||||
|
|
||||||
|
if (addToList) {
|
||||||
|
const draft = addToList.draftAddPerson(pubkey);
|
||||||
|
console.log(draft);
|
||||||
|
} else if (removeFromList) {
|
||||||
|
const draft = removeFromList.draftRemovePerson(pubkey);
|
||||||
|
console.log(draft);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
toast({ description: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Array.from(Object.entries(lists)).map(([name, list]) => (
|
{listsArray.length > 0 && (
|
||||||
<MenuItem isDisabled={account.readonly} isTruncated maxW="90vw">
|
<MenuOptionGroup title="Lists" type="checkbox" value={inLists.map((l) => l.name)} onChange={handleChange}>
|
||||||
{name}
|
{listsArray.map((list) => (
|
||||||
</MenuItem>
|
<MenuItemOption
|
||||||
))}
|
key={list.event.id}
|
||||||
|
value={list.name}
|
||||||
|
isDisabled={account.readonly && isLoading}
|
||||||
|
isTruncated
|
||||||
|
maxW="90vw"
|
||||||
|
>
|
||||||
|
{list.name}
|
||||||
|
</MenuItemOption>
|
||||||
|
))}
|
||||||
|
</MenuOptionGroup>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,7 +93,7 @@ export const UserFollowButton = ({
|
|||||||
const followLabel = account && isFollowingMe ? "Follow Back" : "Follow";
|
const followLabel = account && isFollowingMe ? "Follow Back" : "Follow";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu closeOnSelect={false}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
colorScheme="brand"
|
colorScheme="brand"
|
||||||
@@ -83,15 +123,12 @@ export const UserFollowButton = ({
|
|||||||
)}
|
)}
|
||||||
{account && (
|
{account && (
|
||||||
<>
|
<>
|
||||||
<MenuItem icon={<TrashIcon />} isDisabled={account.readonly}>
|
|
||||||
Remove from all
|
|
||||||
</MenuItem>
|
|
||||||
<MenuDivider />
|
|
||||||
<UsersLists />
|
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
|
<UsersLists pubkey={pubkey} />
|
||||||
|
{/* <MenuDivider />
|
||||||
<MenuItem icon={<PlusCircleIcon />} isDisabled={account.readonly}>
|
<MenuItem icon={<PlusCircleIcon />} isDisabled={account.readonly}>
|
||||||
New list
|
New list
|
||||||
</MenuItem>
|
</MenuItem> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
@@ -61,6 +61,17 @@ export class List {
|
|||||||
|
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draftRemovePerson(pubkey: string) {
|
||||||
|
const draft: DraftNostrEvent = {
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
kind: this.event.kind,
|
||||||
|
content: this.event.content,
|
||||||
|
tags: this.event.tags.filter((t) => t[0] !== "p" || t[1] !== pubkey),
|
||||||
|
};
|
||||||
|
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListsService {
|
class ListsService {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext, Link as RouterLink } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -40,6 +40,10 @@ import { readablizeSats } from "../../helpers/bolt11";
|
|||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||||
|
import { ChatIcon } from "@chakra-ui/icons";
|
||||||
|
import { UserFollowButton } from "../../components/user-follow-button";
|
||||||
|
import { UserTipButton } from "../../components/user-tip-button";
|
||||||
|
import { UserProfileMenu } from "./components/user-profile-menu";
|
||||||
|
|
||||||
function buildDescriptionContent(description: string) {
|
function buildDescriptionContent(description: string) {
|
||||||
let content: EmbedableContent = [description.trim()];
|
let content: EmbedableContent = [description.trim()];
|
||||||
@@ -105,6 +109,20 @@ export default function UserAboutTab() {
|
|||||||
<Heading>{getUserDisplayName(metadata, pubkey)}</Heading>
|
<Heading>{getUserDisplayName(metadata, pubkey)}</Heading>
|
||||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Flex gap="2" ml="auto">
|
||||||
|
<UserTipButton pubkey={pubkey} size="sm" variant="link" />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
as={RouterLink}
|
||||||
|
size="sm"
|
||||||
|
icon={<ChatIcon />}
|
||||||
|
aria-label="Message"
|
||||||
|
to={`/dm/${npub ?? pubkey}`}
|
||||||
|
/>
|
||||||
|
<UserFollowButton pubkey={pubkey} size="sm" />
|
||||||
|
<UserProfileMenu pubkey={pubkey} aria-label="More Options" size="sm" />
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={expanded.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}
|
icon={expanded.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}
|
||||||
@@ -170,9 +188,7 @@ export default function UserAboutTab() {
|
|||||||
<Stat>
|
<Stat>
|
||||||
<StatLabel>Following</StatLabel>
|
<StatLabel>Following</StatLabel>
|
||||||
<StatNumber>{contacts ? readablizeSats(contacts.contacts.length) : "Unknown"}</StatNumber>
|
<StatNumber>{contacts ? readablizeSats(contacts.contacts.length) : "Unknown"}</StatNumber>
|
||||||
{contacts && (
|
{contacts && <StatHelpText>Updated {dayjs.unix(contacts.created_at).fromNow()}</StatHelpText>}
|
||||||
<StatHelpText>Updated {dayjs.unix(contacts.created_at).fromNow()}</StatHelpText>
|
|
||||||
)}
|
|
||||||
</Stat>
|
</Stat>
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
|
@@ -1,21 +1,13 @@
|
|||||||
import { Flex, Heading, SkeletonText, Text, Link, IconButton, Spacer } from "@chakra-ui/react";
|
import { Flex, Heading, IconButton, Spacer } from "@chakra-ui/react";
|
||||||
import { useNavigate, Link as RouterLink } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
import { EditIcon } from "../../../components/icons";
|
||||||
import { ChatIcon, EditIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons";
|
|
||||||
import { QrIconButton } from "./share-qr-button";
|
|
||||||
import { UserAvatar } from "../../../components/user-avatar";
|
import { UserAvatar } from "../../../components/user-avatar";
|
||||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||||
import { UserFollowButton } from "../../../components/user-follow-button";
|
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||||
import { UserTipButton } from "../../../components/user-tip-button";
|
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
|
|
||||||
import { truncatedId } from "../../../helpers/nostr-event";
|
|
||||||
import { fixWebsiteUrl, getUserDisplayName } from "../../../helpers/user-metadata";
|
|
||||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
||||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||||
import { UserProfileMenu } from "./user-profile-menu";
|
import { UserProfileMenu } from "./user-profile-menu";
|
||||||
import { embedUrls } from "../../../helpers/embeds";
|
|
||||||
import { renderGenericUrl } from "../../../components/embed-types";
|
|
||||||
|
|
||||||
export default function Header({
|
export default function Header({
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -27,7 +19,6 @@ export default function Header({
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
|
||||||
|
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const isSelf = pubkey === account?.pubkey;
|
const isSelf = pubkey === account?.pubkey;
|
||||||
@@ -48,19 +39,6 @@ export default function Header({
|
|||||||
onClick={() => navigate("/profile")}
|
onClick={() => navigate("/profile")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isSelf && (
|
|
||||||
<>
|
|
||||||
<UserTipButton pubkey={pubkey} size="sm" variant="link" />
|
|
||||||
<IconButton
|
|
||||||
as={RouterLink}
|
|
||||||
size="sm"
|
|
||||||
icon={<ChatIcon />}
|
|
||||||
aria-label="Message"
|
|
||||||
to={`/dm/${npub ?? pubkey}`}
|
|
||||||
/>
|
|
||||||
<UserFollowButton pubkey={pubkey} size="sm" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<UserProfileMenu
|
<UserProfileMenu
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
aria-label="More Options"
|
aria-label="More Options"
|
||||||
|
@@ -17,7 +17,7 @@ export const UserProfileMenu = ({
|
|||||||
pubkey,
|
pubkey,
|
||||||
showRelaySelectionModal,
|
showRelaySelectionModal,
|
||||||
...props
|
...props
|
||||||
}: { pubkey: string; showRelaySelectionModal: () => void } & Omit<MenuIconButtonProps, "children">) => {
|
}: { pubkey: string; showRelaySelectionModal?: () => void } & Omit<MenuIconButtonProps, "children">) => {
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const userRelays = useUserRelays(pubkey);
|
const userRelays = useUserRelays(pubkey);
|
||||||
const infoModal = useDisclosure();
|
const infoModal = useDisclosure();
|
||||||
@@ -52,9 +52,11 @@ export const UserProfileMenu = ({
|
|||||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||||
View Raw
|
View Raw
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<RelayIcon />} onClick={showRelaySelectionModal}>
|
{showRelaySelectionModal && (
|
||||||
Relay selection
|
<MenuItem icon={<RelayIcon />} onClick={showRelaySelectionModal}>
|
||||||
</MenuItem>
|
Relay selection
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
{infoModal.isOpen && (
|
{infoModal.isOpen && (
|
||||||
<UserDebugModal pubkey={pubkey} isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl" />
|
<UserDebugModal pubkey={pubkey} isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl" />
|
||||||
|
Reference in New Issue
Block a user