diff --git a/packages/social-media-app/frontend/package.json b/packages/social-media-app/frontend/package.json index bacc93a7..8002c340 100644 --- a/packages/social-media-app/frontend/package.json +++ b/packages/social-media-app/frontend/package.json @@ -23,6 +23,8 @@ "@radix-ui/react-toggle": "^1.0.3", "axios": "^1.4.0", "iframe-resizer": "^5", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-to-string": "^4.0.0", "peerbit": "^4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -35,12 +37,12 @@ }, "devDependencies": { "@peerbit/vite": "^1", + "@tailwindcss/vite": "^4.0.14", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4", "autoprefixer": "^10.4.20", "tailwindcss": "^4.0.14", - "@tailwindcss/vite": "^4.0.14", "typescript": "^5.6.3", "vite": "^6.0.6" }, diff --git a/packages/social-media-app/frontend/src/Header.tsx b/packages/social-media-app/frontend/src/Header.tsx index d5b097c5..d41e1056 100644 --- a/packages/social-media-app/frontend/src/Header.tsx +++ b/packages/social-media-app/frontend/src/Header.tsx @@ -81,7 +81,7 @@ export const Header = forwardRef((props: HeaderProps, ref) => { {/* helper block to cover the distance between overlay and breadcrumb bar */} {isBreadcrumbExpanded && ( -
+
)} diff --git a/packages/social-media-app/frontend/src/Home.tsx b/packages/social-media-app/frontend/src/Home.tsx index e68b27c4..01a1c32f 100644 --- a/packages/social-media-app/frontend/src/Home.tsx +++ b/packages/social-media-app/frontend/src/Home.tsx @@ -1,9 +1,12 @@ import { CanvasAndReplies } from "./canvas/CanvasAndReplies"; +import { ViewProvider } from "./view/View"; export const Home = () => { return ( <> - + + + ); }; diff --git a/packages/social-media-app/frontend/src/canvas/CanvasAndReplies.tsx b/packages/social-media-app/frontend/src/canvas/CanvasAndReplies.tsx index 34f800d5..95a41e1b 100644 --- a/packages/social-media-app/frontend/src/canvas/CanvasAndReplies.tsx +++ b/packages/social-media-app/frontend/src/canvas/CanvasAndReplies.tsx @@ -14,9 +14,9 @@ export const CanvasAndReplies = () => { const { peer } = usePeer(); const { root, path: canvases, loading } = useCanvases(); const [lastCanvas, setLastCanvas] = useState(undefined); - const [sortCriteria, setSortCriteria] = useState<"new" | "old" | "best">( - "new" - ); + const [sortCriteria, setSortCriteria] = useState< + "new" | "old" | "best" | "chat" + >("new"); // Refs for header, toolbar, and scroll container const toolbarRef = useRef(null); @@ -122,18 +122,21 @@ export const CanvasAndReplies = () => { > {/* Set the scroll container height dynamically */}
-
- {/* dont show header on root post */} - {canvases.length > 1 && ( -
- )} - - - +
+
+ {/* dont show header on root post */} + {canvases.length > 1 && ( +
+ )} + + + +
+ void; -} + variant: VariantType; +}; + +type StandardVariantProps = BaseCanvasPreviewProps & { + variant: Exclude; + align?: never; // Explicitly forbid align +}; + +type ChatMessageVariantProps = BaseCanvasPreviewProps & { + variant: "chat-message"; + align: "left" | "right"; +}; + +type CanvasPreviewProps = StandardVariantProps | ChatMessageVariantProps; const PreviewFrame = ({ element, @@ -25,6 +47,7 @@ const PreviewFrame = ({ fit, noPadding, onClick, + charLimit, }: { element: Element; previewLines?: number; @@ -33,6 +56,7 @@ const PreviewFrame = ({ fit?: "cover" | "contain"; noPadding?: boolean; onClick?: () => void; + charLimit?: number; }) => (
{}} + setActive={() => {}} delete={() => {}} editMode={false} showEditControls={false} @@ -57,222 +81,357 @@ const PreviewFrame = ({ onClick={onClick} /> {bgBlur && ( - <> - - - - - -
- {}} - delete={() => {}} - editMode={false} - showEditControls={false} - element={element} - replace={async () => {}} - onLoad={() => {}} - onContentChange={() => {}} - pending={false} - fit="cover" - noPadding={noPadding} - /> -
- + )}
); +const BlurredBackground = ({ + element, + noPadding, +}: { + element: Element; + noPadding?: boolean; +}) => ( + <> + + + + + +
+ {}} + delete={() => {}} + editMode={false} + showEditControls={false} + element={element} + replace={async () => {}} + onLoad={() => {}} + onContentChange={() => {}} + pending={false} + fit="cover" + noPadding={noPadding} + /> +
+ +); + interface SeparatedRects { text: Element[]; other: Element[]; } /* Separates rects by preview relevant types: text and other. Also sorts by y layout location. */ -const seperateAndSortRects = (rects: Element[]) => { - const seperatedRects: SeparatedRects = { text: [], other: [] }; +const separateAndSortRects = ( + rects: Element[] +): SeparatedRects => { + const separatedRects: SeparatedRects = { text: [], other: [] }; rects.forEach((rect) => { if (rectIsStaticMarkdownText(rect)) { - seperatedRects.text.push(rect); + separatedRects.text.push(rect); } else { - seperatedRects.other.push(rect); + separatedRects.other.push(rect); } }); - seperatedRects.text.sort((a, b) => a.location.y - b.location.y); - seperatedRects.other.sort((a, b) => a.location.y - b.location.y); + separatedRects.text.sort((a, b) => a.location.y - b.location.y); + separatedRects.other.sort((a, b) => a.location.y - b.location.y); - return seperatedRects; + return separatedRects; }; -type RectsForVariant< - V extends "tiny" | "post" | "breadcrumb" | "expanded-breadcrumb" -> = V extends "tiny" - ? Element | undefined - : V extends "breadcrumb" +type RectsForVariant = V extends "tiny" | "breadcrumb" ? Element | undefined - : V extends "expanded-breadcrumb" - ? { text?: Element; other: Element[] } - : V extends "post" - ? { text?: Element; other: Element[] } - : never; - -function getRectsForVariant< - Variant extends "tiny" | "post" | "breadcrumb" | "expanded-breadcrumb" ->(separatedRects: SeparatedRects, variant: Variant): RectsForVariant { + : { + text?: Element; + other: Element[]; + }; + +function getRectsForVariant( + separatedRects: SeparatedRects, + variant: V +): RectsForVariant { switch (variant) { case "tiny": case "breadcrumb": return (separatedRects.other[0] ?? separatedRects.text[0] ?? - undefined) as RectsForVariant; + undefined) as RectsForVariant; case "post": case "expanded-breadcrumb": + case "chat-message": return { text: separatedRects.text[0], other: separatedRects.other, - } as RectsForVariant; + } as RectsForVariant; } } +const TinyPreview = ({ + rect, + onClick, +}: { + rect: Element; + onClick?: () => void; +}) => ( + +); + +const BreadcrumbPreview = ({ + rect, + onClick, +}: { + rect: Element; + onClick?: () => void; +}) => { + let isText: boolean = false; + let textLength: number | undefined = undefined; + if ( + rect.content instanceof StaticContent && + rect.content.content instanceof StaticMarkdownText + ) { + isText = rectIsStaticMarkdownText(rect); + textLength = toString(fromMarkdown(rect.content.content.text)).length; + } + + return ( +
10 ? "w-[10ch]" : "w-fit") : "w-6", + isText && "px-1", + "flex-none h-6 rounded-md overflow-hidden border border-neutral-950 dark:border-neutral-50" + )} + > + +
+ ); +}; + +const ExpandedBreadcrumbPreview = ({ + rects, + onClick, +}: { + rects: { text?: Element; other: Element[] }; + onClick?: () => void; +}) => { + const { other: apps, text } = rects; + + return ( +
+ {apps.slice(0, 2).map((app, i) => ( +
+ + {i === 1 && apps.slice(2).length > 0 && ( +
+ +{apps.slice(2).length} +
+ )} +
+ ))} + + {text && ( +
+ +
+ )} +
+ ); +}; + +const PostPreview = ({ + rects, + onClick, +}: { + rects: { text?: Element; other: Element[] }; + onClick?: () => void; +}) => { + const [firstApp, ...secondaryApps] = rects.other; + const { text } = rects; + + return ( + <> + {firstApp && ( + + )} + + {secondaryApps.length > 0 && ( +
+ {secondaryApps.map((app, i) => ( + + ))} +
+ )} + + {text && ( + + )} + + ); +}; + +const ChatMessagePreview = ({ + rects, + onClick, +}: { + rects: { text?: Element; other: Element[] }; + onClick?: () => void; +}) => { + const { other: apps, text } = rects; + + return ( + <> + {apps.map((app) => ( + + ))} + + {text && ( + + )} + + ); +}; + export const CanvasPreview = ({ variant, onClick }: CanvasPreviewProps) => { - const { pendingRects, rects, canvas } = useCanvas(); + const { pendingRects, rects } = useCanvas(); const variantRects = useMemo( () => getRectsForVariant( - seperateAndSortRects([...rects, ...pendingRects]), + separateAndSortRects([...rects, ...pendingRects]), variant ), [rects, pendingRects, variant] ); if (!variantRects) return null; - if (variant === "tiny") { - return ( - } - fit="cover" - maximizeHeight - onClick={onClick} - /> - ); - } - if (variant === "breadcrumb") { - return ( -
- ) - ? "w-[10ch] max-w-20% px-1" - : "w-6" - } flex-none h-6 rounded-md overflow-hidden border border-neutral-950 dark:border-neutral-50`} - > - } - fit="cover" - previewLines={1} - noPadding={rectIsStaticMarkdownText( - variantRects as RectsForVariant<"breadcrumb"> - )} - maximizeHeight + + // Render the appropriate component based on variant + switch (variant) { + case "tiny": + return ( + } onClick={onClick} /> -
- ); - } - if (variant === "expanded-breadcrumb") { - const variantRectsTyped = - variantRects as RectsForVariant<"expanded-breadcrumb">; - const apps = variantRectsTyped.other || []; - const text = variantRectsTyped.text; - - return ( -
- {apps.slice(0, 2).map((app, i) => ( -
- - {i === 1 && apps.slice(2).length > 0 && ( -
- +{apps.slice(2).length} -
- )} -
- ))} - - {text && ( -
- -
- )} -
- ); - } - if (variant === "post") { - const [firstApp, ...secondaryApps] = ( - variantRects as RectsForVariant<"post"> - ).other; - const text = (variantRects as RectsForVariant<"post">).text; - return ( -
- {firstApp && ( -
- -
- )} - {secondaryApps.length > 0 && ( -
- {secondaryApps.map((app, i) => ( -
- -
- ))} -
- )} - - {text && ( - - )} -
- ); + ); + + case "breadcrumb": + return ( + } + onClick={onClick} + /> + ); + + case "expanded-breadcrumb": + return ( + ; + other: Element[]; + } + } + onClick={onClick} + /> + ); + + case "post": + return ( + ; + other: Element[]; + } + } + onClick={onClick} + /> + ); + + case "chat-message": + return ( + ; + other: Element[]; + } + } + onClick={onClick} + /> + ); + + default: + return null; } }; diff --git a/packages/social-media-app/frontend/src/canvas/Replies.tsx b/packages/social-media-app/frontend/src/canvas/Replies.tsx index 3689cad1..e8940bc5 100644 --- a/packages/social-media-app/frontend/src/canvas/Replies.tsx +++ b/packages/social-media-app/frontend/src/canvas/Replies.tsx @@ -6,9 +6,11 @@ import { SearchRequest } from "@peerbit/document-interface"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { Reply } from "./Reply"; +import { useView } from "../view/View"; import { OnlineProfilesDropdown } from "../profile/OnlinePeersButton"; +import { tw } from "../utils/tailwind"; -type SortCriteria = "new" | "old" | "best"; +type SortCriteria = "new" | "old" | "best" | "chat"; interface RepliesProps { canvas?: CanvasDB; @@ -21,12 +23,20 @@ export const Replies = (props: RepliesProps) => { const [query, setQuery] = useState< { query: SearchRequest; id: string } | undefined >(undefined); + const { setView, view } = useView(); const { peers } = useOnline(canvas, { id: canvas?.idString, }); useEffect(() => { + // Set the view based on sortCriteria + if (sortCriteria === "chat") { + setView("chat"); + } else { + setView("thread"); + } + if (sortCriteria === "best") { setQuery({ query: new SearchRequest({ @@ -51,7 +61,7 @@ export const Replies = (props: RepliesProps) => { sort: new Sort({ key: ["__context", "created"], direction: - sortCriteria === "new" + sortCriteria === "new" || sortCriteria === "chat" ? SortDirection.ASC : SortDirection.DESC, }), @@ -59,57 +69,81 @@ export const Replies = (props: RepliesProps) => { id: sortCriteria, }); } - }, [sortCriteria]); + }, [sortCriteria, setView]); const sortedReplies = useLocal(canvas?.replies, query); return (
-
- - - Replies sorted by {sortCriteria} - - - - setSortCriteria("new")} +
+
+ + + Replies sorted by {sortCriteria} + + + - New - - setSortCriteria("old")} - > - Old - - setSortCriteria("best")} - > - Best - - - + {["new", "old", "best", "chat"].map( + (sortCriterium, index) => ( + + setSortCriteria( + sortCriterium as any + ) + } + > + {sortCriterium.charAt(0).toUpperCase() + + sortCriterium.slice(1)} + + ) + )} + + +
{/* */}
{sortedReplies.length > 0 ? ( -
- {sortedReplies.map((reply) => ( - +
+ {sortedReplies.map((reply, i) => ( + <> +
+ + ))}
) : ( -
+
No replies yet
)} diff --git a/packages/social-media-app/frontend/src/canvas/Reply.tsx b/packages/social-media-app/frontend/src/canvas/Reply.tsx index b1cf1320..82ddfb66 100644 --- a/packages/social-media-app/frontend/src/canvas/Reply.tsx +++ b/packages/social-media-app/frontend/src/canvas/Reply.tsx @@ -9,6 +9,8 @@ import { getCanvasPath } from "../routes"; import { Header } from "./header/Header"; import { CanvasWrapper } from "./CanvasWrapper"; import { LuMessageSquare } from "react-icons/lu"; +import { useView, ViewType } from "../view/View"; +import { tw } from "../utils/tailwind"; // 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) { @@ -40,22 +42,65 @@ const ReplyButton = ({ ); }; +/** + * Arrow svg for expanded breadcrumb view. + * @param props - Component props + * @param props.hidden - Whether the arrow should be hidden + */ +const SvgArrowExpandedBreadcrumb = ({ hidden }: { hidden?: boolean }) => { + return ( + + ); +}; + +/** + * Reply component for displaying a Canvas reply. + * @param props - Component props + * @param props.canvas - The canvas data object to display + * @param props.variant - type for displaying the reply + * - "chat": Optimized for chat-like display + * - "thread": Standard threaded variant + * - "expanded-breadcrumb": Compact display for breadcrumbs or nested variant + * @param props.index - Optional index of the reply in a list + * @param props.onClick - Optional click handler for the reply + * @param props.hideHeader - Whether to hide the header section + */ export const Reply = ({ canvas, - variant, index, onClick, + variant = "thread", + hideHeader = false, }: { canvas: WithContext; - variant: "tiny" | "large"; + variant?: ViewType | "expanded-breadcrumb"; index?: number; onClick?: () => void; + hideHeader?: boolean; }) => { const [replyCount, setReplyCount] = useState(0); const [showMore, setShowMore] = useState(false); const { peer } = usePeer(); const navigate = useNavigate(); + const align = + canvas.publicKey === peer.identity.publicKey ? "right" : "left"; + + const isExpandedBreadcrumb = variant === "expanded-breadcrumb"; + useEffect(() => { const listener = async () => { if (canvas.closed) { @@ -81,86 +126,105 @@ export const Reply = ({ }; }, [canvas, canvas.closed]); - return ( -
-
- - - - -
-
+ const handleCanvasClick = () => { + navigate(getCanvasPath(canvas), {}); + onClick && onClick(); + }; - - {variant === "large" && ( -
- { - setShowMore((showMore) => !showMore); - onClick && onClick(); - }} - > - {showMore ? "Show less" : "Show more"} - - { - navigate(getCanvasPath(canvas), {}); - onClick && onClick(); - }} - >{`Open ${ - replyCount > 0 ? `(${replyCount})` : "" - }`} + > + + {isExpandedBreadcrumb ? ( + + ) : variant === "chat" ? ( + + ) : showMore ? ( +
+ +
+ ) : ( + + )} +
- )} -
+ {/* Gap between header and reply content */} + {variant === "thread" && !isExpandedBreadcrumb && ( +
+ { + setShowMore((showMore) => !showMore); + onClick && onClick(); + }} + > + {showMore ? "Show less" : "Show more"} + + + {`Open ${replyCount > 0 ? `(${replyCount})` : ""}`} + +
+ )} +
+ ); }; diff --git a/packages/social-media-app/frontend/src/canvas/header/Header.tsx b/packages/social-media-app/frontend/src/canvas/header/Header.tsx index 38e7caeb..cf197311 100644 --- a/packages/social-media-app/frontend/src/canvas/header/Header.tsx +++ b/packages/social-media-app/frontend/src/canvas/header/Header.tsx @@ -16,12 +16,14 @@ export const Header = ({ className, variant, onClick, + reverseLayout, }: { canvas?: Canvas | WithContext; direction?: "row" | "col"; className?: string; - variant: "tiny" | "large"; + variant: "tiny" | "large" | "medium"; onClick?: () => void; + reverseLayout?: boolean; }) => { const [bgColor, setBgColor] = useState("transparent"); const { peer } = usePeer(); @@ -34,8 +36,14 @@ export const Header = ({ <> {canvas && (
- +
+ +
{"__context" in canvas && ( diff --git a/packages/social-media-app/frontend/src/canvas/toolbar/DebugGeneratePostButton.tsx b/packages/social-media-app/frontend/src/canvas/toolbar/DebugGeneratePostButton.tsx index 5fe1bbd6..b89d741e 100644 --- a/packages/social-media-app/frontend/src/canvas/toolbar/DebugGeneratePostButton.tsx +++ b/packages/social-media-app/frontend/src/canvas/toolbar/DebugGeneratePostButton.tsx @@ -12,6 +12,7 @@ import { StaticImage, StaticMarkdownText, } from "@dao-xyz/social"; +import { Ed25519Keypair } from "@peerbit/crypto"; const generateATextInMarkdown = (length: number = 100) => { let text = ""; @@ -114,14 +115,20 @@ export const DebugGeneratePostButton = () => { ["image", "image"], "text", ]; - for (const type of postsToCreate) { + + for (const [px, type] of postsToCreate.entries()) { + let publicKeyTuse = + px % 2 === 0 + ? peer.identity.publicKey + : (await Ed25519Keypair.create()).publicKey; + const typeArray = Array.isArray(type) ? type : [type]; // create a post (canvas) that references its parent const canvas = new Canvas({ parent: new CanvasAddressReference({ canvas: path[path.length - 1], }), - publicKey: peer.identity.publicKey, + publicKey: publicKeyTuse, }); // open it (so we can insert elements) diff --git a/packages/social-media-app/frontend/src/content/Frame.tsx b/packages/social-media-app/frontend/src/content/Frame.tsx index 78723be9..d21f6e4c 100644 --- a/packages/social-media-app/frontend/src/content/Frame.tsx +++ b/packages/social-media-app/frontend/src/content/Frame.tsx @@ -6,6 +6,29 @@ import { useApps } from "./useApps"; import { CuratedWebApp } from "@giga-app/app-service"; import { HostProvider as GigaHost } from "@giga-app/sdk"; +/** + * Frame component for displaying different types of content with controls. + * + * @param props - Component props + * @param props.pending - Whether the frame is in a pending state + * @param props.element - The element to display + * @param props.active - Whether the frame is in active state + * @param props.setActive - Function to set the active state + * @param props.editMode - Whether the content is in edit mode + * @param props.showCanvasControls - Whether to show canvas controls + * @param props.thumbnail - Whether to display as a thumbnail + * @param props.replace - Function to replace content with a new URL + * @param props.onLoad - Callback when the content loads + * @param props.onContentChange - Callback when the content changes + * @param props.key - React key for the component + * @param props.delete - Function to delete the frame + * @param props.fit - How this frame should fit in its container + * @param props.previewLines - Number of lines (text) to show in preview mode + * @param props.noPadding - Whether to remove padding from contained apps + * @param props.onClick - Callback when the frame is clicked + * + * @returns Frame component with appropriate content and controls + */ export const Frame = (properties: { pending: boolean; element: Element; diff --git a/packages/social-media-app/frontend/src/content/native/Markdown.tsx b/packages/social-media-app/frontend/src/content/native/Markdown.tsx index bd4d7f73..d9cee8d5 100644 --- a/packages/social-media-app/frontend/src/content/native/Markdown.tsx +++ b/packages/social-media-app/frontend/src/content/native/Markdown.tsx @@ -19,6 +19,20 @@ const textHasOneOrMoreLines = (text: string) => { return text.split("\n").length > 1; }; +/** + * Component for rendering markdown content with various display options. + * + * @param props - Component props + * @param props.content - The markdown content to display + * @param props.onResize - Callback when the content resizes, provides dimensions {width, height} + * @param props.editable - Whether the content can be edited by the user (default: false) + * @param props.onChange - Callback when content is changed during editing + * @param props.thumbnail - Whether the content is displayed as a thumbnail + * @param props.previewLines - Number of lines to show in preview mode, content will be truncated + * @param props.noPadding - Whether to remove padding from the container + * + * @returns Rendered markdown content + */ export const MarkdownContent = ({ content, onResize, @@ -43,7 +57,7 @@ export const MarkdownContent = ({ useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { - for (let entry of entries) { + for (const entry of entries) { const { width, height } = entry.contentRect; const newDims = { width, height }; if ( diff --git a/packages/social-media-app/frontend/src/content/native/NativeContent.tsx b/packages/social-media-app/frontend/src/content/native/NativeContent.tsx index 1641ca89..af33e381 100644 --- a/packages/social-media-app/frontend/src/content/native/NativeContent.tsx +++ b/packages/social-media-app/frontend/src/content/native/NativeContent.tsx @@ -7,6 +7,9 @@ import { MarkdownContent } from "./Markdown"; import { ImageContent } from "./image/Image"; import { ChangeCallback } from "./types"; +/** + * Props for the EditableStaticContent component. + */ export type EditableStaticContentProps = { staticContent: StaticContent["content"]; onResize: (dims: { width: number; height: number }) => void; @@ -19,6 +22,21 @@ export type EditableStaticContentProps = { inFullscreen?: boolean; }; +/** + * Component for rendering different types of static content with editing capabilities. + * + * @param props - Component props + * @param props.staticContent - The static content to display + * @param props.onResize - Callback when the content resizes, provides dimensions {width, height} + * @param props.editable - Whether the content can be edited by the user (default: false) + * @param props.onChange - Callback when content is changed during editing + * @param props.thumbnail - Whether the content is displayed as a thumbnail + * @param props.fit - How images should fit in their container (cover or contain) + * @param props.previewLines - Number of lines to show in preview mode, content will be truncated + * @param props.noPadding - Whether to remove padding from the container + * + * @returns Rendered content based on type + */ export const EditableStaticContent = ({ staticContent, onResize, diff --git a/packages/social-media-app/frontend/src/context/CanvasPath.tsx b/packages/social-media-app/frontend/src/context/CanvasPath.tsx index 7740d3e4..90489783 100644 --- a/packages/social-media-app/frontend/src/context/CanvasPath.tsx +++ b/packages/social-media-app/frontend/src/context/CanvasPath.tsx @@ -1,13 +1,11 @@ import { useEffect, useState, useRef } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import TagInput from "./TagInput"; // import the controlled TagInput above import { useCanvases } from "../canvas/useCanvas"; import { getCanvasPath } from "../routes"; -import { IoIosArrowBack } from "react-icons/io"; -import { Canvas } from "../canvas/Canvas"; import { Canvas as CanvasDB } from "@dao-xyz/social"; import { CanvasWrapper } from "../canvas/CanvasWrapper"; import { CanvasPreview } from "../canvas/Preview"; +import { tw } from "../utils/tailwind"; export const CanvasPath = ({ isBreadcrumbExpanded, @@ -79,26 +77,27 @@ export const CanvasPath = ({ setIsBreadcrumbExpanded((breadcrumb) => !breadcrumb) } > -
- {path.slice(0).map((x, ix) => { - return ( -
+ {path.slice(0).map((x, ix) => ( + <> + - {ix > 1 && ( - - / - + / + + - {renderBreadcrumb(x, ix)} -
-
- ); - })} + > + {renderBreadcrumb(x, ix)} + + + ))} {
); return ( -
+
{path.slice(1).map((p, i) => ( ))}
diff --git a/packages/social-media-app/frontend/src/identity/useIdentities.tsx b/packages/social-media-app/frontend/src/identity/useIdentities.tsx index 70066ce2..68721fbb 100644 --- a/packages/social-media-app/frontend/src/identity/useIdentities.tsx +++ b/packages/social-media-app/frontend/src/identity/useIdentities.tsx @@ -1,8 +1,7 @@ import { useLocal, usePeer, useProgram } from "@peerbit/react"; import React, { useContext } from "react"; -import { Connection, Identities, Profiles } from "@dao-xyz/social"; +import { Connection, Identities } from "@dao-xyz/social"; import { And, BoolQuery, ByteMatchQuery, Or } from "@peerbit/indexer-interface"; -import { CiCircleRemove } from "react-icons/ci"; interface IIdentitiesContext { identities?: Identities; diff --git a/packages/social-media-app/frontend/src/utils/tailwind.ts b/packages/social-media-app/frontend/src/utils/tailwind.ts new file mode 100644 index 00000000..a48c76bd --- /dev/null +++ b/packages/social-media-app/frontend/src/utils/tailwind.ts @@ -0,0 +1,27 @@ +export function TW(...strings: (string | undefined)[]): TWClass { + return new TWClass(strings); +} + +export function tw(...strings: (string | undefined)[]): string { + return TW(...strings).toString(); +} + +class TWClass { + private classes: string[]; + + constructor(classes: (string | undefined)[]) { + this.classes = classes.filter( + (cls): cls is string => cls !== undefined + ); + } + + toString(): string { + return this.classes.join(" "); + } +} + +// Make TypeScript happy with the dual function/constructor pattern +export interface TW { + (...strings: (string | undefined)[]): TWClass; + new (...strings: (string | undefined)[]): TWClass; +} diff --git a/packages/social-media-app/frontend/src/view/View.tsx b/packages/social-media-app/frontend/src/view/View.tsx new file mode 100644 index 00000000..b5838625 --- /dev/null +++ b/packages/social-media-app/frontend/src/view/View.tsx @@ -0,0 +1,40 @@ +import React, { createContext, useState, useContext } from "react"; + +// Define the view type +export type ViewType = "chat" | "thread"; + +// Create the context +const ViewContext = createContext< + | { + view: ViewType; + setView: React.Dispatch>; + } + | undefined +>(undefined); + +// Custom hook to use the view context +export const useView = () => { + const context = useContext(ViewContext); + if (!context) { + throw new Error("useView must be used within a ViewProvider"); + } + return context; +}; + +// Provider component +export const ViewProvider = ({ + children, + initialView = "chat" as ViewType, +}) => { + const [view, setView] = useState(initialView); + + // Value object to be provided to consumers + const value = { + view, + setView, + }; + + return ( + {children} + ); +}; diff --git a/yarn.lock b/yarn.lock index 2819abf8..691b55a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9594,7 +9594,7 @@ mdast-util-find-and-replace@^3.0.0: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -mdast-util-from-markdown@^2.0.0: +mdast-util-from-markdown@^2.0.0, mdast-util-from-markdown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== @@ -12860,7 +12860,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12968,7 +12977,14 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13950,7 +13966,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13968,6 +13984,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"