mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +02:00
add emoji pack edit view
This commit is contained in:
parent
8bf5d8213c
commit
1b5ee345b7
5
.changeset/brown-mugs-nail.md
Normal file
5
.changeset/brown-mugs-nail.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add emoji edit view
|
36
src/components/embed-event/event-types/embedded-unknown.tsx
Normal file
36
src/components/embed-event/event-types/embedded-unknown.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, Link, Text } from "@chakra-ui/react";
|
||||
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { UserAvatarLink } from "../../user-avatar-link";
|
||||
import { UserLink } from "../../user-link";
|
||||
import { truncatedId } from "../../../helpers/nostr/events";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const address = getSharableEventAddress(event);
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="md" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Link ml="auto" href={address ? buildAppSelectUrl(address) : ""} isExternal>
|
||||
{dayjs.unix(event.created_at).fromNow()}
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<Flex gap="2">
|
||||
<Text>Kind: {event.kind}</Text>
|
||||
<Link href={address ? buildAppSelectUrl(address) : ""} isExternal color="blue.500">
|
||||
{address && truncatedId(address)}
|
||||
</Link>
|
||||
</Flex>
|
||||
<Text whiteSpace="pre-wrap">{event.content}</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import type { DecodeResult } from "nostr-tools/lib/nip19";
|
||||
import { Link } from "@chakra-ui/react";
|
||||
|
||||
import EmbeddedNote from "./event-types/embedded-note";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
@ -10,13 +9,12 @@ import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import RelayCard from "../../views/relays/components/relay-card";
|
||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||
import { GOAL_KIND } from "../../helpers/nostr/goal";
|
||||
import GoalCard from "../../views/goals/components/goal-card";
|
||||
import { getSharableEventAddress, safeDecode } from "../../helpers/nip19";
|
||||
import { safeDecode } from "../../helpers/nip19";
|
||||
import EmbeddedStream from "./event-types/embedded-stream";
|
||||
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
|
||||
import EmbeddedEmojiPack from "./event-types/embedded-emoji-pack";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
|
||||
import EmbeddedGoal from "./event-types/embedded-goal";
|
||||
import EmbeddedUnknown from "./event-types/embedded-unknown";
|
||||
|
||||
export function EmbedEvent({ event }: { event: NostrEvent }) {
|
||||
switch (event.kind) {
|
||||
@ -30,12 +28,7 @@ export function EmbedEvent({ event }: { event: NostrEvent }) {
|
||||
return <EmbeddedEmojiPack pack={event} />;
|
||||
}
|
||||
|
||||
const address = getSharableEventAddress(event);
|
||||
return (
|
||||
<Link href={address ? buildAppSelectUrl(address) : ""} isExternal color="blue.500">
|
||||
{address}
|
||||
</Link>
|
||||
);
|
||||
return <EmbeddedUnknown event={event} />;
|
||||
}
|
||||
|
||||
export function EmbedEventPointer({ pointer }: { pointer: DecodeResult }) {
|
||||
|
@ -2,18 +2,19 @@ import { useMemo } from "react";
|
||||
import { Link, LinkProps } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getSharableNoteId } from "../helpers/nip19";
|
||||
|
||||
import { truncatedId } from "../helpers/nostr/events";
|
||||
|
||||
export type NoteLinkProps = LinkProps & {
|
||||
noteId: string;
|
||||
};
|
||||
|
||||
export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => {
|
||||
const encoded = useMemo(() => getSharableNoteId(noteId), [noteId]);
|
||||
const encoded = useMemo(() => nip19.noteEncode(noteId), [noteId]);
|
||||
|
||||
return (
|
||||
<Link as={RouterLink} to={`/n/${encoded}`} color={color} {...props}>
|
||||
{children || nip19.noteEncode(noteId)}
|
||||
{children || truncatedId(nip19.noteEncode(noteId))}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
80
src/views/emoji-packs/create-modal.tsx
Normal file
80
src/views/emoji-packs/create-modal.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
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 { getSharableEventAddress } from "../../helpers/nip19";
|
||||
|
||||
export default function EmojiPackCreateModal({ onClose, ...props }: Omit<ModalProps, "children">) {
|
||||
const toast = useToast();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const navigate = useNavigate();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
},
|
||||
});
|
||||
|
||||
const submit = handleSubmit(async (values) => {
|
||||
const draft: DraftNostrEvent = {
|
||||
kind: EMOJI_PACK_KIND,
|
||||
created_at: dayjs().unix(),
|
||||
content: "",
|
||||
tags: [["d", values.title]],
|
||||
};
|
||||
|
||||
try {
|
||||
const signed = await requestSignature(draft);
|
||||
const pub = new NostrPublishAction("Create emoji pack", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
navigate(`/emojis/${getSharableEventAddress(signed)}`);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} size="xl" {...props}>
|
||||
<ModalOverlay />
|
||||
<ModalContent as="form" onSubmit={submit}>
|
||||
<ModalHeader p="4">Create Emoji pack</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody px="4" py="0">
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Pack name</FormLabel>
|
||||
<Input type="name" {...register("title", { required: true })} autoComplete="off" />
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter p="4">
|
||||
<Button mr="2" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,56 +1,118 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useThrottle } from "react-use";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
Input,
|
||||
SimpleGrid,
|
||||
Spacer,
|
||||
Tag,
|
||||
Tooltip,
|
||||
TagCloseButton,
|
||||
TagLabel,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { UserLink } from "../../components/user-link";
|
||||
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";
|
||||
import { EMOJI_PACK_KIND, getEmojisFromPack, getPackName } from "../../helpers/nostr/emoji-packs";
|
||||
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 { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
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;
|
||||
function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string }) => void }) {
|
||||
const { register, handleSubmit, watch, getValues, reset } = useForm({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
|
||||
const submit = handleSubmit((values) => {
|
||||
onAdd(values);
|
||||
reset();
|
||||
});
|
||||
|
||||
watch("url");
|
||||
const previewURL = useThrottle(getValues().url);
|
||||
|
||||
return (
|
||||
<Flex as="form" gap="2" onSubmit={submit}>
|
||||
<Input placeholder="name" {...register("name", { required: true })} autoComplete="off" />
|
||||
<Input placeholder="https://example.com/emoji.png" {...register("url", { required: true })} autoComplete="off" />
|
||||
{previewURL && <Image aspectRatio={1} h="10" src={previewURL} />}
|
||||
<Button flexShrink={0} type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmojiPackView() {
|
||||
function EmojiTag({ name, url, onRemove, scale }: { name: string; url: string; onRemove?: () => void; scale: number }) {
|
||||
return (
|
||||
<Tag>
|
||||
<Image key={name + url} src={url} title={name} w={scale} h={scale} ml="-1" mr="2" my="1" borderRadius="md" />
|
||||
<TagLabel flex={1}>{name}</TagLabel>
|
||||
{onRemove && <TagCloseButton onClick={onRemove} />}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiPackPage({ pack }: { pack: NostrEvent }) {
|
||||
const navigate = useNavigate();
|
||||
const coordinate = useListCoordinate();
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
const account = useCurrentAccount();
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
const { requestSignature } = useSigningContext();
|
||||
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);
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draftEmojis, setDraft] = useState(emojis);
|
||||
|
||||
const startEdit = () => {
|
||||
setDraft(emojis);
|
||||
setEditing(true);
|
||||
};
|
||||
const addEmoji = (emoji: { name: string; url: string }) => {
|
||||
setDraft((a) => a.concat(emoji));
|
||||
};
|
||||
const removeEmoji = (name: string) => {
|
||||
setDraft((a) => a.filter((e) => e.name !== name));
|
||||
};
|
||||
const cancelEdit = () => {
|
||||
setDraft([]);
|
||||
setEditing(false);
|
||||
};
|
||||
const saveEdit = async () => {
|
||||
const draft: DraftNostrEvent = {
|
||||
kind: EMOJI_PACK_KIND,
|
||||
content: pack.content || "",
|
||||
created_at: dayjs().unix(),
|
||||
tags: [...pack.tags.filter((t) => t[0] !== "emoji"), ...draftEmojis.map(({ name, url }) => ["emoji", name, url])],
|
||||
};
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
const pub = new NostrPublishAction("Update emoji pack", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" px="2" pt="2" pb="8" overflowY="auto" overflowX="hidden" h="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
@ -65,45 +127,89 @@ export default function EmojiPackView() {
|
||||
|
||||
<Spacer />
|
||||
|
||||
{isAuthor && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(pack).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<EmojiPackMenu aria-label="More options" pack={pack} />
|
||||
<ButtonGroup>
|
||||
{isAuthor && (
|
||||
<>
|
||||
{!editing && (
|
||||
<Button colorScheme="brand" onClick={startEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(pack).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<EmojiPackMenu aria-label="More options" pack={pack} />
|
||||
</ButtonGroup>
|
||||
</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>
|
||||
{!editing && (
|
||||
<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">
|
||||
<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>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<SimpleGrid columns={{ base: 2, sm: 3, md: 2, lg: 4, xl: 6 }} gap="2">
|
||||
{(editing ? draftEmojis : emojis).map(({ name, url }) => (
|
||||
<EmojiTag
|
||||
key={name + url}
|
||||
scale={scale}
|
||||
name={name}
|
||||
url={url}
|
||||
onRemove={editing ? () => removeEmoji(name) : undefined}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<Flex gap="2">
|
||||
<AddEmojiForm onAdd={addEmoji} />
|
||||
<Button ml="auto" onClick={cancelEdit}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={saveEdit}>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
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 coordinate = useListCoordinate();
|
||||
const pack = useReplaceableEvent(coordinate);
|
||||
|
||||
if (!pack) {
|
||||
return (
|
||||
<>
|
||||
Looking for pack "{coordinate.identifier}" created by <UserLink pubkey={coordinate.pubkey} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmojiPackPage pack={pack} />;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Divider, Flex, Heading, Link, SimpleGrid, Spacer } from "@chakra-ui/react";
|
||||
import { Button, Divider, Flex, Heading, Link, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
@ -11,6 +11,7 @@ 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";
|
||||
import EmojiPackCreateModal from "./create-modal";
|
||||
|
||||
function UserEmojiPackMangerPage() {
|
||||
const account = useCurrentAccount()!;
|
||||
@ -67,6 +68,7 @@ function UserEmojiPackMangerPage() {
|
||||
|
||||
export default function EmojiPacksView() {
|
||||
const account = useCurrentAccount();
|
||||
const createModal = useDisclosure();
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="10" gap="2" px={["2", "2", 0]}>
|
||||
@ -78,9 +80,15 @@ export default function EmojiPacksView() {
|
||||
<Button as={Link} href="https://emojis-iota.vercel.app/" isExternal rightIcon={<ExternalLinkIcon />}>
|
||||
Emoji pack manager
|
||||
</Button>
|
||||
{account && (
|
||||
<Button colorScheme="brand" onClick={createModal.onOpen}>
|
||||
Create Emoji pack
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{account && <UserEmojiPackMangerPage />}
|
||||
{createModal.isOpen && <EmojiPackCreateModal isOpen={createModal.isOpen} onClose={createModal.onClose} />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ function UserGoalsManagerPage() {
|
||||
|
||||
if (goals.length === 0) {
|
||||
return (
|
||||
<Center p="10" fontSize="lg" whiteSpace="pre-wrap">
|
||||
<Center p="10" fontSize="lg" whiteSpace="pre">
|
||||
You don't have any goals,{" "}
|
||||
<Link as={RouterLink} to="/goals/browse" color="blue.500">
|
||||
Find a goal
|
||||
|
Loading…
x
Reference in New Issue
Block a user