add simple article view

add support for wiki links in text notes
This commit is contained in:
hzrd149
2024-08-31 16:29:55 -05:00
parent 50dbdcbe55
commit f53f5ca9cb
19 changed files with 438 additions and 137 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add support for wiki links in text notes

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add simple article view

View File

@@ -103,6 +103,8 @@ import PerformanceSettings from "./views/settings/performance";
import PrivacySettings from "./views/settings/privacy";
import PostSettings from "./views/settings/post";
import AccountSettings from "./views/settings/accounts";
import ArticlesHomeView from "./views/articles";
import ArticleView from "./views/articles/article";
const TracksView = lazy(() => import("./views/tracks"));
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const UserVideosTab = lazy(() => import("./views/user/videos"));
@@ -407,6 +409,13 @@ const router = createHashRouter([
{ path: "explore", element: <CommunitiesExploreView /> },
],
},
{
path: "articles",
children: [
{ path: "", element: <ArticlesHomeView /> },
{ path: ":naddr", element: <ArticleView /> },
],
},
{
path: "c/:community",
children: [

View File

@@ -1,5 +1,5 @@
import { useContext } from "react";
import { Box, Card, CardBody, CardProps, Flex, Heading, Image, LinkBox, Tag, Text, useToast } from "@chakra-ui/react";
import { Box, Card, CardBody, CardProps, Flex, Heading, Image, LinkBox, Text, useToast } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import {
getArticleImage,
@@ -11,25 +11,19 @@ import { NostrEvent } from "../../../types/nostr-event";
import UserAvatarLink from "../../user/user-avatar-link";
import UserLink from "../../user/user-link";
import Timestamp from "../../timestamp";
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
import relayHintService from "../../../services/event-relay-hint";
import ArticleTags from "../../../views/articles/components/article-tags";
import HoverLinkOverlay from "../../hover-link-overlay";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
export default function EmbeddedArticle({ article, ...props }: Omit<CardProps, "children"> & { article: NostrEvent }) {
const toast = useToast();
const title = getArticleTitle(article);
const image = getArticleImage(article);
const summary = getArticleSummary(article);
const { openAddress } = useContext(AppHandlerContext);
const open = () => {
const naddr = relayHintService.getSharableEventAddress(article);
if (naddr) openAddress(naddr);
else toast({ status: "error", description: "Failed to get address" });
};
const naddr = useShareableEventAddress(article);
return (
<Card as={LinkBox} size="sm" onClick={open} cursor="pointer" {...props}>
<Card as={LinkBox} size="sm" {...props}>
{image && (
<Box
backgroundImage={image}
@@ -50,15 +44,14 @@ export default function EmbeddedArticle({ article, ...props }: Omit<CardProps, "
<UserLink pubkey={article.pubkey} fontWeight="bold" isTruncated />
<Timestamp timestamp={getArticlePublishDate(article) ?? article.created_at} />
</Flex>
<Heading size="md">{title}</Heading>
<Heading size="md">
<HoverLinkOverlay as={RouterLink} to={`/articles/${naddr}`}>
{title}
</HoverLinkOverlay>
</Heading>
<Text mb="2">{summary}</Text>
{article.tags
.filter((t) => t[0] === "t" && t[1])
.map(([_, hashtag]: string[], i) => (
<Tag key={hashtag + i} mr="2" mb="2">
#{hashtag}
</Tag>
))}
<ArticleTags article={article} />
</CardBody>
</Card>
);

View File

@@ -0,0 +1,24 @@
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
import WikiLink from "../../../views/wiki/components/wiki-link";
export function embedNostrWikiLinks(content: EmbedableContent) {
return embedJSX(content, {
name: "embedWikiLinks",
regexp: /\[\[(\w+)(?:\|(\w+))?\]\]/gi,
render: (match, isEndOfLine) => {
try {
const topic = match[1];
const label = match[2] || topic;
if (topic) {
return <WikiLink topic={topic}>{label}</WikiLink>;
}
} catch (e) {
if (e instanceof Error) {
console.error("Failed to embed link", match[0], e.message);
}
}
return null;
},
});
}

View File

@@ -34,6 +34,7 @@ import {
} from "../../external-embeds";
import { LightboxProvider } from "../../lightbox-provider";
import MediaOwnerProvider from "../../../providers/local/media-owner-provider";
import { embedNostrWikiLinks } from "../../external-embeds/types/wiki";
function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) {
let content: EmbedableContent = [event.content.trim()];
@@ -75,6 +76,7 @@ function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) {
content = embedNostrHashtags(content, event);
content = embedNipDefinitions(content);
content = embedEmoji(content, event);
content = embedNostrWikiLinks(content);
return content;
}

View File

@@ -1,18 +0,0 @@
import { memo } from "react";
import { Box } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import EmbeddedArticle from "../../embed-event/event-types/embedded-article";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
function ArticleNote({ article }: { article: NostrEvent }) {
const ref = useEventIntersectionRef(article);
return (
<Box ref={ref}>
<EmbeddedArticle article={article} />
</Box>
);
}
export default memo(ArticleNote);

View File

@@ -5,7 +5,6 @@ import { Box, Text } from "@chakra-ui/react";
import { ErrorBoundary } from "../../error-boundary";
import ReplyNote from "./reply-note";
import RepostEvent from "./repost-event";
import ArticleNote from "./article-note";
import StreamNote from "./stream-note";
import RelayRecommendation from "./relay-recommendation";
import BadgeAwardCard from "../../../views/badges/components/badge-award-card";
@@ -29,9 +28,6 @@ function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visibl
case kinds.GenericRepost:
content = <RepostEvent event={event} />;
break;
case kinds.LongFormArticle:
content = <ArticleNote article={event} />;
break;
case STREAM_KIND:
content = <StreamNote event={event} />;
break;

View File

@@ -5,7 +5,7 @@ export default function Timestamp({ timestamp, ...props }: { timestamp: number }
const date = dayjs.unix(timestamp);
const now = dayjs();
let display = date.format("L");
let display = date.format("ll");
if (now.diff(date, "week") <= 6) {
if (now.diff(date, "d") >= 1) {

View File

@@ -11,6 +11,7 @@ import { safeDecode } from "../nip19";
import { safeRelayUrl, safeRelayUrls } from "../relay";
import RelaySet from "../../classes/relay-set";
import { truncateId } from "../string";
import { getNip10References } from "./threading";
export { truncateId as truncatedId };
@@ -40,7 +41,7 @@ export function isReply(event: NostrEvent | DraftNostrEvent) {
if (event[isReplySymbol] !== undefined) return event[isReplySymbol] as boolean;
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
const isReply = !!getThreadReferences(event).reply;
const isReply = !!getNip10References(event).reply;
// @ts-expect-error
event[isReplySymbol] = isReply;
return isReply;
@@ -123,94 +124,8 @@ export function filterTagsByContentRefs(content: string, tags: Tag[], referenced
return tags.filter((t) => contentTagRefs.includes(t) === referenced);
}
export function interpretThreadTags(event: NostrEvent | DraftNostrEvent) {
const eTags = event.tags.filter(isETag);
const aTags = event.tags.filter(isATag);
// find the root and reply tags.
let rootETag = eTags.find((t) => t[3] === "root");
let replyETag = eTags.find((t) => t[3] === "reply");
let rootATag = aTags.find((t) => t[3] === "root");
let replyATag = aTags.find((t) => t[3] === "reply");
if (!rootETag || !replyETag) {
// a direct reply does not need a "reply" reference
// https://github.com/nostr-protocol/nips/blob/master/10.md
// this is not necessarily to spec. but if there is only one id (root or reply) then assign it to both
// this handles the cases where a client only set a "reply" tag and no root
rootETag = replyETag = rootETag || replyETag;
}
if (!rootATag || !replyATag) {
rootATag = replyATag = rootATag || replyATag;
}
if (!rootETag && !replyETag) {
const contentTagRefs = getContentTagRefs(event.content, eTags);
// legacy behavior
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
const legacyETags = eTags.filter((t) => {
// ignore it if there is a type
if (t[3]) return false;
if (contentTagRefs.includes(t)) return false;
return true;
});
if (legacyETags.length >= 1) {
// first tag is the root
rootETag = legacyETags[0];
// last tag is reply
replyETag = legacyETags[legacyETags.length - 1] ?? rootETag;
}
}
return {
root: rootETag || rootATag ? { e: rootETag, a: rootATag } : undefined,
reply: replyETag || replyATag ? { e: replyETag, a: replyATag } : undefined,
} as {
root?: { e: ETag; a: undefined } | { e: undefined; a: ATag } | { e: ETag; a: ATag };
reply?: { e: ETag; a: undefined } | { e: undefined; a: ATag } | { e: ETag; a: ATag };
};
}
export type ThreadReferences = {
root?:
| { e: EventPointer; a: undefined }
| { e: undefined; a: AddressPointer }
| { e: EventPointer; a: AddressPointer };
reply?:
| { e: EventPointer; a: undefined }
| { e: undefined; a: AddressPointer }
| { e: EventPointer; a: AddressPointer };
};
export const threadRefsSymbol = Symbol("threadRefs");
export type EventWithThread = (NostrEvent | DraftNostrEvent) & { [threadRefsSymbol]: ThreadReferences };
export function getThreadReferences(event: NostrEvent | DraftNostrEvent): ThreadReferences {
// @ts-expect-error
if (Object.hasOwn(event, threadRefsSymbol)) return event[threadRefsSymbol];
const e = event as EventWithThread;
const tags = interpretThreadTags(e);
const threadRef = {
root: tags.root && {
e: tags.root.e && eTagToEventPointer(tags.root.e),
a: tags.root.a && aTagToAddressPointer(tags.root.a),
},
reply: tags.reply && {
e: tags.reply.e && eTagToEventPointer(tags.reply.e),
a: tags.reply.a && aTagToAddressPointer(tags.reply.a),
},
} as ThreadReferences;
// @ts-expect-error
event[threadRefsSymbol] = threadRef;
return threadRef;
}
/** @deprecated */
export { getNip10References as getThreadReferences };
export function getEventCoordinate(event: NostrEvent) {
const d = event.tags.find(isDTag)?.[1];

View File

@@ -0,0 +1,94 @@
import { EventTemplate, NostrEvent } from "nostr-tools";
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
import { ATag, ETag, isATag, isETag } from "../../types/nostr-event";
import { aTagToAddressPointer, eTagToEventPointer, getContentTagRefs } from "./event";
export function interpretThreadTags(event: NostrEvent | EventTemplate) {
const eTags = event.tags.filter(isETag);
const aTags = event.tags.filter(isATag);
// find the root and reply tags.
let rootETag = eTags.find((t) => t[3] === "root");
let replyETag = eTags.find((t) => t[3] === "reply");
let rootATag = aTags.find((t) => t[3] === "root");
let replyATag = aTags.find((t) => t[3] === "reply");
if (!rootETag || !replyETag) {
// a direct reply does not need a "reply" reference
// https://github.com/nostr-protocol/nips/blob/master/10.md
// this is not necessarily to spec. but if there is only one id (root or reply) then assign it to both
// this handles the cases where a client only set a "reply" tag and no root
rootETag = replyETag = rootETag || replyETag;
}
if (!rootATag || !replyATag) {
rootATag = replyATag = rootATag || replyATag;
}
if (!rootETag && !replyETag) {
const contentTagRefs = getContentTagRefs(event.content, eTags);
// legacy behavior
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
const legacyETags = eTags.filter((t) => {
// ignore it if there is a type
if (t[3]) return false;
if (contentTagRefs.includes(t)) return false;
return true;
});
if (legacyETags.length >= 1) {
// first tag is the root
rootETag = legacyETags[0];
// last tag is reply
replyETag = legacyETags[legacyETags.length - 1] ?? rootETag;
}
}
return {
root: rootETag || rootATag ? { e: rootETag, a: rootATag } : undefined,
reply: replyETag || replyATag ? { e: replyETag, a: replyATag } : undefined,
} as {
root?: { e: ETag; a: undefined } | { e: undefined; a: ATag } | { e: ETag; a: ATag };
reply?: { e: ETag; a: undefined } | { e: undefined; a: ATag } | { e: ETag; a: ATag };
};
}
export type ThreadReferences = {
root?:
| { e: EventPointer; a: undefined }
| { e: undefined; a: AddressPointer }
| { e: EventPointer; a: AddressPointer };
reply?:
| { e: EventPointer; a: undefined }
| { e: undefined; a: AddressPointer }
| { e: EventPointer; a: AddressPointer };
};
export const threadRefsSymbol = Symbol("threadRefs");
export type EventWithThread = (NostrEvent | EventTemplate) & { [threadRefsSymbol]: ThreadReferences };
export function getNip10References(event: NostrEvent | EventTemplate): ThreadReferences {
// @ts-expect-error
if (Object.hasOwn(event, threadRefsSymbol)) return event[threadRefsSymbol];
const e = event as EventWithThread;
const tags = interpretThreadTags(e);
const threadRef = {
root: tags.root && {
e: tags.root.e && eTagToEventPointer(tags.root.e),
a: tags.root.a && aTagToAddressPointer(tags.root.a),
},
reply: tags.reply && {
e: tags.reply.e && eTagToEventPointer(tags.reply.e),
a: tags.reply.a && aTagToAddressPointer(tags.reply.a),
},
} as ThreadReferences;
// @ts-expect-error
event[threadRefsSymbol] = threadRef;
return threadRef;
}

View File

@@ -1,5 +1,5 @@
import { NostrEvent } from "../types/nostr-event";
import { ThreadReferences, getThreadReferences } from "./nostr/event";
import { getNip10References, ThreadReferences } from "./nostr/threading";
export function countReplies(replies: ThreadItem[]): number {
return replies.reduce((c, item) => c + countReplies(item.replies), 0) + replies.length;
@@ -37,7 +37,7 @@ export function buildThread(events: NostrEvent[]) {
const replies = new Map<string, ThreadItem>();
for (const event of events) {
const refs = getThreadReferences(event);
const refs = getNip10References(event);
if (refs.reply?.e) {
idToChildren[refs.reply.e.id] = idToChildren[refs.reply.e.id] || [];

View File

@@ -0,0 +1,72 @@
import { NostrEvent } from "nostr-tools";
import { Box, Flex, Heading, Image, Spinner, Text } from "@chakra-ui/react";
import dayjs from "dayjs";
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import VerticalPageLayout from "../../components/vertical-page-layout";
import {
getArticleImage,
getArticlePublishDate,
getArticleSummary,
getArticleTitle,
} from "../../helpers/nostr/long-form";
import UserLink from "../../components/user/user-link";
import UserAvatarLink from "../../components/user/user-avatar-link";
import UserDnsIdentityIcon from "../../components/user/user-dns-identity-icon";
import MarkdownContent from "../wiki/components/markdown";
import ArticleMenu from "./components/article-menu";
import ArticleTags from "./components/article-tags";
import NoteReactions from "../../components/note/timeline-note/components/note-reactions";
import NoteZapButton from "../../components/note/note-zap-button";
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
function ArticlePage({ article }: { article: NostrEvent }) {
const image = getArticleImage(article);
const title = getArticleTitle(article);
const published = getArticlePublishDate(article);
const summary = getArticleSummary(article);
return (
<VerticalPageLayout pt={{ base: "2", lg: "8" }} pb="12">
<Box mx="auto" maxW="4xl" w="full" mb="2">
<ArticleMenu article={article} aria-label="More Options" float="right" />
<Heading size="xl">{title}</Heading>
<Text>{summary}</Text>
<Box py="2">
<UserAvatarLink pubkey={article.pubkey} float="left" mr="3" mb="2" />
<UserLink pubkey={article.pubkey} fontWeight="bold" fontSize="xl" mr="2" tab="articles" />
<UserDnsIdentityIcon pubkey={article.pubkey} />
<br />
<Text>{dayjs.unix(published ?? article.created_at).format("LL")}</Text>
</Box>
<ArticleTags article={article} />
</Box>
{image && <Image src={image} maxW="6xl" w="full" mx="auto" maxH="60vh" />}
<Box mx="auto" maxW="4xl" w="full">
<ZapBubbles event={article} />
<Flex gap="2">
<NoteZapButton event={article} size="sm" variant="ghost" showEventPreview={false} />
<NoteReactions event={article} size="sm" variant="ghost" />
</Flex>
<Box fontSize="lg">
<MarkdownContent event={article} />
</Box>
<Flex gap="2">
<NoteZapButton event={article} size="sm" variant="ghost" showEventPreview={false} />
<NoteReactions event={article} size="sm" variant="ghost" />
</Flex>
</Box>
</VerticalPageLayout>
);
}
export default function ArticleView() {
const pointer = useParamsAddressPointer("naddr");
const article = useReplaceableEvent(pointer);
if (!article) return <Spinner />;
return <ArticlePage article={article} />;
}

View File

@@ -0,0 +1,64 @@
import { Box, Card, Flex, Heading, LinkBox, Spacer, Text } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import {
getArticleImage,
getArticlePublishDate,
getArticleSummary,
getArticleTitle,
} from "../../../helpers/nostr/long-form";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import Timestamp from "../../../components/timestamp";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import ArticleTags from "./article-tags";
import ArticleMenu from "./article-menu";
import ZapBubbles from "../../../components/note/timeline-note/components/zap-bubbles";
export default function ArticleCard({ article }: { article: NostrEvent }) {
const image = getArticleImage(article);
const title = getArticleTitle(article);
const published = getArticlePublishDate(article);
const summary = getArticleSummary(article);
const naddr = useShareableEventAddress(article);
return (
<Card as={LinkBox} display="block" p="2" position="relative" variant="ghost">
<Flex gap="2" alignItems="center" mb="2">
<UserAvatar pubkey={article.pubkey} size="sm" />
<UserName pubkey={article.pubkey} />
<Timestamp timestamp={published ?? article.created_at} />
<Spacer />
<ArticleMenu aria-label="More Options" article={article} variant="ghost" size="sm" zIndex={10} />
</Flex>
{image && (
<Box
aspectRatio={16 / 9}
backgroundImage={image}
backgroundPosition="center"
backgroundRepeat="no-repeat"
backgroundSize="cover"
float={{ base: undefined, lg: "right" }}
mx={{ base: "auto", lg: 2 }}
mb={{ base: "2", lg: undefined }}
minH="10rem"
maxH="15rem"
/>
)}
<Heading size="md">
<HoverLinkOverlay as={RouterLink} to={`/articles/${naddr}`}>
{title}
</HoverLinkOverlay>
</Heading>
<Text>{summary}</Text>
<ArticleTags article={article} />
<ZapBubbles event={article} mt="2" mr="2" />
</Card>
);
}

View File

@@ -0,0 +1,52 @@
import { useCallback } from "react";
import { MenuItem } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { NostrEvent } from "nostr-tools";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button";
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
import ShareLinkMenuItem from "../../../components/common-menu-items/share-link";
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
import Recording02 from "../../../components/icons/recording-02";
import Translate01 from "../../../components/icons/translate-01";
import { BroadcastEventIcon } from "../../../components/icons";
import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item";
export default function ArticleMenu({
article,
...props
}: { article: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
const publish = usePublishEvent();
const address = useShareableEventAddress(article);
const broadcast = useCallback(async () => {
await publish("Broadcast", article);
}, []);
return (
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={article} />
<ShareLinkMenuItem event={article} />
<CopyEmbedCodeMenuItem event={article} />
<DeleteEventMenuItem event={article} />
{/* <MenuItem as={RouterLink} icon={<Recording02 />} to={`/tools/transform/${address}?tab=tts`}>
Text to speech
</MenuItem>
<MenuItem as={RouterLink} icon={<Translate01 />} to={`/tools/transform/${address}?tab=translation`}>
Translate
</MenuItem> */}
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
Broadcast
</MenuItem>
<DebugEventMenuItem event={article} />
</DotsMenuButton>
</>
);
}

View File

@@ -0,0 +1,16 @@
import { Tag } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
export default function ArticleTags({ article }: { article: NostrEvent }) {
return (
<>
{article.tags
.filter((t) => t[0] === "t" && t[1])
.map(([_, hashtag]: string[], i) => (
<Tag key={hashtag + i} mr="2" mt="2" whiteSpace="pre" flexShrink={0}>
#{hashtag}
</Tag>
))}
</>
);
}

View File

@@ -0,0 +1,71 @@
import { useCallback, useMemo } from "react";
import { Filter, kinds, NostrEvent } from "nostr-tools";
import { Button, Flex, Heading, Spacer } from "@chakra-ui/react";
import { getEventUID } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import VerticalPageLayout from "../../components/vertical-page-layout";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import Plus from "../../components/icons/plus";
import { useReadRelays } from "../../hooks/use-client-relays";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
import ArticleCard from "./components/article-card";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import { getArticleTitle } from "../../helpers/nostr/long-form";
function ArticlesHomePage() {
const relays = useReadRelays();
const userMuteFilter = useClientSideMuteFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (userMuteFilter(event)) return false;
return true;
},
[userMuteFilter],
);
const { filter, listId } = usePeopleListContext();
const query = useMemo<Filter[] | undefined>(() => {
if (!filter) return undefined;
return [{ authors: filter.authors, kinds: [kinds.LongFormArticle] }];
}, [filter]);
const timeline = useTimelineLoader(`${listId ?? "global"}-articles`, relays, query, { eventFilter });
const articles = useSubject(timeline.timeline).filter((article) => !!getArticleTitle(article));
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<VerticalPageLayout>
<Flex gap="2">
<Heading>Articles</Heading>
<PeopleListSelection />
<Spacer />
{/* <Button as={RouterLink} to="/articles/new" colorScheme="primary" leftIcon={<Plus boxSize={6} />}>
New
</Button> */}
</Flex>
<IntersectionObserverProvider callback={callback}>
{articles.map((article) => (
<ArticleCard key={getEventUID(article)} article={article} />
))}
<TimelineActionAndStatus timeline={timeline} />
</IntersectionObserverProvider>
</VerticalPageLayout>
);
}
export default function ArticlesHomeView() {
return (
<PeopleListProvider>
<ArticlesHomePage />
</PeopleListProvider>
);
}

View File

@@ -53,6 +53,7 @@ export const internalApps: App[] = [
{ title: "Lists", description: "Browse and create lists", icon: ListsIcon, id: "lists", to: "/lists" },
{ title: "Tracks", description: "Browse stemstr tracks", icon: TrackIcon, id: "tracks", to: "/tracks" },
{ title: "Videos", description: "Browse flare videos", icon: Film02, id: "videos", to: "/videos" },
{ title: "Articles", description: "Browse articles", icon: Edit04, id: "articles", to: "/articles" },
];
export const internalTools: App[] = [

View File

@@ -8,7 +8,7 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
import VerticalPageLayout from "../../components/vertical-page-layout";
import ArticleNote from "../../components/timeline-page/generic-note-timeline/article-note";
import ArticleCard from "../articles/components/article-card";
export default function UserArticlesTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
@@ -26,7 +26,7 @@ export default function UserArticlesTab() {
<IntersectionObserverProvider callback={callback}>
<VerticalPageLayout>
{articles.map((article) => (
<ArticleNote key={article.id} article={article} />
<ArticleCard key={article.id} article={article} />
))}
<TimelineActionAndStatus timeline={timeline} />
</VerticalPageLayout>