diff --git a/docs/todo.md b/docs/todo.md index 34c1183..e6b113f 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -5,10 +5,18 @@ updated: 2026-04-08 ## Bugs +- [ ] Quoted posts should link to the original post + - [ ] They should also nest properly + ## High Priority Updates -- [ ] Profile RSS - - OK. So making an RSS reader with share to BlueSky would be cool... +- [ ] Feed view +- [ ] Starter pack view +- [ ] List view + +## Updates + +- [ ] Profile RSS? ## Multicolumn Layouts diff --git a/src/components/feeds/DraftsList.tsx b/src/components/feeds/DraftsList.tsx index 43f8bb7..c6d1d40 100644 --- a/src/components/feeds/DraftsList.tsx +++ b/src/components/feeds/DraftsList.tsx @@ -1,7 +1,7 @@ import { Icon } from "$/components/shared/Icon"; import { DraftController } from "$/lib/api/drafts"; -import { formatRelativeTime } from "$/lib/feeds"; import type { Draft } from "$/lib/types"; +import { formatRelativeTime } from "$/lib/utils/text"; import { normalizeError } from "$/lib/utils/text"; import * as logger from "@tauri-apps/plugin-log"; import { createEffect, createSignal, For, Show } from "solid-js"; diff --git a/src/components/feeds/ImageGallery.tsx b/src/components/feeds/ImageGallery.tsx index 4216c6e..852926d 100644 --- a/src/components/feeds/ImageGallery.tsx +++ b/src/components/feeds/ImageGallery.tsx @@ -1,14 +1,172 @@ import { type MediaNotice, MediaNoticeToast } from "$/components/feeds/MediaNoticeToast"; import { Icon } from "$/components/shared/Icon"; import { MediaController } from "$/lib/api/media"; -import { clamp, normalizeError } from "$/lib/utils/text"; +import { clamp } from "$/lib/utils/text"; import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js"; import { Portal } from "solid-js/web"; import { Motion, Presence } from "solid-motionone"; +import { filenameFromPath, toDownloadErrorMessage } from "./embeds/shared"; type GalleryImage = { alt?: string; fullsize?: string; thumb?: string }; +type GalleryOverlayProps = { + authorHandle?: string; + authorHref?: string; + downloadPending: boolean; + expanded: boolean; + hasManyImages: boolean; + imageCount: number; + index: number; + postText?: string; + selectedImage: GalleryImage | null; + showPostTextToggle: boolean; + onClose: () => void; + onDownload: () => void; + onStep: (offset: -1 | 1) => void; + onToggleExpand: () => void; +}; + +function GalleryOverlay(props: GalleryOverlayProps) { + return ( + + + + + + ); +} + +function ArrowButton(props: { direction: "left" | "right"; onClick: () => void }) { + return ( + + ); +} + +type CaptionPanelProps = { + alt?: string; + authorHandle?: string; + authorHref?: string; + expanded: boolean; + postText?: string; + showToggle: boolean; + onToggleExpand: () => void; +}; + +function CaptionPanel(props: CaptionPanelProps) { + const label = () => props.expanded ? "Show less" : "Show more"; + return ( +
+ {(alt) =>

{alt()}

}
+ 0}> +
+

+ {props.postText} +

+ + + +
+
+ + + {props.authorHandle} + + +
+ ); +} + type ImageGalleryProps = { authorHandle?: string; authorHref?: string; @@ -120,7 +278,7 @@ export function ImageGallery(props: ImageGalleryProps) { : await MediaController.downloadImage(currentImage); queueNotice({ kind: "success", message: `Saved ${filenameFromPath(result.path)}.`, path: result.path }); } catch (error) { - queueNotice({ kind: "error", message: toDownloadErrorMessage(error) }); + queueNotice({ kind: "error", message: toDownloadErrorMessage(error, "Couldn't save this image right now.") }); } finally { setDownloadPending(false); } @@ -138,6 +296,7 @@ export function ImageGallery(props: ImageGalleryProps) { + {/* FIXME: this needs to be simplified */} ); } - -function GalleryOverlay( - props: { - authorHandle?: string; - authorHref?: string; - downloadPending: boolean; - expanded: boolean; - hasManyImages: boolean; - imageCount: number; - index: number; - postText?: string; - selectedImage: GalleryImage | null; - showPostTextToggle: boolean; - onClose: () => void; - onDownload: () => void; - onStep: (offset: -1 | 1) => void; - onToggleExpand: () => void; - }, -) { - return ( - - - - - - ); -} - -function ArrowButton(props: { direction: "left" | "right"; onClick: () => void }) { - return ( - - ); -} - -function CaptionPanel( - props: { - alt?: string; - authorHandle?: string; - authorHref?: string; - expanded: boolean; - postText?: string; - showToggle: boolean; - onToggleExpand: () => void; - }, -) { - const label = () => props.expanded ? "Show less" : "Show more"; - return ( -
- {(alt) =>

{alt()}

}
- 0}> -
-

- {props.postText} -

- - - -
-
- - - {props.authorHandle} - - -
- ); -} - -function filenameFromPath(path: string) { - const parts = path.split(/[/\\]/u); - return parts.at(-1) || "downloaded file"; -} - -function toDownloadErrorMessage(error: unknown) { - const message = normalizeError(error); - if (/download folder|writable|save|directory|exists/iu.test(message)) { - return "Couldn't save — check that the download folder exists."; - } - - return "Couldn't save this image right now."; -} diff --git a/src/components/feeds/PostCard.tsx b/src/components/feeds/PostCard.tsx index 01ef6a6..bb7d6d7 100644 --- a/src/components/feeds/PostCard.tsx +++ b/src/components/feeds/PostCard.tsx @@ -9,15 +9,14 @@ import { PostRichText } from "$/components/shared/PostRichText"; import { ModerationController } from "$/lib/api/moderation"; import { buildPublicPostUrl, - formatRelativeTime, getAvatarLabel, getDisplayName, getPostCreatedAt, getPostFacets, getPostText, hasKnownThreadContext, - isReplyItem, } from "$/lib/feeds"; +import { isReplyItem } from "$/lib/feeds/type-guards"; import { collectModerationLabels } from "$/lib/moderation"; import type { PostEngagementTab } from "$/lib/post-engagement-routes"; import { buildProfileRoute, getProfileRouteActor } from "$/lib/profile"; @@ -30,6 +29,7 @@ import type { PostView, RichTextFacet, } from "$/lib/types"; +import { formatRelativeTime } from "$/lib/utils/text"; import { formatCount, formatHandle, normalizeError } from "$/lib/utils/text"; import * as logger from "@tauri-apps/plugin-log"; import { createMemo, createSignal, type ParentProps, Show, splitProps } from "solid-js"; diff --git a/src/components/feeds/embeds/ContentEmbed.tsx b/src/components/feeds/embeds/ContentEmbed.tsx index c628a56..5ecc452 100644 --- a/src/components/feeds/embeds/ContentEmbed.tsx +++ b/src/components/feeds/embeds/ContentEmbed.tsx @@ -1,21 +1,34 @@ import { QuotedPostPreview } from "$/components/shared/QuotedPostPreview"; -import { getQuotedAuthor, getQuotedHref, getQuotedText, getQuotedUri, postRkeyFromUri } from "$/lib/feeds"; +import { normalizeEmbed, postRkeyFromUri } from "$/lib/feeds"; +import type { NormalizedEmbed, QuotedRecordPresentation } from "$/lib/feeds"; +import { isNormalizedEmbed } from "$/lib/feeds/type-guards"; import { buildPostRoute } from "$/lib/post-routes"; -import type { EmbedView, ImagesEmbedView, PostView } from "$/lib/types"; -import { createMemo, Match, Show, Switch } from "solid-js"; +import type { EmbedView, PostView } from "$/lib/types"; +import { createMemo, For, type JSX, Show } from "solid-js"; import { ExternalEmbed } from "./ExternalEmbed"; import { ImageEmbed } from "./ImageEmbed"; import { VideoEmbed } from "./VideoEmbed"; -export function EmbedContent(props: { embed: EmbedView; onOpenPost?: (uri: string) => void; post: PostView }) { - const postRkey = createMemo(() => postRkeyFromUri(props.post.uri)); - const media = () => ("media" in props.embed ? props.embed.media : null); - const quotedUri = createMemo(() => getQuotedUri(props.embed)); +const MAX_EMBED_DEPTH = 3; + +function RecognizedEmbedNotice(props: { message: string }) { + return ( +
+

{props.message}

+
+ ); +} + +function RenderQuotedPreview( + props: { quoted: QuotedRecordPresentation; depth: number; post: PostView; onOpenPost?: (uri: string) => void }, +) { + const quotedExternalHref = createMemo(() => props.quoted.href); + const quotedUri = createMemo(() => props.quoted.uri); const quotedInternalHref = createMemo(() => { const uri = quotedUri(); return uri ? `#${buildPostRoute(uri)}` : null; }); - const quotedExternalHref = createMemo(() => getQuotedHref(props.embed)); + const openQuotedPost = () => { const uri = quotedUri(); if (!uri || !props.onOpenPost) { @@ -25,49 +38,123 @@ export function EmbedContent(props: { embed: EmbedView; onOpenPost?: (uri: strin props.onOpenPost(uri); }; + const quotedPostForEmbeds = createMemo(() => { + const value = props.quoted; + if (value.kind !== "post" || !value.uri) { + return null; + } + + return { + author: value.author ?? props.post.author, + cid: "", + indexedAt: props.post.indexedAt, + record: { createdAt: props.post.indexedAt, facets: value.facets ?? [], text: value.text ?? "" }, + uri: value.uri, + }; + }); + return ( - - - - - - - - - - - - - - -
- - {(current) => ( - - )} + <> + + + {(quotedPost) => ( + 0}> +
+ + {(embed) => ( + + )} + +
- -
-
-
+ )} +
+ ); } + +export function EmbedContent( + props: { depth?: number; embed: EmbedView | NormalizedEmbed; onOpenPost?: (uri: string) => void; post: PostView }, +) { + const depth = createMemo(() => props.depth ?? 0); + const normalized = createMemo(() => + isNormalizedEmbed(props.embed) ? props.embed : normalizeEmbed(props.embed, { depth: depth(), source: "top" }) + ); + const postRkey = createMemo(() => postRkeyFromUri(props.post.uri)); + + const content = createMemo(() => { + const embed = normalized(); + if (depth() >= MAX_EMBED_DEPTH || embed.meta.depthLimited) { + return ; + } + if (embed.meta.cycle) { + return ; + } + + switch (embed.kind) { + case "images": { + return ; + } + case "external": { + return ( + + ); + } + case "video": { + return ( + + ); + } + case "record": { + return ( + + ); + } + case "recordWithMedia": { + return ( +
+ + {(mediaEmbed) => ( + + )} + + +
+ ); + } + default: { + return null; + } + } + }); + + return <>{content()}; +} diff --git a/src/components/feeds/embeds/ExternalEmbed.tsx b/src/components/feeds/embeds/ExternalEmbed.tsx index 66f67ee..2f013f4 100644 --- a/src/components/feeds/embeds/ExternalEmbed.tsx +++ b/src/components/feeds/embeds/ExternalEmbed.tsx @@ -3,7 +3,7 @@ import { Show } from "solid-js"; export function ExternalEmbed(props: { description?: string; thumb?: string; title?: string; uri?: string }) { return ( props.embed.images.slice(0, 4)); const postRkey = createMemo(() => postRkeyFromUri(props.post.uri)); @@ -118,7 +105,7 @@ export function ImageEmbed(props: { embed: ImagesEmbedView; post: PostView }) { queueNotice({ kind: "success", message: `Saved ${filenameFromPath(result.path)}.`, path: result.path }); } catch (error) { - queueNotice({ kind: "error", message: toDownloadErrorMessage(error) }); + queueNotice({ kind: "error", message: toDownloadErrorMessage(error, "Couldn't save this image right now.") }); } finally { setDownloadPending(false); } @@ -131,7 +118,7 @@ export function ImageEmbed(props: { embed: ImagesEmbedView; post: PostView }) { {(image, index) => (