From f0df4c7297a05bd592d8717b8997284c80fd0500 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 17 Sep 2025 15:56:00 +0700 Subject: argh --- front/src/components/composer/Composer.tsx | 67 ++++ front/src/components/composer/Snippets.tsx | 62 ++++ front/src/components/feed/Body.tsx | 174 ---------- front/src/components/feed/Card.tsx | 9 - front/src/components/feed/Composer.tsx | 52 --- front/src/components/feed/External.tsx | 41 --- front/src/components/feed/Footer.tsx | 237 -------------- front/src/components/feed/Header.tsx | 33 -- front/src/components/feed/Media.tsx | 35 -- front/src/components/feed/NostrIcon.tsx | 22 -- front/src/components/feed/Post.tsx | 79 ----- front/src/components/feed/PostData.tsx | 160 --------- front/src/components/feed/PostList.tsx | 3 +- front/src/components/feed/Quote.tsx | 37 --- front/src/components/feed/RP.tsx | 47 --- front/src/components/feed/Reactions.tsx | 118 ------- front/src/components/feed/StatsModal.tsx | 106 ------ front/src/components/layout/Sidebar.tsx | 2 +- front/src/components/post/Body.tsx | 174 ++++++++++ front/src/components/post/Card.tsx | 9 + front/src/components/post/External.tsx | 41 +++ front/src/components/post/Footer.tsx | 238 ++++++++++++++ front/src/components/post/Header.tsx | 40 +++ front/src/components/post/Loader.tsx | 160 +++++++++ front/src/components/post/Media.tsx | 35 ++ front/src/components/post/Post.tsx | 84 +++++ front/src/components/post/PostWrapper.tsx | 14 + front/src/components/post/Quote.tsx | 64 ++++ front/src/components/post/RP.tsx | 47 +++ front/src/components/post/Reactions.tsx | 118 +++++++ front/src/components/post/StatsModal.tsx | 106 ++++++ front/src/components/post/wrappers/Nostr.tsx | 15 + front/src/components/post/wrappers/NostrIcon.tsx | 22 ++ front/src/components/snippets/Snippets.tsx | 395 ----------------------- 34 files changed, 1299 insertions(+), 1547 deletions(-) create mode 100644 front/src/components/composer/Composer.tsx create mode 100644 front/src/components/composer/Snippets.tsx delete mode 100644 front/src/components/feed/Body.tsx delete mode 100644 front/src/components/feed/Card.tsx delete mode 100644 front/src/components/feed/Composer.tsx delete mode 100644 front/src/components/feed/External.tsx delete mode 100644 front/src/components/feed/Footer.tsx delete mode 100644 front/src/components/feed/Header.tsx delete mode 100644 front/src/components/feed/Media.tsx delete mode 100644 front/src/components/feed/NostrIcon.tsx delete mode 100644 front/src/components/feed/Post.tsx delete mode 100644 front/src/components/feed/PostData.tsx delete mode 100644 front/src/components/feed/Quote.tsx delete mode 100644 front/src/components/feed/RP.tsx delete mode 100644 front/src/components/feed/Reactions.tsx delete mode 100644 front/src/components/feed/StatsModal.tsx create mode 100644 front/src/components/post/Body.tsx create mode 100644 front/src/components/post/Card.tsx create mode 100644 front/src/components/post/External.tsx create mode 100644 front/src/components/post/Footer.tsx create mode 100644 front/src/components/post/Header.tsx create mode 100644 front/src/components/post/Loader.tsx create mode 100644 front/src/components/post/Media.tsx create mode 100644 front/src/components/post/Post.tsx create mode 100644 front/src/components/post/PostWrapper.tsx create mode 100644 front/src/components/post/Quote.tsx create mode 100644 front/src/components/post/RP.tsx create mode 100644 front/src/components/post/Reactions.tsx create mode 100644 front/src/components/post/StatsModal.tsx create mode 100644 front/src/components/post/wrappers/Nostr.tsx create mode 100644 front/src/components/post/wrappers/NostrIcon.tsx delete mode 100644 front/src/components/snippets/Snippets.tsx (limited to 'front/src/components') diff --git a/front/src/components/composer/Composer.tsx b/front/src/components/composer/Composer.tsx new file mode 100644 index 0000000..795188e --- /dev/null +++ b/front/src/components/composer/Composer.tsx @@ -0,0 +1,67 @@ +import useLocalState from "@/state/state"; +import type { Poast } from "@/types/trill"; +import Sigil from "@/components/Sigil"; +import { useState, type FormEvent } from "react"; +import type { ComposerData } from "@/types/ui"; +import Snippets, { ReplySnippet } from "./Snippets"; +import toast from "react-hot-toast"; +import { useLocation } from "wouter"; + +function Composer({ + isAnon, + replying, +}: { + isAnon?: boolean; + replying?: Poast; +}) { + const [loc, navigate] = useLocation(); + const { api, composerData } = useLocalState(); + const our = api!.airlock.our!; + const [input, setInput] = useState(replying ? `${replying}: ` : ""); + async function poast(e: FormEvent) { + e.preventDefault(); + // TODO + // const parent = replying ? replying : null; + // const tokens = tokenize(input); + // const post: SentPoast = { + // host: parent ? parent.host : our, + // author: our, + // thread: parent ? parent.thread : null, + // parent: parent ? parent.id : null, + // contents: input, + // read: openLock, + // write: openLock, + // tags: input.match(HASHTAGS_REGEX) || [], + // }; + // TODO make it user choosable + const res = await api!.addPost(input); + if (res) { + setInput(""); + toast.success("post sent"); + navigate(`/feed/${our}`); + } + } + const placeHolder = isAnon ? "> be me" : "What's going on in Urbit"; + return ( +
+
+ +
+ + {composerData && composerData.type === "reply" && ( + + )} + setInput(e.currentTarget.value)} + placeholder={placeHolder} + /> + {composerData && composerData.type === "quote" && ( + + )} + + + ); +} + +export default Composer; diff --git a/front/src/components/composer/Snippets.tsx b/front/src/components/composer/Snippets.tsx new file mode 100644 index 0000000..30498d0 --- /dev/null +++ b/front/src/components/composer/Snippets.tsx @@ -0,0 +1,62 @@ +import Quote from "@/components/post/Quote"; +import type { ComposerData, SPID } from "@/types/ui"; +import { NostrSnippet } from "../post/wrappers/Nostr"; + +export default Snippets; +function Snippets({ post }: { post: SPID }) { + return ( + + + + ); +} + +export function ComposerSnippet({ + onClick, + children, +}: { + onClick?: any; + children: any; +}) { + function onc(e: React.MouseEvent) { + e.stopPropagation(); + onClick(); + } + return ( +
+
+ {children} +
+ ); +} +function PostSnippet({ post }: { post: SPID }) { + if ("trill" in post) return ; + else if ("nostr" in post) return ; + // else if ("twatter" in post) + // return ( + //
+ // + //
+ // ); + // else if ("rumors" in post) + // return ( + //
+ //
+ // + // {}} /> + // {date_diff(post.post.time, "short")} + //
+ //
+ // ); + else return <>; +} + +export function ReplySnippet({ post }: { post: SPID }) { + if ("trill" in post) + return ( +
+ +
+ ); + else return
; +} diff --git a/front/src/components/feed/Body.tsx b/front/src/components/feed/Body.tsx deleted file mode 100644 index 2f11962..0000000 --- a/front/src/components/feed/Body.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import type { - // TODO ref backend fetching!! - Reference, - Block, - Inline, - Media as MediaType, - ExternalContent, -} from "@/types/trill"; -import crow from "@/assets/icons/crow.svg"; -import type { PostProps } from "./Post"; -import Media from "./Media"; -import JSONContent, { YoutubeSnippet } from "./External"; -import { useLocation } from "wouter"; -import Quote from "./Quote"; -import PostData from "./PostData"; -import Card from "./Card.tsx"; -import type { Ship } from "@/types/urbit.ts"; - -function Body(props: PostProps) { - const text = props.poast.contents.filter((c) => { - return ( - "paragraph" in c || - "blockquote" in c || - "heading" in c || - "codeblock" in c || - "list" in c - ); - }); - - const media: MediaType[] = props.poast.contents.filter( - (c): c is MediaType => "media" in c, - ); - - const refs = props.poast.contents.filter((c): c is Reference => "ref" in c); - const json = props.poast.contents.filter( - (c): c is ExternalContent => "json" in c, - ); - - return ( -
-
- {text.map((b, i) => ( - - ))} -
- {media.length > 0 && } - {refs.map((r, i) => ( - - ))} - -
- ); -} -export default Body; - -function TextBlock({ block }: { block: Block }) { - const key = JSON.stringify(block); - return "paragraph" in block ? ( -
- {block.paragraph.map((i, ind) => ( - - ))} -
- ) : "blockquote" in block ? ( -
- {block.blockquote.map((i, ind) => ( - - ))} -
- ) : "heading" in block ? ( - - ) : "codeblock" in block ? ( -
-      
-        {block.codeblock.code}
-      
-    
- ) : "list" in block ? ( - block.list.ordered ? ( -
    - {block.list.text.map((i, ind) => ( -
  1. - -
  2. - ))} -
- ) : ( -
    - {block.list.text.map((i, ind) => ( -
  • - -
  • - ))} -
- ) - ) : null; -} -function Inlin({ i }: { i: Inline }) { - const [_, navigate] = useLocation(); - function gotoShip(e: React.MouseEvent, ship: Ship) { - e.stopPropagation(); - navigate(`/feed/${ship}`); - } - return "text" in i ? ( - {i.text} - ) : "italic" in i ? ( - {i.italic} - ) : "bold" in i ? ( - {i.bold} - ) : "strike" in i ? ( - {i.strike} - ) : "underline" in i ? ( - {i.underline} - ) : "sup" in i ? ( - {i.sup} - ) : "sub" in i ? ( - {i.sub} - ) : "ship" in i ? ( - gotoShip(e, i.ship)} - > - {i.ship} - - ) : "codespan" in i ? ( - {i.codespan} - ) : "link" in i ? ( - - ) : "break" in i ? ( -
- ) : null; -} - -function LinkParser({ href, show }: { href: string; show: string }) { - const YOUTUBE_REGEX_1 = /(youtube\.com\/watch\?v=)(\w+)/; - const YOUTUBE_REGEX_2 = /(youtu\.be\/)([a-zA-Z0-9-_]+)/; - const m1 = href.match(YOUTUBE_REGEX_1); - const m2 = href.match(YOUTUBE_REGEX_2); - const ytb = m1 && m1[2] ? m1[2] : m2 && m2[2] ? m2[2] : ""; - return ytb ? ( - - ) : ( - {show} - ); -} -function Heading({ string, num }: { string: string; num: number }) { - return num === 1 ? ( -

{string}

- ) : num === 2 ? ( -

{string}

- ) : num === 3 ? ( -

{string}

- ) : num === 4 ? ( -

{string}

- ) : num === 5 ? ( -
{string}
- ) : num === 6 ? ( -
{string}
- ) : null; -} - -function Ref({ r, nest }: { r: Reference; nest: number }) { - if (r.ref.type === "nostril") { - const comp = PostData({ - host: r.ref.ship, - id: r.ref.path.slice(1), - nest: nest + 1, - className: "quote-in-post", - })(Quote); - return {comp}; - } - return <>; -} diff --git a/front/src/components/feed/Card.tsx b/front/src/components/feed/Card.tsx deleted file mode 100644 index 37f4911..0000000 --- a/front/src/components/feed/Card.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function ({ children, logo, cn}: { cn?: string; logo: string; children: any }) { - const className = "trill-post-card" + (cn ? ` ${cn}`: "") - return ( -
- - {children} -
- ); -} diff --git a/front/src/components/feed/Composer.tsx b/front/src/components/feed/Composer.tsx deleted file mode 100644 index 27da392..0000000 --- a/front/src/components/feed/Composer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { openLock } from "@/logic/bunts"; -import { HASHTAGS_REGEX } from "@/logic/constants"; -import useLocalState from "@/state/state"; -import type { Poast, SentPoast } from "@/types/trill"; -import Sigil from "@/components/Sigil"; -import { useState } from "react"; - -function Composer({ - isAnon, - replying, -}: { - isAnon?: boolean; - replying?: Poast; -}) { - const { api, keys } = useLocalState(); - const our = api!.airlock.our!; - const [input, setInput] = useState(replying ? `${replying}: ` : ""); - async function poast() { - // TODO - // const parent = replying ? replying : null; - // const tokens = tokenize(input); - // const post: SentPoast = { - // host: parent ? parent.host : our, - // author: our, - // thread: parent ? parent.thread : null, - // parent: parent ? parent.id : null, - // contents: input, - // read: openLock, - // write: openLock, - // tags: input.match(HASHTAGS_REGEX) || [], - // }; - // TODO make it user choosable - const pubkey = keys[0]!; - await api!.addPost(pubkey, input); - } - const placeHolder = isAnon ? "> be me" : "What's going on in Urbit"; - return ( -
-
- -
- setInput(e.currentTarget.value)} - placeholder={placeHolder} - /> - -
- ); -} - -export default Composer; diff --git a/front/src/components/feed/External.tsx b/front/src/components/feed/External.tsx deleted file mode 100644 index 0ea1500..0000000 --- a/front/src/components/feed/External.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { ExternalContent } from "@/types/trill"; -import youtube from "@/assets/icons/youtube.svg"; -import Card from "./Card"; - -interface JSONProps { - content: ExternalContent[]; -} - -function JSONContent({ content }: JSONProps) { - return ( - <> - {content.map((c, i) => { - if (!JSON.parse(c.json.content)) return

Error

; - else - return ( -

- External content from "{c.json.origin}", use - UFA - to display. -

- ); - })} - - ); -} -export default JSONContent; - -export function YoutubeSnippet({ href, id }: { href: string; id: string }) { - const thumbnail = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; - // todo styiling - return ( - - - - - - ); -} diff --git a/front/src/components/feed/Footer.tsx b/front/src/components/feed/Footer.tsx deleted file mode 100644 index 938a8c7..0000000 --- a/front/src/components/feed/Footer.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import type { PostProps } from "./Post"; -import reply from "@/assets/icons/reply.svg"; -import quote from "@/assets/icons/quote.svg"; -import repost from "@/assets/icons/rt.svg"; -import { useState } from "react"; -import useLocalState from "@/state/state"; -import { useLocation } from "wouter"; -import { displayCount } from "@/logic/utils"; -import { TrillReactModal, stringToReact } from "./Reactions"; -import toast from "react-hot-toast"; -import NostrIcon from "./NostrIcon"; -function Footer({ poast, refetch }: PostProps) { - const [_showMenu, setShowMenu] = useState(false); - const [location, navigate] = useLocation(); - const [reposting, _setReposting] = useState(false); - const { api, setComposerData, setModal } = useLocalState(); - const our = api!.airlock.our!; - function doReply(e: React.MouseEvent) { - e.stopPropagation(); - setComposerData({ type: "reply", post: { service: "trill", post: poast } }); - navigate("/composer"); - } - function doQuote(e: React.MouseEvent) { - e.stopPropagation(); - setComposerData({ - type: "quote", - post: { service: "trill", post: poast }, - }); - navigate("/composer"); - } - const childrenCount = poast.children - ? poast.children.length - ? poast.children.length - : Object.keys(poast.children).length - : 0; - const myRP = poast.engagement.shared.find((r) => r.pid.ship === our); - async function cancelRP(e: React.MouseEvent) { - e.stopPropagation(); - const r = await api!.deletePost(our); - if (r) toast.success("Repost deleted"); - refetch(); - if (location.includes(poast.id)) navigate("/"); - } - async function sendRP(e: React.MouseEvent) { - // TODO update backend because contents are only markdown now - e.stopPropagation(); - // const c = [ - // { - // ref: { - // type: "trill", - // ship: poast.host, - // path: `/${poast.id}`, - // }, - // }, - // ]; - // const post: SentPoast = { - // host: our, - // author: our, - // thread: null, - // parent: null, - // contents: input, - // read: openLock, - // write: openLock, - // tags: [], // TODO - // }; - // const r = await api!.addPost(post, false); - // setReposting(true); - // if (r) { - // setReposting(false); - // toast.success("Your post was published"); - // } - } - function doReact(e: React.MouseEvent) { - e.stopPropagation(); - const modal = ; - setModal(modal); - } - function showReplyCount() { - if (poast.children[0]) fetchAndShow(); // Flatpoast - // else { - // const authors = Object.keys(poast.children).map( - // (i) => poast.children[i].post.author - // ); - // setEngagement({ type: "replies", ships: authors }, poast); - // } - } - async function fetchAndShow() { - // let authors = []; - // for (let i of poast.children as string[]) { - // const res = await scrypoastFull(poast.host, i); - // if (res) - // authors.push(res.post.author || "deleter"); - // } - // setEngagement({ type: "replies", ships: authors }, poast); - } - function showRepostCount() { - // const ships = poast.engagement.shared.map((entry) => entry.host); - // setEngagement({ type: "reposts", ships: ships }, poast); - } - function showQuoteCount() { - // setEngagement({ type: "quotes", quotes: poast.engagement.quoted }, poast); - } - function showReactCount() { - // setEngagement({ type: "reacts", reacts: poast.engagement.reacts }, poast); - } - - const mostCommonReact = Object.values(poast.engagement.reacts).reduce( - (acc: any, item) => { - if (!acc.counts[item]) acc.counts[item] = 0; - acc.counts[item] += 1; - if (!acc.winner || acc.counts[item] > acc.counts[acc.winner]) - acc.winner = item; - return acc; - }, - { counts: {}, winner: "" }, - ).winner; - const reactIcon = stringToReact(mostCommonReact); - - // TODO round up all helpers - - return ( -
-
-
- - {displayCount(childrenCount)} - - -
-
- - {displayCount(poast.engagement.quoted.length)} - - -
-
- - {displayCount(poast.engagement.shared.length)} - - {reposting ? ( -

...

- ) : myRP ? ( - - ) : ( - - )} -
-
- - {displayCount(Object.keys(poast.engagement.reacts).length)} - - {reactIcon} -
- -
-
- ); -} -export default Footer; - -// function Menu({ -// poast, -// setShowMenu, -// refetch, -// }: { -// poast: Poast; -// setShowMenu: Function; -// refetch: Function; -// }) { -// const ref = useRef(null); -// const [location, navigate] = useLocation(); -// // TODO this is a mess and the event still propagates -// useEffect(() => { -// const checkIfClickedOutside = (e: any) => { -// e.stopPropagation(); -// if (ref && ref.current && !ref.current.contains(e.target)) -// setShowMenu(false); -// }; -// document.addEventListener("mousedown", checkIfClickedOutside); -// return () => { -// document.removeEventListener("mousedown", checkIfClickedOutside); -// }; -// }, []); -// const { our, setModal, setAlert } = useLocalState(); -// const mine = our === poast.host || our === poast.author; -// async function doDelete(e: React.MouseEvent) { -// e.stopPropagation(); -// deletePost(poast.host, poast.id); -// setAlert("Post deleted"); -// setShowMenu(false); -// refetch(); -// if (location.includes(poast.id)) navigate("/"); -// } -// async function copyLink(e: React.MouseEvent) { -// e.stopPropagation(); -// const link = trillPermalink(poast); -// await navigator.clipboard.writeText(link); -// // some alert -// setShowMenu(false); -// } -// function openStats(e: React.MouseEvent) { -// e.stopPropagation(); -// e.preventDefault(); -// const m = setModal(null)} />; -// setModal(m); -// } -// return ( -//
-// {/*

Share to Groups

*/} -//

-// See Stats -//

-//

-// Permalink -//

-// {mine && ( -//

-// Delete Post -//

-// )} -//
-// ); -// } diff --git a/front/src/components/feed/Header.tsx b/front/src/components/feed/Header.tsx deleted file mode 100644 index 7658bfb..0000000 --- a/front/src/components/feed/Header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { date_diff } from "@/logic/utils"; -import type { PostProps } from "./Post"; -import { useLocation } from "wouter"; -function Header(props: PostProps) { - const [_, navigate] = useLocation(); - function go(e: React.MouseEvent) { - e.stopPropagation(); - } - function openThread(e: React.MouseEvent) { - e.stopPropagation(); - const sel = window.getSelection()?.toString(); - if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); - } - const { poast } = props; - const name = ( -
-

{poast.author}

-
- ); - return ( -
-
- {name} -
-
-

- {date_diff(poast.time, "short")} -

-
-
- ); -} -export default Header; diff --git a/front/src/components/feed/Media.tsx b/front/src/components/feed/Media.tsx deleted file mode 100644 index 04ea156..0000000 --- a/front/src/components/feed/Media.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Media } from "@/types/trill"; -interface Props { - media: Media[]; -} -function M({ media }: Props) { - return ( -
- {media.map((m, i) => { - return "video" in m.media ? ( -
- ); -} -export default M; - -function Images({ urls }: { urls: string[] }) { - return ( - <> - {urls.map((u, i) => ( - - ))} - - ); -} diff --git a/front/src/components/feed/NostrIcon.tsx b/front/src/components/feed/NostrIcon.tsx deleted file mode 100644 index 0c368fb..0000000 --- a/front/src/components/feed/NostrIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import nostrIcon from "@/assets/icons/nostr.svg"; -import useLocalState from "@/state/state"; -import toast from "react-hot-toast"; -import type { Poast } from "@/types/trill"; -export default function ({ poast }: { poast: Poast }) { - const { relays, api, keys } = useLocalState(); - - async function sendToRelay(e: React.MouseEvent) { - e.stopPropagation(); - // - const urls = Object.keys(relays); - await api!.relayPost(poast.host, poast.id, urls); - toast.success("Post relayed"); - } - // TODO round up all helpers - - return ( -
- -
- ); -} diff --git a/front/src/components/feed/Post.tsx b/front/src/components/feed/Post.tsx deleted file mode 100644 index 1211a97..0000000 --- a/front/src/components/feed/Post.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { PostID, Poast, Reference } from "@/types/trill"; - -import Header from "./Header"; -import Body from "./Body"; -import Footer from "./Footer"; -import { useLocation } from "wouter"; -import useLocalState from "@/state/state"; -import RP from "./RP"; -import ShipModal from "../modals/ShipModal"; -import type { Ship } from "@/types/urbit"; -import Sigil from "../Sigil"; - -export interface PostProps { - poast: Poast; - fake?: boolean; - rter?: Ship; - rtat?: number; - rtid?: PostID; - nest?: number; - refetch: Function; -} -function Post(props: PostProps) { - const { poast } = props; - console.log({ poast }); - if (!poast || poast.contents === null) { - return null; - } - const isRP = - poast.contents.length === 1 && - "ref" in poast.contents[0] && - poast.contents[0].ref.type === "trill"; - if (isRP) { - const ref = (poast.contents[0] as Reference).ref; - return ( - - ); - } else return ; -} -export default Post; - -function TrillPost(props: PostProps) { - const { poast, fake } = props; - const { setModal } = useLocalState(); - const [_, navigate] = useLocation(); - function openThread(_e: React.MouseEvent) { - const sel = window.getSelection()?.toString(); - if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); - } - - function openModal(e: React.MouseEvent) { - e.stopPropagation(); - setModal(); - } - const avatar = ( -
- -
- ); - return ( -
-
{avatar}
-
-
- - {!fake &&
} -
-
- ); -} diff --git a/front/src/components/feed/PostData.tsx b/front/src/components/feed/PostData.tsx deleted file mode 100644 index f3c4715..0000000 --- a/front/src/components/feed/PostData.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import spinner from "@/assets/triangles.svg"; -import { useEffect, useRef, useState } from "react"; -import useLocalState from "@/state/state"; -import type { PostID } from "@/types/trill"; -import type { Ship } from "@/types/urbit"; - -function PostData(props: { - host: Ship; - id: PostID; - rter?: Ship; - rtat?: number; - rtid?: PostID; - nest?: number; // nested quotes - className?: string; -}) { - const { api } = useLocalState(); - const { host, id, nest } = props; - const [enest, setEnest] = useState(nest); - useEffect(() => { - setEnest(nest); - }, [nest]); - - return function (Component: React.ElementType) { - // const [showNested, setShowNested] = useState(nest <= 3); - const handleShowNested = (e: React.MouseEvent) => { - e.stopPropagation(); - setEnest(enest! - 3); - }; - const [dead, setDead] = useState(false); - const [denied, setDenied] = useState(false); - const { isLoading, isError, data, refetch } = useQuery({ - queryKey: ["trill-thread", host, id], - queryFn: fetchNode, - }); - const queryClient = useQueryClient(); - const dataRef = useRef(data); - useEffect(() => { - dataRef.current = data; - }, [data]); - - async function fetchNode(): Promise { - const res = await api!.scryPost(host, id, null, null); - if ("fpost" in res) return res; - else { - const existing = queryClient.getQueryData(["trill-thread", host, id]); - const existingData = existing || data; - if ("bugen" in res) { - // we peek for the actual node - peekTheNode(); - // if we have a cache we don't invalidate it - if (existingData && "fpost" in existingData) return existingData; - // if we don't have a cache then we show the loading screen - else return res; - } - if ("no-node" in res) { - if (existingData && "fpost" in existingData) return existingData; - else return res; - } - } - } - function peekTheNode() { - let timer; - peekNode({ ship: host, id }); - timer = setTimeout(() => { - const gotPost = dataRef.current && "fpost" in dataRef.current; - setDead(!gotPost); - // clearTimeout(timer); - }, 10_000); - } - - useEffect(() => { - const path = `${host}/${id}`; - if (path in peekedPosts) { - queryClient.setQueryData(["trill-thread", host, id], { - fpost: peekedPosts[path], - }); - } else if (path in deniedPosts) { - setDenied(true); - } - }, [peekedPosts]); - useEffect(() => { - const path = `${host}/${id}`; - if (path in deniedPosts) setDenied(true); - }, [deniedPosts]); - - useEffect(() => { - const l = lastThread; - if (l && l.thread == id) { - queryClient.setQueryData(["trill-thread", host, id], { fpost: l }); - } - }, [lastThread]); - function retryPeek(e: React.MouseEvent) { - e.stopPropagation(); - setDead(false); - peekTheNode(); - } - if (enest > 3) - return ( -
-
- -
-
- ); - else - return data ? ( - dead ? ( -
-
-

{host} did not respond

- -
-
- ) : denied ? ( -
-

- {host} denied you access to this post -

-
- ) : "no-node" in data || "bucun" in data ? ( -
-

Post not found

-
- ) : "bugen" in data ? ( -
-
-

Post not found, requesting...

- -
-
- ) : "fpost" in data && data.fpost.contents === null ? ( -
-

Post deleted

-
- ) : ( - - ) - ) : // no data - isLoading || isError ? ( -
- -
- ) : ( -
-

...

-
- ); - }; -} -export default PostData; diff --git a/front/src/components/feed/PostList.tsx b/front/src/components/feed/PostList.tsx index 3d41ff8..b09a0e9 100644 --- a/front/src/components/feed/PostList.tsx +++ b/front/src/components/feed/PostList.tsx @@ -1,4 +1,4 @@ -import TrillPost from "./Post"; +import TrillPost from "@/components/post/Post"; import type { FC } from "@/types/trill"; // import { useEffect } from "react"; // import { useQueryClient } from "@tanstack/react-query"; @@ -22,6 +22,7 @@ function TrillFeed({ data, refetch }: { data: FC; refetch: Function }) { {Object.keys(data.feed) .sort() .reverse() + .slice(0, 50) .map((i) => ( ))} diff --git a/front/src/components/feed/Quote.tsx b/front/src/components/feed/Quote.tsx deleted file mode 100644 index d71be40..0000000 --- a/front/src/components/feed/Quote.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { FullNode } from "@/types/trill"; -import { date_diff } from "@/logic/utils"; -import { useLocation } from "wouter"; -import Body from "./Body"; -import Sigil from "../Sigil"; -import { toFlat } from "./RP"; - -function Quote({ - data, - refetch, - nest, -}: { - data: FullNode; - refetch?: Function; - nest: number; -}) { - const [_, navigate] = useLocation(); - function gotoQuote(e: React.MouseEvent) { - e.stopPropagation(); - navigate(`/feed/${data.host}/${data.id}`); - } - return ( -
-
- ( -
- - {data.author} -
- ){date_diff(data.time, "short")} -
- -
- ); -} - -export default Quote; diff --git a/front/src/components/feed/RP.tsx b/front/src/components/feed/RP.tsx deleted file mode 100644 index dc733cc..0000000 --- a/front/src/components/feed/RP.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Post from "./Post"; -import type { Ship } from "@/types/urbit"; -import type { Poast, FullNode, ID } from "@/types/trill"; -import PostData from "./PostData"; -export default function (props: { - host: string; - id: string; - rter: Ship; - rtat: number; - rtid: ID; - refetch?: Function; -}) { - return PostData(props)(RP); -} - -function RP({ - data, - refetch, - rter, - rtat, - rtid, -}: { - data: FullNode; - refetch: Function; - rter: Ship; - rtat: number; - rtid: ID; -}) { - return ( - - ); -} - -export function toFlat(n: FullNode): Poast { - return { - ...n, - children: !n.children - ? [] - : Object.keys(n.children).map((c) => n.children[c].id), - }; -} diff --git a/front/src/components/feed/Reactions.tsx b/front/src/components/feed/Reactions.tsx deleted file mode 100644 index 58662cd..0000000 --- a/front/src/components/feed/Reactions.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import type { Poast } from "@/types/trill"; -import yeschad from "@/assets/reacts/yeschad.png"; -import cringe from "@/assets/reacts/cringe.png"; -import cry from "@/assets/reacts/cry.png"; -import doom from "@/assets/reacts/doom.png"; -import galaxy from "@/assets/reacts/galaxy.png"; -import gigachad from "@/assets/reacts/gigachad.png"; -import pepechin from "@/assets/reacts/pepechin.png"; -import pepeeyes from "@/assets/reacts/pepeeyes.png"; -import pepegmi from "@/assets/reacts/pepegmi.png"; -import pepesad from "@/assets/reacts/pepesad.png"; -import pink from "@/assets/reacts/pink.png"; -import soy from "@/assets/reacts/soy.png"; -import chad from "@/assets/reacts/chad.png"; -import pika from "@/assets/reacts/pika.png"; -import facepalm from "@/assets/reacts/facepalm.png"; -import emoji from "@/assets/icons/emoji.svg"; -import emojis from "@/logic/emojis.json"; -import Modal from "../modals/Modal"; -import useLocalState from "@/state/state"; - -export function ReactModal({ send }: { send: (s: string) => Promise }) { - const { setModal } = useLocalState(); - async function sendReact(e: React.MouseEvent, s: string) { - e.stopPropagation(); - const res = await send(s); - if (res) setModal(null); - } - // todo one more meme - return ( - -
- sendReact(e, "❤️")}>️️❤️ - sendReact(e, "🤔")}>🤔 - sendReact(e, "😅")}>😅 - sendReact(e, "🤬")}>🤬 - sendReact(e, "😂")}>😂️ - sendReact(e, "🫡")}>🫡️ - sendReact(e, "🤢")}>🤢 - sendReact(e, "😭")}>😭 - sendReact(e, "😱")}>😱 - sendReact(e, "facepalm")} - src={facepalm} - alt="" - /> - sendReact(e, "👍")}>👍️ - sendReact(e, "👎")}>👎️ - sendReact(e, "☝")}>☝️ - sendReact(e, "🤝")}>🤝️ - sendReact(e, "🙏")}>🙏 - sendReact(e, "🤡")}>🤡 - sendReact(e, "👀")}>👀 - sendReact(e, "🎤")}>🎤 - sendReact(e, "💯")}>💯 - sendReact(e, "🔥")}>🔥 - sendReact(e, "yeschad")} src={yeschad} alt="" /> - sendReact(e, "gigachad")} - src={gigachad} - alt="" - /> - sendReact(e, "pika")} src={pika} alt="" /> - sendReact(e, "cringe")} src={cringe} alt="" /> - sendReact(e, "pepegmi")} src={pepegmi} alt="" /> - sendReact(e, "pepesad")} src={pepesad} alt="" /> - sendReact(e, "galaxy")} src={galaxy} alt="" /> - sendReact(e, "pink")} src={pink} alt="" /> - sendReact(e, "soy")} src={soy} alt="" /> - sendReact(e, "cry")} src={cry} alt="" /> - sendReact(e, "doom")} src={doom} alt="" /> -
-
- ); -} - -export function stringToReact(s: string) { - const em = (emojis as Record)[s.replace(/\:/g, "")]; - if (s === "yeschad") - return ; - if (s === "facepalm") - return ; - if (s === "yes.jpg") - return ; - if (s === "gigachad") - return ; - if (s === "pepechin") - return ; - if (s === "pepeeyes") - return ; - if (s === "pepegmi") - return ; - if (s === "pepesad") - return ; - if (s === "") - return ; - if (s === "cringe") return ; - if (s === "cry") return ; - if (s === "crywojak") return ; - if (s === "doom") return ; - if (s === "galaxy") return ; - if (s === "pink") return ; - if (s === "pinkwojak") return ; - if (s === "soy") return ; - if (s === "chad") return ; - if (s === "pika") return ; - if (em) return {em}; - else if (s.length > 2) return ; - else return {s}; -} - -export function TrillReactModal({ poast }: { poast: Poast }) { - const { api } = useLocalState(); - async function sendReact(s: string) { - return await api!.addReact(poast.host, poast.id, s); - } - return ; -} diff --git a/front/src/components/feed/StatsModal.tsx b/front/src/components/feed/StatsModal.tsx deleted file mode 100644 index 4720b2a..0000000 --- a/front/src/components/feed/StatsModal.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import type { Poast } from "@/types/trill"; -import Modal from "../modals/Modal"; -import { useState } from "react"; -import Post from "./Post"; -import RP from "./RP"; -import Avatar from "../Avatar"; -import { stringToReact } from "./Reactions"; - -function StatsModal({ poast, close }: { close: any; poast: Poast }) { - const [tab, setTab] = useState("replies"); - const replies = poast.children || []; - const quotes = poast.engagement.quoted; - const reposts = poast.engagement.shared; - const reacts = poast.engagement.reacts; - function set(e: React.MouseEvent, s: string) { - e.stopPropagation(); - setTab(s); - } - // TODO revise the global thingy here - return ( - -
- {}} /> -
-
set(e, "replies")} - > -

Replies

-
-
set(e, "quotes")} - > -

Quotes

-
-
set(e, "reposts")} - > -

Reposts

-
-
set(e, "reacts")} - > -

Reacts

-
-
-
- {tab === "replies" ? ( -
- {replies.map((p) => ( -
- -
- ))} -
- ) : tab === "quotes" ? ( -
- {quotes.map((p) => ( -
- -
- ))} -
- ) : tab === "reposts" ? ( -
- {reposts.map((p) => ( -
- -
- ))} -
- ) : tab === "reacts" ? ( -
- {Object.keys(reacts).map((p) => ( -
- - {stringToReact(reacts[p])} -
- ))} -
- ) : null} -
-
-
- ); -} -export default StatsModal; diff --git a/front/src/components/layout/Sidebar.tsx b/front/src/components/layout/Sidebar.tsx index 1568421..4055454 100644 --- a/front/src/components/layout/Sidebar.tsx +++ b/front/src/components/layout/Sidebar.tsx @@ -22,7 +22,7 @@ function SlidingMenu() {

Feeds

goto(`/feed/global`)}> diff --git a/front/src/components/post/Body.tsx b/front/src/components/post/Body.tsx new file mode 100644 index 0000000..2e4e2f8 --- /dev/null +++ b/front/src/components/post/Body.tsx @@ -0,0 +1,174 @@ +import type { + // TODO ref backend fetching!! + Reference, + Block, + Inline, + Media as MediaType, + ExternalContent, +} from "@/types/trill"; +import crow from "@/assets/icons/crow.svg"; +import type { PostProps } from "./Post"; +import Media from "./Media"; +import JSONContent, { YoutubeSnippet } from "./External"; +import { useLocation } from "wouter"; +import Quote from "./Quote"; +import PostData from "./Loader"; +import Card from "./Card.tsx"; +import type { Ship } from "@/types/urbit.ts"; + +function Body(props: PostProps) { + const text = props.poast.contents.filter((c) => { + return ( + "paragraph" in c || + "blockquote" in c || + "heading" in c || + "codeblock" in c || + "list" in c + ); + }); + + const media: MediaType[] = props.poast.contents.filter( + (c): c is MediaType => "media" in c, + ); + + const refs = props.poast.contents.filter((c): c is Reference => "ref" in c); + const json = props.poast.contents.filter( + (c): c is ExternalContent => "json" in c, + ); + + return ( +
+
+ {text.map((b, i) => ( + + ))} +
+ {media.length > 0 && } + {refs.map((r, i) => ( + + ))} + +
+ ); +} +export default Body; + +function TextBlock({ block }: { block: Block }) { + const key = JSON.stringify(block); + return "paragraph" in block ? ( +
+ {block.paragraph.map((i, ind) => ( + + ))} +
+ ) : "blockquote" in block ? ( +
+ {block.blockquote.map((i, ind) => ( + + ))} +
+ ) : "heading" in block ? ( + + ) : "codeblock" in block ? ( +
+      
+        {block.codeblock.code}
+      
+    
+ ) : "list" in block ? ( + block.list.ordered ? ( +
    + {block.list.text.map((i, ind) => ( +
  1. + +
  2. + ))} +
+ ) : ( +
    + {block.list.text.map((i, ind) => ( +
  • + +
  • + ))} +
+ ) + ) : null; +} +function Inlin({ i }: { i: Inline }) { + const [_, navigate] = useLocation(); + function gotoShip(e: React.MouseEvent, ship: Ship) { + e.stopPropagation(); + navigate(`/feed/${ship}`); + } + return "text" in i ? ( + {i.text} + ) : "italic" in i ? ( + {i.italic} + ) : "bold" in i ? ( + {i.bold} + ) : "strike" in i ? ( + {i.strike} + ) : "underline" in i ? ( + {i.underline} + ) : "sup" in i ? ( + {i.sup} + ) : "sub" in i ? ( + {i.sub} + ) : "ship" in i ? ( + gotoShip(e, i.ship)} + > + {i.ship} + + ) : "codespan" in i ? ( + {i.codespan} + ) : "link" in i ? ( + + ) : "break" in i ? ( +
+ ) : null; +} + +function LinkParser({ href, show }: { href: string; show: string }) { + const YOUTUBE_REGEX_1 = /(youtube\.com\/watch\?v=)(\w+)/; + const YOUTUBE_REGEX_2 = /(youtu\.be\/)([a-zA-Z0-9-_]+)/; + const m1 = href.match(YOUTUBE_REGEX_1); + const m2 = href.match(YOUTUBE_REGEX_2); + const ytb = m1 && m1[2] ? m1[2] : m2 && m2[2] ? m2[2] : ""; + return ytb ? ( + + ) : ( + {show} + ); +} +function Heading({ string, num }: { string: string; num: number }) { + return num === 1 ? ( +

{string}

+ ) : num === 2 ? ( +

{string}

+ ) : num === 3 ? ( +

{string}

+ ) : num === 4 ? ( +

{string}

+ ) : num === 5 ? ( +
{string}
+ ) : num === 6 ? ( +
{string}
+ ) : null; +} + +function Ref({ r, nest }: { r: Reference; nest: number }) { + if (r.ref.type === "nostril") { + const comp = PostData({ + host: r.ref.ship, + id: r.ref.path.slice(1), + nest: nest + 1, + className: "quote-in-post", + })(Quote); + return {comp}; + } + return <>; +} diff --git a/front/src/components/post/Card.tsx b/front/src/components/post/Card.tsx new file mode 100644 index 0000000..37f4911 --- /dev/null +++ b/front/src/components/post/Card.tsx @@ -0,0 +1,9 @@ +export default function ({ children, logo, cn}: { cn?: string; logo: string; children: any }) { + const className = "trill-post-card" + (cn ? ` ${cn}`: "") + return ( +
+ + {children} +
+ ); +} diff --git a/front/src/components/post/External.tsx b/front/src/components/post/External.tsx new file mode 100644 index 0000000..0ea1500 --- /dev/null +++ b/front/src/components/post/External.tsx @@ -0,0 +1,41 @@ +import type { ExternalContent } from "@/types/trill"; +import youtube from "@/assets/icons/youtube.svg"; +import Card from "./Card"; + +interface JSONProps { + content: ExternalContent[]; +} + +function JSONContent({ content }: JSONProps) { + return ( + <> + {content.map((c, i) => { + if (!JSON.parse(c.json.content)) return

Error

; + else + return ( +

+ External content from "{c.json.origin}", use + UFA + to display. +

+ ); + })} + + ); +} +export default JSONContent; + +export function YoutubeSnippet({ href, id }: { href: string; id: string }) { + const thumbnail = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; + // todo styiling + return ( + + + + + + ); +} diff --git a/front/src/components/post/Footer.tsx b/front/src/components/post/Footer.tsx new file mode 100644 index 0000000..3b48241 --- /dev/null +++ b/front/src/components/post/Footer.tsx @@ -0,0 +1,238 @@ +import type { PostProps } from "./Post"; +import reply from "@/assets/icons/reply.svg"; +import quote from "@/assets/icons/quote.svg"; +import repost from "@/assets/icons/rt.svg"; +import { useState } from "react"; +import useLocalState from "@/state/state"; +import { useLocation } from "wouter"; +import { displayCount } from "@/logic/utils"; +import { TrillReactModal, stringToReact } from "./Reactions"; +import toast from "react-hot-toast"; +import NostrIcon from "./wrappers/NostrIcon"; +// TODO abstract this somehow + +function Footer({ poast, refetch }: PostProps) { + const [_showMenu, setShowMenu] = useState(false); + const [location, navigate] = useLocation(); + const [reposting, _setReposting] = useState(false); + const { api, setComposerData, setModal } = useLocalState(); + const our = api!.airlock.our!; + function doReply(e: React.MouseEvent) { + e.stopPropagation(); + setComposerData({ type: "reply", post: { trill: poast } }); + } + function doQuote(e: React.MouseEvent) { + e.stopPropagation(); + setComposerData({ + type: "quote", + post: { trill: poast }, + }); + navigate("/composer"); + } + const childrenCount = poast.children + ? poast.children.length + ? poast.children.length + : Object.keys(poast.children).length + : 0; + const myRP = poast.engagement.shared.find((r) => r.pid.ship === our); + async function cancelRP(e: React.MouseEvent) { + e.stopPropagation(); + const r = await api!.deletePost(our); + if (r) toast.success("Repost deleted"); + refetch(); + if (location.includes(poast.id)) navigate("/"); + } + async function sendRP(e: React.MouseEvent) { + // TODO update backend because contents are only markdown now + e.stopPropagation(); + // const c = [ + // { + // ref: { + // type: "trill", + // ship: poast.host, + // path: `/${poast.id}`, + // }, + // }, + // ]; + // const post: SentPoast = { + // host: our, + // author: our, + // thread: null, + // parent: null, + // contents: input, + // read: openLock, + // write: openLock, + // tags: [], // TODO + // }; + // const r = await api!.addPost(post, false); + // setReposting(true); + // if (r) { + // setReposting(false); + // toast.success("Your post was published"); + // } + } + function doReact(e: React.MouseEvent) { + e.stopPropagation(); + const modal = ; + setModal(modal); + } + function showReplyCount() { + if (poast.children[0]) fetchAndShow(); // Flatpoast + // else { + // const authors = Object.keys(poast.children).map( + // (i) => poast.children[i].post.author + // ); + // setEngagement({ type: "replies", ships: authors }, poast); + // } + } + async function fetchAndShow() { + // let authors = []; + // for (let i of poast.children as string[]) { + // const res = await scrypoastFull(poast.host, i); + // if (res) + // authors.push(res.post.author || "deleter"); + // } + // setEngagement({ type: "replies", ships: authors }, poast); + } + function showRepostCount() { + // const ships = poast.engagement.shared.map((entry) => entry.host); + // setEngagement({ type: "reposts", ships: ships }, poast); + } + function showQuoteCount() { + // setEngagement({ type: "quotes", quotes: poast.engagement.quoted }, poast); + } + function showReactCount() { + // setEngagement({ type: "reacts", reacts: poast.engagement.reacts }, poast); + } + + const mostCommonReact = Object.values(poast.engagement.reacts).reduce( + (acc: any, item) => { + if (!acc.counts[item]) acc.counts[item] = 0; + acc.counts[item] += 1; + if (!acc.winner || acc.counts[item] > acc.counts[acc.winner]) + acc.winner = item; + return acc; + }, + { counts: {}, winner: "" }, + ).winner; + const reactIcon = stringToReact(mostCommonReact); + + // TODO round up all helpers + + return ( +
+
+
+ + {displayCount(childrenCount)} + + +
+
+ + {displayCount(poast.engagement.quoted.length)} + + +
+
+ + {displayCount(poast.engagement.shared.length)} + + {reposting ? ( +

...

+ ) : myRP ? ( + + ) : ( + + )} +
+
+ + {displayCount(Object.keys(poast.engagement.reacts).length)} + + {reactIcon} +
+ +
+
+ ); +} +export default Footer; + +// function Menu({ +// poast, +// setShowMenu, +// refetch, +// }: { +// poast: Poast; +// setShowMenu: Function; +// refetch: Function; +// }) { +// const ref = useRef(null); +// const [location, navigate] = useLocation(); +// // TODO this is a mess and the event still propagates +// useEffect(() => { +// const checkIfClickedOutside = (e: any) => { +// e.stopPropagation(); +// if (ref && ref.current && !ref.current.contains(e.target)) +// setShowMenu(false); +// }; +// document.addEventListener("mousedown", checkIfClickedOutside); +// return () => { +// document.removeEventListener("mousedown", checkIfClickedOutside); +// }; +// }, []); +// const { our, setModal, setAlert } = useLocalState(); +// const mine = our === poast.host || our === poast.author; +// async function doDelete(e: React.MouseEvent) { +// e.stopPropagation(); +// deletePost(poast.host, poast.id); +// setAlert("Post deleted"); +// setShowMenu(false); +// refetch(); +// if (location.includes(poast.id)) navigate("/"); +// } +// async function copyLink(e: React.MouseEvent) { +// e.stopPropagation(); +// const link = trillPermalink(poast); +// await navigator.clipboard.writeText(link); +// // some alert +// setShowMenu(false); +// } +// function openStats(e: React.MouseEvent) { +// e.stopPropagation(); +// e.preventDefault(); +// const m = setModal(null)} />; +// setModal(m); +// } +// return ( +//
+// {/*

Share to Groups

*/} +//

+// See Stats +//

+//

+// Permalink +//

+// {mine && ( +//

+// Delete Post +//

+// )} +//
+// ); +// } diff --git a/front/src/components/post/Header.tsx b/front/src/components/post/Header.tsx new file mode 100644 index 0000000..e541fa5 --- /dev/null +++ b/front/src/components/post/Header.tsx @@ -0,0 +1,40 @@ +import { date_diff } from "@/logic/utils"; +import type { PostProps } from "./Post"; +import { useLocation } from "wouter"; +import useLocalState from "@/state/state"; +function Header(props: PostProps) { + const [_, navigate] = useLocation(); + const { profiles } = useLocalState(); + const profile = profiles.get(props.poast.author); + // console.log("profile", profile); + // console.log(props.poast.author.length, "length"); + function go(e: React.MouseEvent) { + e.stopPropagation(); + } + function openThread(e: React.MouseEvent) { + e.stopPropagation(); + const sel = window.getSelection()?.toString(); + if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); + } + const { poast } = props; + const name = profile ? ( + profile.name + ) : ( +
+

{poast.author}

+
+ ); + return ( +
+
+ {name} +
+
+

+ {date_diff(poast.time, "short")} +

+
+
+ ); +} +export default Header; diff --git a/front/src/components/post/Loader.tsx b/front/src/components/post/Loader.tsx new file mode 100644 index 0000000..f3c4715 --- /dev/null +++ b/front/src/components/post/Loader.tsx @@ -0,0 +1,160 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import spinner from "@/assets/triangles.svg"; +import { useEffect, useRef, useState } from "react"; +import useLocalState from "@/state/state"; +import type { PostID } from "@/types/trill"; +import type { Ship } from "@/types/urbit"; + +function PostData(props: { + host: Ship; + id: PostID; + rter?: Ship; + rtat?: number; + rtid?: PostID; + nest?: number; // nested quotes + className?: string; +}) { + const { api } = useLocalState(); + const { host, id, nest } = props; + const [enest, setEnest] = useState(nest); + useEffect(() => { + setEnest(nest); + }, [nest]); + + return function (Component: React.ElementType) { + // const [showNested, setShowNested] = useState(nest <= 3); + const handleShowNested = (e: React.MouseEvent) => { + e.stopPropagation(); + setEnest(enest! - 3); + }; + const [dead, setDead] = useState(false); + const [denied, setDenied] = useState(false); + const { isLoading, isError, data, refetch } = useQuery({ + queryKey: ["trill-thread", host, id], + queryFn: fetchNode, + }); + const queryClient = useQueryClient(); + const dataRef = useRef(data); + useEffect(() => { + dataRef.current = data; + }, [data]); + + async function fetchNode(): Promise { + const res = await api!.scryPost(host, id, null, null); + if ("fpost" in res) return res; + else { + const existing = queryClient.getQueryData(["trill-thread", host, id]); + const existingData = existing || data; + if ("bugen" in res) { + // we peek for the actual node + peekTheNode(); + // if we have a cache we don't invalidate it + if (existingData && "fpost" in existingData) return existingData; + // if we don't have a cache then we show the loading screen + else return res; + } + if ("no-node" in res) { + if (existingData && "fpost" in existingData) return existingData; + else return res; + } + } + } + function peekTheNode() { + let timer; + peekNode({ ship: host, id }); + timer = setTimeout(() => { + const gotPost = dataRef.current && "fpost" in dataRef.current; + setDead(!gotPost); + // clearTimeout(timer); + }, 10_000); + } + + useEffect(() => { + const path = `${host}/${id}`; + if (path in peekedPosts) { + queryClient.setQueryData(["trill-thread", host, id], { + fpost: peekedPosts[path], + }); + } else if (path in deniedPosts) { + setDenied(true); + } + }, [peekedPosts]); + useEffect(() => { + const path = `${host}/${id}`; + if (path in deniedPosts) setDenied(true); + }, [deniedPosts]); + + useEffect(() => { + const l = lastThread; + if (l && l.thread == id) { + queryClient.setQueryData(["trill-thread", host, id], { fpost: l }); + } + }, [lastThread]); + function retryPeek(e: React.MouseEvent) { + e.stopPropagation(); + setDead(false); + peekTheNode(); + } + if (enest > 3) + return ( +
+
+ +
+
+ ); + else + return data ? ( + dead ? ( +
+
+

{host} did not respond

+ +
+
+ ) : denied ? ( +
+

+ {host} denied you access to this post +

+
+ ) : "no-node" in data || "bucun" in data ? ( +
+

Post not found

+
+ ) : "bugen" in data ? ( +
+
+

Post not found, requesting...

+ +
+
+ ) : "fpost" in data && data.fpost.contents === null ? ( +
+

Post deleted

+
+ ) : ( + + ) + ) : // no data + isLoading || isError ? ( +
+ +
+ ) : ( +
+

...

+
+ ); + }; +} +export default PostData; diff --git a/front/src/components/post/Media.tsx b/front/src/components/post/Media.tsx new file mode 100644 index 0000000..04ea156 --- /dev/null +++ b/front/src/components/post/Media.tsx @@ -0,0 +1,35 @@ +import type { Media } from "@/types/trill"; +interface Props { + media: Media[]; +} +function M({ media }: Props) { + return ( +
+ {media.map((m, i) => { + return "video" in m.media ? ( +
+ ); +} +export default M; + +function Images({ urls }: { urls: string[] }) { + return ( + <> + {urls.map((u, i) => ( + + ))} + + ); +} diff --git a/front/src/components/post/Post.tsx b/front/src/components/post/Post.tsx new file mode 100644 index 0000000..e61efb0 --- /dev/null +++ b/front/src/components/post/Post.tsx @@ -0,0 +1,84 @@ +import type { PostID, Poast, Reference } from "@/types/trill"; + +import Header from "./Header"; +import Body from "./Body"; +import Footer from "./Footer"; +import { useLocation } from "wouter"; +import useLocalState from "@/state/state"; +import RP from "./RP"; +import ShipModal from "../modals/ShipModal"; +import type { Ship } from "@/types/urbit"; +import Sigil from "../Sigil"; +import type { UserProfile } from "@/types/nostrill"; + +export interface PostProps { + poast: Poast; + fake?: boolean; + rter?: Ship; + rtat?: number; + rtid?: PostID; + nest?: number; + refetch?: Function; + profile?: UserProfile; +} +function Post(props: PostProps) { + const { poast } = props; + if (!poast || poast.contents === null) { + return null; + } + const isRP = + poast.contents.length === 1 && + "ref" in poast.contents[0] && + poast.contents[0].ref.type === "trill"; + if (isRP) { + const ref = (poast.contents[0] as Reference).ref; + return ( + + ); + } else return ; +} +export default Post; + +function TrillPost(props: PostProps) { + const { poast, profile, fake } = props; + const { setModal } = useLocalState(); + const [_, navigate] = useLocation(); + function openThread(_e: React.MouseEvent) { + const sel = window.getSelection()?.toString(); + if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); + } + + function openModal(e: React.MouseEvent) { + e.stopPropagation(); + setModal(); + } + const avatar = profile ? ( +
+ +
+ ) : ( +
+ +
+ ); + return ( +
+
{avatar}
+
+
+ + {!fake &&
} +
+
+ ); +} diff --git a/front/src/components/post/PostWrapper.tsx b/front/src/components/post/PostWrapper.tsx new file mode 100644 index 0000000..c4e754f --- /dev/null +++ b/front/src/components/post/PostWrapper.tsx @@ -0,0 +1,14 @@ +import useLocalState from "@/state/state"; +import type { NostrPost, PostWrapper } from "@/types/nostrill"; + +export default Post; +function Post(pw: PostWrapper) { + if ("nostr" in pw) return ; + else return ; +} + +function NostrPost({ post, event, relay }: NostrPost) { + const { profiles } = useLocalState(); + const profile = profiles.get(event.pubkey); + return <>; +} diff --git a/front/src/components/post/Quote.tsx b/front/src/components/post/Quote.tsx new file mode 100644 index 0000000..28149f0 --- /dev/null +++ b/front/src/components/post/Quote.tsx @@ -0,0 +1,64 @@ +import type { FullNode, Poast } from "@/types/trill"; +import { date_diff } from "@/logic/utils"; +import { useLocation } from "wouter"; +import Body from "./Body"; +import Sigil from "../Sigil"; + +// function Quote({ +// data, +// refetch, +// nest, +// }: { +// data: FullNode; +// refetch?: Function; +// nest: number; +// }) { +// const [_, navigate] = useLocation(); +// function gotoQuote(e: React.MouseEvent) { +// e.stopPropagation(); +// navigate(`/feed/${data.host}/${data.id}`); +// } +// return ( +//
+//
+// ( +//
+// +// {data.author} +//
+// ){date_diff(data.time, "short")} +//
+// +//
+// ); +// } +function Quote({ + data, + refetch, + nest, +}: { + data: Poast; + refetch?: Function; + nest: number; +}) { + const [_, navigate] = useLocation(); + function gotoQuote(e: React.MouseEvent) { + e.stopPropagation(); + navigate(`/feed/${data.host}/${data.id}`); + } + return ( +
+
+ ( +
+ + {data.author} +
+ ){date_diff(data.time, "short")} +
+ +
+ ); +} + +export default Quote; diff --git a/front/src/components/post/RP.tsx b/front/src/components/post/RP.tsx new file mode 100644 index 0000000..27fa02d --- /dev/null +++ b/front/src/components/post/RP.tsx @@ -0,0 +1,47 @@ +import Post from "./Post"; +import type { Ship } from "@/types/urbit"; +import type { Poast, FullNode, ID } from "@/types/trill"; +import PostData from "./Loader"; +export default function (props: { + host: string; + id: string; + rter: Ship; + rtat: number; + rtid: ID; + refetch?: Function; +}) { + return PostData(props)(RP); +} + +function RP({ + data, + refetch, + rter, + rtat, + rtid, +}: { + data: FullNode; + refetch: Function; + rter: Ship; + rtat: number; + rtid: ID; +}) { + return ( + + ); +} + +export function toFlat(n: FullNode): Poast { + return { + ...n, + children: !n.children + ? [] + : Object.keys(n.children).map((c) => n.children[c].id), + }; +} diff --git a/front/src/components/post/Reactions.tsx b/front/src/components/post/Reactions.tsx new file mode 100644 index 0000000..58662cd --- /dev/null +++ b/front/src/components/post/Reactions.tsx @@ -0,0 +1,118 @@ +import type { Poast } from "@/types/trill"; +import yeschad from "@/assets/reacts/yeschad.png"; +import cringe from "@/assets/reacts/cringe.png"; +import cry from "@/assets/reacts/cry.png"; +import doom from "@/assets/reacts/doom.png"; +import galaxy from "@/assets/reacts/galaxy.png"; +import gigachad from "@/assets/reacts/gigachad.png"; +import pepechin from "@/assets/reacts/pepechin.png"; +import pepeeyes from "@/assets/reacts/pepeeyes.png"; +import pepegmi from "@/assets/reacts/pepegmi.png"; +import pepesad from "@/assets/reacts/pepesad.png"; +import pink from "@/assets/reacts/pink.png"; +import soy from "@/assets/reacts/soy.png"; +import chad from "@/assets/reacts/chad.png"; +import pika from "@/assets/reacts/pika.png"; +import facepalm from "@/assets/reacts/facepalm.png"; +import emoji from "@/assets/icons/emoji.svg"; +import emojis from "@/logic/emojis.json"; +import Modal from "../modals/Modal"; +import useLocalState from "@/state/state"; + +export function ReactModal({ send }: { send: (s: string) => Promise }) { + const { setModal } = useLocalState(); + async function sendReact(e: React.MouseEvent, s: string) { + e.stopPropagation(); + const res = await send(s); + if (res) setModal(null); + } + // todo one more meme + return ( + +
+ sendReact(e, "❤️")}>️️❤️ + sendReact(e, "🤔")}>🤔 + sendReact(e, "😅")}>😅 + sendReact(e, "🤬")}>🤬 + sendReact(e, "😂")}>😂️ + sendReact(e, "🫡")}>🫡️ + sendReact(e, "🤢")}>🤢 + sendReact(e, "😭")}>😭 + sendReact(e, "😱")}>😱 + sendReact(e, "facepalm")} + src={facepalm} + alt="" + /> + sendReact(e, "👍")}>👍️ + sendReact(e, "👎")}>👎️ + sendReact(e, "☝")}>☝️ + sendReact(e, "🤝")}>🤝️ + sendReact(e, "🙏")}>🙏 + sendReact(e, "🤡")}>🤡 + sendReact(e, "👀")}>👀 + sendReact(e, "🎤")}>🎤 + sendReact(e, "💯")}>💯 + sendReact(e, "🔥")}>🔥 + sendReact(e, "yeschad")} src={yeschad} alt="" /> + sendReact(e, "gigachad")} + src={gigachad} + alt="" + /> + sendReact(e, "pika")} src={pika} alt="" /> + sendReact(e, "cringe")} src={cringe} alt="" /> + sendReact(e, "pepegmi")} src={pepegmi} alt="" /> + sendReact(e, "pepesad")} src={pepesad} alt="" /> + sendReact(e, "galaxy")} src={galaxy} alt="" /> + sendReact(e, "pink")} src={pink} alt="" /> + sendReact(e, "soy")} src={soy} alt="" /> + sendReact(e, "cry")} src={cry} alt="" /> + sendReact(e, "doom")} src={doom} alt="" /> +
+
+ ); +} + +export function stringToReact(s: string) { + const em = (emojis as Record)[s.replace(/\:/g, "")]; + if (s === "yeschad") + return ; + if (s === "facepalm") + return ; + if (s === "yes.jpg") + return ; + if (s === "gigachad") + return ; + if (s === "pepechin") + return ; + if (s === "pepeeyes") + return ; + if (s === "pepegmi") + return ; + if (s === "pepesad") + return ; + if (s === "") + return ; + if (s === "cringe") return ; + if (s === "cry") return ; + if (s === "crywojak") return ; + if (s === "doom") return ; + if (s === "galaxy") return ; + if (s === "pink") return ; + if (s === "pinkwojak") return ; + if (s === "soy") return ; + if (s === "chad") return ; + if (s === "pika") return ; + if (em) return {em}; + else if (s.length > 2) return ; + else return {s}; +} + +export function TrillReactModal({ poast }: { poast: Poast }) { + const { api } = useLocalState(); + async function sendReact(s: string) { + return await api!.addReact(poast.host, poast.id, s); + } + return ; +} diff --git a/front/src/components/post/StatsModal.tsx b/front/src/components/post/StatsModal.tsx new file mode 100644 index 0000000..4720b2a --- /dev/null +++ b/front/src/components/post/StatsModal.tsx @@ -0,0 +1,106 @@ +import type { Poast } from "@/types/trill"; +import Modal from "../modals/Modal"; +import { useState } from "react"; +import Post from "./Post"; +import RP from "./RP"; +import Avatar from "../Avatar"; +import { stringToReact } from "./Reactions"; + +function StatsModal({ poast, close }: { close: any; poast: Poast }) { + const [tab, setTab] = useState("replies"); + const replies = poast.children || []; + const quotes = poast.engagement.quoted; + const reposts = poast.engagement.shared; + const reacts = poast.engagement.reacts; + function set(e: React.MouseEvent, s: string) { + e.stopPropagation(); + setTab(s); + } + // TODO revise the global thingy here + return ( + +
+ {}} /> +
+
set(e, "replies")} + > +

Replies

+
+
set(e, "quotes")} + > +

Quotes

+
+
set(e, "reposts")} + > +

Reposts

+
+
set(e, "reacts")} + > +

Reacts

+
+
+
+ {tab === "replies" ? ( +
+ {replies.map((p) => ( +
+ +
+ ))} +
+ ) : tab === "quotes" ? ( +
+ {quotes.map((p) => ( +
+ +
+ ))} +
+ ) : tab === "reposts" ? ( +
+ {reposts.map((p) => ( +
+ +
+ ))} +
+ ) : tab === "reacts" ? ( +
+ {Object.keys(reacts).map((p) => ( +
+ + {stringToReact(reacts[p])} +
+ ))} +
+ ) : null} +
+
+
+ ); +} +export default StatsModal; diff --git a/front/src/components/post/wrappers/Nostr.tsx b/front/src/components/post/wrappers/Nostr.tsx new file mode 100644 index 0000000..bdc5ba9 --- /dev/null +++ b/front/src/components/post/wrappers/Nostr.tsx @@ -0,0 +1,15 @@ +import type { NostrMetadata, NostrPost } from "@/types/nostrill"; +import Post from "../Post"; +import useLocalState from "@/state/state"; + +export default NostrPost; +function NostrPost({ data }: { data: NostrPost }) { + const { profiles } = useLocalState(); + const profile = profiles.get(data.event.pubkey); + + return ; +} + +export function NostrSnippet({ eventId, pubkey, relay }: NostrMetadata) { + return
wtf
; +} diff --git a/front/src/components/post/wrappers/NostrIcon.tsx b/front/src/components/post/wrappers/NostrIcon.tsx new file mode 100644 index 0000000..0c368fb --- /dev/null +++ b/front/src/components/post/wrappers/NostrIcon.tsx @@ -0,0 +1,22 @@ +import nostrIcon from "@/assets/icons/nostr.svg"; +import useLocalState from "@/state/state"; +import toast from "react-hot-toast"; +import type { Poast } from "@/types/trill"; +export default function ({ poast }: { poast: Poast }) { + const { relays, api, keys } = useLocalState(); + + async function sendToRelay(e: React.MouseEvent) { + e.stopPropagation(); + // + const urls = Object.keys(relays); + await api!.relayPost(poast.host, poast.id, urls); + toast.success("Post relayed"); + } + // TODO round up all helpers + + return ( +
+ +
+ ); +} diff --git a/front/src/components/snippets/Snippets.tsx b/front/src/components/snippets/Snippets.tsx deleted file mode 100644 index 68f5446..0000000 --- a/front/src/components/snippets/Snippets.tsx +++ /dev/null @@ -1,395 +0,0 @@ -import { fetchTweet, lurkTweet } from "@/logic/twatter/calls"; -import { pokeDister, scryDister, scryGangs } from "@/logic/requests/tlon"; -import { useEffect, useState } from "react"; -import Tweet from "@/sections/twatter/Tweet"; -import { toFlat } from "@/sections/feed/thread/helpers"; -import PostData from "@/sections/feed/PostData"; -import Post from "@/sections/feed/post/Post"; -import { FullNode, SortugRef } from "@/types/trill"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { subscribe, unsub } from "@/logic/requests/generic"; -import { AppData, GroupMetadata } from "@/types/tlon"; -import comet from "@/assets/icons/comet.svg"; -import Sigil from "@/ui/Sigil"; -import { PollLoader } from "@/sections/feed/poll/Show"; -import { parseThread, parseTweet } from "@/logic/twatter/parser"; -import { Tweet as TweetType } from "@/types/twatter"; -import { scryRadio } from "@/logic/requests/nostril"; -import useLocalState from "@/state/state"; -import { RadioTower, ScheduledRadio, radioLink } from "@/logic/requests/radio"; -import { Ship } from "@/types/urbit"; -import { RADIO } from "@/logic/constants"; -import { SigilOnly } from "../Avatar"; -import { date_diff } from "@/logic/utils"; -import ShipsModal from "../modals/ShipsModal"; - -export function TrillSnippet({ r }: { r: SortugRef }) { - const { ship, path } = r; - return PostData({ host: ship, id: path.slice(1) })(TrillSnippetMarkup); -} -function TrillSnippetMarkup({ - data, - refetch, -}: { - data: FullNode; - refetch: Function; -}) { - return ( -
- -
- ); -} -//
{ -// if (pop) pop(link); -// }} -// className="chat-snippet trill-snippet" -// > -// Post not found -//
-// ); - -export function TweetSnippet({ - link, - giveBack, -}: { - link: string; - giveBack?: Function; -}) { - const id = link.split("/")[5]; - const { isLoading, isError, data } = useQuery({ - queryKey: ["twatter-thread", id], - queryFn: () => lurkTweet(id), - }); - const [tw, setTw] = useState(); - useEffect(() => { - if (data && "thread-lurk" in data) { - const js = JSON.parse(data["thread-lurk"]).data.tweetResult; - if (JSON.stringify(js) === "{}") return; - if (giveBack) giveBack(JSON.stringify(parseTweet(js.result))); - } - }, [data]); - if (isLoading || isError) - return ( -
-

Fetching Tweet from your Urbit...

-
- ); - else { - if ("no-coki" in data) - return ( - - ); - if ("fail" in data) - return ( -

- Bad request. Please send some feedback (here) of what you were trying - to fetch. -

- ); - if ("thread-lurk" in data) { - const js = JSON.parse(data["thread-lurk"]).data.tweetResult; - if (JSON.stringify(js) === "{}") - return null; // TODO wtf - else - return ( -
- -
- ); - } - // else { - // const head = parseThread(JSON.parse(data.thread)); - // const tweet = head.thread.tweets[0] - // giveBack(JSON.stringify(tweet)) - // return ( - //
- // - //
- // ); - // } - } -} - -export function AppSnippet({ r }: { r: SortugRef }) { - async function sub() { - if (!subn) { - const s = await subscribe( - "treaty", - "/treaties", - (data: { add: AppData }) => { - if ("ini" in data) { - const app = Object.values(data.ini).find((d) => d.desk === name); - setApp(app); - } - if ("add" in data && data.add.desk === name) setApp(data.add); - if (appData) unsub(subn); - }, - ); - setSub(s); - const res = await pokeDister(ship); - } - } - const { ship, path } = r; - const name = path.slice(1); - const [appData, setApp] = useState(); - const [subn, setSub] = useState(); - const { isLoading, data, isError } = useQuery({ - queryKey: ["dister", ship], - queryFn: () => scryDister(ship), - }); - if (isLoading || isError) return
...
; - else { - const app = Object.values(data.ini).find((d) => d.desk === name); - if (!app && !appData) sub(); - const a = app - ? app - : appData - ? appData - : { title: name, image: comet, info: "", ship }; - return ( -
- -
- ); - } -} -function AppDiv({ app }: { app: Partial }) { - return ( - <> - -
-

{app.title}

-

{app.info}

-

App from {app.ship}

-
-

- -

- - ); -} - -export function TlonSnippet({ r }: { r: SortugRef }) { - if (r.type === "app") return ; - if (r.type === "groups") return ; -} -export function GroupSnippet({ r }: { r: SortugRef }) { - const queryClient = useQueryClient(); - async function sub() { - if (!subn) { - const path = `/gangs/index/${ship}`; - const s = await subscribe("groups", path, (data: any) => { - const key = `${ship}/${name}`; - const val = data[key]; - queryClient.setQueryData(["gangs"], (old: any) => { - return { ...old, [key]: { preview: val } }; - }); - }); - setSub(s); - } - } - const { ship, path } = r; - const name = path.slice(1); - const [groupData, setGroup] = useState(); - const [subn, setSub] = useState(); - const { isLoading, data, isError } = useQuery({ - queryKey: ["gangs"], - queryFn: scryGangs, - }); - if (isLoading || isError) return
...
; - else { - const group = data[`${ship}/${name}`]; - if (!group && !groupData) sub(); - const a = - group && group.preview - ? group.preview.meta - : groupData - ? groupData - : { title: name, image: comet, cover: "", description: "" }; - return ( -
- {a.image.startsWith("#") ? ( -
- ) : ( - - )} -
-

{a.title}

-

- {a.description.length > 25 - ? a.description.substring(0, 25) + "..." - : a.description} -

-

Group by {ship}

-
- {/*

- -

*/} -
- ); - } -} - -export function PollSnippet({ r }: { r: SortugRef }) { - return ( -
- -
- ); -} - -export function SnippetHandler(props: { r: SortugRef }) { - if (props.r.type === "trill") return ; - if (props.r.type === "trill-polls") return ; - if (props.r.type === "app") return ; - if (props.r.type === "groups") return ; -} - -export function RadioSnippet({ ship }: { ship: Ship }) { - const { our } = useLocalState(); - return ship === our ? : ; -} - -function DudesRadio({ ship }: { ship }) { - function onc() { - radioLink(ship); - } - const { radioTowers } = useLocalState(); - const tower = radioTowers.find((t) => t.location === ship); - if (!tower) - return ( -
-

{RADIO}

-
-

Radio data not published. Click and check.

; -
-
- ); - else - return ( -
-

{RADIO}

-
-

Radio Session. Playing: {tower.description}

-

Started {new Date(tower.time).toLocaleString()}

-
-
- - - {tower.viewers} - 👀 - -
-
- ); -} - -function OwnRadio() { - const { currentRadio, our, setModal, radioTowers } = useLocalState(); - const [scheduled, setS] = useState(null); - function onc() { - radioLink(our); - } - useEffect(() => { - scryRadio().then((r) => { - if (r) setS(r.radio); - }); - }, []); - function showViewers() { - const modal = ( - - ); - setModal(modal); - } - if (scheduled && scheduled.time > Date.now()) - return ( -
-

{RADIO}

-
-

- Radio Session. Playing: - - {scheduled.desc} - -

-

Starting at {new Date(scheduled.time).toLocaleString()}

-
-
- -
-
- ); - else if (!currentRadio) - return ( -
-

{RADIO}

-
-

Radio unavailable

-
-
- ); - else - return ( -
-

{RADIO}

-
-

- Radio Session. Playing: - - {currentRadio.description} - -

- {/*

Started {date_diff(currentRadio.time, "long")}

*/} -
-
- - - {currentRadio?.viewers?.length || ""} - 👀 - -
-
- ); - - // return ( - // {scheduled > Date.now() - // ? (<> - //

- // Radio Session. Playing: - // - // {currentRadio.description} - // - //

- - //

Starting at {new Date(scheduled).toLocaleString()}

- // - - // ): scheduled !== 0() - - // } - //

- // Radio Session. Playing: - // - // {currentRadio.description} - // - //

- // {scheduled && scheduled > Date.now() ? ( - //

Starting at {new Date(scheduled).toLocaleString()}

- // ) : scheduled !== 0 ? ( - //

Started {date_diff(new Date(scheduled), "long")}. Click to join.

- // ) : ( - //

Unscheduled session. Click to join.

- // )} - // ); -} -- cgit v1.2.3