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

View File

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

View File

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

View File

@@ -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 { 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 { 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 = {
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,91 @@ 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 />}
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>
)}
</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>
) : (
<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
) : (
<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 { useParams } from "react-router-dom";
import { useParams, Link as RouterLink } from "react-router-dom";
import Note from "../../components/note";
import { isHexKey } from "../../helpers/nip19";
@@ -52,9 +52,19 @@ export default function NoteView() {
pageContent = (
<>
{parentPosts.map((parent) => (
<Note key={parent.event.id + "-rely"} event={parent.event} hideDrawerButton />
))}
{parentPosts.length > 1 && (
<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} />
</>
);
@@ -62,5 +72,5 @@ export default function NoteView() {
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 { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
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 { TrustProvider } from "../../providers/trust";

View File

@@ -9,7 +9,7 @@ import StarRating from "../../../components/star-rating";
import { safeJson } from "../../../helpers/parse";
import { NostrEvent } from "../../../types/nostr-event";
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 { getEventUID } from "../../../helpers/nostr/events";
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 replaceableEventLoaderService from "../../../services/replaceable-event-requester";
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 useReplaceableEvent from "../../../hooks/use-replaceable-event";
import OpenGraphCard from "../../../components/open-graph-card";