mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 20:11:31 +02:00
move ghost timeline to sidebar
This commit is contained in:
@@ -81,7 +81,7 @@ export default function DebugEventTags({ event }: { event: NostrEvent }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="link" color="GrayText" fontFamily="monospace" onClick={expand.onToggle}>
|
||||
<Button variant="link" color="GrayText" fontFamily="monospace" onClick={expand.onToggle} isTruncated>
|
||||
[{expand.isOpen ? "-" : "+"}] Tags ({event.tags.length})
|
||||
</Button>
|
||||
{expand.isOpen && (
|
||||
|
@@ -1,128 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Box, Card, CloseButton, Divider, Flex, FlexProps, Spacer, Text } from "@chakra-ui/react";
|
||||
import { kinds, nip18, nip19, nip25 } from "nostr-tools";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useInterval } from "react-use";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import accountService from "../../services/account";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import UserLink from "../user/user-link";
|
||||
import { GhostIcon } from "../icons";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import TimelineLoader from "../../classes/timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import { safeRelayUrls } from "../../helpers/relay";
|
||||
|
||||
const kindColors: Record<number, FlexProps["bg"]> = {
|
||||
[kinds.ShortTextNote]: "blue.500",
|
||||
[kinds.RecommendRelay]: "pink",
|
||||
[kinds.EncryptedDirectMessage]: "orange.500",
|
||||
[kinds.Repost]: "yellow",
|
||||
[kinds.GenericRepost]: "yellow",
|
||||
[kinds.Reaction]: "green.500",
|
||||
[kinds.LongFormArticle]: "purple.500",
|
||||
};
|
||||
|
||||
function EventChunk({ event, ...props }: { event: NostrEvent } & Omit<FlexProps, "children">) {
|
||||
const navigate = useNavigate();
|
||||
const handleClick = useCallback(() => {
|
||||
switch (event.kind) {
|
||||
case kinds.Reaction: {
|
||||
const pointer = nip25.getReactedEventPointer(event);
|
||||
if (pointer) navigate(`/l/${nip19.neventEncode(pointer)}`);
|
||||
return;
|
||||
}
|
||||
case kinds.Repost: {
|
||||
const pointer = nip18.getRepostedEventPointer(event);
|
||||
if (pointer?.relays) pointer.relays = safeRelayUrls(pointer.relays);
|
||||
if (pointer) navigate(`/l/${nip19.neventEncode(pointer)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
navigate(`/l/${getSharableEventAddress(event)}`);
|
||||
}, [event]);
|
||||
|
||||
const getTitle = () => {
|
||||
switch (event.kind) {
|
||||
case kinds.ShortTextNote:
|
||||
return "Note";
|
||||
case kinds.Reaction:
|
||||
return "Reaction";
|
||||
case kinds.EncryptedDirectMessage:
|
||||
return "Direct Message";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" cursor="pointer" onClick={handleClick} title={getTitle()} overflow="hidden" {...props}>
|
||||
<Box bg={kindColors[event.kind] || "gray.500"} h="8" p="2" fontSize="sm">
|
||||
{getTitle()}
|
||||
</Box>
|
||||
<Divider />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactEventTimeline({ timeline, ...props }: { timeline: TimelineLoader } & Omit<FlexProps, "children">) {
|
||||
const events = useSubject(timeline.timeline);
|
||||
const [now, setNow] = useState(dayjs().unix());
|
||||
|
||||
useInterval(() => setNow(dayjs().unix()), 1000 * 10);
|
||||
|
||||
return (
|
||||
<Flex {...props}>
|
||||
{Array.from(events)
|
||||
.reverse()
|
||||
.map((event, i, arr) => {
|
||||
const next = arr[i + 1];
|
||||
return (
|
||||
<EventChunk
|
||||
key={event.id}
|
||||
event={event}
|
||||
flex={next ? next.created_at - event.created_at : now - event.created_at}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GhostToolbar() {
|
||||
const account = useCurrentAccount()!;
|
||||
const isGhost = useSubject(accountService.isGhost);
|
||||
|
||||
const readRelays = useReadRelays();
|
||||
const [since] = useState(dayjs().subtract(6, "hours").unix());
|
||||
|
||||
const timeline = useTimelineLoader(`${account.pubkey}-ghost`, readRelays, { since, authors: [account.pubkey] });
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="2"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
gap="2"
|
||||
position="fixed"
|
||||
bottom="0"
|
||||
left="0"
|
||||
right="0"
|
||||
>
|
||||
<GhostIcon fontSize="2rem" />
|
||||
<Text>Ghosting: </Text>
|
||||
<UserAvatar pubkey={account.pubkey} size="sm" />
|
||||
<UserLink pubkey={account.pubkey} fontWeight="bold" />
|
||||
<Spacer />
|
||||
<CompactEventTimeline w="70%" timeline={timeline} />
|
||||
<Spacer />
|
||||
<CloseButton onClick={() => accountService.stopGhost()} />
|
||||
</Card>
|
||||
);
|
||||
}
|
26
src/components/layout/ghost/sidebar.tsx
Normal file
26
src/components/layout/ghost/sidebar.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Box, CloseButton, Flex, FlexProps } from "@chakra-ui/react";
|
||||
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import GhostTimeline from "./timeline";
|
||||
import UserAvatar from "../../user/user-avatar";
|
||||
import UserLink from "../../user/user-link";
|
||||
import UserDnsIdentity from "../../user/user-dns-identity";
|
||||
import accountService from "../../../services/account";
|
||||
|
||||
export default function GhostSideBar({ ...props }: Omit<FlexProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
|
||||
return (
|
||||
<Flex direction="column" borderWidth={1} overflow="hidden" maxH="100vh" position="sticky" top="0" {...props}>
|
||||
<Flex gap="2" borderBottomWidth={1} p="4" alignItems="center">
|
||||
<UserAvatar pubkey={account.pubkey} size="md" />
|
||||
<Flex direction="column">
|
||||
<UserLink pubkey={account.pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentity pubkey={account.pubkey} />
|
||||
</Flex>
|
||||
<CloseButton ml="auto" mb="auto" onClick={() => accountService.stopGhost()} />
|
||||
</Flex>
|
||||
<GhostTimeline p="4" flex={1} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
115
src/components/layout/ghost/timeline.tsx
Normal file
115
src/components/layout/ghost/timeline.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useRef } from "react";
|
||||
import { Code, Flex, FlexProps, LinkBox, Text } from "@chakra-ui/react";
|
||||
import { NostrEvent, kinds, nip19, nip25 } from "nostr-tools";
|
||||
import { getEventUID } from "nostr-idb";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import TimelineActionAndStatus from "../../timeline-page/timeline-action-and-status";
|
||||
import IntersectionObserverProvider, {
|
||||
useRegisterIntersectionEntity,
|
||||
} from "../../../providers/local/intersection-observer";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { getDMRecipient, getDMSender } from "../../../helpers/nostr/dms";
|
||||
import UserName from "../../user/user-name";
|
||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
|
||||
const kindColors: Record<number, FlexProps["bg"]> = {
|
||||
[kinds.ShortTextNote]: "blue.500",
|
||||
[kinds.EncryptedDirectMessage]: "orange.500",
|
||||
[kinds.Repost]: "yellow.500",
|
||||
[kinds.GenericRepost]: "yellow.500",
|
||||
[kinds.Reaction]: "green.500",
|
||||
[kinds.LongFormArticle]: "purple.500",
|
||||
};
|
||||
|
||||
function KindTag({ event }: { event: NostrEvent }) {
|
||||
return (
|
||||
<Code
|
||||
px="2"
|
||||
fontFamily="monospace"
|
||||
fontWeight="bold"
|
||||
borderLeftWidth={4}
|
||||
borderLeftColor={kindColors[event.kind] || "gray.500"}
|
||||
fontSize="md"
|
||||
>
|
||||
{event.kind}
|
||||
</Code>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelineItem({ event }: { event: NostrEvent }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(event));
|
||||
|
||||
const renderContent = () => {
|
||||
switch (event.kind) {
|
||||
case kinds.EncryptedDirectMessage:
|
||||
const sender = getDMSender(event);
|
||||
const recipient = getDMRecipient(event);
|
||||
return (
|
||||
<Text>
|
||||
<UserName pubkey={sender} fontWeight="bold" /> messaged <UserName pubkey={recipient} fontWeight="bold" />
|
||||
</Text>
|
||||
);
|
||||
case kinds.Contacts:
|
||||
return (
|
||||
<Text noOfLines={1} isTruncated>
|
||||
Updated contacts
|
||||
</Text>
|
||||
);
|
||||
case kinds.Reaction:
|
||||
const pointer = nip25.getReactedEventPointer(event);
|
||||
return (
|
||||
<HoverLinkOverlay
|
||||
as={RouterLink}
|
||||
to={`/l/${pointer ? nip19.neventEncode(pointer) : ""}`}
|
||||
noOfLines={1}
|
||||
isTruncated
|
||||
>
|
||||
{event.content}
|
||||
</HoverLinkOverlay>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<HoverLinkOverlay as={RouterLink} to={`/l/${getSharableEventAddress(event)}`} noOfLines={1} isTruncated>
|
||||
{event.content}
|
||||
</HoverLinkOverlay>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex as={LinkBox} ref={ref} gap="2" py="1" overflow="hidden" flexShrink={0}>
|
||||
<KindTag event={event} />
|
||||
{renderContent()}
|
||||
<Timestamp timestamp={event.created_at} ml="auto" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GhostTimeline({ ...props }: Omit<FlexProps, "children">) {
|
||||
const account = useCurrentAccount()!;
|
||||
const readRelays = useReadRelays();
|
||||
|
||||
const timeline = useTimelineLoader(`${account.pubkey}-ghost`, readRelays, { authors: [account.pubkey] });
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex direction="column" overflow="auto" {...props}>
|
||||
{events.map((event) => (
|
||||
<TimelineItem key={event.id} event={event} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</Flex>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
@@ -7,8 +7,8 @@ import DesktopSideNav from "./desktop-side-nav";
|
||||
import MobileBottomNav from "./mobile-bottom-nav";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import accountService from "../../services/account";
|
||||
import GhostToolbar from "./ghost-toolbar";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import GhostSideBar from "./ghost/sidebar";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
@@ -34,7 +34,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</Container>
|
||||
{!isMobile && <Box flexShrink={1} maxW="15rem" flex={1} />}
|
||||
{!isMobile && isGhost ? <GhostSideBar maxW="lg" minW="md" /> : <Box flexShrink={1} maxW="15rem" flex={1} />}
|
||||
{isMobile && (
|
||||
<MobileBottomNav
|
||||
position="fixed"
|
||||
@@ -47,7 +47,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
)}
|
||||
<Spacer display={["none", null, "block"]} />
|
||||
</Flex>
|
||||
{isGhost && <GhostToolbar />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user