Cleanup thread view

This commit is contained in:
hzrd149
2023-10-29 18:39:28 -05:00
parent d1181ef97c
commit cc4247dc88
10 changed files with 147 additions and 54 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Thread view improvements

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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>
)}
</>
);
}; };

View File

@@ -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>;
} }

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";