Skip to content

refactor: improve canvas preview and layout consistency #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 13, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -20,18 +20,14 @@ const CanvasWithReplies = (props: { canvas?: CanvasDB }) => {
const { peer } = usePeer();

return (
<div className="p-5 flex flex-col">
<div className="flex flex-col">
<Header canvas={props.canvas} />
<div className="rounded-md flex">
<CanvasWrapper canvas={props.canvas}>
<Canvas draft={false} />
</CanvasWrapper>
</div>
{showReplies && (
<div className="mt-[3px] p-2 rounded-md">
<RepliesView canvas={props.canvas} />
</div>
)}
{showReplies && <RepliesView canvas={props.canvas} />}
</div>
);
};
176 changes: 163 additions & 13 deletions packages/social-media-app/frontend/src/canvas/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,168 @@
import { Canvas as CanvasDB } from "@dao-xyz/social";
import {
Canvas as CanvasDB,
StaticContent,
StaticImage,
StaticMarkdownText,
ElementContent,
Element,
} from "@dao-xyz/social";
import { Canvas } from "./Canvas";
import { CanvasWrapper } from "./CanvasWrapper";
export const CanvasPreview = (properties: { canvas: CanvasDB }) => {
import { CanvasWrapper, useCanvas } from "./CanvasWrapper";
import { useMemo } from "react";
import { Frame } from "../content/Frame";

interface CanvasPreviewProps {
variant: "tiny" | "post";
}

const rectIsStaticMarkdownText = (rect: Element<ElementContent>): boolean => {
return (
<CanvasWrapper canvas={properties.canvas}>
<div className="w-full flex flex-col items-center relative overflow-hidden">
{/* Real image preview */}
<Canvas fitHeight />
<div className="absolute inset-0 -z-10">
<div className="relative blur-xl w-full h-full">
<Canvas fitHeight fitWidth />
rect.content instanceof StaticContent &&
rect.content.content instanceof StaticMarkdownText
);
};

const rectIsStaticImage = (rect: Element<ElementContent>): boolean => {
return (
rect.content instanceof StaticContent &&
rect.content.content instanceof StaticImage
);
};

const PreviewFrame = ({
element,
coverParent,
previewLines,
}: {
element: Element<ElementContent>;
coverParent: boolean;
previewLines?: number;
}) => (
<Frame
thumbnail={false}
active={false}
setActive={(v) => {}}
delete={() => {}}
editMode={false}
showCanvasControls={false}
element={element}
replace={() => {}}
onLoad={() => {}}
onContentChange={() => {}}
pending={false}
coverParent={coverParent}
fit="cover"
/>
);

interface SeparatedRects {
text: Element<ElementContent>[];
other: Element<ElementContent>[];
}

/* Separates rects by preview relevant types: text and other. Also sorts by y layout location. */
const seperateAndSortRects = (rects: Element<ElementContent>[]) => {
const seperatedRects: SeparatedRects = { text: [], other: [] };

rects.forEach((rect) => {
if (rectIsStaticMarkdownText(rect)) {
seperatedRects.text.push(rect);
} else {
seperatedRects.other.push(rect);
}
});

seperatedRects.text.sort((a, b) => a.location[0].y - b.location[0].y);
seperatedRects.other.sort((a, b) => a.location[0].y - b.location[0].y);

return seperatedRects;
};

type RectsForVariant<V> = V extends "tiny"
? Element<ElementContent> | undefined
: V extends "post"
? { text?: Element<ElementContent>; other: Element<ElementContent>[] }
: never;

function getRectsForVariant<Variant extends "tiny" | "post">(
separatedRects: SeparatedRects,
variant: Variant
): RectsForVariant<Variant> {
// get image, or if not present text, or if not present undefined
switch (variant) {
case "tiny":
return (separatedRects.other[0] ??
separatedRects.text[0] ??
undefined) as RectsForVariant<Variant>;
case "post":
return {
text: separatedRects.text[0],
other: separatedRects.other,
} as RectsForVariant<Variant>;
}
return undefined;
}

export const CanvasPreview = ({ variant }: CanvasPreviewProps) => {
const { pendingRects, rects, canvas } = useCanvas();

const variantRects = useMemo(
() =>
getRectsForVariant(
seperateAndSortRects([...rects, ...pendingRects]),
variant
),
[rects, pendingRects, variant]
);
// variantRects needs to be defined.
// TODO @marcus @ben - investigate why it isnt for some previews!
if (!variantRects) return null;
if (variant === "tiny") {
return (
<PreviewFrame
element={variantRects as RectsForVariant<"tiny">}
coverParent={true}
/>
);
}
if (variant === "post") {
const [firstApp, ...secondaryApps] = (
variantRects as RectsForVariant<"post">
).other;
const text = (variantRects as RectsForVariant<"post">).text;
return (
<div className="w-full flex flex-col gap-4">
{firstApp && (
<div className="w-full max-h-[40vh] rounded-md overflow-hidden">
<PreviewFrame element={firstApp} coverParent={true} />
</div>
)}
{secondaryApps.length > 0 && (
<div className="flex overflow-x-scroll no-scrollbar px-2.5">
{secondaryApps.map((app, i) => (
<div
className="aspect-[1] w-12 rounded-md overflow-hidden"
key={i}
>
<PreviewFrame
element={app}
coverParent={true}
/>
</div>
))}
</div>
</div>
)}

{text && (
<div className="px-2.5">
<PreviewFrame
element={text}
coverParent={false}
previewLines={3}
/>
</div>
)}
</div>
</CanvasWrapper>
);
);
}
};
60 changes: 34 additions & 26 deletions packages/social-media-app/frontend/src/canvas/Reply.tsx
Original file line number Diff line number Diff line change
@@ -3,10 +3,10 @@ import { Canvas as CanvasDB } from "@dao-xyz/social";
import { usePeer } from "@peerbit/react";
import { CanvasPreview } from "./Preview";
import { WithContext } from "@peerbit/document";
import RelativeTimestamp from "./header/RelativeTimestamp";
import { useNavigate } from "react-router-dom";
import { getCanvasPath } from "../routes";
import { Header } from "./header/Header";
import { CanvasWrapper } from "./CanvasWrapper";

// Debounce helper that triggers on the leading edge and then ignores calls for the next delay ms.
function debounceLeading(func: (...args: any[]) => void, delay: number) {
@@ -24,6 +24,20 @@ function debounceLeading(func: (...args: any[]) => void, delay: number) {
};
}

const ReplyButton = ({
children,
...rest
}: React.PropsWithChildren<React.ButtonHTMLAttributes<HTMLButtonElement>>) => {
return (
<button
className="border border-black rounded-md px-1.5 py-1 bg-white"
{...rest}
>
{children}
</button>
);
};

export const Reply = (properties: { canvas: WithContext<CanvasDB> }) => {
const [replyCount, setReplyCount] = useState(0);
const { peer } = usePeer();
@@ -58,34 +72,28 @@ export const Reply = (properties: { canvas: WithContext<CanvasDB> }) => {
}, [properties.canvas, properties.canvas.closed]);

return (
<div>
<div className=" w-full flex flex-row p-0 border border-solid max-h-[40vh] overflow-hidden">
<Header canvas={properties.canvas} direction="col" />
<button
className="btn w-full flex flex-row"
onClick={async () => {
navigate(getCanvasPath(properties.canvas), {});
}}
>
<CanvasPreview canvas={properties.canvas} />
</button>
<div className="py-4">
<div className="px-2.5 mb-2.5">
<Header canvas={properties.canvas} direction="row" />
</div>

<div className="flex w-full mt-1">
<span className="mr-auto text-sm underline">
{`Replies (${replyCount})`}
</span>
<RelativeTimestamp
timestamp={
new Date(
Number(
properties.canvas.__context.created /
BigInt(1000000)
)
)
<button
onClick={async () => {
navigate(getCanvasPath(properties.canvas), {});
}}
className="w-full flex flex-row p-0 overflow-hidden"
>
<CanvasWrapper canvas={properties.canvas}>
<CanvasPreview variant="post" />
</CanvasWrapper>
</button>
<div className="flex gap-2.5 px-2.5 mt-4">
<ReplyButton>Show more</ReplyButton>
<ReplyButton
onClick={async () =>
navigate(getCanvasPath(properties.canvas), {})
}
className="ml-auto text-sm"
/>
>{`Open | Reply (${replyCount})`}</ReplyButton>
</div>
</div>
);
25 changes: 19 additions & 6 deletions packages/social-media-app/frontend/src/canvas/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { useState } from "react";
import { PublicSignKey } from "@peerbit/crypto";
import { ProfileButton } from "../../profile/ProfileButton";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { HiDotsHorizontal } from "react-icons/hi";
import { usePeer } from "@peerbit/react";
import { useProfiles } from "../../profile/useProfiles";
import { Canvas } from "@dao-xyz/social";
import RelativeTimestamp from "./RelativeTimestamp";
import { WithContext } from "@peerbit/document";

// Assume peer is imported or available from context

export const Header = (properties: {
canvas?: Canvas;
canvas?: Canvas | WithContext<Canvas>;
direction?: "row" | "col";
}) => {
const [bgColor, setBgColor] = useState("transparent");
@@ -26,11 +27,9 @@ export const Header = (properties: {
<>
{properties.canvas && (
<div
className={`flex items-center gap-4 ${
className={`flex items-center gap-6 ${
properties.direction === "col" ? "flex-col" : "flex-row"
}
bg-[linear-gradient(333deg,rgba(255,255,255,0.6)_34%,var(--bgcolor)_79%)]
dark:bg-[linear-gradient(333deg,rgba(31,41,55,0.6)_34%,var(--bgcolor)_79%)]`}
}`}
style={
{
"--bgcolor": bgColor
@@ -43,6 +42,20 @@ export const Header = (properties: {
publicKey={properties.canvas.publicKey}
setBgColor={setBgColor}
/>
{"__context" in properties.canvas && (
<RelativeTimestamp
timestamp={
new Date(
Number(
properties.canvas.__context.created /
BigInt(1000000)
)
)
}
className="text-sm"
/>
)}

{/* Additional management menu for the post if the user is the author */}
{isOwner && (
<DropdownMenu.Root>
11 changes: 10 additions & 1 deletion packages/social-media-app/frontend/src/content/Frame.tsx
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ export const Frame = (properties: {
delete(): void;
coverParent?: boolean;
fit?: "cover" | "contain";
previewLines?: number;
}) => {
const navigate = useNavigate();

@@ -45,7 +46,14 @@ export const Frame = (properties: {
}
};

const renderContent = ({ coverParent }: { coverParent?: boolean }) => {
const renderContent = ({
coverParent,
previewLines,
}: {
coverParent?: boolean;

previewLines?: number;
}) => {
// For iframes, continue to use the iframe as before.
if (properties.element.content instanceof IFrameContent) {
return (
@@ -80,6 +88,7 @@ export const Frame = (properties: {
}}
coverParent={coverParent}
fit={properties.fit}
previewLines={previewLines}
/>
);
}
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ export type MarkdownContentProps = {
editable?: boolean;
onChange?: (newContent: StaticMarkdownText) => void;
thumbnail?: boolean;
previewLines?: number;
};

export const MarkdownContent = ({
@@ -16,6 +17,7 @@ export const MarkdownContent = ({
editable = false,
onChange,
thumbnail,
previewLines,
}: MarkdownContentProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const lastDims = useRef<{ width: number; height: number } | null>(null);
@@ -71,7 +73,7 @@ export const MarkdownContent = ({
}
}, [text, isEditing, autoResize]);

const padding = !thumbnail ? "px-4 py-2" : "p-1";
const padding = !thumbnail ? "" : "p-1";

// When the user clicks the container (and we're editable), start editing.
const handleStartEditing = () => {
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export type EditableStaticContentProps = {
thumbnail?: boolean;
coverParent?: boolean;
fit?: "cover" | "contain";
previewLines?: number;
};

export const EditableStaticContent = ({
@@ -24,6 +25,7 @@ export const EditableStaticContent = ({
thumbnail,
coverParent,
fit,
previewLines,
}: EditableStaticContentProps) => {
if (staticContent instanceof StaticMarkdownText) {
return (
@@ -33,6 +35,7 @@ export const EditableStaticContent = ({
editable={editable}
onChange={onChange}
thumbnail={thumbnail}
previewLines={previewLines}
/>
);
}
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import { CanvasPreview } from "../canvas/Preview";
import { ProfilePhotoGenerated } from "./ProfilePhotoGenerated";
import { useNavigate } from "react-router-dom";
import { getCanvasPath, MISSING_PROFILE } from "../routes";
import { CanvasWrapper } from "../canvas/CanvasWrapper";

// Extend the props type so any additional button props are allowed
export const ProfileButton = forwardRef<
@@ -31,8 +32,10 @@ export const ProfileButton = forwardRef<
]);

const content = profile ? (
<div className="w-[40px] h-[40px]">
<CanvasPreview canvas={profile.profile} />
<div className="w-8 h-8">
<CanvasWrapper canvas={profile.profile}>
<CanvasPreview variant="tiny" />
</CanvasWrapper>
</div>
) : (
<ProfilePhotoGenerated