From f0df4c7297a05bd592d8717b8997284c80fd0500 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 17 Sep 2025 15:56:00 +0700 Subject: argh --- .gitignore | 2 + NOTES.md | 47 +++ desk/app/nostrill.hoon | 40 ++- desk/lib/constants.hoon | 1 + desk/lib/json/nostr.hoon | 16 +- desk/lib/json/nostrill.hoon | 53 ++- desk/lib/nostrill.hoon | 35 +- desk/lib/shim.hoon | 3 - desk/lib/trill/feed.hoon | 51 +++ desk/sur/nostrill.hoon | 25 +- front/.envrc | 10 + front/.gitignore | 10 + front/devenv.lock | 103 ++++++ front/devenv.nix | 57 ++++ front/devenv.yaml | 15 + front/src/App.tsx | 2 +- front/src/Router.tsx | 2 +- front/src/components/composer/Composer.tsx | 67 ++++ front/src/components/composer/Snippets.tsx | 62 ++++ front/src/components/feed/Body.tsx | 174 ---------- front/src/components/feed/Card.tsx | 9 - front/src/components/feed/Composer.tsx | 52 --- front/src/components/feed/External.tsx | 41 --- front/src/components/feed/Footer.tsx | 237 -------------- front/src/components/feed/Header.tsx | 33 -- front/src/components/feed/Media.tsx | 35 -- front/src/components/feed/NostrIcon.tsx | 22 -- front/src/components/feed/Post.tsx | 79 ----- front/src/components/feed/PostData.tsx | 160 --------- front/src/components/feed/PostList.tsx | 3 +- front/src/components/feed/Quote.tsx | 37 --- front/src/components/feed/RP.tsx | 47 --- front/src/components/feed/Reactions.tsx | 118 ------- front/src/components/feed/StatsModal.tsx | 106 ------ front/src/components/layout/Sidebar.tsx | 2 +- front/src/components/post/Body.tsx | 174 ++++++++++ front/src/components/post/Card.tsx | 9 + front/src/components/post/External.tsx | 41 +++ front/src/components/post/Footer.tsx | 238 ++++++++++++++ front/src/components/post/Header.tsx | 40 +++ front/src/components/post/Loader.tsx | 160 +++++++++ front/src/components/post/Media.tsx | 35 ++ front/src/components/post/Post.tsx | 84 +++++ front/src/components/post/PostWrapper.tsx | 14 + front/src/components/post/Quote.tsx | 64 ++++ front/src/components/post/RP.tsx | 47 +++ front/src/components/post/Reactions.tsx | 118 +++++++ front/src/components/post/StatsModal.tsx | 106 ++++++ front/src/components/post/wrappers/Nostr.tsx | 15 + front/src/components/post/wrappers/NostrIcon.tsx | 22 ++ front/src/components/snippets/Snippets.tsx | 395 ----------------------- front/src/logic/api.ts | 2 +- front/src/logic/nostril.ts | 36 --- front/src/logic/nostrill.ts | 118 +++++++ front/src/logic/requests/nostril.ts | 139 -------- front/src/logic/requests/nostrill.ts | 139 ++++++++ front/src/pages/Feed.tsx | 12 +- front/src/pages/User.tsx | 2 + front/src/state/state.ts | 47 ++- front/src/styles/feed.css | 4 + front/src/types/nostr.ts | 3 +- front/src/types/nostril.ts | 6 - front/src/types/nostrill.ts | 23 ++ front/src/types/trill.ts | 2 +- front/src/types/ui.ts | 35 +- wtfdesk/NOTES.md | 44 --- 66 files changed, 2072 insertions(+), 1858 deletions(-) create mode 100644 NOTES.md create mode 100644 desk/lib/trill/feed.hoon create mode 100644 front/.envrc create mode 100644 front/devenv.lock create mode 100644 front/devenv.nix create mode 100644 front/devenv.yaml create mode 100644 front/src/components/composer/Composer.tsx create mode 100644 front/src/components/composer/Snippets.tsx delete mode 100644 front/src/components/feed/Body.tsx delete mode 100644 front/src/components/feed/Card.tsx delete mode 100644 front/src/components/feed/Composer.tsx delete mode 100644 front/src/components/feed/External.tsx delete mode 100644 front/src/components/feed/Footer.tsx delete mode 100644 front/src/components/feed/Header.tsx delete mode 100644 front/src/components/feed/Media.tsx delete mode 100644 front/src/components/feed/NostrIcon.tsx delete mode 100644 front/src/components/feed/Post.tsx delete mode 100644 front/src/components/feed/PostData.tsx delete mode 100644 front/src/components/feed/Quote.tsx delete mode 100644 front/src/components/feed/RP.tsx delete mode 100644 front/src/components/feed/Reactions.tsx delete mode 100644 front/src/components/feed/StatsModal.tsx create mode 100644 front/src/components/post/Body.tsx create mode 100644 front/src/components/post/Card.tsx create mode 100644 front/src/components/post/External.tsx create mode 100644 front/src/components/post/Footer.tsx create mode 100644 front/src/components/post/Header.tsx create mode 100644 front/src/components/post/Loader.tsx create mode 100644 front/src/components/post/Media.tsx create mode 100644 front/src/components/post/Post.tsx create mode 100644 front/src/components/post/PostWrapper.tsx create mode 100644 front/src/components/post/Quote.tsx create mode 100644 front/src/components/post/RP.tsx create mode 100644 front/src/components/post/Reactions.tsx create mode 100644 front/src/components/post/StatsModal.tsx create mode 100644 front/src/components/post/wrappers/Nostr.tsx create mode 100644 front/src/components/post/wrappers/NostrIcon.tsx delete mode 100644 front/src/components/snippets/Snippets.tsx delete mode 100644 front/src/logic/nostril.ts create mode 100644 front/src/logic/nostrill.ts delete mode 100644 front/src/logic/requests/nostril.ts create mode 100644 front/src/logic/requests/nostrill.ts create mode 100644 front/src/styles/feed.css delete mode 100644 front/src/types/nostril.ts create mode 100644 front/src/types/nostrill.ts delete mode 100644 wtfdesk/NOTES.md diff --git a/.gitignore b/.gitignore index 681dbf2..13bf55e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ oldfront +backupdesk .claude + diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..79406c2 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,47 @@ +# NIPS to implement + + +- NIP-2 for follow lists +- NIP-25 for reactions (with content), kind 7. kind 17 with content + +- NIP-17 for private DMs +https://github.com/nostr-protocol/nips/blob/master/17.md + +- NIP-10 for mentions and replies ("marked e tags") + +- NIP-18 for Quotes and RTs + +- NIP-57 lightning zaps + +- NIP-51 lists? +- NIP-22 replies? +# Relay discovery + +https://nostr.watch/ + +# Tag specs +https://nostr-nips.com/#standardized-tags + +- 'e' for event, +- 'p' for people, +- 't' for hashtag +- 'r' for relays +- 'd' for identifier # wtf? +- 'm' for mime type +- '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 + +- Default keypair that of groundwire comet? + +lol forget the frontend make it work for Primal + +# WTFs +- ~It appears relays ignore you if you pass more than 11 authors on a filter~ +- Turns out the pubkeys were wrong because we were not padding the hex strings to 64 chars. fixed now +- Relay send a notice if too many concurrent REQs 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 ( - +
diff --git a/front/src/components/composer/Composer.tsx b/front/src/components/composer/Composer.tsx new file mode 100644 index 0000000..795188e --- /dev/null +++ b/front/src/components/composer/Composer.tsx @@ -0,0 +1,67 @@ +import useLocalState from "@/state/state"; +import type { Poast } from "@/types/trill"; +import Sigil from "@/components/Sigil"; +import { useState, type FormEvent } from "react"; +import type { ComposerData } from "@/types/ui"; +import Snippets, { ReplySnippet } from "./Snippets"; +import toast from "react-hot-toast"; +import { useLocation } from "wouter"; + +function Composer({ + isAnon, + replying, +}: { + isAnon?: boolean; + replying?: Poast; +}) { + const [loc, navigate] = useLocation(); + const { api, composerData } = useLocalState(); + const our = api!.airlock.our!; + const [input, setInput] = useState(replying ? `${replying}: ` : ""); + async function poast(e: FormEvent) { + e.preventDefault(); + // TODO + // const parent = replying ? replying : null; + // const tokens = tokenize(input); + // const post: SentPoast = { + // host: parent ? parent.host : our, + // author: our, + // thread: parent ? parent.thread : null, + // parent: parent ? parent.id : null, + // contents: input, + // read: openLock, + // write: openLock, + // tags: input.match(HASHTAGS_REGEX) || [], + // }; + // TODO make it user choosable + const res = await api!.addPost(input); + if (res) { + setInput(""); + toast.success("post sent"); + navigate(`/feed/${our}`); + } + } + const placeHolder = isAnon ? "> be me" : "What's going on in Urbit"; + return ( +
+
+ +
+ + {composerData && composerData.type === "reply" && ( + + )} + setInput(e.currentTarget.value)} + placeholder={placeHolder} + /> + {composerData && composerData.type === "quote" && ( + + )} + + + ); +} + +export default Composer; diff --git a/front/src/components/composer/Snippets.tsx b/front/src/components/composer/Snippets.tsx new file mode 100644 index 0000000..30498d0 --- /dev/null +++ b/front/src/components/composer/Snippets.tsx @@ -0,0 +1,62 @@ +import Quote from "@/components/post/Quote"; +import type { ComposerData, SPID } from "@/types/ui"; +import { NostrSnippet } from "../post/wrappers/Nostr"; + +export default Snippets; +function Snippets({ post }: { post: SPID }) { + return ( + + + + ); +} + +export function ComposerSnippet({ + onClick, + children, +}: { + onClick?: any; + children: any; +}) { + function onc(e: React.MouseEvent) { + e.stopPropagation(); + onClick(); + } + return ( +
+
+ {children} +
+ ); +} +function PostSnippet({ post }: { post: SPID }) { + if ("trill" in post) return ; + else if ("nostr" in post) return ; + // else if ("twatter" in post) + // return ( + //
+ // + //
+ // ); + // else if ("rumors" in post) + // return ( + //
+ //
+ // + // {}} /> + // {date_diff(post.post.time, "short")} + //
+ //
+ // ); + else return <>; +} + +export function ReplySnippet({ post }: { post: SPID }) { + if ("trill" in post) + return ( +
+ +
+ ); + else return
; +} diff --git a/front/src/components/feed/Body.tsx b/front/src/components/feed/Body.tsx deleted file mode 100644 index 2f11962..0000000 --- a/front/src/components/feed/Body.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import type { - // TODO ref backend fetching!! - Reference, - Block, - Inline, - Media as MediaType, - ExternalContent, -} from "@/types/trill"; -import crow from "@/assets/icons/crow.svg"; -import type { PostProps } from "./Post"; -import Media from "./Media"; -import JSONContent, { YoutubeSnippet } from "./External"; -import { useLocation } from "wouter"; -import Quote from "./Quote"; -import PostData from "./PostData"; -import Card from "./Card.tsx"; -import type { Ship } from "@/types/urbit.ts"; - -function Body(props: PostProps) { - const text = props.poast.contents.filter((c) => { - return ( - "paragraph" in c || - "blockquote" in c || - "heading" in c || - "codeblock" in c || - "list" in c - ); - }); - - const media: MediaType[] = props.poast.contents.filter( - (c): c is MediaType => "media" in c, - ); - - const refs = props.poast.contents.filter((c): c is Reference => "ref" in c); - const json = props.poast.contents.filter( - (c): c is ExternalContent => "json" in c, - ); - - return ( -
-
- {text.map((b, i) => ( - - ))} -
- {media.length > 0 && } - {refs.map((r, i) => ( - - ))} - -
- ); -} -export default Body; - -function TextBlock({ block }: { block: Block }) { - const key = JSON.stringify(block); - return "paragraph" in block ? ( -
- {block.paragraph.map((i, ind) => ( - - ))} -
- ) : "blockquote" in block ? ( -
- {block.blockquote.map((i, ind) => ( - - ))} -
- ) : "heading" in block ? ( - - ) : "codeblock" in block ? ( -
-      
-        {block.codeblock.code}
-      
-    
- ) : "list" in block ? ( - block.list.ordered ? ( -
    - {block.list.text.map((i, ind) => ( -
  1. - -
  2. - ))} -
- ) : ( -
    - {block.list.text.map((i, ind) => ( -
  • - -
  • - ))} -
- ) - ) : null; -} -function Inlin({ i }: { i: Inline }) { - const [_, navigate] = useLocation(); - function gotoShip(e: React.MouseEvent, ship: Ship) { - e.stopPropagation(); - navigate(`/feed/${ship}`); - } - return "text" in i ? ( - {i.text} - ) : "italic" in i ? ( - {i.italic} - ) : "bold" in i ? ( - {i.bold} - ) : "strike" in i ? ( - {i.strike} - ) : "underline" in i ? ( - {i.underline} - ) : "sup" in i ? ( - {i.sup} - ) : "sub" in i ? ( - {i.sub} - ) : "ship" in i ? ( - gotoShip(e, i.ship)} - > - {i.ship} - - ) : "codespan" in i ? ( - {i.codespan} - ) : "link" in i ? ( - - ) : "break" in i ? ( -
- ) : null; -} - -function LinkParser({ href, show }: { href: string; show: string }) { - const YOUTUBE_REGEX_1 = /(youtube\.com\/watch\?v=)(\w+)/; - const YOUTUBE_REGEX_2 = /(youtu\.be\/)([a-zA-Z0-9-_]+)/; - const m1 = href.match(YOUTUBE_REGEX_1); - const m2 = href.match(YOUTUBE_REGEX_2); - const ytb = m1 && m1[2] ? m1[2] : m2 && m2[2] ? m2[2] : ""; - return ytb ? ( - - ) : ( - {show} - ); -} -function Heading({ string, num }: { string: string; num: number }) { - return num === 1 ? ( -

{string}

- ) : num === 2 ? ( -

{string}

- ) : num === 3 ? ( -

{string}

- ) : num === 4 ? ( -

{string}

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

Error

; - else - return ( -

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

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

...

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

Share to Groups

*/} -//

-// See Stats -//

-//

-// Permalink -//

-// {mine && ( -//

-// Delete Post -//

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

{poast.author}

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

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

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

{host} did not respond

- -
-
- ) : denied ? ( -
-

- {host} denied you access to this post -

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

Post not found

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

Post not found, requesting...

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

Post deleted

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

...

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

Replies

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

Quotes

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

Reposts

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

Reacts

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

Feeds

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

{string}

+ ) : num === 2 ? ( +

{string}

+ ) : num === 3 ? ( +

{string}

+ ) : num === 4 ? ( +

{string}

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

Error

; + else + return ( +

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

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

...

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

Share to Groups

*/} +//

+// See Stats +//

+//

+// Permalink +//

+// {mine && ( +//

+// Delete Post +//

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

{poast.author}

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

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

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

{host} did not respond

+ +
+
+ ) : denied ? ( +
+

+ {host} denied you access to this post +

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

Post not found

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

Post not found, requesting...

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

Post deleted

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

...

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

Replies

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

Quotes

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

Reposts

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

Reacts

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

Fetching Tweet from your Urbit...

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

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

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

{app.title}

-

{app.info}

-

App from {app.ship}

-
-

- -

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

{a.title}

-

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

-

Group by {ship}

-
- {/*

- -

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

{RADIO}

-
-

Radio data not published. Click and check.

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

{RADIO}

-
-

Radio Session. Playing: {tower.description}

-

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

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

{RADIO}

-
-

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

-

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

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

{RADIO}

-
-

Radio unavailable

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

{RADIO}

-
-

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

- {/*

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

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

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

- - //

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

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

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

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

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

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

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

- // ) : ( - //

Unscheduled session. Click to join.

- // )} - // ); -} 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 { 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): 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/nostril.ts deleted file mode 100644 index 6f0edcf..0000000 --- a/front/src/logic/requests/nostril.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type Urbit from "urbit-api"; -import type { Cursor, PostID, SentPoast } from "@/types/trill"; -import type { Ship } from "@/types/urbit"; -import { FeedPostCount } from "../constants"; -import type { UserProfile } from "@/types/nostril"; - -// Subscribe -type Handler = (date: any) => void; -export default class IO { - airlock; - constructor(airlock: Urbit) { - this.airlock = airlock; - } - private async poke(json: any) { - return this.airlock.poke({ app: "nostril", mark: "json", json }); - } - private async scry(path: string) { - return this.airlock.scry({ app: "nostril", path }); - } - private async sub(path: string, handler: Handler) { - const err = (err: any, _id: string) => - console.log(err, "error on nostril subscription"); - const quit = (data: any) => - console.log(data, "nostril subscription kicked"); - const res = await this.airlock.subscribe({ - app: "nostril", - path, - event: handler, - err, - quit, - }); - console.log(res, "subscribed to nostril agent"); - } - async unsub(sub: number) { - return await this.airlock.unsubscribe(sub); - } - // subs - async subscribeStore(handler: Handler) { - const res = await this.sub("/ui", handler); - return res; - } - // scries - - async scryFeed(start: Cursor, end: Cursor, desc = true) { - const order = desc ? 1 : 0; - const term = "feed"; - - const path = `/j/feed/${term}/${start}/${end}/${FeedPostCount}/${order}`; - return await this.scry(path); - } - async scryPost( - host: Ship, - id: PostID, - start: Cursor, - end: Cursor, - desc = true, - ) { - const order = desc ? 1 : 0; - - const path = `/j/post/${host}/${id}/${start}/${end}/${FeedPostCount}/${order}`; - return await this.scry(path); - } - // pokes - - async pokeAlive() { - return await this.poke({ alive: true }); - } - async addPost(pubkey: string, content: string) { - const json = { add: { pubkey, content } }; - return this.poke({ post: json }); - } - // async addPost(post: SentPoast, gossip: boolean) { - // const json = { - // "new-post": { - // "sent-post": post, - // gossip, - // }, - // }; - // return this.poke(json); - // } - - async deletePost(id: string) { - const host = `~${this.airlock.ship}`; - const json = { - "del-post": { - ship: host, - id: id, - }, - }; - return this.poke(json); - } - - async addReact(ship: Ship, id: PostID, reaction: string) { - const json = { - "new-react": { - react: reaction, - pid: { - id: id, - ship: ship, - }, - }, - }; - - return this.poke(json); - } - - // follows - async follow(ship: Ship) { - const json = { add: ship }; - return this.poke({ fols: json }); - } - - async unfollow(ship: Ship) { - const json = { del: ship }; - return await this.poke({ fols: json }); - } - // profiles - async createProfile(pubkey: string, profile: UserProfile) { - const json = { add: { pubkey, profile } }; - return await this.poke({ prof: json }); - } - async createKey() { - const json = { add: null }; - return await this.poke({ keys: json }); - } - async removeKey(pubkey: string) { - const json = { del: pubkey }; - return await this.poke({ keys: json }); - } - // relaying - async relayPost(host: string, id: string, relays: string[]) { - const json = { send: { host, id, relays } }; - return await this.poke({ rela: json }); - } -} - -// notifications - -// mark as read diff --git a/front/src/logic/requests/nostrill.ts b/front/src/logic/requests/nostrill.ts new file mode 100644 index 0000000..6334c34 --- /dev/null +++ b/front/src/logic/requests/nostrill.ts @@ -0,0 +1,139 @@ +import type Urbit from "urbit-api"; +import type { Cursor, PostID } from "@/types/trill"; +import type { Ship } from "@/types/urbit"; +import { FeedPostCount } from "../constants"; +import type { UserProfile } from "@/types/nostrill"; + +// Subscribe +type Handler = (date: any) => void; +export default class IO { + airlock; + constructor(airlock: Urbit) { + this.airlock = airlock; + } + private async poke(json: any) { + return this.airlock.poke({ app: "nostrill", mark: "json", json }); + } + private async scry(path: string) { + 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 nostrill subscription"); + const quit = (data: any) => + console.log(data, "nostrill subscription kicked"); + const res = await this.airlock.subscribe({ + app: "nostrill", + path, + event: handler, + err, + quit, + }); + console.log(res, "subscribed to nostrill agent"); + } + async unsub(sub: number) { + return await this.airlock.unsubscribe(sub); + } + // subs + async subscribeStore(handler: Handler) { + const res = await this.sub("/ui", handler); + return res; + } + // scries + + async scryFeed(start: Cursor, end: Cursor, desc = true) { + const order = desc ? 1 : 0; + const term = "feed"; + + const path = `/j/feed/${term}/${start}/${end}/${FeedPostCount}/${order}`; + return await this.scry(path); + } + async scryPost( + host: Ship, + id: PostID, + start: Cursor, + end: Cursor, + desc = true, + ) { + const order = desc ? 1 : 0; + + const path = `/j/post/${host}/${id}/${start}/${end}/${FeedPostCount}/${order}`; + return await this.scry(path); + } + // pokes + + async pokeAlive() { + return await this.poke({ alive: true }); + } + async addPost(content: string) { + const json = { add: { content } }; + return this.poke({ post: json }); + } + // async addPost(post: SentPoast, gossip: boolean) { + // const json = { + // "new-post": { + // "sent-post": post, + // gossip, + // }, + // }; + // return this.poke(json); + // } + + async deletePost(id: string) { + const host = `~${this.airlock.ship}`; + const json = { + "del-post": { + ship: host, + id: id, + }, + }; + return this.poke(json); + } + + async addReact(ship: Ship, id: PostID, reaction: string) { + const json = { + "new-react": { + react: reaction, + pid: { + id: id, + ship: ship, + }, + }, + }; + + return this.poke(json); + } + + // follows + async follow(ship: Ship) { + const json = { add: ship }; + return this.poke({ fols: json }); + } + + async unfollow(ship: Ship) { + const json = { del: ship }; + return await this.poke({ fols: json }); + } + // profiles + async createProfile(pubkey: string, profile: UserProfile) { + const json = { add: { pubkey, profile } }; + return await this.poke({ prof: json }); + } + async createKey() { + const json = { add: null }; + return await this.poke({ keys: json }); + } + async removeKey(pubkey: string) { + const json = { del: pubkey }; + return await this.poke({ keys: json }); + } + // relaying + async relayPost(host: string, id: string, relays: string[]) { + const json = { send: { host, id, relays } }; + return await this.poke({ rela: json }); + } +} + +// notifications + +// mark as read 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

Error

; } function Nostr() { - const { relays } = useLocalState(); - const feed = eventsToFc(relays); + const { nostrFeed } = useLocalState(); + const feed = eventsToFc(nostrFeed); console.log({ feed }); const refetch = () => feed; return ; 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 (
+
); 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; profiles: Map; // pubkey key following: Map; @@ -26,7 +27,7 @@ export type LocalState = { }; const creator = create(); -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)); + 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)); - 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; -}; 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; +}; + +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[] }>; diff --git a/wtfdesk/NOTES.md b/wtfdesk/NOTES.md deleted file mode 100644 index e6d4743..0000000 --- a/wtfdesk/NOTES.md +++ /dev/null @@ -1,44 +0,0 @@ -# NIPS to implement - - -- NIP-2 for follow lists -- NIP-25 for reactions (with content), kind 7. kind 17 with content - -- NIP-17 for private DMs -https://github.com/nostr-protocol/nips/blob/master/17.md - -- NIP-10 for mentions and replies ("marked e tags") - -- NIP-18 for Quotes and RTs - -- NIP-57 lightning zaps - -- NIP-51 lists? -- NIP-22 replies? -# Relay discovery - -https://nostr.watch/ - -# Tag specs -https://nostr-nips.com/#standardized-tags - -- 'e' for event, -- 'p' for people, -- 't' for hashtag -- 'r' for relays -- 'd' for identifier # wtf? -- 'm' for mime type -- 'a' for event coordinates -- 'g' for geohash - - -# TODO - -- Default keypair that of groundwire comet? - -lol forget the frontend make it work for Primal - -# WTFs -- ~It appears relays ignore you if you pass more than 11 authors on a filter~ -- Turns out the pubkeys were wrong because we were not padding the hex strings to 64 chars. fixed now -- Relay send a notice if too many concurrent REQs -- cgit v1.2.3