mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
Merge branch 'next'
This commit is contained in:
commit
61fee4e9d9
5
.changeset/brave-squids-exercise.md
Normal file
5
.changeset/brave-squids-exercise.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add "DM Feed" tool
|
5
.changeset/cold-seals-rhyme.md
Normal file
5
.changeset/cold-seals-rhyme.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Thread view improvements
|
5
.changeset/new-cobras-draw.md
Normal file
5
.changeset/new-cobras-draw.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to search communities in search view
|
5
.changeset/ninety-ligers-move.md
Normal file
5
.changeset/ninety-ligers-move.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add "create $prism" link to lists
|
5
.changeset/smart-turkeys-tickle.md
Normal file
5
.changeset/smart-turkeys-tickle.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add people list to search and hashtag views
|
5
.changeset/smooth-chairs-bow.md
Normal file
5
.changeset/smooth-chairs-bow.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix link cards breaking lines
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Talha Buğra Bulut
|
||||
Copyright (c) 2023 hzrd149
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:18
|
||||
FROM node:20
|
||||
WORKDIR /app
|
||||
COPY . /app/
|
||||
ENV VITE_COMMIT_HASH=""
|
||||
|
@ -70,6 +70,7 @@ import RelayView from "./views/relays/relay";
|
||||
import RelayReviewsView from "./views/relays/reviews";
|
||||
import PopularRelaysView from "./views/relays/popular";
|
||||
import UserDMsTab from "./views/user/dms";
|
||||
import DMFeedView from "./views/tools/dm-feed";
|
||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||
|
||||
const ToolsHomeView = lazy(() => import("./views/tools"));
|
||||
@ -230,6 +231,7 @@ const router = createHashRouter([
|
||||
{ path: "network", element: <NetworkView /> },
|
||||
{ path: "network-mute-graph", element: <NetworkMuteGraphView /> },
|
||||
{ path: "network-dm-graph", element: <NetworkDMGraphView /> },
|
||||
{ path: "dm-feed", element: <DMFeedView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
121
src/components/chat-windows/chat-window.tsx
Normal file
121
src/components/chat-windows/chat-window.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Card, CardBody, CardHeader, CloseButton, Flex, Heading, IconButton, useToast } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "../icons";
|
||||
import UserName from "../user-name";
|
||||
import MagicTextArea from "../magic-textarea";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
||||
import { RelayMode } from "../../classes/relay";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import Message from "../../views/messages/message";
|
||||
import { LightboxProvider } from "../lightbox-provider";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import { correctContentMentions, createEmojiTags } from "../../helpers/nostr/post";
|
||||
import { useContextEmojis } from "../../providers/emoji-provider";
|
||||
|
||||
export default function ChatWindow({ pubkey, onClose }: { pubkey: string; onClose: () => void }) {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount()!;
|
||||
const emojis = useContextEmojis();
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
const usersRelays = useUserRelays(pubkey);
|
||||
const readRelays = useReadRelayUrls(usersRelays.filter((c) => c.mode & RelayMode.WRITE).map((c) => c.url));
|
||||
const writeRelays = useWriteRelayUrls(usersRelays.filter((c) => c.mode & RelayMode.WRITE).map((c) => c.url));
|
||||
|
||||
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, readRelays, [
|
||||
{ authors: [account.pubkey], kinds: [Kind.EncryptedDirectMessage], "#p": [pubkey] },
|
||||
{ authors: [pubkey], kinds: [Kind.EncryptedDirectMessage], "#p": [account.pubkey] },
|
||||
]);
|
||||
|
||||
const { handleSubmit, getValues, setValue, formState, watch, reset } = useForm({ defaultValues: { content: "" } });
|
||||
watch("content");
|
||||
const { requestSignature, requestEncrypt } = useSigningContext();
|
||||
const submit = handleSubmit(async (values) => {
|
||||
try {
|
||||
if (!values.content) return;
|
||||
let draft: DraftNostrEvent = {
|
||||
kind: Kind.EncryptedDirectMessage,
|
||||
content: values.content,
|
||||
tags: [["p", pubkey]],
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
|
||||
draft = createEmojiTags(draft, emojis);
|
||||
draft.content = correctContentMentions(draft.content);
|
||||
|
||||
// encrypt content
|
||||
draft.content = await requestEncrypt(draft.content, pubkey);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
const pub = new NostrPublishAction("Send DM", writeRelays, signed);
|
||||
|
||||
reset();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
const messages = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<Card size="sm" borderRadius="md" w={expanded ? "md" : "xs"} variant="outline">
|
||||
<CardHeader display="flex" gap="2" alignItems="center">
|
||||
<Heading size="md" mr="8">
|
||||
<UserName pubkey={pubkey} />
|
||||
</Heading>
|
||||
<IconButton
|
||||
aria-label="Toggle Window"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
variant="ghost"
|
||||
icon={expanded ? <ChevronDownIcon /> : <ChevronUpIcon />}
|
||||
ml="auto"
|
||||
size="sm"
|
||||
/>
|
||||
<CloseButton onClick={onClose} />
|
||||
</CardHeader>
|
||||
{expanded && (
|
||||
<>
|
||||
<CardBody
|
||||
maxH="lg"
|
||||
overflowX="hidden"
|
||||
overflowY="auto"
|
||||
pt="0"
|
||||
display="flex"
|
||||
flexDirection="column-reverse"
|
||||
gap="2"
|
||||
>
|
||||
<LightboxProvider>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{messages.map((event) => (
|
||||
<Message key={event.id} event={event} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
</LightboxProvider>
|
||||
</CardBody>
|
||||
<Flex as="form" onSubmit={submit} gap="2">
|
||||
<MagicTextArea
|
||||
isRequired
|
||||
value={getValues().content}
|
||||
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||
/>
|
||||
<Button type="submit" isLoading={formState.isSubmitting}>
|
||||
Send
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
90
src/components/chat-windows/contacts-window.tsx
Normal file
90
src/components/chat-windows/contacts-window.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CloseButton,
|
||||
Heading,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ChevronDownIcon, ChevronUpIcon, SearchIcon } from "../icons";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import directMessagesService from "../../services/direct-messages";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import UserName from "../user-name";
|
||||
|
||||
export default function ContactsWindow({
|
||||
onClose,
|
||||
onSelectPubkey,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSelectPubkey: (pubkey: string) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
// TODO: find a better way to load recent contacts
|
||||
const [from, setFrom] = useState(() => dayjs().subtract(2, "days"));
|
||||
const conversations = useSubject(directMessagesService.conversations);
|
||||
useEffect(() => directMessagesService.loadDateRange(from), [from]);
|
||||
const sortedConversations = useMemo(() => {
|
||||
return Array.from(conversations).sort((a, b) => {
|
||||
const latestA = directMessagesService.getUserMessages(a).value[0]?.created_at ?? 0;
|
||||
const latestB = directMessagesService.getUserMessages(b).value[0]?.created_at ?? 0;
|
||||
|
||||
return latestB - latestA;
|
||||
});
|
||||
}, [conversations]);
|
||||
|
||||
return (
|
||||
<Card size="sm" borderRadius="md" minW={expanded ? "sm" : 0}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center">
|
||||
<Heading size="md" mr="8">
|
||||
Contacts
|
||||
</Heading>
|
||||
<IconButton
|
||||
aria-label="Toggle Window"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
variant="ghost"
|
||||
icon={expanded ? <ChevronDownIcon /> : <ChevronUpIcon />}
|
||||
ml="auto"
|
||||
size="sm"
|
||||
/>
|
||||
<CloseButton onClick={onClose} />
|
||||
</CardHeader>
|
||||
{expanded && (
|
||||
<CardBody maxH="lg" overflowX="hidden" overflowY="auto" pt="0" display="flex" flexDirection="column" gap="2">
|
||||
<Alert status="warning">
|
||||
<AlertIcon />
|
||||
Work in progress!
|
||||
</Alert>
|
||||
{/* <InputGroup>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<SearchIcon />
|
||||
</InputLeftElement>
|
||||
<Input autoFocus />
|
||||
</InputGroup> */}
|
||||
{sortedConversations.map((pubkey) => (
|
||||
<Button
|
||||
key={pubkey}
|
||||
leftIcon={<UserAvatar pubkey={pubkey} size="sm" />}
|
||||
justifyContent="flex-start"
|
||||
p="2"
|
||||
variant="ghost"
|
||||
onClick={() => onSelectPubkey(pubkey)}
|
||||
>
|
||||
<UserName pubkey={pubkey} />
|
||||
</Button>
|
||||
))}
|
||||
</CardBody>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
56
src/components/chat-windows/index.tsx
Normal file
56
src/components/chat-windows/index.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Flex, IconButton } from "@chakra-ui/react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
|
||||
import ContactsWindow from "./contacts-window";
|
||||
import { DirectMessagesIcon } from "../icons";
|
||||
import ChatWindow from "./chat-window";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
|
||||
export default function ChatWindows() {
|
||||
const account = useCurrentAccount();
|
||||
const [pubkeys, setPubkeys] = useState<string[]>([]);
|
||||
const [show, setShow] = useLocalStorage("show-chat-windows", false);
|
||||
|
||||
const openPubkey = useCallback(
|
||||
(pubkey: string) => {
|
||||
setPubkeys((keys) => (keys.includes(pubkey) ? keys : keys.concat(pubkey)));
|
||||
},
|
||||
[setPubkeys],
|
||||
);
|
||||
|
||||
const closePubkey = useCallback(
|
||||
(pubkey: string) => {
|
||||
setPubkeys((keys) => keys.filter((key) => key !== pubkey));
|
||||
},
|
||||
[setPubkeys],
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!show) {
|
||||
return (
|
||||
<IconButton
|
||||
icon={<DirectMessagesIcon boxSize={6} />}
|
||||
aria-label="Show Contacts"
|
||||
onClick={() => setShow(true)}
|
||||
position="fixed"
|
||||
bottom="0"
|
||||
right="0"
|
||||
size="lg"
|
||||
zIndex={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex direction="row-reverse" position="fixed" bottom="0" right="0" gap="4" alignItems="flex-end" zIndex={1}>
|
||||
<ContactsWindow onClose={() => setShow(false)} onSelectPubkey={openPubkey} />
|
||||
{pubkeys.map((pubkey) => (
|
||||
<ChatWindow key={pubkey} pubkey={pubkey} onClose={() => closePubkey(pubkey)} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
41
src/components/embed-event/event-types/embedded-dm.tsx
Normal file
41
src/components/embed-event/event-types/embedded-dm.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Card, CardBody, CardHeader, CardProps, LinkBox, Spacer, Text } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { TrustProvider } from "../../../providers/trust";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import { UserLink } from "../../user-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import DecryptPlaceholder from "../../../views/messages/decrypt-placeholder";
|
||||
import { MessageContent } from "../../../views/messages/message";
|
||||
import { getMessageRecipient } from "../../../services/direct-messages";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
|
||||
export default function EmbeddedDM({ dm, ...props }: Omit<CardProps, "children"> & { dm: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const isOwnMessage = account?.pubkey === dm.pubkey;
|
||||
|
||||
const sender = dm.pubkey;
|
||||
const receiver = getMessageRecipient(dm);
|
||||
|
||||
if (!receiver) return "Broken DM";
|
||||
|
||||
return (
|
||||
<TrustProvider event={dm}>
|
||||
<Card as={LinkBox} variant="outline" {...props}>
|
||||
<CardHeader display="flex" gap="2" p="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={sender} size="xs" />
|
||||
<UserLink pubkey={sender} fontWeight="bold" isTruncated fontSize="lg" />
|
||||
<Text mx="2">Messaged</Text>
|
||||
<UserAvatarLink pubkey={receiver} size="xs" />
|
||||
<UserLink pubkey={receiver} fontWeight="bold" isTruncated fontSize="lg" />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
</CardHeader>
|
||||
<CardBody px="2" pt="0" pb="2">
|
||||
<DecryptPlaceholder data={dm.content} pubkey={isOwnMessage ? getMessageRecipient(dm) ?? "" : dm.pubkey}>
|
||||
{(text) => <MessageContent event={dm} text={text} />}
|
||||
</DecryptPlaceholder>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
31
src/components/embed-event/event-types/embedded-reaction.tsx
Normal file
31
src/components/embed-event/event-types/embedded-reaction.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Card, CardProps, Flex, LinkBox, Spacer, Text } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { TrustProvider } from "../../../providers/trust";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import { UserLink } from "../../user-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import ReactionIcon from "../../event-reactions/reaction-icon";
|
||||
import { NoteLink } from "../../note-link";
|
||||
import { nip25 } from "nostr-tools";
|
||||
|
||||
export default function EmbeddedReaction({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const pointer = nip25.getReactedEventPointer(event);
|
||||
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
<Card as={LinkBox} {...props}>
|
||||
<Flex p="2" gap="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
|
||||
<Text as="span">Reacted with</Text>
|
||||
<ReactionIcon emoji={event.content} url={event.tags.find((t) => t[0] === "emoji")?.[1]} />
|
||||
<Text as="span">to</Text>
|
||||
{pointer && <NoteLink noteId={pointer.id} />}
|
||||
<Spacer />
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
</Flex>
|
||||
</Card>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
@ -58,7 +58,7 @@ export default function EmbeddedStemstrTrack({ track, ...props }: Omit<CardProps
|
||||
{hashtags.length > 0 && (
|
||||
<Flex wrap="wrap" gap="2">
|
||||
{hashtags.map((hashtag) => (
|
||||
<Tag>#{hashtag}</Tag>
|
||||
<Tag key={hashtag}>#{hashtag}</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -26,6 +26,8 @@ import EmbeddedArticle from "./event-types/embedded-article";
|
||||
import EmbeddedBadge from "./event-types/embedded-badge";
|
||||
import EmbeddedStreamMessage from "./event-types/embedded-stream-message";
|
||||
import EmbeddedCommunity from "./event-types/embedded-community";
|
||||
import EmbeddedReaction from "./event-types/embedded-reaction";
|
||||
import EmbeddedDM from "./event-types/embedded-dm";
|
||||
const EmbeddedStemstrTrack = lazy(() => import("./event-types/embedded-stemstr-track"));
|
||||
|
||||
export type EmbedProps = {
|
||||
@ -40,6 +42,10 @@ export function EmbedEvent({
|
||||
switch (event.kind) {
|
||||
case Kind.Text:
|
||||
return <EmbeddedNote event={event} {...cardProps} />;
|
||||
case Kind.Reaction:
|
||||
return <EmbeddedReaction event={event} {...cardProps} />;
|
||||
case Kind.EncryptedDirectMessage:
|
||||
return <EmbeddedDM dm={event} {...cardProps} />;
|
||||
case STREAM_KIND:
|
||||
return <EmbeddedStream event={event} {...cardProps} />;
|
||||
case GOAL_KIND:
|
||||
|
@ -2,6 +2,7 @@ import { Link } from "@chakra-ui/react";
|
||||
|
||||
import OpenGraphCard from "../open-graph-card";
|
||||
import { isVideoURL } from "../../helpers/url";
|
||||
import OpenGraphLink from "../open-graph-link";
|
||||
|
||||
export function renderVideoUrl(match: URL) {
|
||||
if (!isVideoURL(match)) return null;
|
||||
@ -23,6 +24,6 @@ export function renderGenericUrl(match: URL) {
|
||||
);
|
||||
}
|
||||
|
||||
export function renderOpenGraphUrl(match: URL) {
|
||||
return <OpenGraphCard url={match} />;
|
||||
export function renderOpenGraphUrl(match: URL, isEndOfLine: boolean) {
|
||||
return isEndOfLine ? <OpenGraphCard url={match} /> : <OpenGraphLink url={match} />;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { Box, useColorMode } from "@chakra-ui/react";
|
||||
import { EmbedEventPointer } from "../embed-event";
|
||||
|
||||
const setZIndex: CSSProperties = { zIndex: 1, position: "relative" };
|
||||
|
||||
@ -110,3 +111,13 @@ export function renderSongDotLinkUrl(match: URL) {
|
||||
></Box>
|
||||
);
|
||||
}
|
||||
|
||||
// nostr:nevent1qqs95384ynfcgugz29u25ltl7qs6d5chve8ksw7ms3ega8eyem3n5agpz9mhxue69uhkummnw3e82efwvdhk6qgnwaehxw309aex2mrp09skymr99ehhyec6lyxqd
|
||||
export function renderStemstrUrl(match: URL) {
|
||||
if (match.hostname !== "stemstr.app") return null;
|
||||
|
||||
const [_, base, id] = match.pathname.split("/");
|
||||
if (base !== "thread" || id.length !== 64) return null;
|
||||
|
||||
return <EmbedEventPointer pointer={{ type: "nevent", data: { id, relays: ["wss://relay.stemstr.app"] } }} />;
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ import appSettings from "../../services/settings/app-settings";
|
||||
import { renderOpenGraphUrl } from "./common";
|
||||
|
||||
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js
|
||||
export const TWITTER_DOMAINS = ["twitter.com", "www.twitter.com", "mobile.twitter.com", "pbs.twimg.com"];
|
||||
export const TWITTER_DOMAINS = ["x.com", "twitter.com", "www.twitter.com", "mobile.twitter.com", "pbs.twimg.com"];
|
||||
|
||||
export function renderTwitterUrl(match: URL) {
|
||||
export function renderTwitterUrl(match: URL, isLineEnd: boolean) {
|
||||
if (!TWITTER_DOMAINS.includes(match.hostname)) return null;
|
||||
|
||||
const { twitterRedirect } = appSettings.value;
|
||||
if (twitterRedirect) return renderOpenGraphUrl(replaceDomain(match, twitterRedirect));
|
||||
else return renderOpenGraphUrl(match);
|
||||
if (twitterRedirect) return renderOpenGraphUrl(replaceDomain(match, twitterRedirect), isLineEnd);
|
||||
else return renderOpenGraphUrl(match, isLineEnd);
|
||||
}
|
||||
|
@ -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 <LikeIcon />;
|
||||
if (emoji === "-") return <DislikeIcon />;
|
||||
if (url) return <Image src={url} title={emoji} alt={emoji} w="1em" h="1em" display="inline" />;
|
||||
return <span>{emoji}</span>;
|
||||
}
|
||||
|
||||
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} />} title={emoji} {...props}>
|
||||
{count > 1 && count}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<ReactionGroupButton
|
||||
key={group.emoji}
|
||||
emoji={group.emoji}
|
||||
url={group.url}
|
||||
count={group.pubkeys.length}
|
||||
onClick={() => addReaction(group.emoji, group.url)}
|
||||
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
/>
|
||||
))}
|
||||
<Button onClick={detailsModal.onOpen}>Show all</Button>
|
||||
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
|
||||
</>
|
||||
);
|
||||
}
|
37
src/components/event-reactions/common-hooks.tsx
Normal file
37
src/components/event-reactions/common-hooks.tsx
Normal file
@ -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],
|
||||
);
|
||||
}
|
41
src/components/event-reactions/event-reactions.tsx
Normal file
41
src/components/event-reactions/event-reactions.tsx
Normal file
@ -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) => (
|
||||
<ReactionGroupButton
|
||||
key={group.emoji}
|
||||
emoji={group.emoji}
|
||||
url={group.url}
|
||||
count={group.pubkeys.length}
|
||||
onClick={() => addReaction(group.emoji, group.url)}
|
||||
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
/>
|
||||
))}
|
||||
<Button onClick={detailsModal.onOpen}>Show all</Button>
|
||||
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
|
||||
</>
|
||||
);
|
||||
}
|
18
src/components/event-reactions/reaction-group-button.tsx
Normal file
18
src/components/event-reactions/reaction-group-button.tsx
Normal file
@ -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<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} />} title={emoji} {...props}>
|
||||
{count > 1 && count}
|
||||
</Button>
|
||||
);
|
||||
}
|
9
src/components/event-reactions/reaction-icon.tsx
Normal file
9
src/components/event-reactions/reaction-icon.tsx
Normal file
@ -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 <LikeIcon />;
|
||||
if (emoji === "-") return <DislikeIcon />;
|
||||
if (url) return <Image src={url} title={emoji} alt={emoji} w="1em" h="1em" display="inline" />;
|
||||
return <span>{emoji}</span>;
|
||||
}
|
28
src/components/event-reactions/simple-like-button.tsx
Normal file
28
src/components/event-reactions/simple-like-button.tsx
Normal file
@ -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<ButtonProps, "children"> & { 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 (
|
||||
<ReactionGroupButton
|
||||
emoji="+"
|
||||
count={group?.pubkeys.length ?? 0}
|
||||
onClick={() => addReaction("+")}
|
||||
colorScheme={account && group?.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -3,7 +3,6 @@ import { createIcon, IconProps } from "@chakra-ui/icons";
|
||||
import SearchMd from "./icons/search-md";
|
||||
import Settings02 from "./icons/settings-02";
|
||||
import Mail01 from "./icons/mail-01";
|
||||
import BookmarkCheck from "./icons/bookmark-check";
|
||||
import StickerSquare from "./icons/sticker-square";
|
||||
import Code01 from "./icons/code-01";
|
||||
import DistributeSpacingVertical from "./icons/distribute-spacing-vertical";
|
||||
@ -60,6 +59,7 @@ import Bookmark from "./icons/bookmark";
|
||||
import BankNote01 from "./icons/bank-note-01";
|
||||
import Wallet02 from "./icons/wallet-02";
|
||||
import Download01 from "./icons/download-01";
|
||||
import Repeat01 from "./icons/repeat-01";
|
||||
|
||||
const defaultProps: IconProps = { boxSize: 4 };
|
||||
|
||||
@ -93,7 +93,7 @@ export const BroadcastEventIcon = Share07;
|
||||
export const ExternalLinkIcon = Share04;
|
||||
|
||||
export const SearchIcon = SearchMd;
|
||||
export const RepostIcon = Share07;
|
||||
export const RepostIcon = Repeat01;
|
||||
|
||||
export const ReplyIcon = MessageCircle01;
|
||||
|
||||
@ -227,4 +227,4 @@ export const GhostIcon = createIcon({
|
||||
|
||||
export const ECashIcon = BankNote01;
|
||||
export const WalletIcon = Wallet02;
|
||||
export const DownloadIcon = Download01
|
||||
export const DownloadIcon = Download01;
|
||||
|
@ -4,7 +4,7 @@ import { getDecodedToken, Token } from "@cashu/cashu-ts";
|
||||
|
||||
import { CopyIconButton } from "./copy-icon-button";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import { ECashIcon, WalletIcon } from "./icons";
|
||||
|
||||
function RedeemButton({ token }: { token: string }) {
|
||||
|
@ -9,7 +9,7 @@ import accountService, { Account } from "../../services/account";
|
||||
import { AddIcon, ChevronDownIcon, ChevronUpIcon } from "../icons";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import AccountInfoBadge from "../account-info-badge";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
|
||||
function AccountItem({ account, onClick }: { account: Account; onClick?: () => void }) {
|
||||
const pubkey = account.pubkey;
|
||||
|
@ -3,7 +3,7 @@ import { Avatar, Box, Button, Flex, FlexProps, Heading, LinkOverlay } from "@cha
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { css } from "@emotion/react";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import AccountSwitcher from "./account-switcher";
|
||||
import PublishLog from "../publish-log";
|
||||
import NavItems from "./nav-items";
|
||||
|
@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useInterval } from "react-use";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import accountService from "../../services/account";
|
||||
import UserAvatar from "../user-avatar";
|
||||
|
@ -12,6 +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";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
@ -65,6 +66,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Flex>
|
||||
{isGhost && <GhostToolbar />}
|
||||
{searchModal.isOpen && <SearchModal isOpen onClose={searchModal.onClose} />}
|
||||
{/* {!isMobile && <ChatWindows />} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Avatar, Flex, FlexProps, IconButton, useDisclosure } from "@chakra-ui/r
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { DirectMessagesIcon, NotesIcon, NotificationsIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
||||
import UserAvatar from "../user-avatar";
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import AccountSwitcher from "./account-switcher";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import NavItems from "./nav-items";
|
||||
|
||||
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
NotesIcon,
|
||||
LightningIcon,
|
||||
} from "../icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import accountService from "../../services/account";
|
||||
|
||||
export default function NavItems() {
|
||||
|
@ -34,7 +34,7 @@ import UserAvatarLink from "./user-avatar-link";
|
||||
import { UserLink } from "./user-link";
|
||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||
import styled from "@emotion/styled";
|
||||
import { getSharableNoteId } from "../helpers/nip19";
|
||||
import { getSharableEventAddress } from "../helpers/nip19";
|
||||
|
||||
type RefType = MutableRefObject<HTMLElement | null>;
|
||||
|
||||
@ -101,7 +101,7 @@ function getRefPath(ref: RefType) {
|
||||
}
|
||||
|
||||
function EventSlideHeader({ event, ...props }: { event: NostrEvent } & Omit<FlexProps, "children">) {
|
||||
const encoded = useMemo(() => getSharableNoteId(event.id), [event.id]);
|
||||
const encoded = useMemo(() => getSharableEventAddress(event), [event]);
|
||||
|
||||
return (
|
||||
<Flex gap="2" alignItems="center" p="2" {...props}>
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import useUserLists from "../../../hooks/use-user-lists";
|
||||
import {
|
||||
|
@ -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";
|
||||
|
||||
|
@ -25,7 +25,7 @@ import clientRelaysService from "../../../services/client-relays";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
|
||||
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
IconButton,
|
||||
Link,
|
||||
LinkBox,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
@ -34,18 +33,17 @@ import NoteContentWithWarning from "./note-content-with-warning";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import BookmarkButton from "./components/bookmark-button";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import NoteReactions from "./components/note-reactions";
|
||||
import ReplyForm from "../../views/note/components/reply-form";
|
||||
import { getReferences } from "../../helpers/nostr/events";
|
||||
import Timestamp from "../timestamp";
|
||||
import OpenInDrawerButton from "../open-in-drawer-button";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import { getCommunityName, getEventCommunityPointer } from "../../helpers/nostr/communities";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import { useBreakpointValue } from "../../providers/breakpoint-provider";
|
||||
import HoverLinkOverlay from "../hover-link-overlay";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import NoteCommunityMetadata from "./note-community-metadata";
|
||||
|
||||
export type NoteProps = Omit<CardProps, "children"> & {
|
||||
event: NostrEvent;
|
||||
@ -75,8 +73,6 @@ export const Note = React.memo(
|
||||
|
||||
// find mostr external link
|
||||
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
|
||||
const communityPointer = useMemo(() => getEventCommunityPointer(event), [event]);
|
||||
const community = useReplaceableEvent(communityPointer ?? undefined);
|
||||
|
||||
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
@ -107,15 +103,7 @@ export const Note = React.memo(
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
</Link>
|
||||
</Flex>
|
||||
{community && (
|
||||
<Text fontStyle="italic">
|
||||
Posted in{" "}
|
||||
<Link as={RouterLink} to={`/c/${getCommunityName(community)}/${community.pubkey}`} color="blue.500">
|
||||
{getCommunityName(community)}
|
||||
</Link>{" "}
|
||||
community
|
||||
</Text>
|
||||
)}
|
||||
<NoteCommunityMetadata event={event} />
|
||||
</CardHeader>
|
||||
<CardBody p="0">
|
||||
<NoteContentWithWarning event={event} />
|
||||
|
25
src/components/note/note-community-metadata.tsx
Normal file
25
src/components/note/note-community-metadata.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { useMemo } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Link, Text, TextProps } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { getEventCommunityPointer } from "../../helpers/nostr/communities";
|
||||
|
||||
export default function NoteCommunityMetadata({
|
||||
event,
|
||||
...props
|
||||
}: Omit<TextProps, "children"> & { event: NostrEvent }) {
|
||||
const communityPointer = useMemo(() => getEventCommunityPointer(event), [event]);
|
||||
|
||||
if (!communityPointer) return null;
|
||||
|
||||
return (
|
||||
<Text fontStyle="italic" {...props}>
|
||||
Posted in{" "}
|
||||
<Link as={RouterLink} to={`/c/${communityPointer.identifier}/${communityPointer.pubkey}`} color="blue.500">
|
||||
{communityPointer.identifier}
|
||||
</Link>{" "}
|
||||
community
|
||||
</Text>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
import { NoteContents } from "./note-contents";
|
||||
import { NoteContents } from "./text-note-contents";
|
||||
import { useExpand } from "../../providers/expanded";
|
||||
import SensitiveContentWarning from "../sensitive-content-warning";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
} from "../icons";
|
||||
import NoteReactionsModal from "./note-zaps-modal";
|
||||
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
|
@ -2,7 +2,7 @@ import { Button, ButtonProps, IconButton, useDisclosure } from "@chakra-ui/react
|
||||
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import { totalZaps } from "../../helpers/nostr/zaps";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useEventZaps from "../../hooks/use-event-zaps";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import eventZapsService from "../../services/event-zaps";
|
||||
@ -21,7 +21,7 @@ export type NoteZapButtonProps = Omit<ButtonProps, "children"> & {
|
||||
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);
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
renderGenericUrl,
|
||||
renderSongDotLinkUrl,
|
||||
embedCashuTokens,
|
||||
renderStemstrUrl,
|
||||
} from "../embed-types";
|
||||
import { LightboxProvider } from "../lightbox-provider";
|
||||
import { renderRedditUrl } from "../embed-types/reddit";
|
||||
@ -42,6 +43,7 @@ function buildContents(event: NostrEvent | DraftNostrEvent, simpleLinks = false)
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
simpleLinks ? renderGenericUrl : renderOpenGraphUrl,
|
12
src/components/open-graph-link.tsx
Normal file
12
src/components/open-graph-link.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Link, LinkProps } from "@chakra-ui/react";
|
||||
import useOpenGraphData from "../hooks/use-open-graph-data";
|
||||
|
||||
export default function OpenGraphLink({ url, ...props }: { url: URL } & Omit<LinkProps, "children">) {
|
||||
const { value: data } = useOpenGraphData(url);
|
||||
|
||||
return (
|
||||
<Link href={url.toString()} isExternal color="blue.500" {...props}>
|
||||
{data?.ogTitle?.trim() ?? data?.dcTitle?.trim() ?? decodeURI(url.toString())}
|
||||
</Link>
|
||||
);
|
||||
}
|
@ -11,7 +11,7 @@ import {
|
||||
|
||||
import { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { PEOPLE_LIST_KIND, getListName } from "../../helpers/nostr/lists";
|
||||
import { getEventCoordinate } from "../../helpers/nostr/events";
|
||||
import useFavoriteLists from "../../hooks/use-favorite-lists";
|
||||
|
@ -2,7 +2,7 @@ import { forwardRef } from "react";
|
||||
import { Select, SelectProps } from "@chakra-ui/react";
|
||||
|
||||
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-list";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { getCommunityName } from "../../helpers/nostr/communities";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
|
@ -26,7 +26,7 @@ import { ChevronDownIcon, ChevronUpIcon, UploadImageIcon } from "../icons";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import { NoteContents } from "../note/note-contents";
|
||||
import { NoteContents } from "../note/text-note-contents";
|
||||
import { PublishDetails } from "../publish-details";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import {
|
||||
@ -44,7 +44,7 @@ import { nostrBuildUploadImage as nostrBuildUpload } from "../../helpers/nostr-b
|
||||
import CommunitySelect from "./community-select";
|
||||
import ZapSplitCreator, { fillRemainingPercent } from "./zap-split-creator";
|
||||
import { EventSplit } from "../../helpers/nostr/zaps";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useCacheForm from "../../hooks/use-cache-form";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import NostrPublishAction from "../classes/nostr-publish-action";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import { RelayPaidTag } from "../views/relays/components/relay-card";
|
||||
import { EmbedEvent } from "./embed-event";
|
||||
|
||||
export type PostResultsProps = {
|
||||
pub: NostrPublishAction;
|
||||
@ -14,6 +15,7 @@ export const PublishDetails = ({ pub }: PostResultsProps & Omit<FlexProps, "chil
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<EmbedEvent event={pub.event} />
|
||||
<Progress value={(results.length / pub.relays.length) * 100} size="lg" hasStripe />
|
||||
{results.map((result) => (
|
||||
<Alert key={result.relay.url} status={result.status ? "success" : "warning"}>
|
||||
|
@ -63,7 +63,7 @@ function PublishAction({ pub }: { pub: NostrPublishAction }) {
|
||||
<PublishActionStatusTag ml="auto" pub={pub} />
|
||||
</Flex>
|
||||
{details.isOpen && (
|
||||
<Modal isOpen onClose={details.onClose}>
|
||||
<Modal isOpen onClose={details.onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader pt="4" px="4" pb="0">
|
||||
|
@ -18,9 +18,9 @@ import { useMemo } from "react";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { groupReactions } from "../helpers/nostr/reactions";
|
||||
import { ReactionIcon } from "./event-reactions";
|
||||
import UserAvatarLink from "./user-avatar-link";
|
||||
import { UserLink } from "./user-link";
|
||||
import ReactionIcon from "./event-reactions/reaction-icon";
|
||||
|
||||
export type ReactionDetailsModalProps = Omit<ModalProps, "children"> & {
|
||||
reactions: NostrEvent[];
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Divider, Flex, IconButton, Image, Text } from "@chakra-ui/react";
|
||||
|
||||
import { DislikeIcon, LikeIcon } from "./icons";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||
import { getEmojisFromPack, getPackCordsFromFavorites, getPackName } from "../helpers/nostr/emoji-packs";
|
||||
import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs";
|
||||
|
@ -7,7 +7,7 @@ import useSubject from "../../../hooks/use-subject";
|
||||
import { getMatchLink } from "../../../helpers/regexp";
|
||||
import { LightboxProvider } from "../../lightbox-provider";
|
||||
import { isImageURL } from "../../../helpers/url";
|
||||
import { EmbeddedImage, EmbeddedImageProps, GalleryImage } from "../../embed-types";
|
||||
import { EmbeddedImageProps, GalleryImage } from "../../embed-types";
|
||||
import { TrustProvider } from "../../../providers/trust";
|
||||
import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import { ChevronDownIcon, FollowIcon, MuteIcon, PlusCircleIcon, UnfollowIcon, UnmuteIcon } from "./icons";
|
||||
import useUserLists from "../hooks/use-user-lists";
|
||||
import {
|
||||
|
14
src/components/user-name.tsx
Normal file
14
src/components/user-name.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Text, TextProps } from "@chakra-ui/react";
|
||||
|
||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
|
||||
export default function UserName({ pubkey, ...props }: Omit<TextProps, "children"> & { pubkey: string }) {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<Text as="span" whiteSpace="nowrap" fontWeight="bold" {...props}>
|
||||
{getUserDisplayName(metadata, pubkey)}
|
||||
</Text>
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ import { getMatchLink } from "./regexp";
|
||||
export type EmbedableContent = (string | JSX.Element)[];
|
||||
export type EmbedType = {
|
||||
regexp: RegExp;
|
||||
render: (match: RegExpMatchArray) => JSX.Element | string | null;
|
||||
render: (match: RegExpMatchArray, isEndOfLine: boolean) => JSX.Element | string | null;
|
||||
name: string;
|
||||
getLocation?: (match: RegExpMatchArray) => { start: number; end: number };
|
||||
};
|
||||
@ -35,7 +35,8 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable
|
||||
|
||||
const before = str.slice(0, start - cursor);
|
||||
const after = str.slice(end - cursor, str.length);
|
||||
let render = embed.render(match);
|
||||
const isEndOfLine = /^\p{Z}*(\n|$)/iu.test(after);
|
||||
let render = embed.render(match, isEndOfLine);
|
||||
if (render === null) continue;
|
||||
|
||||
if (typeof render !== "string" && !render.props.key) {
|
||||
@ -68,18 +69,18 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable
|
||||
.flat();
|
||||
}
|
||||
|
||||
export type LinkEmbedHandler = (link: URL) => JSX.Element | string | null;
|
||||
export type LinkEmbedHandler = (link: URL, isEndOfLine: boolean) => JSX.Element | string | null;
|
||||
|
||||
export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[]) {
|
||||
return embedJSX(content, {
|
||||
name: "embedUrls",
|
||||
regexp: getMatchLink(),
|
||||
render: (match) => {
|
||||
render: (match, isEndOfLine) => {
|
||||
try {
|
||||
const url = new URL(match[0]);
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
const content = handler(url);
|
||||
const content = handler(url, isEndOfLine);
|
||||
if (content) return content;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
@ -67,30 +67,19 @@ export function normalizeToHex(hex: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function getSharableNoteId(eventId: string) {
|
||||
const relays = getEventRelays(eventId).value;
|
||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||
const onlyTwo = ranked.slice(0, 2);
|
||||
|
||||
if (onlyTwo.length > 0) {
|
||||
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
|
||||
} else return nip19.noteEncode(eventId);
|
||||
}
|
||||
|
||||
export function getSharableEventAddress(event: NostrEvent) {
|
||||
const relays = getEventRelays(getEventUID(event)).value;
|
||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||
const onlyTwo = ranked.slice(0, 2);
|
||||
const maxTwo = ranked.slice(0, 2);
|
||||
|
||||
if (isReplaceable(event.kind)) {
|
||||
const d = event.tags.find(isDTag)?.[1];
|
||||
if (!d) return null;
|
||||
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays: onlyTwo });
|
||||
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays: maxTwo });
|
||||
} else {
|
||||
if (onlyTwo.length > 0) {
|
||||
return nip19.neventEncode({ id: event.id, relays: onlyTwo });
|
||||
} else return nip19.noteEncode(event.id);
|
||||
if (maxTwo.length == 2) {
|
||||
return nip19.neventEncode({ id: event.id, relays: maxTwo });
|
||||
} else return nip19.neventEncode({ id: event.id, relays: maxTwo, author: event.pubkey });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useWordMuteFilter from "./use-mute-word-filter";
|
||||
import useUserMuteFilter from "./use-user-mute-filter";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { COMMUNITY_DEFINITION_KIND, SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../helpers/nostr/communities";
|
||||
import { NOTE_LIST_KIND, getParsedCordsFromList } from "../helpers/nostr/lists";
|
||||
import { RequestOptions } from "../services/replaceable-event-requester";
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
|
||||
export default function useJoinedCommunitiesList(pubkey?: string, opts?: RequestOptions) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import accountService from "../services/account";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useCurrentAccount() {
|
||||
export default function useCurrentAccount() {
|
||||
return useSubject(accountService.current);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import { USER_EMOJI_LIST_KIND } from "../helpers/nostr/emoji-packs";
|
||||
import { RequestOptions } from "../services/replaceable-event-requester";
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import { getCoordinatesFromList } from "../helpers/nostr/lists";
|
||||
import useReplaceableEvents from "./use-replaceable-events";
|
||||
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { useAsync } from "react-use";
|
||||
import extractMetaTags from "../lib/open-graph-scraper/extract";
|
||||
import { fetchWithCorsFallback } from "../helpers/cors";
|
||||
import { OgObjectInteral } from "../lib/open-graph-scraper/types";
|
||||
import type { OgObjectInteral } from "../lib/open-graph-scraper/types";
|
||||
import useAppSettings from "./use-app-settings";
|
||||
|
||||
const pageExtensions = [".html", ".php", "htm"];
|
||||
|
||||
const openGraphDataCache = new Map<string, OgObjectInteral>();
|
||||
|
||||
export default function useOpenGraphData(url: URL) {
|
||||
const { loadOpenGraphData } = useAppSettings();
|
||||
|
||||
return useAsync(async () => {
|
||||
if (!loadOpenGraphData) return null;
|
||||
|
||||
const { default: extractMetaTags } = await import("../lib/open-graph-scraper/extract");
|
||||
|
||||
if (openGraphDataCache.has(url.toString())) return openGraphDataCache.get(url.toString());
|
||||
|
||||
const ext = url.pathname.match(/\.[\w+d]+$/)?.[0];
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useUserMuteList from "./use-user-mute-list";
|
||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
@ -11,7 +11,7 @@ import { useSigningContext } from "../providers/signing-provider";
|
||||
import clientRelaysService from "../services/client-relays";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import useAsyncErrorHandler from "./use-async-error-handler";
|
||||
import { useCurrentAccount } from "./use-current-account";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useUserMuteList from "./use-user-mute-list";
|
||||
|
||||
export default function useUserMuteFunctions(pubkey: string) {
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
import { Event, Kind } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import signingService from "../services/signing";
|
||||
import createDefer, { Deferred } from "../classes/deferred";
|
||||
import useEventRelays from "../hooks/use-event-relays";
|
||||
|
@ -2,7 +2,7 @@ import { PropsWithChildren, createContext, useContext } from "react";
|
||||
import { lib } from "emojilib";
|
||||
|
||||
import useReplaceableEvents from "../hooks/use-replaceable-events";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
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";
|
||||
|
@ -23,7 +23,7 @@ import { useInterval } from "react-use";
|
||||
|
||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import {
|
||||
createEmptyMuteList,
|
||||
getPubkeysExpiration,
|
||||
|
@ -2,7 +2,7 @@ import { PropsWithChildren, createContext, useCallback, useContext, useEffect, u
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import TimelineLoader from "../classes/timeline-loader";
|
||||
import timelineCacheService from "../services/timeline-cache";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
@ -2,7 +2,7 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo } fr
|
||||
import { Kind } from "nostr-tools";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PropsWithChildren, useContext } from "react";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import useUserContactList from "../hooks/use-user-contact-list";
|
||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||
|
||||
|
@ -10,9 +10,10 @@ import accountService from "./account";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
|
||||
export function getMessageRecipient(event: NostrEvent): string | undefined {
|
||||
return event.tags.filter(isPTag)[0][1];
|
||||
return event.tags.find(isPTag)?.[1];
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
class DirectMessagesService {
|
||||
incomingSub: NostrMultiSubscription;
|
||||
outgoingSub: NostrMultiSubscription;
|
||||
@ -129,6 +130,7 @@ class DirectMessagesService {
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
const directMessagesService = new DirectMessagesService();
|
||||
|
||||
export default directMessagesService;
|
||||
|
@ -27,14 +27,9 @@ export type AppSettingsV1 = Omit<AppSettingsV0, "version"> & {
|
||||
mutedWords?: string;
|
||||
maxPageWidth: "none" | "md" | "lg" | "xl";
|
||||
};
|
||||
export type AppSettingsV2 = Omit<AppSettingsV1, "version"> & {
|
||||
version: 2;
|
||||
theme: string;
|
||||
};
|
||||
export type AppSettingsV3 = Omit<AppSettingsV2, "version"> & {
|
||||
version: 3;
|
||||
quickReactions: string[];
|
||||
};
|
||||
export type AppSettingsV2 = Omit<AppSettingsV1, "version"> & { version: 2; theme: string };
|
||||
export type AppSettingsV3 = Omit<AppSettingsV2, "version"> & { version: 3; quickReactions: string[] };
|
||||
export type AppSettingsV4 = Omit<AppSettingsV3, "version"> & { version: 4; loadOpenGraphData: boolean };
|
||||
|
||||
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
|
||||
return settings.version === undefined || settings.version === 0;
|
||||
@ -48,17 +43,21 @@ export function isV2(settings: { version: number }): settings is AppSettingsV2 {
|
||||
export function isV3(settings: { version: number }): settings is AppSettingsV3 {
|
||||
return settings.version === 3;
|
||||
}
|
||||
export function isV4(settings: { version: number }): settings is AppSettingsV4 {
|
||||
return settings.version === 4;
|
||||
}
|
||||
|
||||
export type AppSettings = AppSettingsV3;
|
||||
export type AppSettings = AppSettingsV4;
|
||||
|
||||
export const defaultSettings: AppSettings = {
|
||||
version: 3,
|
||||
version: 4,
|
||||
theme: "default",
|
||||
colorMode: "system",
|
||||
maxPageWidth: "none",
|
||||
blurImages: true,
|
||||
autoShowMedia: true,
|
||||
proxyUserMedia: false,
|
||||
loadOpenGraphData: true,
|
||||
showReactions: true,
|
||||
showSignatureVerification: false,
|
||||
|
||||
@ -77,10 +76,11 @@ export const defaultSettings: AppSettings = {
|
||||
};
|
||||
|
||||
export function upgradeSettings(settings: { version: number }): AppSettings | null {
|
||||
if (isV0(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||
if (isV1(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||
if (isV2(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||
if (isV3(settings)) return settings;
|
||||
if (isV0(settings)) return { ...defaultSettings, ...settings, version: 4 };
|
||||
if (isV1(settings)) return { ...defaultSettings, ...settings, version: 4 };
|
||||
if (isV2(settings)) return { ...defaultSettings, ...settings, version: 4 };
|
||||
if (isV3(settings)) return { ...defaultSettings, ...settings, version: 4 };
|
||||
if (isV4(settings)) return settings;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
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 { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import UserAvatar from "../../../components/user-avatar";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import { TrashIcon } from "../../../components/icons";
|
||||
|
@ -4,7 +4,7 @@ import { Button, ButtonProps, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER, getCommunityName } from "../../../helpers/nostr/communities";
|
||||
import { NOTE_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
|
||||
import { getEventCoordinate } from "../../../helpers/nostr/events";
|
||||
|
@ -26,7 +26,7 @@ import dayjs from "dayjs";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-list";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import CommunityCard from "./components/community-card";
|
||||
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
getCommunityImage,
|
||||
getCommunityName,
|
||||
COMMUNITY_APPROVAL_KIND,
|
||||
getCommunityMods,
|
||||
buildApprovalMap,
|
||||
} from "../../helpers/nostr/communities";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
@ -28,6 +30,8 @@ import { WritingIcon } from "../../components/icons";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import CommunityEditModal from "./components/community-edit-modal";
|
||||
import TimelineLoader from "../../classes/timeline-loader";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
|
||||
function getCommunityPath(community: NostrEvent) {
|
||||
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
|
||||
@ -36,6 +40,7 @@ function getCommunityPath(community: NostrEvent) {
|
||||
export type RouterContext = { community: NostrEvent; timeline: TimelineLoader };
|
||||
|
||||
export default function CommunityHomePage({ community }: { community: NostrEvent }) {
|
||||
const muteFilter = useClientSideMuteFilter();
|
||||
const image = getCommunityImage(community);
|
||||
const location = useLocation();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
@ -51,6 +56,12 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
||||
"#a": [communityCoordinate],
|
||||
});
|
||||
|
||||
// get pending notes
|
||||
const events = useSubject(timeline.timeline);
|
||||
const mods = getCommunityMods(community);
|
||||
const approvals = buildApprovalMap(events, mods);
|
||||
const pending = events.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && !approvals.has(e.id) && !muteFilter(e));
|
||||
|
||||
let active = "newest";
|
||||
if (location.pathname.endsWith("/newest")) active = "newest";
|
||||
if (location.pathname.endsWith("/pending")) active = "pending";
|
||||
@ -127,7 +138,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
||||
colorScheme={active == "pending" ? "primary" : "gray"}
|
||||
replace
|
||||
>
|
||||
Pending
|
||||
Pending ({pending.length})
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { CodeIcon, ExternalLinkIcon, RepostIcon } from "../../../components/icon
|
||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import PencilLine from "../../../components/icons/pencil-line";
|
||||
|
||||
export default function CommunityMenu({
|
||||
|
@ -2,7 +2,7 @@ import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { useMuteModalContext } from "../../../providers/mute-modal-provider";
|
||||
import useUserMuteFunctions from "../../../hooks/use-user-mute-functions";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Card, CardProps, IconButton, Text, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import { draftEventReaction, groupReactions } from "../../../helpers/nostr/reactions";
|
||||
|
@ -17,11 +17,12 @@ import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeli
|
||||
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
||||
import { CheckIcon } from "../../../components/icons";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import CommunityPost from "../components/community-post";
|
||||
import { RouterContext } from "../community-home";
|
||||
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
|
||||
|
||||
type PendingProps = {
|
||||
event: NostrEvent;
|
||||
@ -84,13 +85,14 @@ function ModPendingPost({ event, community, approvals }: PendingProps) {
|
||||
|
||||
export default function CommunityPendingView() {
|
||||
const account = useCurrentAccount();
|
||||
const muteFilter = useUserMuteFilter();
|
||||
const { community, timeline } = useOutletContext<RouterContext>();
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
const mods = getCommunityMods(community);
|
||||
const approvals = buildApprovalMap(events, mods);
|
||||
const pending = events.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && !approvals.has(e.id));
|
||||
const pending = events.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && !approvals.has(e.id) && !muteFilter(e));
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
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 { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { ChevronLeftIcon } from "../../components/icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
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";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Button, Divider, Flex, Heading, Link, SimpleGrid, useDisclosure } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
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";
|
||||
|
@ -3,7 +3,7 @@ import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
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 { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Button, Center, Divider, Flex, Heading, Link, SimpleGrid, Spacer } from "@chakra-ui/react";
|
||||
import { Navigate, Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { ExternalLinkIcon } from "../../components/icons";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
useEditableControls,
|
||||
} from "@chakra-ui/react";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { isReply } from "../../helpers/nostr/events";
|
||||
@ -27,6 +27,8 @@ import useRelaysChanged from "../../hooks/use-relays-changed";
|
||||
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
||||
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
|
||||
function EditableControls() {
|
||||
const { isEditing, getSubmitButtonProps, getCancelButtonProps, getEditButtonProps } = useEditableControls();
|
||||
@ -43,6 +45,7 @@ function EditableControls() {
|
||||
|
||||
function HashTagPage() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { hashtag } = useParams() as { hashtag: string };
|
||||
const [editableHashtag, setEditableHashtag] = useState(hashtag);
|
||||
useEffect(() => setEditableHashtag(hashtag), [hashtag]);
|
||||
@ -52,6 +55,7 @@ function HashTagPage() {
|
||||
const readRelays = useRelaySelectionRelays();
|
||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||
|
||||
const { listId, filter } = usePeopleListContext();
|
||||
const timelinePageEventFilter = useTimelinePageEventFilter();
|
||||
const muteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
@ -63,16 +67,16 @@ function HashTagPage() {
|
||||
[showReplies, muteFilter, timelinePageEventFilter],
|
||||
);
|
||||
const timeline = useTimelineLoader(
|
||||
`${hashtag}-hashtag`,
|
||||
`${listId ?? "global"}-${hashtag}-hashtag`,
|
||||
readRelays,
|
||||
{ kinds: [1], "#t": [hashtag] },
|
||||
{ kinds: [1], "#t": [hashtag], ...filter },
|
||||
{ eventFilter },
|
||||
);
|
||||
|
||||
useRelaysChanged(readRelays, () => timeline.reset());
|
||||
|
||||
const header = (
|
||||
<Flex gap="4" alignItems="center" wrap="wrap">
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<Editable
|
||||
value={editableHashtag}
|
||||
onChange={(v) => setEditableHashtag(v)}
|
||||
@ -82,7 +86,7 @@ function HashTagPage() {
|
||||
gap="2"
|
||||
alignItems="center"
|
||||
selectAllOnFocus
|
||||
onSubmit={(v) => navigate("/t/" + String(v).toLowerCase())}
|
||||
onSubmit={(v) => navigate("/t/" + String(v).toLowerCase() + location.search)}
|
||||
flexShrink={0}
|
||||
>
|
||||
<div>
|
||||
@ -91,6 +95,7 @@ function HashTagPage() {
|
||||
<Input as={EditableInput} maxW="md" />
|
||||
<EditableControls />
|
||||
</Editable>
|
||||
<PeopleListSelection />
|
||||
<RelaySelectionButton />
|
||||
<FormControl display="flex" alignItems="center" w="auto">
|
||||
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} mr="2" />
|
||||
@ -109,7 +114,9 @@ function HashTagPage() {
|
||||
export default function HashTagView() {
|
||||
return (
|
||||
<RelaySelectionProvider>
|
||||
<HashTagPage />
|
||||
<PeopleListProvider initList="global">
|
||||
<HashTagPage />
|
||||
</PeopleListProvider>
|
||||
</RelaySelectionProvider>
|
||||
);
|
||||
}
|
||||
|
@ -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<LinkProps, "children">) {
|
||||
const article = useReplaceableEvent(pointer);
|
||||
@ -64,62 +70,33 @@ export function ListCardContent({ list, ...props }: Omit<CardProps, "children">
|
||||
const references = getReferencesFromList(list);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Updated: <Timestamp timestamp={list.created_at} />
|
||||
</Text>
|
||||
<SimpleGrid spacing="2" columns={4}>
|
||||
{people.length > 0 && (
|
||||
<>
|
||||
<Text>People ({people.length}):</Text>
|
||||
<AvatarGroup overflow="hidden" mb="2" max={16} size="sm">
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserAvatarLink key={pubkey} pubkey={pubkey} relay={relay} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</>
|
||||
<Text>
|
||||
<User01 boxSize={5} /> {people.length}
|
||||
</Text>
|
||||
)}
|
||||
{notes.length > 0 && (
|
||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
||||
<Text>Notes ({notes.length}):</Text>
|
||||
{notes.slice(0, 4).map(({ id, relay }) => (
|
||||
<NoteLink key={id} noteId={id} />
|
||||
))}
|
||||
</Flex>
|
||||
<Text>
|
||||
<NotesIcon boxSize={5} /> {notes.length}
|
||||
</Text>
|
||||
)}
|
||||
{references.length > 0 && (
|
||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
||||
<Text>References ({references.length})</Text>
|
||||
{references.slice(0, 3).map(({ url, petname }) => (
|
||||
<Link maxW="200" href={url} isExternal whiteSpace="pre" color="blue.500" isTruncated>
|
||||
{petname || url}
|
||||
</Link>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
{communities.length > 0 && (
|
||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
||||
<Text>Communities ({communities.length}):</Text>
|
||||
{communities.map((pointer) => (
|
||||
<Link
|
||||
key={JSON.stringify(pointer)}
|
||||
as={RouterLink}
|
||||
to={`/c/${pointer.identifier}/${nip19.npubEncode(pointer.pubkey)}`}
|
||||
color="blue.500"
|
||||
>
|
||||
{pointer.identifier}
|
||||
</Link>
|
||||
))}
|
||||
</Flex>
|
||||
<Text>
|
||||
<Link01 boxSize={5} /> {references.length}
|
||||
</Text>
|
||||
)}
|
||||
{articles.length > 0 && (
|
||||
<Flex overflow="hidden" direction="column" wrap="wrap">
|
||||
<Text>Articles ({articles.length}):</Text>
|
||||
{articles.slice(0, 4).map((pointer) => (
|
||||
<ArticleLinkLoader key={JSON.stringify(pointer)} pointer={pointer} isTruncated />
|
||||
))}
|
||||
</Flex>
|
||||
<Text>
|
||||
<File02 /> {articles.length}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
{communities.length > 0 && (
|
||||
<Text>
|
||||
<CommunityIcon boxSize={5} /> {communities.length}
|
||||
</Text>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
|
||||
@ -135,12 +112,12 @@ function ListCardRender({
|
||||
useRegisterIntersectionEntity(ref, getEventUID(list));
|
||||
|
||||
return (
|
||||
<Card ref={ref} variant="outline" {...props}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0">
|
||||
<Card as={LinkBox} ref={ref} variant="outline" {...props}>
|
||||
<CardHeader display="flex" gap="2" p="4" alignItems="center">
|
||||
<Heading size="md" isTruncated>
|
||||
<Link as={RouterLink} to={`/lists/${link}`}>
|
||||
<HoverLinkOverlay as={RouterLink} to={`/lists/${link}`}>
|
||||
{getListName(list)}
|
||||
</Link>
|
||||
</HoverLinkOverlay>
|
||||
</Heading>
|
||||
{!hideCreator && (
|
||||
<>
|
||||
@ -149,14 +126,19 @@ function ListCardRender({
|
||||
<UserLink pubkey={list.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
</>
|
||||
)}
|
||||
<ButtonGroup size="xs" variant="ghost" ml="auto">
|
||||
</CardHeader>
|
||||
<CardBody py="0" px="4">
|
||||
<ListCardContent list={list} />
|
||||
</CardBody>
|
||||
<CardFooter p="2">
|
||||
<NoteZapButton event={list} size="sm" variant="ghost" />
|
||||
{/* TODO: reactions are tagging every user in list */}
|
||||
<SimpleLikeButton event={list} variant="ghost" size="sm" />
|
||||
<ButtonGroup size="sm" variant="ghost" ml="auto">
|
||||
<ListFavoriteButton list={list} />
|
||||
<ListMenu list={list} aria-label="list menu" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<ListCardContent list={list} />
|
||||
</CardBody>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { Image, MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { NostrEvent, isPTag } from "../../../types/nostr-event";
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
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 { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
@ -21,6 +21,8 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
|
||||
|
||||
const naddr = getSharableEventAddress(list);
|
||||
|
||||
const hasPeople = list.tags.some(isPTag);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomMenuIconButton {...props}>
|
||||
@ -39,6 +41,14 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
|
||||
Delete List
|
||||
</MenuItem>
|
||||
)}
|
||||
{hasPeople && (
|
||||
<MenuItem
|
||||
icon={<Image w="4" h="4" src="https://www.makeprisms.com/favicon.ico" />}
|
||||
onClick={() => window.open(`https://www.makeprisms.com/create/${naddr}`, "_blank")}
|
||||
>
|
||||
Create $prism
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||
View Raw
|
||||
</MenuItem>
|
||||
|
@ -12,7 +12,7 @@ import { listRemovePerson } from "../../../helpers/nostr/lists";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../../services/client-relays";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { UserFollowButton } from "../../../components/user-follow-button";
|
||||
|
||||
export type UserCardProps = { pubkey: string; relay?: string; list: NostrEvent } & Omit<CardProps, "children">;
|
||||
|
@ -2,7 +2,7 @@ import { Button, Divider, Flex, Heading, Image, Link, SimpleGrid, Spacer, useDis
|
||||
import { useNavigate, Link as RouterLink, Navigate } from "react-router-dom";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { ExternalLinkIcon, PlusCircleIcon } from "../../components/icons";
|
||||
import ListCard from "./components/list-card";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
|
@ -4,7 +4,7 @@ import { Kind, nip19 } from "nostr-tools";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { Button, Flex, Heading, SimpleGrid, Spacer } from "@chakra-ui/react";
|
||||
import { ChevronLeftIcon } from "../../components/icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
import { parseCoordinate } from "../../helpers/nostr/events";
|
||||
import {
|
||||
@ -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 ? <EmbedEvent event={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 (
|
||||
<VerticalPageLayout overflow="hidden" h="full">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ChevronLeftIcon />}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Heading size="md" isTruncated>
|
||||
{getListName(list)}
|
||||
</Heading>
|
||||
<ListFavoriteButton list={list} size="sm" />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<ListFeedButton list={list} />
|
||||
{isAuthor && !isSpecialListKind(list.kind) && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(list).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
<TrustProvider trust>
|
||||
<VerticalPageLayout overflow="hidden" h="full">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ChevronLeftIcon />}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Heading size="md" isTruncated>
|
||||
{getListName(list)}
|
||||
</Heading>
|
||||
<ListFavoriteButton list={list} size="sm" />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<ListFeedButton list={list} />
|
||||
{isAuthor && !isSpecialListKind(list.kind) && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(list).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<ListMenu aria-label="More options" list={list} />
|
||||
</Flex>
|
||||
|
||||
{people.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">People</Heading>
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserCard pubkey={pubkey} relay={relay} list={list} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
<ListMenu aria-label="More options" list={list} />
|
||||
</Flex>
|
||||
|
||||
{people.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">People</Heading>
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserCard pubkey={pubkey} relay={relay} list={list} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notes.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Notes</Heading>
|
||||
<TrustProvider trust>
|
||||
{notes.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Notes</Heading>
|
||||
<Flex gap="2" direction="column">
|
||||
{notes.map(({ id, relay }) => (
|
||||
<NoteCard id={id} relay={relay} />
|
||||
<BookmarkedEvent id={id} relay={relay} />
|
||||
))}
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{references.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">References</Heading>
|
||||
<TrustProvider trust>
|
||||
{references.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">References</Heading>
|
||||
<Flex gap="2" direction="column">
|
||||
{references.map(({ url, petname }) => (
|
||||
<>
|
||||
@ -125,32 +129,32 @@ export default function ListDetailsView() {
|
||||
</>
|
||||
))}
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{communities.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Communities</Heading>
|
||||
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
||||
{communities.map((pointer) => (
|
||||
<EmbedEventPointer key={nip19.naddrEncode(pointer)} pointer={{ type: "naddr", data: pointer }} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
{communities.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Communities</Heading>
|
||||
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
||||
{communities.map((pointer) => (
|
||||
<EmbedEventPointer key={nip19.naddrEncode(pointer)} pointer={{ type: "naddr", data: pointer }} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{articles.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Articles</Heading>
|
||||
<Flex gap="2" direction="column">
|
||||
{articles.map((pointer) => {
|
||||
const decode: DecodeResult = { type: "naddr", data: pointer };
|
||||
return <EmbedEventPointer key={encodePointer(decode)} pointer={decode} />;
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
{articles.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Articles</Heading>
|
||||
<Flex gap="2" direction="column">
|
||||
{articles.map((pointer) => {
|
||||
const decode: DecodeResult = { type: "naddr", data: pointer };
|
||||
return <EmbedEventPointer key={encodePointer(decode)} pointer={decode} />;
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
@ -13,16 +13,15 @@ import { useSigningContext } from "../../providers/signing-provider";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { Message } from "./message";
|
||||
import Message from "./message";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
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 NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import { LightboxProvider } from "../../components/lightbox-provider";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
|
||||
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
const toast = useToast();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Button } from "@chakra-ui/react";
|
||||
import { Alert, AlertDescription, AlertIcon, Button } from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { UnlockIcon } from "../../components/icons";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
|
||||
@ -15,17 +16,30 @@ export default function DecryptPlaceholder({
|
||||
const { requestDecrypt } = useSigningContext();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [decrypted, setDecrypted] = useState<string>();
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const decrypt = async () => {
|
||||
setLoading(true);
|
||||
const decrypted = await requestDecrypt(data, pubkey);
|
||||
if (decrypted) setDecrypted(decrypted);
|
||||
try {
|
||||
const decrypted = await requestDecrypt(data, pubkey);
|
||||
if (decrypted) setDecrypted(decrypted);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (decrypted) {
|
||||
return children(decrypted);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button onClick={decrypt} isLoading={loading} leftIcon={<UnlockIcon />} width="full">
|
||||
Decrypt
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
import { Box, CardProps, Flex } from "@chakra-ui/react";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { getMessageRecipient } from "../../services/direct-messages";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import DecryptPlaceholder from "./decrypt-placeholder";
|
||||
@ -31,7 +31,7 @@ export function MessageContent({ event, text }: { event: NostrEvent; text: strin
|
||||
return <Box whiteSpace="pre-wrap">{content}</Box>;
|
||||
}
|
||||
|
||||
export function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
||||
export default function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const isOwnMessage = account.pubkey === event.pubkey;
|
||||
|
||||
|
@ -7,7 +7,7 @@ import dayjs from "dayjs";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { UserAvatarStack } from "../../../components/compact-user-stack";
|
||||
import { ThreadItem, getThreadMembers } from "../../../helpers/thread";
|
||||
import { NoteContents } from "../../../components/note/note-contents";
|
||||
import { NoteContents } from "../../../components/note/text-note-contents";
|
||||
import {
|
||||
addReplyTags,
|
||||
createEmojiTags,
|
||||
@ -15,7 +15,7 @@ import {
|
||||
finalizeNote,
|
||||
getContentMentions,
|
||||
} from "../../../helpers/nostr/post";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
|
@ -1,22 +1,53 @@
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertIcon, Button, ButtonGroup, Flex, useDisclosure } from "@chakra-ui/react";
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { ChevronDownIcon, ChevronUpIcon, ReplyIcon } from "../../../components/icons";
|
||||
import { Note } from "../../../components/note";
|
||||
import { ReplyIcon } from "../../../components/icons";
|
||||
import { countReplies, ThreadItem } from "../../../helpers/thread";
|
||||
import { TrustProvider } from "../../../providers/trust";
|
||||
import ReplyForm from "./reply-form";
|
||||
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import Timestamp from "../../../components/timestamp";
|
||||
import { NoteContents } from "../../../components/note/text-note-contents";
|
||||
import Expand01 from "../../../components/icons/expand-01";
|
||||
import Minus from "../../../components/icons/minus";
|
||||
import NoteZapButton from "../../../components/note/note-zap-button";
|
||||
import { QuoteRepostButton } from "../../../components/note/components/quote-repost-button";
|
||||
import { RepostButton } from "../../../components/note/components/repost-button";
|
||||
import NoteMenu from "../../../components/note/note-menu";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
||||
import NoteReactions from "../../../components/note/components/note-reactions";
|
||||
import BookmarkButton from "../../../components/note/components/bookmark-button";
|
||||
import NoteCommunityMetadata from "../../../components/note/note-community-metadata";
|
||||
|
||||
const LEVEL_COLORS = ["green", "blue", "red", "purple", "yellow", "cyan", "pink"];
|
||||
|
||||
export type ThreadItemProps = {
|
||||
post: ThreadItem;
|
||||
initShowReplies?: boolean;
|
||||
focusId?: string;
|
||||
level?: number;
|
||||
};
|
||||
|
||||
export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps) => {
|
||||
const [showReplies, setShowReplies] = useState(initShowReplies ?? post.replies.length === 1);
|
||||
const toggle = () => setShowReplies((v) => !v);
|
||||
export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) => {
|
||||
const { showReactions } = useSubject(appSettings);
|
||||
const [expanded, setExpanded] = useState(initShowReplies ?? (level < 2 || post.replies.length <= 1));
|
||||
const toggle = () => setExpanded((v) => !v);
|
||||
const showReplyForm = useDisclosure();
|
||||
|
||||
const muteFilter = useClientSideMuteFilter();
|
||||
@ -38,44 +69,92 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
|
||||
|
||||
if (isMuted && replies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
{isMuted && !alwaysShow ? (
|
||||
muteAlert
|
||||
) : (
|
||||
<TrustProvider trust={focusId === post.event.id ? true : undefined}>
|
||||
<Note
|
||||
event={post.event}
|
||||
borderColor={focusId === post.event.id ? "blue.500" : undefined}
|
||||
clickable={focusId !== post.event.id}
|
||||
hideDrawerButton
|
||||
/>
|
||||
</TrustProvider>
|
||||
)}
|
||||
{showReplyForm.isOpen && (
|
||||
<ReplyForm item={post} onCancel={showReplyForm.onClose} onSubmitted={showReplyForm.onClose} />
|
||||
)}
|
||||
<ButtonGroup variant="link" size="sm" alignSelf="flex-start">
|
||||
{!showReplyForm.isOpen && (
|
||||
<Button onClick={showReplyForm.onOpen} leftIcon={<ReplyIcon />}>
|
||||
Write reply
|
||||
</Button>
|
||||
)}
|
||||
const colorMode = useColorMode().colorMode;
|
||||
const color = LEVEL_COLORS[level % LEVEL_COLORS.length];
|
||||
const colorValue = colorMode === "light" ? 200 : 800;
|
||||
const focusColor = colorMode === "light" ? "blue.300" : "blue.700";
|
||||
|
||||
{replies.length > 0 && (
|
||||
<Button onClick={toggle}>
|
||||
{numberOfReplies} {numberOfReplies > 1 ? "Replies" : "Reply"}
|
||||
{showReplies ? <ChevronDownIcon /> : <ChevronUpIcon />}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
{post.replies.length > 0 && showReplies && (
|
||||
<Flex direction="column" gap="2" pl={[2, 2, 4]} borderLeftColor="gray.500" borderLeftWidth="1px">
|
||||
{post.replies.map((child) => (
|
||||
<ThreadPost key={child.event.id} post={child} focusId={focusId} />
|
||||
))}
|
||||
</Flex>
|
||||
const header = (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={post.event.pubkey} size="sm" />
|
||||
<UserLink pubkey={post.event.pubkey} fontWeight="bold" isTruncated />
|
||||
<Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${nip19.noteEncode(post.event.id)}`}>
|
||||
<Timestamp timestamp={post.event.created_at} />
|
||||
</Link>
|
||||
{replies.length > 0 ? (
|
||||
<Button variant="ghost" onClick={toggle} rightIcon={expanded ? <Minus /> : <Expand01 />}>
|
||||
({numberOfReplies})
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={toggle}
|
||||
icon={expanded ? <Minus /> : <Expand01 />}
|
||||
aria-label={expanded ? "Collapse" : "Expand"}
|
||||
title={expanded ? "Collapse" : "Expand"}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
return isMuted && !alwaysShow ? (
|
||||
muteAlert
|
||||
) : (
|
||||
<>
|
||||
<NoteCommunityMetadata event={post.event} pl="2" />
|
||||
<TrustProvider trust={focusId === post.event.id ? true : undefined} event={post.event}>
|
||||
<NoteContents event={post.event} pl="2" />
|
||||
</TrustProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
|
||||
const reactionButtons = showReactions && (
|
||||
<NoteReactions event={post.event} flexWrap="wrap" variant="ghost" size="sm" />
|
||||
);
|
||||
const footer = (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<ButtonGroup variant="ghost" size="sm">
|
||||
<IconButton aria-label="Reply" title="Reply" onClick={showReplyForm.onToggle} icon={<ReplyIcon />} />
|
||||
|
||||
<RepostButton event={post.event} />
|
||||
<QuoteRepostButton event={post.event} />
|
||||
<NoteZapButton event={post.event} />
|
||||
</ButtonGroup>
|
||||
{!showReactionsOnNewLine && reactionButtons}
|
||||
<BookmarkButton event={post.event} variant="ghost" aria-label="Bookmark" size="sm" ml="auto" />
|
||||
<NoteMenu event={post.event} variant="ghost" size="sm" aria-label="More Options" />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="2"
|
||||
p="2"
|
||||
borderRadius="md"
|
||||
borderWidth=".1rem .1rem .1rem .35rem"
|
||||
borderColor={focusId === post.event.id ? focusColor : undefined}
|
||||
borderLeftColor={color + "." + colorValue}
|
||||
>
|
||||
{header}
|
||||
{expanded && renderContent()}
|
||||
{expanded && showReactionsOnNewLine && reactionButtons}
|
||||
{expanded && footer}
|
||||
</Flex>
|
||||
{showReplyForm.isOpen && (
|
||||
<ReplyForm item={post} onCancel={showReplyForm.onClose} onSubmitted={showReplyForm.onClose} />
|
||||
)}
|
||||
{post.replies.length > 0 && expanded && (
|
||||
<Flex direction="column" gap="2" pl={{ base: 2, md: 4 }}>
|
||||
{post.replies.map((child) => (
|
||||
<ThreadPost key={child.event.id} post={child} focusId={focusId} level={level + 1} />
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user