mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-25 11:13:30 +02:00
clean up reactions a little
This commit is contained in:
@@ -1,52 +1,74 @@
|
|||||||
import { Flex, FlexProps, Image, useDisclosure } from "@chakra-ui/react";
|
import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, IconButton, Image, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import useEventReactions from "../hooks/use-event-reactions";
|
import useEventReactions from "../hooks/use-event-reactions";
|
||||||
import { DislikeIcon, LikeIcon } from "./icons";
|
import { DislikeIcon, LikeIcon } from "./icons";
|
||||||
import { groupReactions } from "../helpers/nostr/reactions";
|
import { draftEventReaction, groupReactions } from "../helpers/nostr/reactions";
|
||||||
import ReactionDetailsModal from "./reaction-details-modal";
|
import ReactionDetailsModal from "./reaction-details-modal";
|
||||||
|
import { useSigningContext } from "../providers/signing-provider";
|
||||||
|
import clientRelaysService from "../services/client-relays";
|
||||||
|
import NostrPublishAction from "../classes/nostr-publish-action";
|
||||||
|
import eventReactionsService from "../services/event-reactions";
|
||||||
|
|
||||||
export function ReactionIcon({ emoji, url, count }: { emoji: string; count: number; url?: string }) {
|
export function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
|
||||||
const renderIcon = () => {
|
if (emoji === "+") return <LikeIcon />;
|
||||||
if (emoji === "+") return <LikeIcon w="0.8em" h="0.8em" />;
|
if (emoji === "-") return <DislikeIcon />;
|
||||||
if (emoji === "-") return <DislikeIcon w="0.8em" h="0.8em" />;
|
if (url) return <Image src={url} title={emoji} alt={emoji} />;
|
||||||
if (url) return <Image src={url} w="0.8em" h="0.8em" title={emoji} alt={emoji} />;
|
return <span>{emoji}</span>;
|
||||||
return <span>{emoji}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (count > 1) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderIcon()}
|
|
||||||
<span>{count}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return renderIcon();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventReactions({ event, ...props }: Omit<FlexProps, "children"> & { event: NostrEvent }) {
|
function ReactionGroupButton({
|
||||||
|
emoji,
|
||||||
|
url,
|
||||||
|
count,
|
||||||
|
...props
|
||||||
|
}: Omit<ButtonProps, "leftIcon" | "children"> & { emoji: string; count: number; url?: string }) {
|
||||||
|
if (count <= 1) {
|
||||||
|
return <IconButton icon={<ReactionIcon emoji={emoji} url={url} />} aria-label="Reaction" {...props} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button leftIcon={<ReactionIcon emoji={emoji} url={url} />} {...props}>
|
||||||
|
{count > 1 && count}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventReactions({
|
||||||
|
event,
|
||||||
|
...props
|
||||||
|
}: Omit<ButtonGroupProps, "children"> & { event: NostrEvent }) {
|
||||||
const detailsModal = useDisclosure();
|
const detailsModal = useDisclosure();
|
||||||
const reactions = useEventReactions(event.id) ?? [];
|
const reactions = useEventReactions(event.id) ?? [];
|
||||||
const grouped = useMemo(() => groupReactions(reactions), [reactions]);
|
const grouped = useMemo(() => groupReactions(reactions), [reactions]);
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
|
||||||
|
const addReaction = useCallback(async (emoji = "+", url?: string) => {
|
||||||
|
const draft = draftEventReaction(event, emoji, url);
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
if (signed) {
|
||||||
|
const writeRelays = clientRelaysService.getWriteUrls();
|
||||||
|
new NostrPublishAction("Reaction", writeRelays, signed);
|
||||||
|
eventReactionsService.handleEvent(signed);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (grouped.length === 0) return null;
|
if (grouped.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<ButtonGroup wrap="wrap" {...props}>
|
||||||
maxW="lg"
|
|
||||||
overflow="hidden"
|
|
||||||
gap="1"
|
|
||||||
alignItems="center"
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={detailsModal.onOpen}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{grouped.map((group) => (
|
{grouped.map((group) => (
|
||||||
<ReactionIcon key={group.emoji} emoji={group.emoji} url={group.url} count={group.count} />
|
<ReactionGroupButton
|
||||||
|
key={group.emoji}
|
||||||
|
emoji={group.emoji}
|
||||||
|
url={group.url}
|
||||||
|
count={group.count}
|
||||||
|
onClick={() => addReaction(group.emoji, group.url)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
<Button onClick={detailsModal.onOpen}>Show all</Button>
|
||||||
|
</ButtonGroup>
|
||||||
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
|
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -1,13 +1,6 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
ButtonProps,
|
ButtonProps,
|
||||||
Flex,
|
|
||||||
HStack,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
IconButtonProps,
|
|
||||||
Image,
|
|
||||||
Popover,
|
Popover,
|
||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
@@ -26,29 +19,16 @@ import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
|||||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||||
import { AddReactionIcon } from "../../icons";
|
import { AddReactionIcon } from "../../icons";
|
||||||
import ReactionPicker from "../../reaction-picker";
|
import ReactionPicker from "../../reaction-picker";
|
||||||
|
import { draftEventReaction } from "../../../helpers/nostr/reactions";
|
||||||
|
|
||||||
export default function ReactionButton({
|
export default function ReactionButton({ event, ...props }: { event: NostrEvent } & Omit<ButtonProps, "children">) {
|
||||||
event: note,
|
|
||||||
...props
|
|
||||||
}: { event: NostrEvent } & Omit<ButtonProps, "children">) {
|
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const account = useCurrentAccount();
|
const reactions = useEventReactions(event.id) ?? [];
|
||||||
const reactions = useEventReactions(note.id) ?? [];
|
|
||||||
|
|
||||||
const addReaction = async (emoji = "+", url?: string) => {
|
const addReaction = async (emoji = "+", url?: string) => {
|
||||||
const event: DraftNostrEvent = {
|
const draft = draftEventReaction(event, emoji, url);
|
||||||
kind: Kind.Reaction,
|
|
||||||
content: url ? ":" + emoji + ":" : emoji,
|
|
||||||
tags: [
|
|
||||||
["e", note.id],
|
|
||||||
["p", note.pubkey], // TODO: pick a relay for the user
|
|
||||||
],
|
|
||||||
created_at: dayjs().unix(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (url) event.tags.push(["emoji", emoji, url]);
|
const signed = await requestSignature(draft);
|
||||||
|
|
||||||
const signed = await requestSignature(event);
|
|
||||||
if (signed) {
|
if (signed) {
|
||||||
const writeRelays = clientRelaysService.getWriteUrls();
|
const writeRelays = clientRelaysService.getWriteUrls();
|
||||||
new NostrPublishAction("Reaction", writeRelays, signed);
|
new NostrPublishAction("Reaction", writeRelays, signed);
|
||||||
|
@@ -8,9 +8,11 @@ import {
|
|||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardProps,
|
CardProps,
|
||||||
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
|
useBreakpointValue,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { UserAvatarLink } from "../user-avatar-link";
|
import { UserAvatarLink } from "../user-avatar-link";
|
||||||
@@ -51,6 +53,8 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
|
|||||||
// find mostr external link
|
// find mostr external link
|
||||||
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
|
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
|
||||||
|
|
||||||
|
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrustProvider event={event}>
|
<TrustProvider event={event}>
|
||||||
<ExpandProvider>
|
<ExpandProvider>
|
||||||
@@ -70,29 +74,39 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
|
|||||||
<CardBody p="0">
|
<CardBody p="0">
|
||||||
<NoteContentWithWarning event={event} />
|
<NoteContentWithWarning event={event} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter padding="2" display="flex" gap="2">
|
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
||||||
<ButtonGroup size="sm" variant="link" isDisabled={account?.readonly ?? true}>
|
{showReactions && showReactionsOnNewLine && (
|
||||||
<RepostButton event={event} />
|
<EventReactions event={event} variant="ghost" size="xs" spacing="1" />
|
||||||
<QuoteRepostButton event={event} />
|
|
||||||
<NoteZapButton event={event} size="sm" />
|
|
||||||
{showReactions && <EventReactions event={event} />}
|
|
||||||
<ReactionButton event={event} size="sm" />
|
|
||||||
</ButtonGroup>
|
|
||||||
<Box flexGrow={1} />
|
|
||||||
{externalLink && (
|
|
||||||
<IconButton
|
|
||||||
as={Link}
|
|
||||||
icon={<ExternalLinkIcon />}
|
|
||||||
aria-label="Open External"
|
|
||||||
href={externalLink}
|
|
||||||
size="sm"
|
|
||||||
variant="link"
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<EventRelays event={event} />
|
<Flex gap="2" w="full" alignItems="center">
|
||||||
<BookmarkButton event={event} aria-label="Bookmark note" size="sm" variant="link" />
|
<ButtonGroup size="xs" variant="ghost" isDisabled={account?.readonly ?? true}>
|
||||||
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
|
<RepostButton event={event} />
|
||||||
|
<QuoteRepostButton event={event} />
|
||||||
|
<NoteZapButton event={event} />
|
||||||
|
<ReactionButton event={event} />
|
||||||
|
</ButtonGroup>
|
||||||
|
{showReactions && !showReactionsOnNewLine && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" h="1.5rem" />
|
||||||
|
<EventReactions event={event} variant="ghost" size="xs" spacing="1" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Box flexGrow={1} />
|
||||||
|
{externalLink && (
|
||||||
|
<IconButton
|
||||||
|
as={Link}
|
||||||
|
icon={<ExternalLinkIcon />}
|
||||||
|
aria-label="Open External"
|
||||||
|
href={externalLink}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<EventRelays event={event} />
|
||||||
|
<BookmarkButton event={event} aria-label="Bookmark note" size="sm" variant="link" />
|
||||||
|
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
|
||||||
|
</Flex>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</ExpandProvider>
|
</ExpandProvider>
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { Kind } from "nostr-tools";
|
||||||
|
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] };
|
export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] };
|
||||||
|
|
||||||
@@ -17,3 +19,19 @@ export function groupReactions(reactions: NostrEvent[]) {
|
|||||||
}
|
}
|
||||||
return Array.from(Object.values(groups)).sort((a, b) => b.count - a.count);
|
return Array.from(Object.values(groups)).sort((a, b) => b.count - a.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) {
|
||||||
|
const draft: DraftNostrEvent = {
|
||||||
|
kind: Kind.Reaction,
|
||||||
|
content: url ? ":" + emoji + ":" : emoji,
|
||||||
|
tags: [
|
||||||
|
["e", event.id],
|
||||||
|
["p", event.pubkey], // TODO: pick a relay for the user
|
||||||
|
],
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (url) draft.tags.push(["emoji", emoji, url]);
|
||||||
|
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
@@ -23,6 +23,7 @@ import RelayReviews from "./relay-reviews";
|
|||||||
import RelayNotes from "./relay-notes";
|
import RelayNotes from "./relay-notes";
|
||||||
import PeopleListProvider from "../../../providers/people-list-provider";
|
import PeopleListProvider from "../../../providers/people-list-provider";
|
||||||
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
|
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
|
||||||
|
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||||
|
|
||||||
function RelayPage({ relay }: { relay: string }) {
|
function RelayPage({ relay }: { relay: string }) {
|
||||||
const { info } = useRelayInfo(relay);
|
const { info } = useRelayInfo(relay);
|
||||||
@@ -30,8 +31,9 @@ function RelayPage({ relay }: { relay: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" alignItems="stretch" gap="2" p="2">
|
<Flex direction="column" alignItems="stretch" gap="2" p="2">
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap" justifyContent="space-between">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<Heading isTruncated size={{ base: "md", sm: "lg" }}>
|
<RelayFavicon relay={relay} />
|
||||||
|
<Heading isTruncated size={{ base: "md", sm: "lg" }} mr="auto">
|
||||||
{relay}
|
{relay}
|
||||||
{info?.payments_url && (
|
{info?.payments_url && (
|
||||||
<Tag
|
<Tag
|
||||||
|
@@ -25,7 +25,7 @@ export default function RelayNotes({ relay }: { relay: string }) {
|
|||||||
const timelineEventFilter = useTimelinePageEventFilter();
|
const timelineEventFilter = useTimelinePageEventFilter();
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => {
|
(event: NostrEvent) => {
|
||||||
if (!isReply(event)) return false;
|
if (isReply(event)) return false;
|
||||||
return timelineEventFilter(event);
|
return timelineEventFilter(event);
|
||||||
},
|
},
|
||||||
[timelineEventFilter],
|
[timelineEventFilter],
|
||||||
|
Reference in New Issue
Block a user