diff --git a/src/components/event-reactions.tsx b/src/components/event-reactions.tsx
deleted file mode 100644
index 6266290e9..000000000
--- a/src/components/event-reactions.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useCallback, useMemo } from "react";
-import { Button, ButtonProps, IconButton, Image, useDisclosure } from "@chakra-ui/react";
-
-import { NostrEvent } from "../types/nostr-event";
-import useEventReactions from "../hooks/use-event-reactions";
-import { DislikeIcon, LikeIcon } from "./icons";
-import { draftEventReaction, groupReactions } from "../helpers/nostr/reactions";
-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";
-import { useCurrentAccount } from "../hooks/use-current-account";
-
-export function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
- if (emoji === "+") return ;
- if (emoji === "-") return ;
- if (url) return ;
- return {emoji};
-}
-
-function ReactionGroupButton({
- emoji,
- url,
- count,
- ...props
-}: Omit & { emoji: string; count: number; url?: string }) {
- if (count <= 1) {
- return } aria-label="Reaction" {...props} />;
- }
- return (
- } title={emoji} {...props}>
- {count > 1 && count}
-
- );
-}
-
-export default function EventReactionButtons({ event, max }: { event: NostrEvent; max?: number }) {
- const account = useCurrentAccount();
- const detailsModal = useDisclosure();
- const reactions = useEventReactions(event.id) ?? [];
- 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;
-
- const clamped = Array.from(grouped);
- if (max !== undefined) clamped.length = max;
-
- return (
- <>
- {clamped.map((group) => (
- addReaction(group.emoji, group.url)}
- colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
- />
- ))}
-
- {detailsModal.isOpen && }
- >
- );
-}
diff --git a/src/components/event-reactions/common-hooks.tsx b/src/components/event-reactions/common-hooks.tsx
new file mode 100644
index 000000000..958191218
--- /dev/null
+++ b/src/components/event-reactions/common-hooks.tsx
@@ -0,0 +1,37 @@
+import { useCallback } from "react";
+import { useToast } from "@chakra-ui/react";
+
+import { ReactionGroup, draftEventReaction } from "../../helpers/nostr/reactions";
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import { useSigningContext } from "../../providers/signing-provider";
+import { NostrEvent } from "../../types/nostr-event";
+import clientRelaysService from "../../services/client-relays";
+import NostrPublishAction from "../../classes/nostr-publish-action";
+import eventReactionsService from "../../services/event-reactions";
+
+export function useAddReaction(event: NostrEvent, grouped: ReactionGroup[]) {
+ const account = useCurrentAccount();
+ const toast = useToast();
+ const { requestSignature } = useSigningContext();
+
+ return useCallback(
+ async (emoji = "+", url?: string) => {
+ try {
+ const group = grouped.find((g) => g.emoji === emoji);
+ if (account && group && group.pubkeys.includes(account?.pubkey)) return;
+
+ 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);
+ }
+ } catch (e) {
+ if (e instanceof Error) toast({ description: e.message, status: "error" });
+ }
+ },
+ [grouped, account, toast, requestSignature],
+ );
+}
diff --git a/src/components/event-reactions/event-reactions.tsx b/src/components/event-reactions/event-reactions.tsx
new file mode 100644
index 000000000..21fec094a
--- /dev/null
+++ b/src/components/event-reactions/event-reactions.tsx
@@ -0,0 +1,41 @@
+import { useMemo } from "react";
+import { Button, useDisclosure } from "@chakra-ui/react";
+
+import { NostrEvent } from "../../types/nostr-event";
+import useEventReactions from "../../hooks/use-event-reactions";
+import { groupReactions } from "../../helpers/nostr/reactions";
+import ReactionDetailsModal from "../reaction-details-modal";
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import ReactionGroupButton from "./reaction-group-button";
+import { useAddReaction } from "./common-hooks";
+
+export default function EventReactionButtons({ event, max }: { event: NostrEvent; max?: number }) {
+ const account = useCurrentAccount();
+ const detailsModal = useDisclosure();
+ const reactions = useEventReactions(event.id) ?? [];
+ const grouped = useMemo(() => groupReactions(reactions), [reactions]);
+
+ const addReaction = useAddReaction(event, grouped);
+
+ if (grouped.length === 0) return null;
+
+ const clamped = Array.from(grouped);
+ if (max !== undefined) clamped.length = max;
+
+ return (
+ <>
+ {clamped.map((group) => (
+ addReaction(group.emoji, group.url)}
+ colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
+ />
+ ))}
+
+ {detailsModal.isOpen && }
+ >
+ );
+}
diff --git a/src/components/event-reactions/reaction-group-button.tsx b/src/components/event-reactions/reaction-group-button.tsx
new file mode 100644
index 000000000..db3d14c9b
--- /dev/null
+++ b/src/components/event-reactions/reaction-group-button.tsx
@@ -0,0 +1,18 @@
+import { Button, ButtonProps, IconButton } from "@chakra-ui/react";
+import ReactionIcon from "./reaction-icon";
+
+export default function ReactionGroupButton({
+ emoji,
+ url,
+ count,
+ ...props
+}: Omit & { emoji: string; count: number; url?: string }) {
+ if (count <= 1) {
+ return } aria-label="Reaction" {...props} />;
+ }
+ return (
+ } title={emoji} {...props}>
+ {count > 1 && count}
+
+ );
+}
diff --git a/src/components/event-reactions/reaction-icon.tsx b/src/components/event-reactions/reaction-icon.tsx
new file mode 100644
index 000000000..ac7540f48
--- /dev/null
+++ b/src/components/event-reactions/reaction-icon.tsx
@@ -0,0 +1,9 @@
+import { Image } from "@chakra-ui/react";
+import { DislikeIcon, LikeIcon } from "../icons";
+
+export default function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
+ if (emoji === "+") return ;
+ if (emoji === "-") return ;
+ if (url) return ;
+ return {emoji};
+}
diff --git a/src/components/event-reactions/simple-like-button.tsx b/src/components/event-reactions/simple-like-button.tsx
new file mode 100644
index 000000000..f8321f90a
--- /dev/null
+++ b/src/components/event-reactions/simple-like-button.tsx
@@ -0,0 +1,28 @@
+import { useMemo } from "react";
+
+import { NostrEvent } from "../../types/nostr-event";
+import useEventReactions from "../../hooks/use-event-reactions";
+import { groupReactions } from "../../helpers/nostr/reactions";
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import ReactionGroupButton from "./reaction-group-button";
+import { useAddReaction } from "./common-hooks";
+import { ButtonProps } from "@chakra-ui/react";
+
+export default function SimpleLikeButton({ event, ...props }: Omit & { event: NostrEvent }) {
+ const account = useCurrentAccount();
+ const reactions = useEventReactions(event.id) ?? [];
+ const grouped = useMemo(() => groupReactions(reactions), [reactions]);
+
+ const addReaction = useAddReaction(event, grouped);
+ const group = grouped.find((g) => g.emoji === "+");
+
+ return (
+ addReaction("+")}
+ colorScheme={account && group?.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
+ {...props}
+ />
+ );
+}
diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx
index 830875546..58ed11337 100644
--- a/src/components/layout/index.tsx
+++ b/src/components/layout/index.tsx
@@ -12,7 +12,7 @@ import GhostToolbar from "./ghost-toolbar";
import { useBreakpointValue } from "../../providers/breakpoint-provider";
import SearchModal from "../search-modal";
import { useLocation } from "react-router-dom";
-import ChatWindows from "../chat-windows";
+// import ChatWindows from "../chat-windows";
export default function Layout({ children }: { children: React.ReactNode }) {
const isMobile = useBreakpointValue({ base: true, md: false });
@@ -66,7 +66,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{isGhost && }
{searchModal.isOpen && }
- {!isMobile && }
+ {/* {!isMobile && } */}
>
);
}
diff --git a/src/components/note/components/note-reactions.tsx b/src/components/note/components/note-reactions.tsx
index 2dc06f8ff..73ddd2945 100644
--- a/src/components/note/components/note-reactions.tsx
+++ b/src/components/note/components/note-reactions.tsx
@@ -2,7 +2,7 @@ import { ButtonGroup, ButtonGroupProps, Divider } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import ReactionButton from "./reaction-button";
-import EventReactionButtons from "../../event-reactions";
+import EventReactionButtons from "../../event-reactions/event-reactions";
import useEventReactions from "../../../hooks/use-event-reactions";
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
diff --git a/src/components/note/note-zap-button.tsx b/src/components/note/note-zap-button.tsx
index 6bd0535f2..5b013b341 100644
--- a/src/components/note/note-zap-button.tsx
+++ b/src/components/note/note-zap-button.tsx
@@ -21,7 +21,7 @@ export type NoteZapButtonProps = Omit & {
export default function NoteZapButton({ event, allowComment, showEventPreview, ...props }: NoteZapButtonProps) {
const account = useCurrentAccount();
const { metadata } = useUserLNURLMetadata(event.pubkey);
- const zaps = useEventZaps(event.id);
+ const zaps = useEventZaps(getEventUID(event));
const { isOpen, onOpen, onClose } = useDisclosure();
const hasZapped = !!account && zaps.some((zap) => zap.request.pubkey === account.pubkey);
diff --git a/src/components/reaction-details-modal.tsx b/src/components/reaction-details-modal.tsx
index 10ea25b5a..eda5d8cc8 100644
--- a/src/components/reaction-details-modal.tsx
+++ b/src/components/reaction-details-modal.tsx
@@ -18,7 +18,7 @@ import { useMemo } from "react";
import { NostrEvent } from "../types/nostr-event";
import { groupReactions } from "../helpers/nostr/reactions";
-import { ReactionIcon } from "./event-reactions";
+import { ReactionIcon } from "./event-reactions/event-reactions";
import UserAvatarLink from "./user-avatar-link";
import { UserLink } from "./user-link";
diff --git a/src/helpers/nostr/reactions.ts b/src/helpers/nostr/reactions.ts
index 3034dd05d..5fea08c01 100644
--- a/src/helpers/nostr/reactions.ts
+++ b/src/helpers/nostr/reactions.ts
@@ -1,6 +1,7 @@
import { Kind } from "nostr-tools";
-import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
+import { DraftNostrEvent, NostrEvent, Tag } from "../../types/nostr-event";
import dayjs from "dayjs";
+import { getEventCoordinate, isReplaceable } from "./events";
export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] };
@@ -20,14 +21,15 @@ export function groupReactions(reactions: NostrEvent[]) {
return Array.from(Object.values(groups)).sort((a, b) => b.pubkeys.length - a.pubkeys.length);
}
-export function draftEventReaction(reacted: NostrEvent, emoji = "+", url?: string) {
- // only keep the e, and p tags on the parent event
- const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
-
+export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) {
+ const tags: Tag[] = [
+ ["e", event.id],
+ ["p", event.pubkey],
+ ];
const draft: DraftNostrEvent = {
kind: Kind.Reaction,
content: url ? ":" + emoji + ":" : emoji,
- tags: [...inheritedTags, ["e", reacted.id], ["p", reacted.pubkey]],
+ tags: isReplaceable(event.kind) ? [...tags, ["a", getEventCoordinate(event)]] : tags,
created_at: dayjs().unix(),
};
diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx
index 6694a1a28..2ea1d53bb 100644
--- a/src/views/lists/components/list-card.tsx
+++ b/src/views/lists/components/list-card.tsx
@@ -1,16 +1,17 @@
import { memo, useRef } from "react";
import { Link as RouterLink } from "react-router-dom";
import {
- AvatarGroup,
ButtonGroup,
Card,
CardBody,
+ CardFooter,
CardHeader,
CardProps,
- Flex,
Heading,
Link,
+ LinkBox,
LinkProps,
+ SimpleGrid,
Text,
} from "@chakra-ui/react";
import { Kind, nip19 } from "nostr-tools";
@@ -29,15 +30,20 @@ import { getSharableEventAddress } from "../../../helpers/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { createCoordinate } from "../../../services/replaceable-event-requester";
-import { NoteLink } from "../../../components/note-link";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import ListFavoriteButton from "./list-favorite-button";
import { getEventUID } from "../../../helpers/nostr/events";
import ListMenu from "./list-menu";
-import Timestamp from "../../../components/timestamp";
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
import { getArticleTitle } from "../../../helpers/nostr/long-form";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
+import { CommunityIcon, NotesIcon } from "../../../components/icons";
+import User01 from "../../../components/icons/user-01";
+import HoverLinkOverlay from "../../../components/hover-link-overlay";
+import NoteZapButton from "../../../components/note/note-zap-button";
+import Link01 from "../../../components/icons/link-01";
+import File02 from "../../../components/icons/file-02";
+import SimpleLikeButton from "../../../components/event-reactions/simple-like-button";
function ArticleLinkLoader({ pointer, ...props }: { pointer: nip19.AddressPointer } & Omit) {
const article = useReplaceableEvent(pointer);
@@ -64,62 +70,33 @@ export function ListCardContent({ list, ...props }: Omit
const references = getReferencesFromList(list);
return (
- <>
-
- Updated:
-
+
{people.length > 0 && (
- <>
- People ({people.length}):
-
- {people.map(({ pubkey, relay }) => (
-
- ))}
-
- >
+
+ {people.length}
+
)}
{notes.length > 0 && (
-
- Notes ({notes.length}):
- {notes.slice(0, 4).map(({ id, relay }) => (
-
- ))}
-
+
+ {notes.length}
+
)}
{references.length > 0 && (
-
- References ({references.length})
- {references.slice(0, 3).map(({ url, petname }) => (
-
- {petname || url}
-
- ))}
-
- )}
- {communities.length > 0 && (
-
- Communities ({communities.length}):
- {communities.map((pointer) => (
-
- {pointer.identifier}
-
- ))}
-
+
+ {references.length}
+
)}
{articles.length > 0 && (
-
- Articles ({articles.length}):
- {articles.slice(0, 4).map((pointer) => (
-
- ))}
-
+
+ {articles.length}
+
)}
- >
+ {communities.length > 0 && (
+
+ {communities.length}
+
+ )}
+
);
}
@@ -135,12 +112,12 @@ function ListCardRender({
useRegisterIntersectionEntity(ref, getEventUID(list));
return (
-
-
+
+
-
+
{getListName(list)}
-
+
{!hideCreator && (
<>
@@ -149,14 +126,19 @@ function ListCardRender({
>
)}
-
+
+
+
+
+
+
+ {/* TODO: reactions are tagging every user in list */}
+
+
-
-
-
-
+
);
}
diff --git a/src/views/lists/list-details.tsx b/src/views/lists/list-details.tsx
index 68e949b29..7c009178d 100644
--- a/src/views/lists/list-details.tsx
+++ b/src/views/lists/list-details.tsx
@@ -18,16 +18,16 @@ import {
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import UserCard from "./components/user-card";
import OpenGraphCard from "../../components/open-graph-card";
-import NoteCard from "./components/note-card";
import { TrustProvider } from "../../providers/trust";
import ListMenu from "./components/list-menu";
import ListFavoriteButton from "./components/list-favorite-button";
import ListFeedButton from "./components/list-feed-button";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
-import { EmbedEventPointer } from "../../components/embed-event";
+import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
import { encodePointer } from "../../helpers/nip19";
import { DecodeResult } from "nostr-tools/lib/types/nip19";
+import useSingleEvent from "../../hooks/use-single-event";
function useListCoordinate() {
const { addr } = useParams() as { addr: string };
@@ -43,6 +43,12 @@ function useListCoordinate() {
return parsed.data;
}
+function BookmarkedEvent({ id, relay }: { id: string; relay?: string }) {
+ const event = useSingleEvent(id, relay ? [relay] : undefined);
+
+ return event ? : <>Loading {id}>;
+}
+
export default function ListDetailsView() {
const navigate = useNavigate();
const coordinate = useListCoordinate();
@@ -67,56 +73,54 @@ export default function ListDetailsView() {
const references = getReferencesFromList(list);
return (
-
-
-
-
-
- {getListName(list)}
-
-
-
-
-
-
- {isAuthor && !isSpecialListKind(list.kind) && (
-
- {people.length > 0 && (
- <>
- People
-
- {people.map(({ pubkey, relay }) => (
-
- ))}
-
- >
- )}
-
- {notes.length > 0 && (
- <>
- Notes
-
+ {notes.length > 0 && (
+ <>
+ Notes
{notes.map(({ id, relay }) => (
-
+
))}
-
- >
- )}
+ >
+ )}
- {references.length > 0 && (
- <>
- References
-
+ {references.length > 0 && (
+ <>
+ References
{references.map(({ url, petname }) => (
<>
@@ -125,32 +129,32 @@ export default function ListDetailsView() {
>
))}
-
- >
- )}
+ >
+ )}
- {communities.length > 0 && (
- <>
- Communities
-
- {communities.map((pointer) => (
-
- ))}
-
- >
- )}
+ {communities.length > 0 && (
+ <>
+ Communities
+
+ {communities.map((pointer) => (
+
+ ))}
+
+ >
+ )}
- {articles.length > 0 && (
- <>
- Articles
-
- {articles.map((pointer) => {
- const decode: DecodeResult = { type: "naddr", data: pointer };
- return ;
- })}
-
- >
- )}
-
+ {articles.length > 0 && (
+ <>
+ Articles
+
+ {articles.map((pointer) => {
+ const decode: DecodeResult = { type: "naddr", data: pointer };
+ return ;
+ })}
+
+ >
+ )}
+
+
);
}
diff --git a/src/views/user/lists.tsx b/src/views/user/lists.tsx
index 9e75fa918..14368d245 100644
--- a/src/views/user/lists.tsx
+++ b/src/views/user/lists.tsx
@@ -13,6 +13,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import { Kind } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { NostrEvent } from "../../types/nostr-event";
+import UserName from "../../components/user-name";
export default function UserListsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@@ -24,30 +25,36 @@ export default function UserListsTab() {
const timeline = useTimelineLoader(
pubkey + "-lists",
readRelays,
- {
- authors: [pubkey],
- kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
- },
+ [
+ {
+ authors: [pubkey],
+ kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
+ },
+ {
+ "#p": [pubkey],
+ kinds: [PEOPLE_LIST_KIND],
+ },
+ ],
{ eventFilter },
);
const lists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
- const peopleLists = lists.filter((event) => event.kind === PEOPLE_LIST_KIND);
- const noteLists = lists.filter((event) => event.kind === NOTE_LIST_KIND);
+ const peopleLists = lists.filter((event) => event.pubkey === pubkey && event.kind === PEOPLE_LIST_KIND);
+ const noteLists = lists.filter((event) => event.pubkey === pubkey && event.kind === NOTE_LIST_KIND);
+ const otherLists = lists.filter((event) => event.pubkey !== pubkey && event.kind === PEOPLE_LIST_KIND);
return (
-
-
+
+
Special lists
-
-
-
-
+
+
+
{peopleLists.length > 0 && (
@@ -55,10 +62,9 @@ export default function UserListsTab() {
People lists
-
{peopleLists.map((event) => (
-
+
))}
>
@@ -69,15 +75,25 @@ export default function UserListsTab() {
Bookmark lists
-
{noteLists.map((event) => (
-
+
))}
>
)}
-
-
+
+
+
+
+ Lists is in
+
+
+ {otherLists.map((event) => (
+
+ ))}
+
+
+
);
}