diff options
Diffstat (limited to 'front/src')
-rw-r--r-- | front/src/App.tsx | 2 | ||||
-rw-r--r-- | front/src/Router.tsx | 2 | ||||
-rw-r--r-- | front/src/components/composer/Composer.tsx (renamed from front/src/components/feed/Composer.tsx) | 37 | ||||
-rw-r--r-- | front/src/components/composer/Snippets.tsx | 62 | ||||
-rw-r--r-- | front/src/components/feed/PostList.tsx | 3 | ||||
-rw-r--r-- | front/src/components/feed/Quote.tsx | 37 | ||||
-rw-r--r-- | front/src/components/layout/Sidebar.tsx | 2 | ||||
-rw-r--r-- | front/src/components/post/Body.tsx (renamed from front/src/components/feed/Body.tsx) | 2 | ||||
-rw-r--r-- | front/src/components/post/Card.tsx (renamed from front/src/components/feed/Card.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/post/External.tsx (renamed from front/src/components/feed/External.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/post/Footer.tsx (renamed from front/src/components/feed/Footer.tsx) | 9 | ||||
-rw-r--r-- | front/src/components/post/Header.tsx (renamed from front/src/components/feed/Header.tsx) | 9 | ||||
-rw-r--r-- | front/src/components/post/Loader.tsx (renamed from front/src/components/feed/PostData.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/post/Media.tsx (renamed from front/src/components/feed/Media.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/post/Post.tsx (renamed from front/src/components/feed/Post.tsx) | 15 | ||||
-rw-r--r-- | front/src/components/post/PostWrapper.tsx | 14 | ||||
-rw-r--r-- | front/src/components/post/Quote.tsx | 64 | ||||
-rw-r--r-- | front/src/components/post/RP.tsx (renamed from front/src/components/feed/RP.tsx) | 2 | ||||
-rw-r--r-- | front/src/components/post/Reactions.tsx (renamed from front/src/components/feed/Reactions.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/post/StatsModal.tsx (renamed from front/src/components/feed/StatsModal.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/post/wrappers/Nostr.tsx | 15 | ||||
-rw-r--r-- | front/src/components/post/wrappers/NostrIcon.tsx (renamed from front/src/components/feed/NostrIcon.tsx) | 0 | ||||
-rw-r--r-- | front/src/components/snippets/Snippets.tsx | 395 | ||||
-rw-r--r-- | front/src/logic/api.ts | 2 | ||||
-rw-r--r-- | front/src/logic/nostril.ts | 36 | ||||
-rw-r--r-- | front/src/logic/nostrill.ts | 118 | ||||
-rw-r--r-- | front/src/logic/requests/nostrill.ts (renamed from front/src/logic/requests/nostril.ts) | 20 | ||||
-rw-r--r-- | front/src/pages/Feed.tsx | 12 | ||||
-rw-r--r-- | front/src/pages/User.tsx | 2 | ||||
-rw-r--r-- | front/src/state/state.ts | 47 | ||||
-rw-r--r-- | front/src/styles/feed.css | 4 | ||||
-rw-r--r-- | front/src/types/nostr.ts | 3 | ||||
-rw-r--r-- | front/src/types/nostril.ts | 6 | ||||
-rw-r--r-- | front/src/types/nostrill.ts | 23 | ||||
-rw-r--r-- | front/src/types/trill.ts | 2 | ||||
-rw-r--r-- | front/src/types/ui.ts | 35 |
36 files changed, 428 insertions, 552 deletions
diff --git a/front/src/App.tsx b/front/src/App.tsx index 60ca66a..f921bbf 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -13,7 +13,7 @@ const queryClient = new QueryClient(); function App() { const [loading, setLoading] = useState(true); - console.log("NOSTRIL INIT"); + console.log("NOSTRILL INIT"); const { init, modal } = useLocalState(); useEffect(() => { init().then((_res: any) => { diff --git a/front/src/Router.tsx b/front/src/Router.tsx index b7b033e..83d212f 100644 --- a/front/src/Router.tsx +++ b/front/src/Router.tsx @@ -8,7 +8,7 @@ import { Switch, Router, Redirect, Route } from "wouter"; export default function r() { return ( <Switch> - <Router base="/apps/nostril"> + <Router base="/apps/nostrill"> <Sidebar /> <main> <Route path="/" component={toGlobal} /> diff --git a/front/src/components/feed/Composer.tsx b/front/src/components/composer/Composer.tsx index 27da392..795188e 100644 --- a/front/src/components/feed/Composer.tsx +++ b/front/src/components/composer/Composer.tsx @@ -1,9 +1,11 @@ -import { openLock } from "@/logic/bunts"; -import { HASHTAGS_REGEX } from "@/logic/constants"; import useLocalState from "@/state/state"; -import type { Poast, SentPoast } from "@/types/trill"; +import type { Poast } from "@/types/trill"; import Sigil from "@/components/Sigil"; -import { useState } from "react"; +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, @@ -12,10 +14,12 @@ function Composer({ isAnon?: boolean; replying?: Poast; }) { - const { api, keys } = useLocalState(); + const [loc, navigate] = useLocation(); + const { api, composerData } = useLocalState(); const our = api!.airlock.our!; const [input, setInput] = useState(replying ? `${replying}: ` : ""); - async function poast() { + async function poast(e: FormEvent<HTMLFormElement>) { + e.preventDefault(); // TODO // const parent = replying ? replying : null; // const tokens = tokenize(input); @@ -30,22 +34,33 @@ function Composer({ // tags: input.match(HASHTAGS_REGEX) || [], // }; // TODO make it user choosable - const pubkey = keys[0]!; - await api!.addPost(pubkey, input); + 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 ( - <div id="composer"> + <form id="composer" onSubmit={poast}> <div className="sigil"> <Sigil patp={our} size={48} /> </div> + + {composerData && composerData.type === "reply" && ( + <ReplySnippet post={composerData?.post} /> + )} <input value={input} onInput={(e) => setInput(e.currentTarget.value)} placeholder={placeHolder} /> - <button onClick={poast}>Post</button> - </div> + {composerData && composerData.type === "quote" && ( + <Snippets post={composerData?.post} /> + )} + <button type="submit">Post</button> + </form> ); } 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 ( + <ComposerSnippet> + <PostSnippet post={post} /> + </ComposerSnippet> + ); +} + +export function ComposerSnippet({ + onClick, + children, +}: { + onClick?: any; + children: any; +}) { + function onc(e: React.MouseEvent) { + e.stopPropagation(); + onClick(); + } + return ( + <div className="composer-snippet"> + <div className="pop-snippet-icon cp" role="link" onClick={onc}></div> + {children} + </div> + ); +} +function PostSnippet({ post }: { post: SPID }) { + if ("trill" in post) return <Quote data={post.trill} nest={0} />; + else if ("nostr" in post) return <NostrSnippet {...post.nostr} />; + // else if ("twatter" in post) + // return ( + // <div id={`composer-${type}`}> + // <Tweet tweet={post.post} quote={true} /> + // </div> + // ); + // else if ("rumors" in post) + // return ( + // <div id={`composer-${type}`}> + // <div className="rumor-quote f1"> + // <img src={rumorIcon} alt="" /> + // <Body poast={post.post} refetch={() => {}} /> + // <span>{date_diff(post.post.time, "short")}</span> + // </div> + // </div> + // ); + else return <></>; +} + +export function ReplySnippet({ post }: { post: SPID }) { + if ("trill" in post) + return ( + <div id="reply"> + <Quote data={post.trill} nest={0} /> + </div> + ); + else return <div />; +} 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) => ( <TrillPost key={i} poast={data.feed[i]} refetch={refetch} /> ))} 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 ( - <div onMouseUp={gotoQuote} className="quote-in-post"> - <header className="btw"> - ( - <div className="quote-author flex"> - <Sigil patp={data.author} size={20} /> - {data.author} - </div> - )<span>{date_diff(data.time, "short")}</span> - </header> - <Body poast={toFlat(data)} nest={nest} refetch={refetch!} /> - </div> - ); -} - -export default Quote; 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() { <div id="left-menu"> <div id="logo"> <img src={logo} /> - <h3> Nostril </h3> + <h3> Nostrill </h3> </div> <h3>Feeds</h3> <div className="opt" role="link" onClick={() => goto(`/feed/global`)}> diff --git a/front/src/components/feed/Body.tsx b/front/src/components/post/Body.tsx index 2f11962..2e4e2f8 100644 --- a/front/src/components/feed/Body.tsx +++ b/front/src/components/post/Body.tsx @@ -12,7 +12,7 @@ import Media from "./Media"; import JSONContent, { YoutubeSnippet } from "./External"; import { useLocation } from "wouter"; import Quote from "./Quote"; -import PostData from "./PostData"; +import PostData from "./Loader"; import Card from "./Card.tsx"; import type { Ship } from "@/types/urbit.ts"; diff --git a/front/src/components/feed/Card.tsx b/front/src/components/post/Card.tsx index 37f4911..37f4911 100644 --- a/front/src/components/feed/Card.tsx +++ b/front/src/components/post/Card.tsx diff --git a/front/src/components/feed/External.tsx b/front/src/components/post/External.tsx index 0ea1500..0ea1500 100644 --- a/front/src/components/feed/External.tsx +++ b/front/src/components/post/External.tsx diff --git a/front/src/components/feed/Footer.tsx b/front/src/components/post/Footer.tsx index 938a8c7..3b48241 100644 --- a/front/src/components/feed/Footer.tsx +++ b/front/src/components/post/Footer.tsx @@ -8,7 +8,9 @@ import { useLocation } from "wouter"; import { displayCount } from "@/logic/utils"; import { TrillReactModal, stringToReact } from "./Reactions"; import toast from "react-hot-toast"; -import NostrIcon from "./NostrIcon"; +import NostrIcon from "./wrappers/NostrIcon"; +// TODO abstract this somehow + function Footer({ poast, refetch }: PostProps) { const [_showMenu, setShowMenu] = useState(false); const [location, navigate] = useLocation(); @@ -17,14 +19,13 @@ function Footer({ poast, refetch }: PostProps) { const our = api!.airlock.our!; function doReply(e: React.MouseEvent) { e.stopPropagation(); - setComposerData({ type: "reply", post: { service: "trill", post: poast } }); - navigate("/composer"); + setComposerData({ type: "reply", post: { trill: poast } }); } function doQuote(e: React.MouseEvent) { e.stopPropagation(); setComposerData({ type: "quote", - post: { service: "trill", post: poast }, + post: { trill: poast }, }); navigate("/composer"); } diff --git a/front/src/components/feed/Header.tsx b/front/src/components/post/Header.tsx index 7658bfb..e541fa5 100644 --- a/front/src/components/feed/Header.tsx +++ b/front/src/components/post/Header.tsx @@ -1,8 +1,13 @@ 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(); } @@ -12,7 +17,9 @@ function Header(props: PostProps) { if (!sel) navigate(`/feed/${poast.host}/${poast.id}`); } const { poast } = props; - const name = ( + const name = profile ? ( + profile.name + ) : ( <div className="name cp"> <p className="p-only">{poast.author}</p> </div> diff --git a/front/src/components/feed/PostData.tsx b/front/src/components/post/Loader.tsx index f3c4715..f3c4715 100644 --- a/front/src/components/feed/PostData.tsx +++ b/front/src/components/post/Loader.tsx diff --git a/front/src/components/feed/Media.tsx b/front/src/components/post/Media.tsx index 04ea156..04ea156 100644 --- a/front/src/components/feed/Media.tsx +++ b/front/src/components/post/Media.tsx diff --git a/front/src/components/feed/Post.tsx b/front/src/components/post/Post.tsx index 1211a97..e61efb0 100644 --- a/front/src/components/feed/Post.tsx +++ b/front/src/components/post/Post.tsx @@ -9,6 +9,7 @@ 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; @@ -17,11 +18,11 @@ export interface PostProps { rtat?: number; rtid?: PostID; nest?: number; - refetch: Function; + refetch?: Function; + profile?: UserProfile; } function Post(props: PostProps) { const { poast } = props; - console.log({ poast }); if (!poast || poast.contents === null) { return null; } @@ -45,7 +46,7 @@ function Post(props: PostProps) { export default Post; function TrillPost(props: PostProps) { - const { poast, fake } = props; + const { poast, profile, fake } = props; const { setModal } = useLocalState(); const [_, navigate] = useLocation(); function openThread(_e: React.MouseEvent) { @@ -57,8 +58,12 @@ function TrillPost(props: PostProps) { e.stopPropagation(); setModal(<ShipModal ship={poast.author} />); } - const avatar = ( - <div className="avatar-w sigil cp" role="link" onMouseUp={openModal}> + const avatar = profile ? ( + <div className="avatar cp" role="link" onMouseUp={openModal}> + <img src={profile.picture} /> + </div> + ) : ( + <div className="avatar sigil cp" role="link" onMouseUp={openModal}> <Sigil patp={poast.author} size={42} /> </div> ); 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 <NostrPost post={pw.nostr} />; + else return <TrillPost post={pw.urbit.post} nostr={pw.urbit.nostr} />; +} + +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 ( +// <div onMouseUp={gotoQuote} className="quote-in-post"> +// <header className="btw"> +// ( +// <div className="quote-author flex"> +// <Sigil patp={data.author} size={20} /> +// {data.author} +// </div> +// )<span>{date_diff(data.time, "short")}</span> +// </header> +// <Body poast={toFlat(data)} nest={nest} refetch={refetch!} /> +// </div> +// ); +// } +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 ( + <div onMouseUp={gotoQuote} className="quote-in-post"> + <header className="btw"> + ( + <div className="quote-author flex"> + <Sigil patp={data.author} size={20} /> + {data.author} + </div> + )<span>{date_diff(data.time, "short")}</span> + </header> + <Body poast={data} nest={nest} refetch={refetch!} /> + </div> + ); +} + +export default Quote; diff --git a/front/src/components/feed/RP.tsx b/front/src/components/post/RP.tsx index dc733cc..27fa02d 100644 --- a/front/src/components/feed/RP.tsx +++ b/front/src/components/post/RP.tsx @@ -1,7 +1,7 @@ import Post from "./Post"; import type { Ship } from "@/types/urbit"; import type { Poast, FullNode, ID } from "@/types/trill"; -import PostData from "./PostData"; +import PostData from "./Loader"; export default function (props: { host: string; id: string; diff --git a/front/src/components/feed/Reactions.tsx b/front/src/components/post/Reactions.tsx index 58662cd..58662cd 100644 --- a/front/src/components/feed/Reactions.tsx +++ b/front/src/components/post/Reactions.tsx diff --git a/front/src/components/feed/StatsModal.tsx b/front/src/components/post/StatsModal.tsx index 4720b2a..4720b2a 100644 --- a/front/src/components/feed/StatsModal.tsx +++ b/front/src/components/post/StatsModal.tsx 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 <Post poast={data.post} profile={profile} />; +} + +export function NostrSnippet({ eventId, pubkey, relay }: NostrMetadata) { + return <div>wtf</div>; +} diff --git a/front/src/components/feed/NostrIcon.tsx b/front/src/components/post/wrappers/NostrIcon.tsx index 0c368fb..0c368fb 100644 --- a/front/src/components/feed/NostrIcon.tsx +++ b/front/src/components/post/wrappers/NostrIcon.tsx 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 ( - <div className="trill-snippet"> - <Post poast={toFlat(data)} refetch={refetch} /> - </div> - ); -} -// <div -// onClick={() => { -// if (pop) pop(link); -// }} -// className="chat-snippet trill-snippet" -// > -// Post not found -// </div> -// ); - -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<TweetType>(); - 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 ( - <div className="tweet-snippet"> - <p>Fetching Tweet from your Urbit...</p> - </div> - ); - else { - if ("no-coki" in data) - return ( - <div id="cookie-error" className="x-center"> - <p className="">Your Twitter cookie isn't working correctly.</p> - <a href="/cookies">Check it out</a> - </div> - ); - if ("fail" in data) - return ( - <p> - Bad request. Please send some feedback (here) of what you were trying - to fetch. - </p> - ); - if ("thread-lurk" in data) { - const js = JSON.parse(data["thread-lurk"]).data.tweetResult; - if (JSON.stringify(js) === "{}") - return null; // TODO wtf - else - return ( - <div className="tweet-snippet"> - <Tweet tweet={parseTweet(js.result)} quote={true} /> - </div> - ); - } - // else { - // const head = parseThread(JSON.parse(data.thread)); - // const tweet = head.thread.tweets[0] - // giveBack(JSON.stringify(tweet)) - // return ( - // <div className="tweet-snippet"> - // <Tweet tweet={tweet} quote={true} /> - // </div> - // ); - // } - } -} - -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<AppData>(); - const [subn, setSub] = useState<number>(); - const { isLoading, data, isError } = useQuery({ - queryKey: ["dister", ship], - queryFn: () => scryDister(ship), - }); - if (isLoading || isError) return <div className="reference">...</div>; - 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 ( - <div className="reference app-ref"> - <AppDiv app={a} /> - </div> - ); - } -} -function AppDiv({ app }: { app: Partial<AppData> }) { - return ( - <> - <img src={app.image} alt="" /> - <div className="text"> - <p className="app-name">{app.title}</p> - <p className="app-info">{app.info}</p> - <p className="app-host">App from {app.ship}</p> - </div> - <p className="ref-ship"> - <Sigil patp={app.ship} size={40} /> - </p> - </> - ); -} - -export function TlonSnippet({ r }: { r: SortugRef }) { - if (r.type === "app") return <AppSnippet r={r} />; - if (r.type === "groups") return <GroupSnippet r={r} />; -} -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<GroupMetadata>(); - const [subn, setSub] = useState<number>(); - const { isLoading, data, isError } = useQuery({ - queryKey: ["gangs"], - queryFn: scryGangs, - }); - if (isLoading || isError) return <div className="reference">...</div>; - 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 ( - <div className="reference app-ref"> - {a.image.startsWith("#") ? ( - <div - className="group-color" - style={{ backgroundColor: a.image }} - ></div> - ) : ( - <img src={a.image} alt="" /> - )} - <div className="text"> - <p className="app-name">{a.title}</p> - <p className="app-info"> - {a.description.length > 25 - ? a.description.substring(0, 25) + "..." - : a.description} - </p> - <p className="group-host">Group by {ship}</p> - </div> - {/* <p className="ref-ship"> - <Sigil patp={ship} size={40} /> - </p> */} - </div> - ); - } -} - -export function PollSnippet({ r }: { r: SortugRef }) { - return ( - <div className="poll-snippet"> - <PollLoader ship={r.ship} id={r.path.slice(1)} /> - </div> - ); -} - -export function SnippetHandler(props: { r: SortugRef }) { - if (props.r.type === "trill") return <TrillSnippet r={props.r} />; - if (props.r.type === "trill-polls") return <PollSnippet r={props.r} />; - if (props.r.type === "app") return <AppSnippet r={props.r} />; - if (props.r.type === "groups") return <GroupSnippet r={props.r} />; -} - -export function RadioSnippet({ ship }: { ship: Ship }) { - const { our } = useLocalState(); - return ship === our ? <OwnRadio /> : <DudesRadio ship={ship} />; -} - -function DudesRadio({ ship }: { ship }) { - function onc() { - radioLink(ship); - } - const { radioTowers } = useLocalState(); - const tower = radioTowers.find((t) => t.location === ship); - if (!tower) - return ( - <div role="link" onMouseUp={onc} className="radio-snippet"> - <p className="img">{RADIO}</p> - <div className="radio-text"> - <p>Radio data not published. Click and check.</p>; - </div> - </div> - ); - else - return ( - <div role="link" onMouseUp={onc} className="radio-snippet"> - <p className="img">{RADIO}</p> - <div className="radio-text"> - <p>Radio Session. Playing: {tower.description}</p> - <p>Started {new Date(tower.time).toLocaleString()}</p> - </div> - <div> - <SigilOnly p={ship} size={42} /> - <span className="viewers"> - {tower.viewers} - <span>👀</span> - </span> - </div> - </div> - ); -} - -function OwnRadio() { - const { currentRadio, our, setModal, radioTowers } = useLocalState(); - const [scheduled, setS] = useState<ScheduledRadio | null>(null); - function onc() { - radioLink(our); - } - useEffect(() => { - scryRadio().then((r) => { - if (r) setS(r.radio); - }); - }, []); - function showViewers() { - const modal = ( - <ShipsModal - ships={currentRadio.viewers} - header={`People watching your %radio show`} - /> - ); - setModal(modal); - } - if (scheduled && scheduled.time > Date.now()) - return ( - <div role="link" onMouseUp={onc} className="radio-snippet"> - <p className="img">{RADIO}</p> - <div className="radio-text"> - <p> - Radio Session. Playing: - <a className="radio-link" href={scheduled.url}> - {scheduled.desc} - </a> - </p> - <p>Starting at {new Date(scheduled.time).toLocaleString()}</p> - </div> - <div> - <SigilOnly p={our} size={42} /> - </div> - </div> - ); - else if (!currentRadio) - return ( - <div role="link" onMouseUp={onc} className="radio-snippet"> - <p className="img">{RADIO}</p> - <div className="radio-text"> - <p>Radio unavailable</p> - </div> - </div> - ); - else - return ( - <div role="link" onMouseUp={onc} className="radio-snippet"> - <p className="img">{RADIO}</p> - <div className="radio-text"> - <p> - Radio Session. Playing: - <a className="radio-link" href={currentRadio.stream}> - {currentRadio.description} - </a> - </p> - {/* <p>Started {date_diff(currentRadio.time, "long")}</p> */} - </div> - <div> - <SigilOnly p={our} size={42} /> - <span onClick={showViewers} className="viewers"> - {currentRadio?.viewers?.length || ""} - <span>👀</span> - </span> - </div> - </div> - ); - - // return ( - // {scheduled > Date.now() - // ? (<> - // <p> - // Radio Session. Playing: - // <a className="radio-link" target="_blank" href={currentRadio.stream}> - // {currentRadio.description} - // </a> - // </p> - - // <p>Starting at {new Date(scheduled).toLocaleString()}</p> - // </> - - // ): scheduled !== 0() - - // } - // <p> - // Radio Session. Playing: - // <a className="radio-link" target="_blank" href={currentRadio.stream}> - // {currentRadio.description} - // </a> - // </p> - // {scheduled && scheduled > Date.now() ? ( - // <p>Starting at {new Date(scheduled).toLocaleString()}</p> - // ) : scheduled !== 0 ? ( - // <p>Started {date_diff(new Date(scheduled), "long")}. Click to join.</p> - // ) : ( - // <p>Unscheduled session. Click to join.</p> - // )} - // ); -} diff --git a/front/src/logic/api.ts b/front/src/logic/api.ts index b8acba2..52635e5 100644 --- a/front/src/logic/api.ts +++ b/front/src/logic/api.ts @@ -8,7 +8,7 @@ export async function start(): Promise<Urbit> { const ship = await res.text(); airlock.ship = ship.slice(1); airlock.our = ship; - airlock.desk = "nostril"; + airlock.desk = "nostrill"; await airlock.poke({ app: "hood", mark: "helm-hi", json: "opening airlock" }); await airlock.eventSource(); return airlock; diff --git a/front/src/logic/nostril.ts b/front/src/logic/nostril.ts deleted file mode 100644 index 4e5549d..0000000 --- a/front/src/logic/nostril.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Event } from "@/types/nostr"; -import type { FC, FlatFeed, Poast } from "@/types/trill"; -import { engagementBunt, openLock } from "./bunts"; -export function eventsToFc(relayData: Record<string, Event[]>): FC { - const start = null; - const end = null; - const feed = Object.values(relayData).reduce((acc: FlatFeed, events) => { - const poasts = events.map(eventToPoast); - for (const p of poasts) { - if (p) acc[p.id] = p; - } - return acc; - }, {}); - return { feed, start, end }; -} -export function eventToPoast(event: Event): Poast | null { - if (event.kind !== 1) return null; - const contents = [{ paragraph: [{ text: event.content }] }]; - const ts = event.created_at * 1000; - const id = `${ts}`; - const poast: Poast = { - id, - host: event.pubkey, - author: event.pubkey, - contents, - thread: id, - parent: null, - read: openLock, - write: openLock, - tags: [], - time: ts, - engagement: engagementBunt, - children: [], - }; - return poast; -} diff --git a/front/src/logic/nostrill.ts b/front/src/logic/nostrill.ts new file mode 100644 index 0000000..bf9212d --- /dev/null +++ b/front/src/logic/nostrill.ts @@ -0,0 +1,118 @@ +import type { Event } from "@/types/nostr"; +import type { Content, FC, Poast } from "@/types/trill"; +import { engagementBunt, openLock } from "./bunts"; +export function eventsToFc(postEvents: Event[]): FC { + const fc = postEvents.reduce( + (acc: FC, event: Event) => { + const p = eventToPoast(event); + if (!p) return acc; + acc.feed[p.id] = p; + if (!acc.start || event.created_at < Number(acc.start)) acc.start = p.id; + if (!acc.end || event.created_at > Number(acc.end)) acc.end = p.id; + return acc; + }, + { feed: {}, start: null, end: null } as FC, + ); + return fc; +} +export function eventToPoast(event: Event): Poast | null { + if (event.kind !== 1) return null; + const contents: Content = [{ paragraph: [{ text: event.content }] }]; + const ts = event.created_at * 1000; + const id = `${ts}`; + const poast: Poast = { + id, + host: event.pubkey, + author: event.pubkey, + contents, + thread: id, + parent: null, + read: openLock, + write: openLock, + tags: [], + time: ts, + engagement: engagementBunt, + children: [], + }; + for (const tag of event.tags) { + const f = tag[0]; + if (!f) continue; + const ff = f.toLowerCase(); + console.log("tag", ff); + if (ff === "e") { + const [, eventId, _relayURL, marker, _pubkey, ..._] = tag; + // TODO + if (marker === "root") poast.thread = eventId; + else if (marker === "reply") poast.parent = eventId; + } + // + if (ff === "r") + contents.push({ + paragraph: [{ link: { show: tag[1]!, href: tag[1]! } }], + }); + if (ff === "p") + contents.push({ + paragraph: [{ ship: tag[1]! }], + }); + if (ff === "q") + contents.push({ + ref: { + type: "nostr", + ship: tag[1]!, + path: tag[2] || "" + `/${tag[3] || ""}`, + }, + }); + } + return poast; +} + +// NOTE common tags: +// imeta +// client +// nonce +// proxy + +// export function parseEventTags(event: Event) { +// const effects: any[] = []; +// for (const tag of event.tags) { +// const f = tag[0]; +// if (!f) continue; +// const ff = f.toLowerCase(); +// switch (ff) { +// case "p": { +// const [, pubkey, relayURL, ..._] = tag; +// // people mention +// break; +// } +// case "e": { +// // marker to be "root" or "reply" +// // event mention +// break; +// } +// case "q": { +// const [, eventId, relayURL, pubkey, ..._] = tag; +// // event mention +// break; +// } +// case "t": { +// const [, hashtag, ..._] = tag; +// // event mention +// break; +// } +// case "r": { +// const [, url, ..._] = tag; +// // event mention +// break; +// } +// case "alt": { +// const [, summary, ..._] = tag; +// // event mention +// break; +// } +// default: { +// break; +// } +// } +// } +// return effects; +// } diff --git a/front/src/logic/requests/nostril.ts b/front/src/logic/requests/nostrill.ts index 6f0edcf..6334c34 100644 --- a/front/src/logic/requests/nostril.ts +++ b/front/src/logic/requests/nostrill.ts @@ -1,8 +1,8 @@ import type Urbit from "urbit-api"; -import type { Cursor, PostID, SentPoast } from "@/types/trill"; +import type { Cursor, PostID } from "@/types/trill"; import type { Ship } from "@/types/urbit"; import { FeedPostCount } from "../constants"; -import type { UserProfile } from "@/types/nostril"; +import type { UserProfile } from "@/types/nostrill"; // Subscribe type Handler = (date: any) => void; @@ -12,24 +12,24 @@ export default class IO { this.airlock = airlock; } private async poke(json: any) { - return this.airlock.poke({ app: "nostril", mark: "json", json }); + return this.airlock.poke({ app: "nostrill", mark: "json", json }); } private async scry(path: string) { - return this.airlock.scry({ app: "nostril", path }); + return this.airlock.scry({ app: "nostrill", path }); } private async sub(path: string, handler: Handler) { const err = (err: any, _id: string) => - console.log(err, "error on nostril subscription"); + console.log(err, "error on nostrill subscription"); const quit = (data: any) => - console.log(data, "nostril subscription kicked"); + console.log(data, "nostrill subscription kicked"); const res = await this.airlock.subscribe({ - app: "nostril", + app: "nostrill", path, event: handler, err, quit, }); - console.log(res, "subscribed to nostril agent"); + console.log(res, "subscribed to nostrill agent"); } async unsub(sub: number) { return await this.airlock.unsubscribe(sub); @@ -65,8 +65,8 @@ export default class IO { async pokeAlive() { return await this.poke({ alive: true }); } - async addPost(pubkey: string, content: string) { - const json = { add: { pubkey, content } }; + async addPost(content: string) { + const json = { add: { content } }; return this.poke({ post: json }); } // async addPost(post: SentPoast, gossip: boolean) { diff --git a/front/src/pages/Feed.tsx b/front/src/pages/Feed.tsx index e29033e..65dee64 100644 --- a/front/src/pages/Feed.tsx +++ b/front/src/pages/Feed.tsx @@ -1,17 +1,17 @@ // import spinner from "@/assets/icons/spinner.svg"; import "@/styles/trill.css"; +import "@/styles/feed.css"; import UserFeed from "./User"; import PostList from "@/components/feed/PostList"; import useLocalState from "@/state/state"; -import { useParams, useLocation } from "wouter"; +import { useParams } from "wouter"; import spinner from "@/assets/triangles.svg"; import { useState } from "react"; -import Composer from "@/components/feed/Composer"; +import Composer from "@/components/composer/Composer"; // import UserFeed from "./User"; import { P404 } from "@/Router"; -import { useQuery } from "@tanstack/react-query"; import { isValidPatp } from "urbit-ob"; -import { eventsToFc } from "@/logic/nostril"; +import { eventsToFc } from "@/logic/nostrill"; type FeedType = "global" | "following" | "nostr"; function Loader() { @@ -88,8 +88,8 @@ function Global() { return <p>Error</p>; } function Nostr() { - const { relays } = useLocalState(); - const feed = eventsToFc(relays); + const { nostrFeed } = useLocalState(); + const feed = eventsToFc(nostrFeed); console.log({ feed }); const refetch = () => feed; return <PostList data={feed} refetch={refetch} />; diff --git a/front/src/pages/User.tsx b/front/src/pages/User.tsx index fc727e4..a1e26f1 100644 --- a/front/src/pages/User.tsx +++ b/front/src/pages/User.tsx @@ -1,4 +1,5 @@ // import spinner from "@/assets/icons/spinner.svg"; +import Composer from "@/components/composer/Composer"; import PostList from "@/components/feed/PostList"; import useLocalState from "@/state/state"; import type { Ship } from "@/types/urbit"; @@ -10,6 +11,7 @@ function UserFeed({ p }: { p: Ship }) { if (p === api!.airlock.our) return ( <div id="feed-proper"> + <Composer /> <PostList data={feed!} refetch={refetch} /> </div> ); diff --git a/front/src/state/state.ts b/front/src/state/state.ts index 28f3fb2..01b8ea1 100644 --- a/front/src/state/state.ts +++ b/front/src/state/state.ts @@ -1,11 +1,11 @@ import type { JSX } from "react"; import { start } from "@/logic/api"; -import IO from "@/logic/requests/nostril"; +import IO from "@/logic/requests/nostrill"; import type { ComposerData } from "@/types/ui"; import { create } from "zustand"; -import type { UserProfile } from "@/types/nostril"; +import type { UserProfile } from "@/types/nostrill"; import type { Event } from "@/types/nostr"; -import type { FC } from "@/types/trill"; +import type { FC, Poast } from "@/types/trill"; // TODO handle airlock connection issues // the SSE pipeline has a "status-update" event FWIW // type AirlockState = "connecting" | "connected" | "failed"; @@ -18,7 +18,8 @@ export type LocalState = { setModal: (modal: JSX.Element | null) => void; composerData: ComposerData | null; setComposerData: (c: ComposerData | null) => void; - keys: string[]; + key: string; + nostrFeed: Event[]; relays: Record<string, Event[]>; profiles: Map<string, UserProfile>; // pubkey key following: Map<string, FC>; @@ -26,7 +27,7 @@ export type LocalState = { }; const creator = create<LocalState>(); -const useLocalState = creator((set, _get) => ({ +const useLocalState = creator((set, get) => ({ isNew: false, api: null, init: async () => { @@ -35,22 +36,38 @@ const useLocalState = creator((set, _get) => ({ console.log({ api }); await api.subscribeStore((data) => { console.log("store sub", data); - const { feed, following, relays, profiles, keys } = data; + if ("state" in data) { + const { feed, nostr, following, relays, profiles, key } = data.state; + const flwing = new Map(Object.entries(following as Record<string, FC>)); + flwing.set(api!.airlock.our!, feed); + set({ + relays, + nostrFeed: nostr, + profiles: new Map(Object.entries(profiles)), + following: flwing, + key, + }); + } else if ("fact" in data) { + if ("post" in data.fact) { + if ("add" in data.fact.post) { + const post: Poast = data.fact.post.add.post; + const following = get().following; + const curr = following.get(post.author); + const fc = curr ? curr : { feed: {}, start: null, end: null }; + fc.feed[post.id] = post; + following.set(post.author, fc); - const flwing = new Map(Object.entries(following as Record<string, FC>)); - flwing.set(api!.airlock.our!, feed); - set({ - relays, - profiles: new Map(Object.entries(profiles)), - following: flwing, - keys, - }); + set({ following }); + } + } + } }); set({ api }); }, - keys: [], + key: "", profiles: new Map(), relays: {}, + nostrFeed: [], following: new Map(), followers: [], UISettings: {}, diff --git a/front/src/styles/feed.css b/front/src/styles/feed.css new file mode 100644 index 0000000..417f94b --- /dev/null +++ b/front/src/styles/feed.css @@ -0,0 +1,4 @@ +.avatar, +.avatar img { + width: 64px; +}
\ No newline at end of file diff --git a/front/src/types/nostr.ts b/front/src/types/nostr.ts index 0ccfaf3..90610d1 100644 --- a/front/src/types/nostr.ts +++ b/front/src/types/nostr.ts @@ -8,4 +8,5 @@ export type Event = { content: string; }; -export type Tag = any[]; +export type NostrEvent = Event; +export type Tag = string[]; diff --git a/front/src/types/nostril.ts b/front/src/types/nostril.ts deleted file mode 100644 index 65a6194..0000000 --- a/front/src/types/nostril.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type UserProfile = { - name: string; - picture: string; // URL - about: string; - other: Record<string, string>; -}; diff --git a/front/src/types/nostrill.ts b/front/src/types/nostrill.ts new file mode 100644 index 0000000..bcd3628 --- /dev/null +++ b/front/src/types/nostrill.ts @@ -0,0 +1,23 @@ +import type { NostrEvent } from "./nostr"; +import type { Poast } from "./trill"; + +export type UserProfile = { + name: string; + picture: string; // URL + about: string; + other: Record<string, string>; +}; + +export type PostWrapper = + | { nostr: NostrPost } + | { urbit: { post: Poast; nostr?: NostrMetadata } }; +export type NostrPost = { + relay: string; + event: NostrEvent; + post: Poast; +}; +export type NostrMetadata = { + pubkey?: string; + eventId: string; + relay?: string; +}; diff --git a/front/src/types/trill.ts b/front/src/types/trill.ts index e0936ad..984b1f3 100644 --- a/front/src/types/trill.ts +++ b/front/src/types/trill.ts @@ -108,7 +108,7 @@ export type ExternalContent = { content: string; }; }; -export type ExternalApp = "twatter" | "insta" | "anon" | "rumors"; +export type ExternalApp = "twatter" | "insta" | "anon" | "rumors" | "nostr"; export interface TwatterReference { json: { origin: "twatter"; diff --git a/front/src/types/ui.ts b/front/src/types/ui.ts index d964d84..c0c61a1 100644 --- a/front/src/types/ui.ts +++ b/front/src/types/ui.ts @@ -1,6 +1,6 @@ -import {Poast } from "./trill"; -import { Tweet } from "./twatter"; -import { Ship } from "./urbit"; +import type { NostrMetadata } from "./nostrill"; +import type { Poast } from "./trill"; +import type { Tweet } from "./twatter"; export type Timestamp = number; export type UrbitTime = string; @@ -9,19 +9,19 @@ export interface ComposerData { type: "quote" | "reply"; post: SPID; } -export type SPID = TrillPID | TwatterPID | RumorsPID; +export type SPID = TrillPID | NostrPID | TwatterPID | RumorsPID; export interface TrillPID { - service: "trill"; - post: Poast; + trill: Poast; +} +export interface NostrPID { + nostr: NostrMetadata; } export interface TwatterPID { - service: "twatter"; - post: Tweet; + twatter: Tweet; } export interface RumorsPID { - service: "rumors"; - post: Poast + rumors: Poast; } export interface Guanxi { trill: Relationship; @@ -36,14 +36,15 @@ export type BucketCreds = { bucket: string; origin: string; // this is the endpoint region: string; - }, creds: { + }; + creds: { credentials: { accessKey: string; secretKey: string; - } - } -} + }; + }; +}; -export type DateStruct = {year: number, month: number, day: number} -export type ChatQuoteParams = {p: Ship, nest: string, id: string} -export type ReactGrouping = Array<{react: string, ships: Ship[]}>
\ No newline at end of file +export type DateStruct = { year: number; month: number; day: number }; +export type ChatQuoteParams = { p: Ship; nest: string; id: string }; +export type ReactGrouping = Array<{ react: string; ships: Ship[] }>; |