Skip to content

Commit

Permalink
feat: preview feed entries (RSSNext#942)
Browse files Browse the repository at this point in the history
Co-authored-by: Whitewater <me@waterwater.moe>
  • Loading branch information
hyoban and lawvs authored Oct 16, 2024
1 parent d1db023 commit 03a3d38
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 33 deletions.
4 changes: 4 additions & 0 deletions apps/renderer/src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export type CombinedEntryModel = EntriesResponse[number] & {
}
}
export type EntryModel = CombinedEntryModel["entries"]
export type EntryModelSimple = Exclude<
ExtractBizResponse<typeof apiClient.feeds.$get>["data"]["entries"],
undefined
>[number]
export type DiscoverResponse = Array<
Exclude<ExtractBizResponse<typeof apiClient.discover.$post>["data"], undefined>[number]
>
Expand Down
36 changes: 22 additions & 14 deletions apps/renderer/src/modules/discover/feed-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { FeedViewType } from "~/lib/enum"
import { getFetchErrorMessage, toastFetchError } from "~/lib/error-parser"
import { getNewIssueUrl } from "~/lib/issues"
import { cn } from "~/lib/utils"
import type { FeedModel } from "~/models"
import type { EntryModelSimple, FeedModel } from "~/models"
import { feed as feedQuery, useFeed } from "~/queries/feed"
import { subscription as subscriptionQuery } from "~/queries/subscriptions"
import { useFeedByIdOrUrl } from "~/store/feed"
Expand Down Expand Up @@ -94,6 +94,7 @@ export const FeedForm: Component<{
asWidget,
onSuccess,
subscriptionData: feedQuery.data?.subscription,
entries: feedQuery.data?.entries,
feed,
}}
/>
Expand Down Expand Up @@ -165,6 +166,7 @@ const FeedInnerForm = ({
onSuccess,
subscriptionData,
feed,
entries,
}: {
defaultValues?: z.infer<typeof formSchema>
id?: string
Expand All @@ -177,6 +179,7 @@ const FeedInnerForm = ({
title?: string | null
}
feed: FeedModel
entries?: EntryModelSimple[]
}) => {
const subscription = useSubscriptionByFeedId(id || "") || subscriptionData
const isSubscribed = !!subscription
Expand Down Expand Up @@ -274,18 +277,6 @@ const FeedInnerForm = ({
</Card>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-1 flex-col gap-y-4">
<FormField
control={form.control}
name="view"
render={() => (
<FormItem>
<FormLabel>{t("feed_form.view")}</FormLabel>

<ViewSelectorRadioGroup {...form.register("view")} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="title"
Expand Down Expand Up @@ -351,7 +342,24 @@ const FeedInnerForm = ({
</FormItem>
)}
/>
<div className="flex flex-1 items-end justify-end gap-4">
<FormField
control={form.control}
name="view"
render={() => (
<FormItem className="mb-16">
<FormLabel>{t("feed_form.view")}</FormLabel>

<ViewSelectorRadioGroup
{...form.register("view")}
entries={entries}
feed={feed}
view={Number(form.getValues("view"))}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="absolute inset-x-0 bottom-0 flex flex-1 items-end justify-end gap-4 bg-theme-background p-4">
{isSubscribed && (
<Button
type="button"
Expand Down
48 changes: 47 additions & 1 deletion apps/renderer/src/modules/entry-column/Items/article-item.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
import { FeedIcon } from "~/components/feed-icon"
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { Skeleton } from "~/components/ui/skeleton"
import { ListItem } from "~/modules/entry-column/templates/list-item-template"

import type { UniversalItemProps } from "../types"
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"

export function ArticleItem({ entryId, entryPreview, translation }: UniversalItemProps) {
return (
<ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} withDetails />
)
}

export function ArticleItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="relative rounded-md text-zinc-700 transition-colors dark:text-neutral-400">
<div className="relative">
<div className="group relative flex py-4 pl-3 pr-2">
<FeedIcon className="mr-2 size-5 rounded-sm" feed={feed} fallback />
<div className="-mt-0.5 line-clamp-4 flex-1 text-sm leading-tight">
<div className="flex gap-1 text-[10px] font-bold text-zinc-400 dark:text-neutral-500">
<span>{feed.title}</span>
<span>·</span>
<span>{!!entry.publishedAt && <RelativeTime date={entry.publishedAt} />}</span>
</div>
<div className="relative my-1 break-words font-medium">{entry.title}</div>
<div className="mt-1.5 text-[13px] text-zinc-400 dark:text-neutral-500">
{entry.description}
</div>
</div>
{entry.media?.[0] ? (
<Media
thumbnail
src={entry.media[0].url}
type={entry.media[0].type}
previewImageUrl={entry.media[0].preview_image_url}
className="ml-2 size-20 overflow-hidden rounded"
mediaContainerClassName={"w-auto h-auto rounded"}
loading="lazy"
proxy={{
width: 160,
height: 160,
}}
height={entry.media[0].height}
width={entry.media[0].width}
blurhash={entry.media[0].blurhash}
/>
) : (
<Skeleton className="ml-2 size-20 overflow-hidden rounded" />
)}
</div>
</div>
</div>
)
}

export const ArticleItemSkeleton = (
<div className="relative h-[120px] rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
<div className="relative">
Expand Down
47 changes: 46 additions & 1 deletion apps/renderer/src/modules/entry-column/Items/audio-item.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,59 @@
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { Skeleton } from "~/components/ui/skeleton"
import { ListItem } from "~/modules/entry-column/templates/list-item-template"

import type { UniversalItemProps } from "../types"
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"

export function AudioItem({ entryId, entryPreview, translation }: UniversalItemProps) {
return (
<ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} withAudio />
)
}

export function AudioItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="relative mx-auto w-full max-w-lg rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
<div className="relative">
<div className="group relative flex py-4 pl-3 pr-2">
<div className="-mt-0.5 line-clamp-4 flex-1 text-sm leading-tight">
<div className="flex gap-1 text-[10px] font-bold text-zinc-400 dark:text-neutral-500">
<span>{feed.title}</span>
<span>·</span>
<span>{!!entry.publishedAt && <RelativeTime date={entry.publishedAt} />}</span>
</div>
<div className="relative my-0.5 line-clamp-3 break-words font-medium">
{entry.description}
</div>
</div>
<div className="relative ml-2 size-20 shrink-0">
{entry.media?.[0] ? (
<Media
thumbnail
src={entry.media[0].url}
type={entry.media[0].type}
previewImageUrl={entry.media[0].preview_image_url}
className="mr-2 size-20 shrink-0 overflow-hidden rounded-sm"
mediaContainerClassName={"w-auto h-auto rounded"}
loading="lazy"
proxy={{
width: 160,
height: 160,
}}
height={entry.media[0].height}
width={entry.media[0].width}
blurhash={entry.media[0].blurhash}
/>
) : (
<Skeleton className="mr-2 size-20 shrink-0 overflow-hidden rounded-sm " />
)}
</div>
</div>
</div>
</div>
)
}

export const AudioItemSkeleton = (
<div className="relative mx-auto w-full max-w-lg rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
<div className="relative">
Expand Down
33 changes: 27 additions & 6 deletions apps/renderer/src/modules/entry-column/Items/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { FeedViewType } from "~/lib/enum"

import { ArticleItem, ArticleItemSkeleton } from "./article-item"
import { AudioItem, AudioItemSkeleton } from "./audio-item"
import { NotificationItem, NotificationItemSkeleton } from "./notification-item"
import { PictureItem, PictureItemSkeleton } from "./picture-item"
import { SocialMediaItem, SocialMediaItemSkeleton } from "./social-media-item"
import { VideoItem, VideoItemSkeleton } from "./video-item"
import { ArticleItem, ArticleItemSkeleton, ArticleItemStateLess } from "./article-item"
import { AudioItem, AudioItemSkeleton, AudioItemStateLess } from "./audio-item"
import {
NotificationItem,
NotificationItemSkeleton,
NotificationItemStateLess,
} from "./notification-item"
import { PictureItem, PictureItemSkeleton, PictureItemStateLess } from "./picture-item"
import {
SocialMediaItem,
SocialMediaItemSkeleton,
SocialMediaItemStateLess,
} from "./social-media-item"
import { VideoItem, VideoItemSkeleton, VideoItemStateLess } from "./video-item"

const ItemMap = {
[FeedViewType.Articles]: ArticleItem,
Expand All @@ -19,6 +27,19 @@ export const getItemComponentByView = (view: FeedViewType) => {
return ItemMap[view] || ArticleItem
}

const StatelessItemMap = {
[FeedViewType.Articles]: ArticleItemStateLess,
[FeedViewType.SocialMedia]: SocialMediaItemStateLess,
[FeedViewType.Pictures]: PictureItemStateLess,
[FeedViewType.Videos]: VideoItemStateLess,
[FeedViewType.Audios]: AudioItemStateLess,
[FeedViewType.Notifications]: NotificationItemStateLess,
}

export const getStatelessItemComponentByView = (view: FeedViewType) => {
return StatelessItemMap[view] || ArticleItemStateLess
}

const SkeletonItemMap = {
[FeedViewType.Articles]: ArticleItemSkeleton,
[FeedViewType.SocialMedia]: SocialMediaItemSkeleton,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
import { FeedIcon } from "~/components/feed-icon"
import { RelativeTime } from "~/components/ui/datetime"
import { Skeleton } from "~/components/ui/skeleton"
import { ListItem } from "~/modules/entry-column/templates/list-item-template"

import type { UniversalItemProps } from "../types"
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"

export function NotificationItem({ entryId, entryPreview, translation }: UniversalItemProps) {
return (
<ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} withFollow />
)
}

export function NotificationItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="relative mx-auto w-full max-w-lg">
<div className="group relative flex py-4 pl-3 pr-2">
<FeedIcon feed={feed} fallback />

<div className="-mt-0.5 line-clamp-4 flex-1 text-sm leading-tight">
<div className="flex gap-1 text-[10px] font-bold text-zinc-400 dark:text-neutral-500">
<span>{feed.title}</span>
<span>·</span>
<span>{!!entry.publishedAt && <RelativeTime date={entry.publishedAt} />}</span>
</div>
<div className="relative my-0.5 break-words">{entry.title}</div>
</div>
</div>
</div>
)
}

export const NotificationItemSkeleton = (
<div className="relative mx-auto w-full max-w-lg">
<div className="group relative flex py-4 pl-3 pr-2">
Expand Down
56 changes: 55 additions & 1 deletion apps/renderer/src/modules/entry-column/Items/picture-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { PropsWithChildren } from "react"
import { memo, useContext, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"

import { FeedIcon } from "~/components/feed-icon"
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { SwipeMedia } from "~/components/ui/media/SwipeMedia"
import { ReactVirtuosoItemPlaceholder } from "~/components/ui/placeholder"
import { Skeleton } from "~/components/ui/skeleton"
Expand All @@ -15,7 +18,7 @@ import { useImageDimensions } from "~/store/image"
import { usePreviewMedia } from "../../../components/ui/media/hooks"
import { EntryItemWrapper } from "../layouts/EntryItemWrapper"
import { GridItem, GridItemFooter } from "../templates/grid-item-template"
import type { UniversalItemProps } from "../types"
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"
import {
MasonryIntersectionContext,
useMasonryItemRatio,
Expand Down Expand Up @@ -195,6 +198,57 @@ const MasonryItemFixedDimensionWrapper = (
)
}

export function PictureItemStateLess({ entry, feed }: EntryItemStatelessProps) {
return (
<div className="relative mx-auto max-w-md rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
<div className="relative">
<div className="p-1.5">
<div className="relative flex gap-2 overflow-x-auto">
<div
className={cn(
"relative flex w-full shrink-0 items-center overflow-hidden rounded-md",
!entry.media?.[0].url && "aspect-square",
)}
>
{entry.media?.[0] ? (
<Media
thumbnail
src={entry.media[0].url}
type={entry.media[0].type}
previewImageUrl={entry.media[0].preview_image_url}
className="size-full overflow-hidden"
mediaContainerClassName={"w-auto h-auto rounded"}
loading="lazy"
proxy={{
width: 0,
height: 0,
}}
height={entry.media[0].height}
width={entry.media[0].width}
blurhash={entry.media[0].blurhash}
/>
) : (
<Skeleton className="size-full overflow-hidden" />
)}
</div>
</div>
<div className="relative flex-1 px-2 pb-3 pt-1 text-sm">
<div className="relative mb-1 mt-1.5 truncate font-medium leading-none">
{entry.title}
</div>
<div className="mt-1 flex items-center gap-1 truncate text-[13px]">
<FeedIcon feed={feed} fallback className="size-4" />
<span>{feed.title}</span>
<span className="text-zinc-500">·</span>
{!!entry.publishedAt && <RelativeTime date={entry.publishedAt} />}
</div>
</div>
</div>
</div>
</div>
)
}

export const PictureItemSkeleton = (
<div className="relative max-w-md rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
<div className="relative">
Expand Down
Loading

0 comments on commit 03a3d38

Please sign in to comment.