mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-08-02 14:42:12 +02:00
Cleanup thread view
This commit is contained in:
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
|
@@ -1,6 +1,6 @@
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
|
||||||
import { NoteContents } from "./note-contents";
|
import { NoteContents } from "./text-note-contents";
|
||||||
import { useExpand } from "../../providers/expanded";
|
import { useExpand } from "../../providers/expanded";
|
||||||
import SensitiveContentWarning from "../sensitive-content-warning";
|
import SensitiveContentWarning from "../sensitive-content-warning";
|
||||||
import useAppSettings from "../../hooks/use-app-settings";
|
import useAppSettings from "../../hooks/use-app-settings";
|
||||||
|
@@ -26,7 +26,7 @@ import { ChevronDownIcon, ChevronUpIcon, UploadImageIcon } from "../icons";
|
|||||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useSigningContext } from "../../providers/signing-provider";
|
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 { PublishDetails } from "../publish-details";
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
import {
|
import {
|
||||||
|
@@ -7,7 +7,7 @@ import dayjs from "dayjs";
|
|||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { UserAvatarStack } from "../../../components/compact-user-stack";
|
import { UserAvatarStack } from "../../../components/compact-user-stack";
|
||||||
import { ThreadItem, getThreadMembers } from "../../../helpers/thread";
|
import { ThreadItem, getThreadMembers } from "../../../helpers/thread";
|
||||||
import { NoteContents } from "../../../components/note/note-contents";
|
import { NoteContents } from "../../../components/note/text-note-contents";
|
||||||
import {
|
import {
|
||||||
addReplyTags,
|
addReplyTags,
|
||||||
createEmojiTags,
|
createEmojiTags,
|
||||||
|
@@ -1,22 +1,53 @@
|
|||||||
import { useState } from "react";
|
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 { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { ChevronDownIcon, ChevronUpIcon, ReplyIcon } from "../../../components/icons";
|
import { ReplyIcon } from "../../../components/icons";
|
||||||
import { Note } from "../../../components/note";
|
|
||||||
import { countReplies, ThreadItem } from "../../../helpers/thread";
|
import { countReplies, ThreadItem } from "../../../helpers/thread";
|
||||||
import { TrustProvider } from "../../../providers/trust";
|
import { TrustProvider } from "../../../providers/trust";
|
||||||
import ReplyForm from "./reply-form";
|
import ReplyForm from "./reply-form";
|
||||||
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
|
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 { nip19 } from "nostr-tools";
|
||||||
|
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 { useCookie } from "react-use";
|
||||||
|
import BookmarkButton from "../../../components/note/components/bookmark-button";
|
||||||
|
|
||||||
|
const LEVEL_COLORS = ["green", "blue", "red", "purple", "yellow", "cyan", "pink"];
|
||||||
|
|
||||||
export type ThreadItemProps = {
|
export type ThreadItemProps = {
|
||||||
post: ThreadItem;
|
post: ThreadItem;
|
||||||
initShowReplies?: boolean;
|
initShowReplies?: boolean;
|
||||||
focusId?: string;
|
focusId?: string;
|
||||||
|
level?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps) => {
|
export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: ThreadItemProps) => {
|
||||||
const [showReplies, setShowReplies] = useState(initShowReplies ?? post.replies.length === 1);
|
const { showReactions } = useSubject(appSettings);
|
||||||
const toggle = () => setShowReplies((v) => !v);
|
const [expanded, setExpanded] = useState(initShowReplies ?? (level < 2 || post.replies.length <= 1));
|
||||||
|
const toggle = () => setExpanded((v) => !v);
|
||||||
const showReplyForm = useDisclosure();
|
const showReplyForm = useDisclosure();
|
||||||
|
|
||||||
const muteFilter = useClientSideMuteFilter();
|
const muteFilter = useClientSideMuteFilter();
|
||||||
@@ -38,44 +69,91 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
|
|||||||
|
|
||||||
if (isMuted && replies.length === 0) return null;
|
if (isMuted && replies.length === 0) return null;
|
||||||
|
|
||||||
return (
|
const colorMode = useColorMode().colorMode;
|
||||||
<Flex direction="column" gap="2">
|
const color = LEVEL_COLORS[level % LEVEL_COLORS.length];
|
||||||
{isMuted && !alwaysShow ? (
|
const colorValue = colorMode === "light" ? 200 : 800;
|
||||||
muteAlert
|
const focusColor = colorMode === "light" ? "blue.300" : "blue.700";
|
||||||
) : (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{replies.length > 0 && (
|
const header = (
|
||||||
<Button onClick={toggle}>
|
<Flex gap="2" alignItems="center">
|
||||||
{numberOfReplies} {numberOfReplies > 1 ? "Replies" : "Reply"}
|
<UserAvatarLink pubkey={post.event.pubkey} size="sm" />
|
||||||
{showReplies ? <ChevronDownIcon /> : <ChevronUpIcon />}
|
<UserLink pubkey={post.event.pubkey} fontWeight="bold" isTruncated />
|
||||||
</Button>
|
<Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${nip19.noteEncode(post.event.id)}`}>
|
||||||
)}
|
<Timestamp timestamp={post.event.created_at} />
|
||||||
</ButtonGroup>
|
</Link>
|
||||||
{post.replies.length > 0 && showReplies && (
|
{replies.length > 0 ? (
|
||||||
<Flex direction="column" gap="2" pl={[2, 2, 4]} borderLeftColor="gray.500" borderLeftWidth="1px">
|
<Button variant="ghost" onClick={toggle} rightIcon={expanded ? <Minus /> : <Expand01 />}>
|
||||||
{post.replies.map((child) => (
|
({numberOfReplies})
|
||||||
<ThreadPost key={child.event.id} post={child} focusId={focusId} />
|
</Button>
|
||||||
))}
|
) : (
|
||||||
</Flex>
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
onClick={toggle}
|
||||||
|
icon={expanded ? <Minus /> : <Expand01 />}
|
||||||
|
aria-label={expanded ? "Collapse" : "Expand"}
|
||||||
|
title={expanded ? "Collapse" : "Expand"}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
return isMuted && !alwaysShow ? (
|
||||||
|
muteAlert
|
||||||
|
) : (
|
||||||
|
<TrustProvider trust={focusId === post.event.id ? true : undefined}>
|
||||||
|
<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=".2rem .25rem"
|
||||||
|
borderColor={focusId === post.event.id ? focusColor : undefined}
|
||||||
|
borderRightColor={color + "." + colorValue}
|
||||||
|
borderLeftColor={color + "." + colorValue}
|
||||||
|
backgroundImage={`linear-gradient(to right, var(--chakra-colors-transparent) 90%, var(--chakra-colors-${color}-${colorValue}) 100%)`}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Flex, Spinner } from "@chakra-ui/react";
|
import { Button, Spinner } from "@chakra-ui/react";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams, Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import Note from "../../components/note";
|
import Note from "../../components/note";
|
||||||
import { isHexKey } from "../../helpers/nip19";
|
import { isHexKey } from "../../helpers/nip19";
|
||||||
@@ -52,9 +52,19 @@ export default function NoteView() {
|
|||||||
|
|
||||||
pageContent = (
|
pageContent = (
|
||||||
<>
|
<>
|
||||||
{parentPosts.map((parent) => (
|
{parentPosts.length > 1 && (
|
||||||
<Note key={parent.event.id + "-rely"} event={parent.event} hideDrawerButton />
|
<Button
|
||||||
))}
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
h="4rem"
|
||||||
|
w="full"
|
||||||
|
as={RouterLink}
|
||||||
|
to={`/n/${nip19.noteEncode(parentPosts[0].event.id)}`}
|
||||||
|
>
|
||||||
|
View full thread ({parentPosts.length - 1})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{post.reply && <Note key={post.reply.event.id + "-rely"} event={post.reply.event} hideDrawerButton />}
|
||||||
<ThreadPost key={post.event.id} post={post} initShowReplies focusId={focusId} />
|
<ThreadPost key={post.event.id} post={post} initShowReplies focusId={focusId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -62,5 +72,5 @@ export default function NoteView() {
|
|||||||
pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton />;
|
pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <VerticalPageLayout>{pageContent}</VerticalPageLayout>;
|
return <VerticalPageLayout px={{ base: 0, md: "2" }}>{pageContent}</VerticalPageLayout>;
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ import { getEventUID, getReferences, parseCoordinate } from "../../helpers/nostr
|
|||||||
import Timestamp from "../../components/timestamp";
|
import Timestamp from "../../components/timestamp";
|
||||||
import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
|
import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
|
||||||
import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown";
|
import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown";
|
||||||
import { NoteContents } from "../../components/note/note-contents";
|
import { NoteContents } from "../../components/note/text-note-contents";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ import StarRating from "../../../components/star-rating";
|
|||||||
import { safeJson } from "../../../helpers/parse";
|
import { safeJson } from "../../../helpers/parse";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import { NoteContents } from "../../../components/note/note-contents";
|
import { NoteContents } from "../../../components/note/text-note-contents";
|
||||||
import { Metadata } from "./relay-card";
|
import { Metadata } from "./relay-card";
|
||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
import { getEventUID } from "../../../helpers/nostr/events";
|
||||||
import Timestamp from "../../../components/timestamp";
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
@@ -16,7 +16,7 @@ import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
|||||||
import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider";
|
import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider";
|
||||||
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
|
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
|
||||||
import useSubject from "../../../hooks/use-subject";
|
import useSubject from "../../../hooks/use-subject";
|
||||||
import { NoteContents } from "../../../components/note/note-contents";
|
import { NoteContents } from "../../../components/note/text-note-contents";
|
||||||
import { isATag } from "../../../types/nostr-event";
|
import { isATag } from "../../../types/nostr-event";
|
||||||
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||||
import OpenGraphCard from "../../../components/open-graph-card";
|
import OpenGraphCard from "../../../components/open-graph-card";
|
||||||
|
Reference in New Issue
Block a user