mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
add emoji pack views
This commit is contained in:
parent
3a2745ebdd
commit
7a5a4b1753
5
.changeset/dry-mirrors-knock.md
Normal file
5
.changeset/dry-mirrors-knock.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add emoji pack views
|
17
src/app.tsx
17
src/app.tsx
@ -1,6 +1,8 @@
|
||||
import React, { Suspense } from "react";
|
||||
import { createHashRouter, Outlet, RouterProvider, ScrollRestoration } from "react-router-dom";
|
||||
import { Spinner } from "@chakra-ui/react";
|
||||
import { css, Global } from "@emotion/react";
|
||||
|
||||
import { ErrorBoundary } from "./components/error-boundary";
|
||||
import Layout from "./components/layout";
|
||||
|
||||
@ -38,9 +40,11 @@ import ListsView from "./views/lists";
|
||||
import ListView from "./views/lists/list";
|
||||
import UserListsTab from "./views/user/lists";
|
||||
|
||||
import "./services/emoji-packs";
|
||||
import BrowseListView from "./views/lists/browse";
|
||||
import { css, Global } from "@emotion/react";
|
||||
import EmojiPacksBrowseView from "./views/emoji-packs/browse";
|
||||
import EmojiPackView from "./views/emoji-packs/pack";
|
||||
import UserEmojiPacksTab from "./views/user/emoji-packs";
|
||||
import EmojiPacksView from "./views/emoji-packs";
|
||||
|
||||
const StreamsView = React.lazy(() => import("./views/streams"));
|
||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||
@ -127,6 +131,7 @@ const router = createHashRouter([
|
||||
{ path: "lists", element: <UserListsTab /> },
|
||||
{ path: "followers", element: <UserFollowersTab /> },
|
||||
{ path: "following", element: <UserFollowingTab /> },
|
||||
{ path: "emojis", element: <UserEmojiPacksTab /> },
|
||||
{ path: "relays", element: <UserRelaysTab /> },
|
||||
{ path: "reports", element: <UserReportsTab /> },
|
||||
],
|
||||
@ -156,6 +161,14 @@ const router = createHashRouter([
|
||||
{ path: ":addr", element: <ListView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "emojis",
|
||||
children: [
|
||||
{ path: "", element: <EmojiPacksView /> },
|
||||
{ path: "browse", element: <EmojiPacksBrowseView /> },
|
||||
{ path: ":addr", element: <EmojiPackView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "streams",
|
||||
element: <StreamsView />,
|
||||
|
@ -367,3 +367,9 @@ export const AddReactionIcon = createIcon({
|
||||
d: "M19.0001 13.9999V16.9999H22.0001V18.9999H18.9991L19.0001 21.9999H17.0001L16.9991 18.9999H14.0001V16.9999H17.0001V13.9999H19.0001ZM20.2426 4.75736C22.505 7.0244 22.5829 10.636 20.4795 12.992L19.06 11.574C20.3901 10.0499 20.3201 7.65987 18.827 6.1701C17.3244 4.67092 14.9076 4.60701 13.337 6.01688L12.0019 7.21524L10.6661 6.01781C9.09098 4.60597 6.67506 4.66808 5.17157 6.17157C3.68183 7.66131 3.60704 10.0473 4.97993 11.6232L13.412 20.069L11.9999 21.485L3.52138 12.993C1.41705 10.637 1.49571 7.01901 3.75736 4.75736C6.02157 2.49315 9.64519 2.41687 12.001 4.52853C14.35 2.42 17.98 2.49 20.2426 4.75736Z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const EmojiIcon = createIcon({
|
||||
displayName: "EmojiIcon",
|
||||
d: "M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM7 12H9C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12H17C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12Z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { AbsoluteCenter, Box, Button, Divider } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ChatIcon,
|
||||
EmojiIcon,
|
||||
FeedIcon,
|
||||
ListIcon,
|
||||
LiveStreamIcon,
|
||||
@ -48,6 +49,9 @@ export default function NavItems({ isInDrawer = false }: { isInDrawer?: boolean
|
||||
<Button onClick={() => navigate("/lists")} leftIcon={<ListIcon />} justifyContent="flex-start">
|
||||
Lists
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/emojis")} leftIcon={<EmojiIcon />} justifyContent="flex-start">
|
||||
Emojis
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/map")} leftIcon={<MapIcon />} justifyContent="flex-start">
|
||||
Map
|
||||
</Button>
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { Button, Divider, Flex, IconButton, Image, Input, Text } from "@chakra-ui/react";
|
||||
import { DislikeIcon, LikeIcon } from "./icons";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useUserEmojiPacks from "../hooks/use-users-emoji-packs";
|
||||
import useEmojiPack from "../hooks/use-emoji-pack";
|
||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||
import { getEmojisFromPack, getPackCordsFromFavorites, getPackName } from "../helpers/nostr/emoji-packs";
|
||||
import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs";
|
||||
|
||||
export type ReactionPickerProps = {
|
||||
onSelect: (emoji: string, url?: string) => void;
|
||||
};
|
||||
|
||||
function EmojiPack({ addr, onSelect }: { addr: string; onSelect: ReactionPickerProps["onSelect"] }) {
|
||||
const pack = useEmojiPack(addr);
|
||||
|
||||
function EmojiPack({ cord, onSelect }: { cord: string; onSelect: ReactionPickerProps["onSelect"] }) {
|
||||
const pack = useReplaceableEvent(cord);
|
||||
if (!pack) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Text whiteSpace="pre">{pack.name}</Text>
|
||||
<Text whiteSpace="pre">{getPackName(pack)}</Text>
|
||||
<Divider />
|
||||
</Flex>
|
||||
<Flex wrap="wrap" gap="2">
|
||||
{pack.emojis.map((emoji) => (
|
||||
{getEmojisFromPack(pack).map((emoji) => (
|
||||
<IconButton
|
||||
key={emoji.name}
|
||||
icon={<Image src={emoji.url} height="1.2rem" />}
|
||||
@ -38,7 +38,7 @@ function EmojiPack({ addr, onSelect }: { addr: string; onSelect: ReactionPickerP
|
||||
|
||||
export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
||||
const account = useCurrentAccount();
|
||||
const { packs = [] } = useUserEmojiPacks(account?.pubkey) ?? {};
|
||||
const favoritePacks = useFavoriteEmojiPacks(account?.pubkey);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
@ -72,9 +72,10 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{packs.map((addr) => (
|
||||
<EmojiPack key={addr} addr={addr} onSelect={onSelect} />
|
||||
))}
|
||||
{favoritePacks &&
|
||||
getPackCordsFromFavorites(favoritePacks).map((cord) => (
|
||||
<EmojiPack key={cord} cord={cord} onSelect={onSelect} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,18 @@
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { NostrEvent, isATag } from "../../types/nostr-event";
|
||||
|
||||
export const EMOJI_PACK_KIND = 30030;
|
||||
export const USER_EMOJI_LIST_KIND = 10030;
|
||||
|
||||
export function getPackName(event: NostrEvent) {
|
||||
return event.tags.find((t) => t[0] === "d")?.[1];
|
||||
}
|
||||
|
||||
export function getEmojisFromPack(pack: NostrEvent) {
|
||||
return pack.tags
|
||||
.filter((t) => t[0] === "emoji" && t[1] && t[2])
|
||||
.map((t) => ({ name: t[1] as string, url: t[2] as string }));
|
||||
}
|
||||
|
||||
export function getPackCordsFromFavorites(event: NostrEvent) {
|
||||
return event.tags.filter(isATag).map((t) => t[1]);
|
||||
}
|
||||
|
@ -187,3 +187,27 @@ export function parseCoordinate(a: string): CustomEventPointer | null {
|
||||
identifier: d,
|
||||
};
|
||||
}
|
||||
|
||||
export function draftAddCoordinate(list: NostrEvent | DraftNostrEvent, coordinate: string, relay?: string) {
|
||||
if (list.tags.some((t) => t[0] === "a" && t[1] === coordinate)) throw new Error("event already in list");
|
||||
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: [...list.tags, relay ? ["a", coordinate, relay] : ["a", coordinate]],
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function draftRemoveCoordinate(list: NostrEvent | DraftNostrEvent, coordinate: string) {
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: list.tags.filter((t) => !(t[0] === "a" && t[1] === coordinate)),
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
@ -93,27 +93,3 @@ export function draftRemoveEvent(list: NostrEvent | DraftNostrEvent, event: stri
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function draftAddCoordinate(list: NostrEvent | DraftNostrEvent, coordinate: string, relay?: string) {
|
||||
if (list.tags.some((t) => t[0] === "a" && t[1] === coordinate)) throw new Error("event already in list");
|
||||
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: [...list.tags, relay ? ["a", coordinate, relay] : ["a", coordinate]],
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function draftRemoveCoordinate(list: NostrEvent | DraftNostrEvent, coordinate: string) {
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: list.tags.filter((t) => !(t[0] === "a" && t[1] === coordinate)),
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
import emojiPacksService from "../services/emoji-packs";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export default function useEmojiPack(addr: string, additionalRelays?: string[]) {
|
||||
const readRelays = useReadRelayUrls(additionalRelays);
|
||||
const subject = useMemo(() => emojiPacksService.requestEmojiPack(addr, readRelays), [addr, readRelays.join("|")]);
|
||||
|
||||
return useSubject(subject);
|
||||
}
|
17
src/hooks/use-favorite-emoji-packs.ts
Normal file
17
src/hooks/use-favorite-emoji-packs.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import { USER_EMOJI_LIST_KIND } from "../helpers/nostr/emoji-packs";
|
||||
|
||||
export const FAVORITE_LISTS_IDENTIFIER = "nostrudel-favorite-lists";
|
||||
|
||||
export default function useFavoriteEmojiPacks(pubkey?: string, additionalRelays: string[] = [], alwaysFetch = false) {
|
||||
const account = useCurrentAccount();
|
||||
const key = pubkey || account?.pubkey;
|
||||
const favoritePacks = useReplaceableEvent(
|
||||
key ? { kind: USER_EMOJI_LIST_KIND, pubkey: key } : undefined,
|
||||
additionalRelays,
|
||||
alwaysFetch,
|
||||
);
|
||||
|
||||
return favoritePacks;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
import emojiPacksService from "../services/emoji-packs";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export default function useUserEmojiPacks(pubkey?: string, additionalRelays?: string[], alwaysFetch = false) {
|
||||
const readRelays = useReadRelayUrls(additionalRelays);
|
||||
const subject = useMemo(() => {
|
||||
if (pubkey) return emojiPacksService.requestUserEmojiList(pubkey, readRelays, alwaysFetch);
|
||||
}, [pubkey, readRelays.join("|")]);
|
||||
|
||||
return useSubject(subject);
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { PropsWithChildren, createContext, useContext } from "react";
|
||||
import { lib } from "emojilib";
|
||||
import useUserEmojiPacks from "../hooks/use-users-emoji-packs";
|
||||
import useReplaceableEvents from "../hooks/use-replaceable-events";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import { isEmojiTag } from "../types/nostr-event";
|
||||
import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs";
|
||||
import { getPackCordsFromFavorites } from "../helpers/nostr/emoji-packs";
|
||||
|
||||
const defaultEmojis = Object.entries(lib).map(([name, emojiObject]) => ({
|
||||
...emojiObject,
|
||||
@ -25,8 +26,8 @@ export function DefaultEmojiProvider({ children }: PropsWithChildren) {
|
||||
|
||||
export function UserEmojiProvider({ children, pubkey }: PropsWithChildren & { pubkey?: string }) {
|
||||
const account = useCurrentAccount();
|
||||
const userPacks = useUserEmojiPacks(pubkey || account?.pubkey, [], true);
|
||||
const events = useReplaceableEvents(userPacks?.packs);
|
||||
const favoritePacks = useFavoriteEmojiPacks(pubkey || account?.pubkey, [], true);
|
||||
const events = useReplaceableEvents(favoritePacks && getPackCordsFromFavorites(favoritePacks));
|
||||
|
||||
const emojis = events
|
||||
.map((event) =>
|
||||
|
@ -1,71 +0,0 @@
|
||||
import Subject from "../classes/subject";
|
||||
import { SuperMap } from "../classes/super-map";
|
||||
import { getEmojisFromPack } from "../helpers/nostr/emoji-packs";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import replaceableEventLoaderService from "./replaceable-event-requester";
|
||||
|
||||
const EMOJI_PACK_KIND = 30030;
|
||||
const USER_EMOJI_LIST_KIND = 10030;
|
||||
|
||||
class EmojiPacksService {
|
||||
emojiPacks = new SuperMap(
|
||||
() => new Subject<{ event: NostrEvent; name: string; emojis: { name: string; url: string }[] }>(),
|
||||
);
|
||||
userEmojiPacks = new SuperMap(() => new Subject<{ packs: string[]; event: NostrEvent }>());
|
||||
|
||||
getEmojiPacks(pubkey: string) {
|
||||
return this.emojiPacks.get(pubkey);
|
||||
}
|
||||
|
||||
requestEmojiPack(addr: string, relays: string[]) {
|
||||
const [kind, pubkey, name] = addr.split(":");
|
||||
const sub = this.emojiPacks.get(addr);
|
||||
|
||||
if (!sub.value) {
|
||||
const request = replaceableEventLoaderService.requestEvent(relays, EMOJI_PACK_KIND, pubkey, name);
|
||||
sub.connectWithHandler(request, (event, next) => {
|
||||
const name = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||
if (!name) return;
|
||||
|
||||
next({
|
||||
name,
|
||||
emojis: getEmojisFromPack(event),
|
||||
event,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
requestUserEmojiList(pubkey: string, relays: string[], alwaysFetch = false) {
|
||||
const sub = this.userEmojiPacks.get(pubkey);
|
||||
const request = replaceableEventLoaderService.requestEvent(
|
||||
relays,
|
||||
USER_EMOJI_LIST_KIND,
|
||||
pubkey,
|
||||
undefined,
|
||||
alwaysFetch,
|
||||
);
|
||||
|
||||
if (!sub.value) {
|
||||
sub.connectWithHandler(request, (event, next) => {
|
||||
next({
|
||||
packs: event.tags.filter((t) => t[0] === "a" && t[1]).map((t) => t[1] as string),
|
||||
event,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
|
||||
const emojiPacksService = new EmojiPacksService();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
//@ts-ignore
|
||||
window.emojiPacksService = emojiPacksService;
|
||||
}
|
||||
|
||||
export default emojiPacksService;
|
64
src/views/emoji-packs/browse.tsx
Normal file
64
src/views/emoji-packs/browse.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useCallback } from "react";
|
||||
import { Flex, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import EmojiPackCard from "./components/emoji-pack-card";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { EMOJI_PACK_KIND, getEmojisFromPack } from "../../helpers/nostr/emoji-packs";
|
||||
|
||||
function EmojiPacksBrowsePage() {
|
||||
const { filter, listId } = usePeopleListContext();
|
||||
const showEmpty = useDisclosure();
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (!showEmpty.isOpen && getEmojisFromPack(event).length === 0) return false;
|
||||
return true;
|
||||
},
|
||||
[showEmpty.isOpen],
|
||||
);
|
||||
const readRelays = useReadRelayUrls();
|
||||
const timeline = useTimelineLoader(
|
||||
`${listId}-browse-emoji-packs`,
|
||||
readRelays,
|
||||
{ ...filter, kinds: [EMOJI_PACK_KIND] },
|
||||
{ enabled: !!filter, eventFilter },
|
||||
);
|
||||
|
||||
const packs = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex direction="column" gap="2" p="2" pb="10">
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<PeopleListSelection />
|
||||
<Switch checked={showEmpty.isOpen} onChange={showEmpty.onToggle} whiteSpace="pre">
|
||||
Show Empty
|
||||
</Switch>
|
||||
</Flex>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
||||
{packs.map((event) => (
|
||||
<EmojiPackCard key={getEventUID(event)} pack={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmojiPacksBrowseView() {
|
||||
return (
|
||||
<PeopleListProvider>
|
||||
<EmojiPacksBrowsePage />
|
||||
</PeopleListProvider>
|
||||
);
|
||||
}
|
67
src/views/emoji-packs/components/emoji-pack-card.tsx
Normal file
67
src/views/emoji-packs/components/emoji-pack-card.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useRef } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
Link,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import { getSharableEventNaddr } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import EmojiPackFavoriteButton from "./emoji-pack-favorite-button";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import { getEmojisFromPack, getPackName } from "../../../helpers/nostr/emoji-packs";
|
||||
import EmojiPackMenu from "./emoji-pack-menu";
|
||||
|
||||
export default function EmojiPackCard({ pack, ...props }: Omit<CardProps, "children"> & { pack: NostrEvent }) {
|
||||
const emojis = getEmojisFromPack(pack);
|
||||
const naddr = getSharableEventNaddr(pack);
|
||||
|
||||
// if there is a parent intersection observer, register this card
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(pack));
|
||||
|
||||
return (
|
||||
<Card ref={ref} {...props}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0" flexWrap="wrap">
|
||||
<Heading size="md">
|
||||
<Link as={RouterLink} to={`/emojis/${naddr}`}>
|
||||
{getPackName(pack)}
|
||||
</Link>
|
||||
</Heading>
|
||||
<Text>by</Text>
|
||||
<UserAvatarLink pubkey={pack.pubkey} size="xs" />
|
||||
<UserLink pubkey={pack.pubkey} isTruncated fontWeight="bold" fontSize="md" />
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
<EmojiPackFavoriteButton pack={pack} />
|
||||
<EmojiPackMenu pack={pack} aria-label="emoji pack menu" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
{emojis.length > 0 && (
|
||||
<Flex mb="2" wrap="wrap" gap="2">
|
||||
{emojis.map(({ name, url }) => (
|
||||
<Image key={name + url} src={url} title={name} w={8} h={8} />
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</CardBody>
|
||||
<CardFooter p="2" display="flex" pt="0">
|
||||
<Text>Updated: {dayjs.unix(pack.created_at).fromNow()}</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import { useState } from "react";
|
||||
import { IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import { StarEmptyIcon, StarFullIcon } from "../../../components/icons";
|
||||
import { draftAddCoordinate, draftRemoveCoordinate, getEventCoordinate } from "../../../helpers/nostr/events";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../../services/client-relays";
|
||||
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
|
||||
import { USER_EMOJI_LIST_KIND } from "../../../helpers/nostr/emoji-packs";
|
||||
import useFavoriteEmojiPacks from "../../../hooks/use-favorite-emoji-packs";
|
||||
|
||||
export default function EmojiPackFavoriteButton({
|
||||
pack,
|
||||
...props
|
||||
}: { pack: NostrEvent } & Omit<IconButtonProps, "children" | "aria-label" | "isLoading" | "onClick">) {
|
||||
const toast = useToast();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const favoritePacks = useFavoriteEmojiPacks();
|
||||
const coordinate = getEventCoordinate(pack);
|
||||
const isFavorite = favoritePacks?.tags.some((t) => t[1] === coordinate);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
const prev: DraftNostrEvent = favoritePacks || {
|
||||
kind: USER_EMOJI_LIST_KIND,
|
||||
created_at: dayjs().unix(),
|
||||
content: "",
|
||||
tags: [],
|
||||
};
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const draft = isFavorite ? draftRemoveCoordinate(prev, coordinate) : draftAddCoordinate(prev, coordinate);
|
||||
const signed = await requestSignature(draft);
|
||||
const pub = new NostrPublishAction(
|
||||
isFavorite ? "Unfavorite Emoji pack" : "Favorite emoji pack",
|
||||
clientRelaysService.getWriteUrls(),
|
||||
signed,
|
||||
);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={isFavorite ? <StarFullIcon /> : <StarEmptyIcon />}
|
||||
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
|
||||
title={isFavorite ? "Unfavorite" : "Favorite"}
|
||||
onClick={handleClick}
|
||||
isLoading={loading}
|
||||
color={isFavorite ? "yellow.400" : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
54
src/views/emoji-packs/components/emoji-pack-menu.tsx
Normal file
54
src/views/emoji-packs/components/emoji-pack-menu.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
|
||||
import { getSharableEventNaddr } from "../../../helpers/nip19";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
|
||||
|
||||
export default function EmojiPackMenu({
|
||||
pack,
|
||||
...props
|
||||
}: { pack: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
const infoModal = useDisclosure();
|
||||
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const naddr = getSharableEventNaddr(pack);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuIconButton {...props}>
|
||||
{naddr && (
|
||||
<>
|
||||
<MenuItem onClick={() => window.open(buildAppSelectUrl(naddr), "_blank")} icon={<ExternalLinkIcon />}>
|
||||
View in app...
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => copyToClipboard("nostr:" + naddr)} icon={<RepostIcon />}>
|
||||
Copy Share Link
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{account?.pubkey === pack.pubkey && (
|
||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(pack)}>
|
||||
Delete Pack
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||
View Raw
|
||||
</MenuItem>
|
||||
</MenuIconButton>
|
||||
|
||||
{infoModal.isOpen && (
|
||||
<NoteDebugModal event={pack} isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
86
src/views/emoji-packs/index.tsx
Normal file
86
src/views/emoji-packs/index.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { Button, Divider, Flex, Heading, Link, SimpleGrid, Spacer } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { ExternalLinkIcon } from "../../components/icons";
|
||||
import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import EmojiPackCard from "./components/emoji-pack-card";
|
||||
import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs";
|
||||
import useReplaceableEvents from "../../hooks/use-replaceable-events";
|
||||
|
||||
function UserEmojiPackMangerPage() {
|
||||
const account = useCurrentAccount()!;
|
||||
|
||||
const favoritePacks = useFavoriteEmojiPacks(account.pubkey);
|
||||
const readRelays = useReadRelayUrls();
|
||||
const timeline = useTimelineLoader(
|
||||
`${account.pubkey}-emoji-packs`,
|
||||
readRelays,
|
||||
{
|
||||
authors: [account.pubkey],
|
||||
kinds: [EMOJI_PACK_KIND],
|
||||
},
|
||||
{ enabled: !!account.pubkey },
|
||||
);
|
||||
|
||||
const favorites = useReplaceableEvents(favoritePacks && getPackCordsFromFavorites(favoritePacks));
|
||||
const packs = useSubject(timeline.timeline).filter((pack) => {
|
||||
const cord = getEventCoordinate(pack);
|
||||
return !favorites.some((e) => getEventCoordinate(e) === cord);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{favorites.length > 0 && (
|
||||
<>
|
||||
<Heading size="md" mt="2">
|
||||
Favorite packs
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
||||
{favorites.map((event) => (
|
||||
<EmojiPackCard key={getEventUID(event)} pack={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
{packs.length > 0 && (
|
||||
<>
|
||||
<Heading size="md" mt="2">
|
||||
Emoji packs
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
||||
{packs.map((event) => (
|
||||
<EmojiPackCard key={getEventUID(event)} pack={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmojiPacksView() {
|
||||
const account = useCurrentAccount();
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
|
||||
<Flex gap="2">
|
||||
<Button as={RouterLink} to="/emojis/browse">
|
||||
Find packs
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button as={Link} href="https://emojis-iota.vercel.app/" isExternal rightIcon={<ExternalLinkIcon />}>
|
||||
Emoji pack manager
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{account && <UserEmojiPackMangerPage />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
111
src/views/emoji-packs/pack.tsx
Normal file
111
src/views/emoji-packs/pack.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
SimpleGrid,
|
||||
Spacer,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import { ArrowLeftSIcon } from "../../components/icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import EmojiPackMenu from "./components/emoji-pack-menu";
|
||||
import EmojiPackFavoriteButton from "./components/emoji-pack-favorite-button";
|
||||
import { getEmojisFromPack, getPackName } from "../../helpers/nostr/emoji-packs";
|
||||
import { useState } from "react";
|
||||
|
||||
function useListCoordinate() {
|
||||
const { addr } = useParams() as { addr: string };
|
||||
const parsed = nip19.decode(addr);
|
||||
if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`);
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export default function EmojiPackView() {
|
||||
const navigate = useNavigate();
|
||||
const coordinate = useListCoordinate();
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
const account = useCurrentAccount();
|
||||
const [scale, setScale] = useState(10);
|
||||
|
||||
const pack = useReplaceableEvent(coordinate);
|
||||
|
||||
if (!pack)
|
||||
return (
|
||||
<>
|
||||
Looking for pack "{coordinate.identifier}" created by <UserLink pubkey={coordinate.pubkey} />
|
||||
</>
|
||||
);
|
||||
|
||||
const isAuthor = account?.pubkey === pack.pubkey;
|
||||
const emojis = getEmojisFromPack(pack);
|
||||
|
||||
return (
|
||||
<Flex direction="column" px="2" pt="2" pb="8" overflowY="auto" overflowX="hidden" h="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Heading size="md" isTruncated>
|
||||
{getPackName(pack)}
|
||||
</Heading>
|
||||
<EmojiPackFavoriteButton pack={pack} size="sm" />
|
||||
|
||||
<Spacer />
|
||||
|
||||
{isAuthor && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(pack).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<EmojiPackMenu aria-label="More options" pack={pack} />
|
||||
</Flex>
|
||||
|
||||
{emojis.length > 0 && (
|
||||
<>
|
||||
<Flex alignItems="flex-end">
|
||||
<Heading size="md">Emojis</Heading>
|
||||
<ButtonGroup size="sm" isAttached ml="auto">
|
||||
<Button variant={scale === 10 ? "solid" : "outline"} onClick={() => setScale(10)}>
|
||||
SM
|
||||
</Button>
|
||||
<Button variant={scale === 16 ? "solid" : "outline"} onClick={() => setScale(16)}>
|
||||
MD
|
||||
</Button>
|
||||
<Button variant={scale === 24 ? "solid" : "outline"} onClick={() => setScale(24)}>
|
||||
LG
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<Divider />
|
||||
<Card variant="elevated">
|
||||
<CardBody p="2">
|
||||
{/* <Flex gap="2" wrap="wrap"> */}
|
||||
<SimpleGrid columns={{ base: 2, sm: 3, md: 2, lg: 4, xl: 6 }} gap="2">
|
||||
{emojis.map(({ name, url }) => (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Image key={name + url} src={url} title={name} w={scale} h={scale} />
|
||||
<Tag>{name}</Tag>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{/* </Flex> */}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -1,5 +1,16 @@
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { AvatarGroup, Card, CardBody, CardFooter, CardHeader, Flex, Heading, Link, Text } from "@chakra-ui/react";
|
||||
import {
|
||||
AvatarGroup,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Flex,
|
||||
Heading,
|
||||
Link,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@ -16,6 +27,7 @@ import { useRegisterIntersectionEntity } from "../../../providers/intersection-o
|
||||
import { useRef } from "react";
|
||||
import ListFavoriteButton from "./list-favorite-button";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import ListMenu from "./list-menu";
|
||||
|
||||
function ListCardRender({ event }: { event: NostrEvent }) {
|
||||
const people = getPubkeysFromList(event);
|
||||
@ -29,12 +41,16 @@ function ListCardRender({ event }: { event: NostrEvent }) {
|
||||
|
||||
return (
|
||||
<Card ref={ref}>
|
||||
<CardHeader p="2" pb="0">
|
||||
<CardHeader display="flex" alignItems="center" p="2" pb="0">
|
||||
<Heading size="md">
|
||||
<Link as={RouterLink} to={`/lists/${link}`}>
|
||||
{getListName(event)}
|
||||
</Link>
|
||||
</Heading>
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
<ListFavoriteButton list={event} />
|
||||
<ListMenu list={event} aria-label="list menu" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<Flex gap="2">
|
||||
@ -65,7 +81,6 @@ function ListCardRender({ event }: { event: NostrEvent }) {
|
||||
)}
|
||||
</CardBody>
|
||||
<CardFooter p="2" display="flex" pt="0">
|
||||
<ListFavoriteButton list={event} size="sm" />
|
||||
<EventRelays event={event} ml="auto" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
@ -4,8 +4,7 @@ import dayjs from "dayjs";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import { StarEmptyIcon, StarFullIcon } from "../../../components/icons";
|
||||
import { getEventCoordinate } from "../../../helpers/nostr/events";
|
||||
import { draftAddCoordinate, draftRemoveCoordinate } from "../../../helpers/nostr/lists";
|
||||
import { draftAddCoordinate, draftRemoveCoordinate, getEventCoordinate } from "../../../helpers/nostr/events";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../../services/client-relays";
|
||||
|
@ -54,7 +54,7 @@ export default function ListView() {
|
||||
return (
|
||||
<Flex direction="column" px="2" pt="2" pb="8" overflowY="auto" overflowX="hidden" h="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button as={RouterList} to="/lists" leftIcon={<ArrowLeftSIcon />}>
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
|
62
src/views/user/emoji-packs.tsx
Normal file
62
src/views/user/emoji-packs.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Divider, Flex, Heading, SimpleGrid, Text } from "@chakra-ui/react";
|
||||
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import EmojiPackCard from "../emoji-packs/components/emoji-pack-card";
|
||||
import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs";
|
||||
import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs";
|
||||
import useReplaceableEvents from "../../hooks/use-replaceable-events";
|
||||
|
||||
export default function UserEmojiPacksTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useTimelineLoader(pubkey + "-emoji-packs", readRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [EMOJI_PACK_KIND],
|
||||
});
|
||||
const packs = useSubject(timeline.timeline);
|
||||
|
||||
const favoritePacks = useFavoriteEmojiPacks(pubkey);
|
||||
const favorites = useReplaceableEvents(favoritePacks && getPackCordsFromFavorites(favoritePacks));
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex gap="2" pt="2" pb="10" px={["2", "2", 0]} direction="column">
|
||||
{packs.length > 0 && (
|
||||
<>
|
||||
<Heading size="md" mt="2">
|
||||
Created packs
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
||||
{packs.map((pack) => (
|
||||
<EmojiPackCard key={getEventUID(pack)} pack={pack} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
{favorites.length > 0 && (
|
||||
<>
|
||||
<Heading size="md" mt="2">
|
||||
Favorite packs
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
|
||||
{favorites.map((event) => (
|
||||
<EmojiPackCard key={getEventUID(event)} pack={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
@ -50,6 +50,7 @@ const tabs = [
|
||||
{ label: "Following", path: "following" },
|
||||
{ label: "Likes", path: "likes" },
|
||||
{ label: "Relays", path: "relays" },
|
||||
{ label: "Emoji Packs", path: "emojis" },
|
||||
{ label: "Reports", path: "reports" },
|
||||
{ label: "Followers", path: "followers" },
|
||||
];
|
||||
@ -116,7 +117,9 @@ const UserView = () => {
|
||||
>
|
||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||
{tabs.map(({ label }) => (
|
||||
<Tab key={label}>{label}</Tab>
|
||||
<Tab key={label} whiteSpace="pre">
|
||||
{label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
|
@ -5,18 +5,17 @@ import { useAdditionalRelayContext } from "../../providers/additional-relay-cont
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { MUTE_LIST_KIND, NOTE_LIST_KIND, PEOPLE_LIST_KIND, PIN_LIST_KIND } from "../../helpers/nostr/lists";
|
||||
import { getEventUID, truncatedId } from "../../helpers/nostr/events";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import ListCard from "../lists/components/list-card";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
export default function UserListsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useTimelineLoader(truncatedId(pubkey) + "-lists", readRelays, {
|
||||
const timeline = useTimelineLoader(pubkey + "-lists", readRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user