diff options
51 files changed, 809 insertions, 595 deletions
@@ -1,2 +1,4 @@ oldfront +backupdesk .claude + diff --git a/wtfdesk/NOTES.md b/NOTES.md index e6d4743..79406c2 100644 --- a/wtfdesk/NOTES.md +++ b/NOTES.md @@ -31,6 +31,9 @@ https://nostr-nips.com/#standardized-tags - 'a' for event coordinates - 'g' for geohash +# Things to note +- We put the nostr pubkey at the hash field of the trill-post +- No signatures # TODO diff --git a/desk/app/nostrill.hoon b/desk/app/nostrill.hoon index 575fa3e..e311b5f 100644 --- a/desk/app/nostrill.hoon +++ b/desk/app/nostrill.hoon @@ -83,10 +83,15 @@ ++ handle-post |= poke=post-poke:ui:sur ?- -.poke %add - =/ sp (build-sp:trill our.bowl our.bowl content.poke) - =/ p (build-post:trill now.bowl pubkey.poke sp) + =/ sp (build-sp:trill our.bowl our.bowl content.poke) + =/ p (build-post:trill now.bowl pub.i.keys sp) =. state (add-to-feed:mutat p) - `this + =/ profile (~(get by profiles) pub.i.keys) + =/ pw [p (some pub.i.keys) ~ ~ profile] + =/ =fact:ui:sur [%post %add pw] + =/ card (update-ui:cards fact) + :_ this :~(card) + %rt `this %del `this == @@ -140,7 +145,6 @@ `this %http - ~& pending=pending `this %rt :: relay test =^ cards state get-posts:shimm @@ -157,20 +161,18 @@ :: (get-profiles:shimm +.ids) :: (get-engagement:shimm -.ids) [cards this] - %rt1 - =/ l ~(tap by pending) - =/ l (scag 1 l) - =| cards=(list card:agent:gall) - |- - ?~ l - ~& cards=(lent cards) [cards this] - =/ [sub-id=@t pf=filter:nsur done=filter:nsur] i.l - =/ diff (diff-filters:nlib pf done) - :: ~& > diff=diff - ?~ authors.pf $(l t.l) - =^ cs state (populate-profiles:mutat u.authors.pf) + :: %rt1 + :: =| cards=(list card:agent:gall) + :: |- + :: ?~ l + :: ~& cards=(lent cards) [cards this] + :: =/ [sub-id=@t pf=filter:nsur done=filter:nsur] i.l + :: =/ diff (diff-filters:nlib pf done) + :: :: ~& > diff=diff + :: ?~ authors.pf $(l t.l) + :: =^ cs state (populate-profiles:mutat u.authors.pf) - $(l t.l, cards (weld cards cs)) + :: $(l t.l, cards (weld cards cs)) %rt2 =/ poasts (tap:norm:sur nostr-feed) @@ -198,6 +200,10 @@ :: =^ cards state (populate-profiles:mutat pks) [cards this] + %ui + =/ =fact:ui:sur [%post %add *post-wrapper:sur] + =/ card (update-ui:cards fact) + :_ this :~(card) == :: diff --git a/desk/lib/constants.hoon b/desk/lib/constants.hoon index c7f72b7..9beab32 100644 --- a/desk/lib/constants.hoon +++ b/desk/lib/constants.hoon @@ -1,3 +1,4 @@ |% +++ feed-page-size 100 ++ http-delay 3.000 -- diff --git a/desk/lib/json/nostr.hoon b/desk/lib/json/nostr.hoon index 9c36eb0..6f93c1c 100644 --- a/desk/lib/json/nostr.hoon +++ b/desk/lib/json/nostr.hoon @@ -38,9 +38,11 @@ :: ++ raw-event |= raw-event:sur :: WTF nostr doesn't want the prefix on the pubkey - =/ pubkeyt (scow:sr %ux pubkey) + =/ scw scow:sr + =/ pubkeyt (scw(min-chars 64) %ux pubkey) ?~ pubkeyt !! - =/ pubkeyj [%s (crip t.pubkeyt)] + :: =/ pubkeyj [%s (crip t.pubkeyt)] + =/ pubkeyj [%s (crip pubkeyt)] :- %a :~ [%n '0'] pubkeyj @@ -51,13 +53,13 @@ == ++ event |= e=event:sur ^- json - =/ pubkeyt (scow:sr %ux pubkey.e) - ?~ pubkeyt !! - =/ pubkeyj [%s (crip t.pubkeyt)] + :: =/ pubkeyt (scow:sr %ux pubkey.e) + :: ?~ pubkeyt !! + :: =/ pubkeyj [%s (crip t.pubkeyt)] %: pairs id+(hex:en:common id.e) - :: pubkey+(hex:en:common pubkey.e) - pubkey+pubkeyj + pubkey+(hex:en:common pubkey.e) + :: pubkey+pubkeyj sig+(hex:en:common sig.e) ['created_at' (numb created-at.e)] kind+(numb kind.e) diff --git a/desk/lib/json/nostrill.hoon b/desk/lib/json/nostrill.hoon index 43f7708..bd34acc 100644 --- a/desk/lib/json/nostrill.hoon +++ b/desk/lib/json/nostrill.hoon @@ -6,6 +6,7 @@ |% :: UI comms ++ state |= state-0:sur ^- json + %+ frond %state %: pairs relays+(en-relays relays) key+(hex:en:common pub.i.keys) @@ -38,27 +39,60 @@ ++ en-profiles |= m=(map @ux user-meta:nsur) %- pairs %+ turn ~(tap by m) |= [key=@ux p=user-meta:nsur] - :- (crip (scow:sr %ux key)) (user-meta:en:nostr p) + =/ jkey (hex:en:common key) + ?> ?=(%s -.jkey) + :- +.jkey (user-meta:en:nostr p) ++ enfollowing |= m=(map @ux feed:feed) ^- json %- pairs %+ turn ~(tap by m) |= [key=@ux f=feed:feed] - :- (crip (scow:sr %ux key)) (feed:en:trill f) + =/ jkey (hex:en:common key) + ?> ?=(%s -.jkey) + :- +.jkey (feed:en:trill f) ++ engraph |= m=(map @ux (set follow:sur)) ^- json %- pairs %+ turn ~(tap by m) |= [key=@ux s=(set follow:sur)] - :- (crip (scow:sr %ux key)) - :- %a %+ turn ~(tap in s) |= f=follow:sur - %- pairs - :~ pubkey+(hex:en:common pubkey.f) - name+s+name.f - :- %relay ?~ relay.f ~ s+u.relay.f - == + =/ jkey (hex:en:common key) + ?> ?=(%s -.jkey) + :- +.jkey + :- %a %+ turn ~(tap in s) |= f=follow:sur + %- pairs + :~ pubkey+(hex:en:common pubkey.f) + name+s+name.f + :- %relay ?~ relay.f ~ s+u.relay.f + == + + :: ui facts + ++ fact |= f=fact:ui:sur ^- json + %+ frond %fact + %+ frond -.f + ?- -.f + %post (postfact +.f) + %enga (enga +.f) + == + ++ postfact |= pf=post-fact:ui:sur ^- json + %+ frond -.pf + (post-wrapper +.pf) + ++ enga |= [pw=post-wrapper:sur reaction=*] + ^- json + ~ + ++ post-wrapper |= p=post-wrapper:sur + %- pairs + :~ post+(poast:en:trill post.p) + ['nostrMeta' (nostr-meta nostr-meta.p)] + == + ++ nostr-meta |= p=nostr-meta:sur + =| l=(list [@t json]) + =. l ?~ pub.p l :_ l ['pubkey' (hex:en:common u.pub.p)] + =. l ?~ ev-id.p l :_ l ['eventId' (hex:en:common u.ev-id.p)] + =. l ?~ relay.p l :_ l ['relay' %s u.relay.p] + =. l ?~ pr.p l :_ l ['profile' (user-meta:en:nostr u.pr.p)] + %- pairs l -- ++ de =, dejs-soft:format @@ -94,7 +128,6 @@ == ++ de-post %- ot :~ - pubkey+hex:de:common content+so == ++ de-rt diff --git a/desk/lib/nostrill.hoon b/desk/lib/nostrill.hoon index c7e940c..0570dbc 100644 --- a/desk/lib/nostrill.hoon +++ b/desk/lib/nostrill.hoon @@ -1,5 +1,5 @@ -/- post=trill-post, nsur=nostr, sur=nostrill -/+ trill=trill-post, nostr, sr=sortug +/- post=trill-post, nsur=nostr, sur=nostrill, gate=trill-gate +/+ trill=trill-post, nostr, sr=sortug, jsonlib=json-nostrill |% :: ++ default-state |= =bowl:gall ^- state:sur @@ -47,9 +47,40 @@ signature == event + +++ event-to-post + |= [=event:nsur profile=(unit user-meta:nsur) relay=(unit @t)] + ^- post-wrapper:sur + + =/ cl (tokenize:trill content.event) + =/ ts (from-unix:jikan:sr created-at.event) + =/ cm=content-map:post (init-content-map:trill cl ts) + + :: TODO more about @ps and stuff + =/ p=post:post :* + id=ts + host=`@p`pubkey.event + author=`@p`pubkey.event + thread=ts + parent=~ + children=~ + contents=cm + read=*lock:gate + write=*lock:gate + *engagement:post + 0v0 + *signature:post + tags=~ + == + =/ meta [(some pubkey.event) (some id.event) relay profile] + [p meta] + ++ cards |_ =bowl:gall ++ shim-binding ^- card:agent:gall [%pass /binding %arvo %e %connect [~ /nostr-shim] dap.bowl] + ++ update-ui |= =fact:ui:sur ^- card:agent:gall + =/ jon (fact:en:jsonlib fact) + [%give %fact ~[/ui] %json !>(jon)] -- -- diff --git a/desk/lib/shim.hoon b/desk/lib/shim.hoon index 4afdf2b..f2e0b8a 100644 --- a/desk/lib/shim.hoon +++ b/desk/lib/shim.hoon @@ -105,9 +105,6 @@ =/ sub-id (gen-sub-id:nlib eny.bowl) =/ kinds (silt ~[0]) =/ total=filter:nsur [~ `pubkeys `kinds ~ ~ ~ ~] - =/ chunk (silt (scag 10 ~(tap in pubkeys))) - =/ =filter:nsur [~ `chunk `kinds ~ ~ ~ ~] - =. pending.state (~(put by pending.state) sub-id [total *filter:nsur]) =/ req=http-req:shim:nsur [relay http-delay:constants sub-id ~[total]] =/ =card (send-http req) :- :~(card) state diff --git a/desk/lib/trill/feed.hoon b/desk/lib/trill/feed.hoon new file mode 100644 index 0000000..c21feb3 --- /dev/null +++ b/desk/lib/trill/feed.hoon @@ -0,0 +1,51 @@ +/- feed=trill-feed, sur=nostrill +/+ sr=sortug, constants +|% +++ latest-page |= f=feed:feed ^- fc:feed + =/ nodelist (tap:orm:feed f) + =/ subset (scag feed-page-size:constants nodelist) + ?~ subset [f ~ ~] + =/ start `id.i.subset + =/ rev (flop subset) + ?~ rev [f ~ ~] + =/ end `id.i.rev + =/ nf (gas:orm:feed *feed:feed subset) + [nf start end] +:: +++ latest-page-nostr |= f=nostr-feed:sur ^- nfc:sur + =/ nodelist (tap:norm:sur f) + =/ subset (scag feed-page-size:constants nodelist) + ?~ subset [f ~ ~] + =/ start `id.i.subset + =/ rev (flop subset) + ?~ rev [f ~ ~] + =/ end `id.i.rev + =/ nf (gas:norm:sur *nostr-feed:sur subset) + [nf start end] +:: +:: NOTE START IS OLD, END IS NEW + +++ subset +|= [=fc:feed replies=? now=@da] ^- fc:feed + ?: ?&(?=(%~ start.fc) ?=(%~ end.fc)) (latest-page feed.fc) + + =/ count feed-page-size:constants + =/ start ?~ start.fc 0 u.start.fc + =/ end ?~ end.fc now u.end.fc + =/ nodelist (tap:orm:feed feed.fc) + + =/ threads %+ skim nodelist + |= [=id:post =post:post] ^- ? + ?. replies + ?& + ?= %~ parent.post + (lte id start) (gte id end) + == + ?& (lte id start) (gte id end) == + =/ thread-count (lent threads) + =/ result=(list [id:post post:post]) ?: newest (scag count threads) (flop (scag count (flop threads))) + =/ cursors=[(unit @da) (unit @da)] ?~ result [~ ~] ?~ threads [~ ~] :- + ?: .=((head result) (head threads)) ~ `id:(head result) + ?: .=((rear result) (rear threads)) ~ `id:(rear result) + [(gas:orm:feed *feed:feed result) -.cursors +.cursors] +-- diff --git a/desk/sur/nostrill.hoon b/desk/sur/nostrill.hoon index ad82661..a9ef8f3 100644 --- a/desk/sur/nostrill.hoon +++ b/desk/sur/nostrill.hoon @@ -1,4 +1,4 @@ -/- trill=trill-feed, nostr +/- trill=trill-feed, tp=trill-post, nostr |% +$ state state-0 +$ state-0 @@ -14,13 +14,21 @@ profiles=(map @ux user-meta:nostr) following=(map @ux =feed:trill) follow-graph=(map @ux (set follow)) - :: for http requests - pending=(map @t [pending=filter:nostr done=filter:nostr]) :: TODO global feed somehow? == +$ nostr-feed ((mop @ud event:nostr) gth) ++ norm ((on @ud event:nostr) gth) ++$ nfc [feed=nostr-feed start=cursor:trill end=cursor:trill] + ++$ post-wrapper [=post:tp nostr-meta=nostr-meta] ++$ nostr-meta +$: pub=(unit @ux) + ev-id=(unit @ux) + relay=(unit @t) + pr=(unit user-meta:nostr) +== + +$ follow [pubkey=@ux name=@t relay=(unit @t)] ++ ui |% @@ -33,7 +41,7 @@ [%rela relay-poke] == +$ post-poke - $% [%add pubkey=@ux content=@t] + $% [%add content=@t] [%rt id=@ux pubkey=@ux relay=@t] :: NIP-18 [%del pubkey=@ux] == @@ -48,5 +56,14 @@ +$ relay-poke $% [%send host=@p id=@ relays=(list @t)] == + :: facts + +$ fact + $% [%post post-fact] + [%enga p=post-wrapper reaction=*] + == + +$ post-fact + $% [%add post-wrapper] + [%del post-wrapper] + == -- -- diff --git a/front/.envrc b/front/.envrc new file mode 100644 index 0000000..7e9a2d6 --- /dev/null +++ b/front/.envrc @@ -0,0 +1,10 @@ +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence the output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/front/.gitignore b/front/.gitignore index 247c9a3..356ff08 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -32,3 +32,13 @@ devenv.local.nix # pre-commit .pre-commit-config.yaml + +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/front/devenv.lock b/front/devenv.lock new file mode 100644 index 0000000..19bac94 --- /dev/null +++ b/front/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1758064567, + "owner": "cachix", + "repo": "devenv", + "rev": "bc697443a9653586e5be5150b4458f3096a93f67", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1757974173, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "302af509428169db34f268324162712d10559f74", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1755783167, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "4a880fb247d24fbca57269af672e8f78935b0328", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/front/devenv.nix b/front/devenv.nix new file mode 100644 index 0000000..e4e3748 --- /dev/null +++ b/front/devenv.nix @@ -0,0 +1,57 @@ +{ + pkgs, + lib, + config, + inputs, + ... +}: { + # https://devenv.sh/basics/ + env.GREET = "devenv"; + + # https://devenv.sh/packages/ + packages = with pkgs; [ + git + nodePackages.typescript-language-server + nodePackages.prettier + ]; + + # https://devenv.sh/languages/ + # languages.rust.enable = true; + languages.javascript = { + enable = true; + bun.enable = true; + }; + + # https://devenv.sh/processes/ + # processes.cargo-watch.exec = "cargo-watch"; + + # https://devenv.sh/services/ + # services.postgres.enable = true; + + # https://devenv.sh/scripts/ + scripts.hello.exec = '' + echo hello from $GREET + ''; + + enterShell = '' + hello + git --version + ''; + + # https://devenv.sh/tasks/ + # tasks = { + # "myproj:setup".exec = "mytool build"; + # "devenv:enterShell".after = [ "myproj:setup" ]; + # }; + + # https://devenv.sh/tests/ + enterTest = '' + echo "Running tests" + git --version | grep --color=auto "${pkgs.git.version}" + ''; + + # https://devenv.sh/git-hooks/ + # git-hooks.hooks.shellcheck.enable = true; + + # See full reference at https://devenv.sh/reference/options/ +} diff --git a/front/devenv.yaml b/front/devenv.yaml new file mode 100644 index 0000000..116a2ad --- /dev/null +++ b/front/devenv.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend 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[] }>; |