From 0968e18e77a9cdd6db649e604a3f273e0565b6d8 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 28 Sep 2026 10:01:59 -0700 Subject: [PATCH] Upgrade to welshman 0.11.3 and drop the workarounds it replaces for mute and role parsing, RSVP relay hints, synced getters and per-relay retry --- .agents/skills/flotilla-state/SKILL.md | 5 ++- .agents/skills/welshman-app/SKILL.md | 1 + .agents/skills/welshman-domain/SKILL.md | 8 ++--- .agents/skills/welshman-store/SKILL.md | 2 +- package.json | 20 +++++------ pnpm-lock.yaml | Bin 400694 -> 400694 bytes src/app/calendar.ts | 9 ++--- src/app/components/ThunkFailure.svelte | 28 +++++---------- src/app/device.ts | 5 +-- src/app/lightning.ts | 15 ++++---- src/app/notifications.ts | 16 +++++---- src/app/push/adapters/common.ts | 46 +++++++++++++----------- src/app/roles.ts | 17 +++------ src/app/rooms.ts | 21 +++-------- src/app/social.ts | 13 +++---- src/app/storage.ts | 10 +----- src/app/sync.ts | 16 +++++---- 17 files changed, 99 insertions(+), 133 deletions(-) diff --git a/.agents/skills/flotilla-state/SKILL.md b/.agents/skills/flotilla-state/SKILL.md index ec113d0a..333b0e2a 100644 --- a/.agents/skills/flotilla-state/SKILL.md +++ b/.agents/skills/flotilla-state/SKILL.md @@ -284,8 +284,7 @@ plugin, and how to turn a row into a key and item and back. It loads the rows, s anything in them outlives a login and is cleared only by logout. Secrets go in `ss`. Each persisted store is declared in its owner module with `synced({key, storage, defaultValue})`, -or `syncedWithGetter` from `storage.ts` when callers need `.get()` (`withGetter`'s type drops -`.ready`). The store emits the default first and loads the stored value in the background, so +or `withGetter(synced(...))` when callers need `.get()`, which keeps `.ready`. The store emits the default first and loads the stored value in the background, so `.ready` resolves once it has arrived: - `theme`, `flTheme` (`theme.ts`), `checked` (`notifications.ts`), `shouldUnwrap` (`sync.ts`), @@ -427,7 +426,7 @@ NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return - **A controller per flow.** `Nip46Controller` (`nip46.ts`) is a class a component instantiates. It holds the writables and actions for a multi-step flow. - **Module-owned values.** `wallet` in `lightning.ts` persists itself to `ss` with - `syncedWithGetter`. + `withGetter(synced(...))`. ## Runes and stores diff --git a/.agents/skills/welshman-app/SKILL.md b/.agents/skills/welshman-app/SKILL.md index a2b59903..79ff271a 100644 --- a/.agents/skills/welshman-app/SKILL.md +++ b/.agents/skills/welshman-app/SKILL.md @@ -177,6 +177,7 @@ await thunk.waitForCompletion() thunk.getError() // string | undefined app.use(Thunks).history // writable — optimistic log app.use(Thunks).retry(thunk) +app.use(Thunks).retry(thunk, [url]) // only to these relays; a merged thunk retries only children that targeted them // Gift-wrapped (NIP-59): single recipient via `recipient`, or many via Wraps: app.use(Thunks).publish({event, relays, recipient: theirPubkey}) diff --git a/.agents/skills/welshman-domain/SKILL.md b/.agents/skills/welshman-domain/SKILL.md index c9404b2a..88bb0949 100644 --- a/.agents/skills/welshman-domain/SKILL.md +++ b/.agents/skills/welshman-domain/SKILL.md @@ -191,7 +191,7 @@ Each row: kind# — NIP — Reader / Writer. | 10063 | Blossom BUD-03 | `BlossomServerListReader` / `BlossomServerListWriter` | | 30002 | NIP-51 | `RelaySetReader` / `RelaySetWriter` | -`ListWriter` mutators (public/private split): `addPublic`/`addPrivate`, `keepPublic`/`keepPrivate`/`keepTags`, `dropPublic`/`dropPrivate`/`dropTags`. Each kind also exposes intent-named helpers, e.g. `FollowListWriter.follow(pubkey, relayHint?, petname?)`/`unfollow`, `MuteListWriter.mutePublicly`/`mutePrivately`/`unmute`, `RelayListWriter.addReadUrl`/`addWriteUrl`/`removeReadUrl`/`removeWriteUrl`/`setReadUrls`/`setWriteUrls`/`setTags`, `RoomListWriter.addRoom`/`removeRoom`/`addRelay`/`removeRelay`/`setRelays`. (Note `FollowList` is a plain `EventWriter`, not a `ListWriter` — follows are public.) +`ListWriter` mutators (public/private split): `addPublic`/`addPrivate`, `keepPublic`/`keepPrivate`/`keepTags`, `dropPublic`/`dropPrivate`/`dropTags`. Each kind also exposes intent-named helpers, e.g. `FollowListWriter.follow(pubkey, relayHint?, petname?)`/`unfollow`, `MuteListWriter.mutePublicly`/`mutePrivately`/`unmute` (its reader has `pubkeys()`, `words()`, `topics()`, `ids()`, `addresses()` over public and decrypted private tags), `RelayListWriter.addReadUrl`/`addWriteUrl`/`removeReadUrl`/`removeWriteUrl`/`setReadUrls`/`setWriteUrls`/`setTags`, `RoomListWriter.addRoom`/`removeRoom`/`addRelay`/`removeRelay`/`setRelays`. (Note `FollowList` is a plain `EventWriter`, not a `ListWriter` — follows are public.) ### Rooms (NIP-29) @@ -214,7 +214,7 @@ Room ops are scoped by the `h` tag and must publish to explicit relays — use ` | 9010 | NIP-29 | `RoomUpdatePinsReader` / `RoomUpdatePinsWriter` — the pin op | | 39005 | NIP-29 | `RoomPinsReader` / `RoomPinsWriter` — the relay-signed pin snapshot | -`RoomMetaReader`: `name`, `about`, `picture`, `pictureMeta`, `isClosed`/`isHidden`/`isPrivate`/`isRestricted`/`hasLivekit`; writer `setName`/`setAbout`/`setPicture`/`setClosed`/`setHidden`/`setPrivate`/`setRestricted`/`setLivekit`. `RoomJoinReader`: `claim()`, `reason()` (free-text `content`); writer `setClaim`/`setReason`. `RoomAddMemberWriter.addPubkey`. +`RoomMetaReader`: `name`, `about`, `picture`, `pictureMeta`, `isClosed`/`isHidden`/`isPrivate`/`isRestricted`/`hasLivekit`; writer `setName`/`setAbout`/`setPicture`/`setClosed`/`setHidden`/`setPrivate`/`setRestricted`/`setLivekit`. `RoomJoinReader`: `claim()`, `reason()` (free-text `content`); writer `setClaim`/`setReason`. `RoomAddMemberWriter.addPubkey`. `RoomAdminsReader`: `pubkeys()`, `rolesFor(pk)`, `rolesByPubkey()` over `["p", pubkey, ...roles]` tags; writer `addPubkey(pk, roles?)` (omitting `roles` keeps the admin's existing ones), `removePubkey`, `setPubkeys` (keeps retained admins' roles). ### Relay membership (Flotilla "spaces" — relay-level, NIP-29-adjacent) @@ -228,7 +228,7 @@ Room ops are scoped by the `h` tag and must publish to explicit relays — use ` | 28935 | NIP-29 | `RelayInviteReader` / `RelayInviteWriter` | | 28936 | Flotilla | `RelayLeaveReader` / `RelayLeaveWriter` | -`RelayMembersReader`: `pubkeys()`, `isMember(pk)`; writer `addPubkey(pk, role?)`/`removePubkey`/`setPubkeys` (its constructor calls `setProtected(true)` per NIP-43). All of these set `requiresRelays` — publish with `forceRoutes(relay(url))` or `setRoom`. +`RelayMembersReader`: `pubkeys()`, `isMember(pk)`, `rolesFor(pk)`, `rolesByPubkey()` over `["member", pubkey, ...roleIds]` tags; writer `addPubkey(pk, roles?)` (omitting `roles` keeps existing ones)/`removePubkey`/`setPubkeys` (keeps retained members' roles) (its constructor calls `setProtected(true)` per NIP-43). All of these set `requiresRelays` — publish with `forceRoutes(relay(url))` or `setRoom`. ### Handlers (NIP-89) @@ -270,7 +270,7 @@ Room ops are scoped by the `h` tag and must publish to explicit relays — use ` | 30023 | NIP-23 | `ArticleReader` / `ArticleWriter` (factory `Article`) | | 31992 | slash commands | `CommandReader` / `CommandWriter` (factory `Command`) | -`CommentReader`: `root()`/`parent()`; writer `setRoot`/`setParent`/`setRootFromEvent`/`setParentFromEvent`, plus `replyTo(event)`, which parents on `event` and roots on the thread it is in — the root tags of a comment, `event` itself for any other kind. `PollReader`: `title`, `options`, `pollType`, `endsAt`, `isClosed`, `urls`, plus `results(responses)`; writer `addOption`, `setPollType`, `setEndsAt`. `ReportWriter`: `setPubkey`/`setEventId`/`setReason` (routes to `[userOutbox()]`). `DateEventReader` (31922): the all-day sibling of `TimeEvent`, with `start`/`end` as `YYYY-MM-DD` strings and `end` exclusive; writer `setTitle`/`setLocation`/`setStart`/`setEnd`, deriving the same `D` day-bucket tags (the start day alone when there is no end). `CalendarRsvpReader` (31925): `calendarEvent()` (the event's address), `calendarEventId()`, `status()`, `freebusy()`; writer `setCalendarEvent`/`setCalendarEventFromEvent` (which also e-tags and p-tags the organizer), `setStatus`/`setFreebusy`/`clearFreebusy`, where declining clears free/busy and `validate()` requires an event and a status; query `forCalendarEvent(address)`, which asks the organizer's inbox. `PictureReader` (20): `title`, `topics()`, `location`, `geohash`, plus the shared `imeta()`; writer `setTitle`/`setTopics`/`setLocation`/`setGeohash` and the shared `addImeta`/`removeImeta`. It derives the top-level `x` and `m` tags from its attached images, and `validate()` requires at least one image. Exported types: `CommentRef`, `ClassifiedPrice`, `PollType`, `PollOption`, `PollResult`, `PinReference`, `RsvpStatus`, `RsvpFreebusy`. +`CommentReader`: `root()`/`parent()`; writer `setRoot`/`setParent`/`setRootFromEvent`/`setParentFromEvent`, plus `replyTo(event)`, which parents on `event` and roots on the thread it is in — the root tags of a comment, `event` itself for any other kind. `PollReader`: `title`, `options`, `pollType`, `endsAt`, `isClosed`, `urls`, plus `results(responses)`; writer `addOption`, `setPollType`, `setEndsAt`. `ReportWriter`: `setPubkey`/`setEventId`/`setReason` (routes to `[userOutbox()]`). `DateEventReader` (31922): the all-day sibling of `TimeEvent`, with `start`/`end` as `YYYY-MM-DD` strings and `end` exclusive; writer `setTitle`/`setLocation`/`setStart`/`setEnd`, deriving the same `D` day-bucket tags (the start day alone when there is no end). `CalendarRsvpReader` (31925): `calendarEvent()` (the event's address), `calendarEventId()`, `status()`, `freebusy()`; writer `setCalendarEvent(address, relay?)`/`setCalendarEventFromEvent(event, relay?)` (which also e-tags and p-tags the organizer; a given `relay` is the hint for `a` and `e`, otherwise the `e` hint comes from the organizer's outbox), `setStatus`/`setFreebusy`/`clearFreebusy`, where declining clears free/busy and `validate()` requires an event and a status; query `forCalendarEvent(address)`, which asks the organizer's inbox. `PictureReader` (20): `title`, `topics()`, `location`, `geohash`, plus the shared `imeta()`; writer `setTitle`/`setTopics`/`setLocation`/`setGeohash` and the shared `addImeta`/`removeImeta`. It derives the top-level `x` and `m` tags from its attached images, and `validate()` requires at least one image. Exported types: `CommentRef`, `ClassifiedPrice`, `PollType`, `PollOption`, `PollResult`, `PinReference`, `RsvpStatus`, `RsvpFreebusy`. `ArticleReader` (long-form, 30023): `title`, `summary`, `image`, `publishedAt`, `topics()`; writer `setTitle`/`setSummary`/`setImage`/`setPublishedAt`/`setTopics`. diff --git a/.agents/skills/welshman-store/SKILL.md b/.agents/skills/welshman-store/SKILL.md index ed38f111..0d910689 100644 --- a/.agents/skills/welshman-store/SKILL.md +++ b/.agents/skills/welshman-store/SKILL.md @@ -138,7 +138,7 @@ interface StorageProvider { | Export | Description | |---|---| | `getter(store, options?)` | Returns `() => T`; auto-switches from `get()` to a subscription when call frequency exceeds `threshold` (default 10/s) | -| `withGetter(store)` | Adds a `.get()` method to a `Readable` or `Writable` store (`WritableWithGetter` / `ReadableWithGetter`) | +| `withGetter(store)` | Adds a `.get()` method to any store, returning `S & {get(): T}` so the store's own members survive in the type (e.g. `withGetter(synced(...)).ready`) | ### Derivation helpers diff --git a/package.json b/package.json index 160511dc..697d8935 100644 --- a/package.json +++ b/package.json @@ -97,16 +97,16 @@ "@types/throttle-debounce": "^5.0.2", "@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/sveltekit": "^1.1.0", - "@welshman/app": "^0.11.2", - "@welshman/content": "^0.11.2", - "@welshman/domain": "^0.11.2", - "@welshman/editor": "^0.11.2", - "@welshman/feeds": "^0.11.2", - "@welshman/lib": "^0.11.2", - "@welshman/net": "^0.11.2", - "@welshman/signer": "^0.11.2", - "@welshman/store": "^0.11.2", - "@welshman/util": "^0.11.2", + "@welshman/app": "^0.11.3", + "@welshman/content": "^0.11.3", + "@welshman/domain": "^0.11.3", + "@welshman/editor": "^0.11.3", + "@welshman/feeds": "^0.11.3", + "@welshman/lib": "^0.11.3", + "@welshman/net": "^0.11.3", + "@welshman/signer": "^0.11.3", + "@welshman/store": "^0.11.3", + "@welshman/util": "^0.11.3", "cheerio": "^1.2.0", "compressorjs-next": "^1.1.2", "dompurify": "^3.4.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f38b7b30fff7a8071c3ae53f92101d1bfb9c8dd1..3d4d1e94de51098636dcdebe50d384f1695c80a5 100644 GIT binary patch delta 3762 zcmcgvS&SRy9oMkm>?XT8NOp5ymi^=+@u@z~=7A(&au*k0S~@i9K8 zl(1SM)B*y@qMroic&IAz0IfvDN>o6lzVxYp)ED~FCMs%I@9xED_^VIB5sdEFCuh+4(9xrV^VGfQh4?q{AiVGi z*oU5wlY=KW`ZTz8-M#SrKZEaVdvX{2`#3)F*6Dp8PxLPxobF#eiQhplu)0HKIfBz! z%AxWE!E1^tup~_}1fgjH2M_%fO!vJrXE)cNeOG64n|Fg99E?WNgvxO=qp-Z9DMX%O z6pi6&g;P0Uw8O%YJGQLJDsVXpKXRPhV*B-L?!Go;G`uj5ABK-U4fgbZG`kh}&BSmNvHqJgQ}E?a z5f$G)v3199rsCSH=s$f@+2YMG&HDeCI=A-l-1@_F*!|i5^iS?dWqA+ZiTZMBgD*+$ zR#FQkO_p^t`C7}_OvN>y)-6;NC7;bGN!dxa{j~^R>c;e_GZ;|Qs#;-e9(y&{2`=_x zIibu{139-2&pd&hg$o^wL3#Y@pTKE!`TSpC4qaaAV1tYEb1;v>d*;OK#5{41B+m_* zJcZuaykn=K(8W#;;K>T*y99A#n%sQ78)>)Mo3r56VsVgQ^(*z}* zHR^oga3C34M0k+edodd65udun#6N!Bu4$=RfJG16$1+`ckT6>Gji(&g$5 z*q$HIfx6wb@gD*hrza=p=kMu%w0C;sJfF?PLowb<#{5P-U#l~jLM7aArF6X-s@Fv| zL-LedSa#pZE^qOa8#e=`R^z_9)B zg{$E3&{?M$Dq96_g0Ur(@=BTH*myb4R}-FM(_iPx%ZLVNxadk|BQ{2eRm`C333SD_ zX032bMak?Ibi&N0M1QEWT#-W((~!bS^IB&I{Ei{_`m3|kaODyhA3Erv38?l zgnMaKNXXupq$Nfr_uZGlLFin?&VI%8wk(SPd}@e;S@O$tB_AS_sQ?j9r(7*ZTF$eS ztm^Kj)b-Rue$5-MC{$QvYaUJRG#x3;y-0Da?z6kv330{WE1HBeTU?QhkUi{USf(|= z!Edv^S=0yN)F)taXnnB|;(B__Oz@N%h{ghpyWApzDNYsJ?I_JkC7~W_Q9-q#B_pc8 zqdUE-RAo%7=-1RGP9z zM+*GyGvFB7;maAYe*RhTv9uF8K)yA0YS76KK1Wl1%u|{^uorF*qQj)=(@Wu z9*#ldacq2-V!A04(0JK_quZo`AKv~MKv*~Ar}H!L2D(|DxqFnM)McRUMC%RH-~>R4GHT9S>hj5d zg9p%L-d!%?uP4A9L{Z!epX*>#yOhA&8J)Ze?u1XC2Pffs7w|*yp{pRgNyYX5N;H&& z>o8x>+TieMbR`VxMu|IB@p@OKmVo$wD3KMk`M(n9C2 afLZvwiN5&fi}>NEQhV_A)SmH+Gyesunp=GU delta 3817 zcmcgvTWs6b83tq0PV6L2YCCq;*m0URb*n8RDN&?0V4bcMDUp(BNt6f_D@9S3s4FE( zqO`89VGKpr4jU$P2WXdeec9W(0Uc_f!_WeI-xgpm-BVkj+lmHTFAsfdMoJc%#7%4j zfgea59-Yhof8T%k^w!(UZ@sU`4{}XJu?r3|4kz^Z=OH?-pnun&kwJkLmtG})fpcY4T;AQ1BMzyzI_nBt_yFj#0%t z<;jy6K~cCz!Q}A?>x&QGv`3X?rGOV*E?JSiMKv#@DkZBpQScHfKHdY~zXs0?UtRpx zWTSQU2&g+kaqm!DAO3I|o7@eyPD0DWcbE4Vb1Q2u@XR!FKX~#5_~`IgXK!47H#Ck( z8@{nH2d;cjSJzXWoW^h9 zDKyl>GcAJ^k|n2>rUpKvNpabBxfc!gOA#g!pxe%T#vv(A-61D?;cC4d$)=SuE~GfG z(PZdE&1f|g$`cEU2Jmf7T>zJQkjDmc-lq1%c=YogG-r>_f=>|y8P8eZog9xWr+3$_ zeQD{y zo|=b3+kOWYlcJ@HWl^XJY1QZr zD(Ot3z)6I=s|~U~)Lm0ZUy#ZgnF8H1)JT)isv;I3$yB(VO;N-k-X`6g9?==0AFbzI z1y?I2cswlc!<*pmCUgNj`LyNtL1D0+2k~q0BKXRL&VZ)|(D9M~F`;Aj$gGkq3p6|E zCY)woOuG_tG#S;zc(>W08+=d!J#XB9X!V_#DvcYnCnyKQfROvUIY7X;@O4A!= zH$qZ17wV4uUjx7SBYbLy_z|>vWK6{_0qq|{i4is>Eh2G(n(f5UdL`zdj3Ar#yMp~# zgVK^rEh!}ho{0Gh%s{Fz)rLyY)r7_ne!3(EyIo%LYq%*ox>=PGH#a%F)=J0n-Eo{) z*qph2OxT7@5-QntRwctP}G6^x`9K>TXB@zmk4*OYBEOtDcSZVMkuiotRJg^;{vK$<4ot+2u58>(2)qyHjrm}L27d$K* z!df&}q|r_Fg*7oLSKf*)Lg@3V}sMzN?fm3R_&Y}q8&U9=8FT-AxQRwDJUIufq>n)#mlrtn$NMho#$F zutD8w^IkY#`X|!2PPn`JY1QnD;MrfpOW@Dj(82-Jd)2yb^oP)9(~gqICm!6fAni#~ zC-^ObV0U%tzduA!aA^ivo|pnYcpeVgnqeBe_6o8zeDSea@Q)e9#`%4KtUkAN6xmri II(>fe{|JwBTL1t6 diff --git a/src/app/calendar.ts b/src/app/calendar.ts index 882a4525..d1994709 100644 --- a/src/app/calendar.ts +++ b/src/app/calendar.ts @@ -7,7 +7,6 @@ import { pushToMapKey, secondsToDate, sortBy, - spec, uniqBy, } from "@welshman/lib" import {EVENT_RSVP, getAddress, tagSpec, tagValue} from "@welshman/util" @@ -263,12 +262,8 @@ export const publishRsvp = async (url: string, event: TrustedEvent, status: Rsvp const eventWriter = await toSpace( writer(CalendarRsvp) .setIdentifier(getAddress(event)) - .setCalendarEventFromEvent(event) - .setStatus(status) - // The writer hints the organizer's outbox, but a space's event lives on the space relay. - .dropTags(spec(["a"])) - .dropTags(spec(["e"])) - .addTags(["a", getAddress(event), url], ["e", event.id, url]), + .setCalendarEventFromEvent(event, url) + .setStatus(status), url, reader(TimeEvent)(event).room(), ) diff --git a/src/app/components/ThunkFailure.svelte b/src/app/components/ThunkFailure.svelte index 2826242e..2fe12d48 100644 --- a/src/app/components/ThunkFailure.svelte +++ b/src/app/components/ThunkFailure.svelte @@ -24,26 +24,16 @@ const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0) const retry = (url: string) => { - for (const child of $thunks.flatten([thunk])) { - if (child.options.relays.includes(url)) { - const retried = $thunks.publish({ - ...child.options, - event: child.options.event, - relays: [url], - }) + const retried = $thunks.retry(thunk, [url]) - if (showToastOnRetry) { - pushToast({ - timeout: 30_000, - children: { - component: ThunkToast, - props: {thunk: retried}, - }, - }) - } - - return - } + if (showToastOnRetry) { + pushToast({ + timeout: 30_000, + children: { + component: ThunkToast, + props: {thunk: retried}, + }, + }) } } diff --git a/src/app/device.ts b/src/app/device.ts index 8904045f..4a8a91c7 100644 --- a/src/app/device.ts +++ b/src/app/device.ts @@ -1,4 +1,5 @@ import {randomId} from "@welshman/lib" -import {kv, syncedWithGetter} from "@app/storage" +import {synced, withGetter} from "@welshman/store" +import {kv} from "@app/storage" -export const device = syncedWithGetter({key: "device", storage: kv, defaultValue: randomId()}) +export const device = withGetter(synced({key: "device", storage: kv, defaultValue: randomId()})) diff --git a/src/app/lightning.ts b/src/app/lightning.ts index 53a57762..3d509e2e 100644 --- a/src/app/lightning.ts +++ b/src/app/lightning.ts @@ -3,7 +3,8 @@ import {bech32ToHex, displayUrl, tryCatch} from "@welshman/lib" import type {Maybe} from "@welshman/lib" import {isNWCWallet, isWebLNWallet, toMsats} from "@welshman/util" import type {Wallet} from "@welshman/util" -import {ss, syncedWithGetter} from "@app/storage" +import {synced, withGetter} from "@welshman/store" +import {ss} from "@app/storage" /** Both lud06 and lud16 normalize into the bech32 lnurl zapping needs, so decode it before display. */ export const displayLnurl = (lnurl: string) => { @@ -19,11 +20,13 @@ export const displayLnurl = (lnurl: string) => { return url ? displayUrl(url) : lnurl } -export const wallet = syncedWithGetter>({ - key: "wallet", - storage: ss, - defaultValue: undefined, -}) +export const wallet = withGetter( + synced>({ + key: "wallet", + storage: ss, + defaultValue: undefined, + }), +) export const getWebLn = () => (window as any).webln diff --git a/src/app/notifications.ts b/src/app/notifications.ts index 86b9e4e6..88f8400e 100644 --- a/src/app/notifications.ts +++ b/src/app/notifications.ts @@ -18,7 +18,7 @@ import { import type {TrustedEvent} from "@welshman/util" import {getIdOrAddress, sortEventsDesc, tagSpec, tagValue, COMMENT, MESSAGE} from "@welshman/util" import {Comment} from "@welshman/domain" -import {throttled} from "@welshman/store" +import {synced, throttled, withGetter} from "@welshman/store" import {Events, Relays} from "@welshman/app" import {app, fromApp, reader} from "@app/core" import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes" @@ -29,16 +29,18 @@ import {dufflepud, DUFFLEPUD_URL} from "@app/env" import {isGroupSpace} from "@app/relays" import {activeSpaceUrls} from "@app/rooms" import {getHttpAuthHeader} from "@app/signer" -import {kv, syncedWithGetter} from "@app/storage" +import {kv} from "@app/storage" import {notificationSettings} from "@app/push/adapters/common" // Checked state -export const checked = syncedWithGetter>({ - key: "checked", - defaultValue: {}, - storage: kv, -}) +export const checked = withGetter( + synced>({ + key: "checked", + defaultValue: {}, + storage: kv, + }), +) export const getChecked = (key: string) => checked.get()[key] diff --git a/src/app/push/adapters/common.ts b/src/app/push/adapters/common.ts index 43a8d1c4..d7b1e1f4 100644 --- a/src/app/push/adapters/common.ts +++ b/src/app/push/adapters/common.ts @@ -14,14 +14,14 @@ import { type Filter, type TrustedEvent, } from "@welshman/util" -import {merged} from "@welshman/store" +import {merged, synced, withGetter} from "@welshman/store" import {User} from "@welshman/app" import {navigate} from "@app/modal" import {app, messagingRelayLists, network, roomLists} from "@app/core" import {DM_KINDS} from "@app/content" import {getMutedRooms, shouldNotify, userSettingsValues} from "@app/settings" import {makeEventPath, goToChat, goToSpace} from "@app/routes" -import {kv, syncedWithGetter} from "@app/storage" +import {kv} from "@app/storage" export type PushSubscription = { key: string @@ -38,26 +38,30 @@ export type PushState = { export const requiresFallback = Capacitor.getPlatform() === "android" && !Capacitor.isPluginAvailable("PushNotifications") -export const pushState = syncedWithGetter({ - key: "notificationState", - storage: kv, - defaultValue: {useFallback: requiresFallback}, -}) +export const pushState = withGetter( + synced({ + key: "notificationState", + storage: kv, + defaultValue: {useFallback: requiresFallback}, + }), +) -export const notificationSettings = syncedWithGetter({ - key: "notificationSettings", - storage: kv, - defaultValue: { - push: false, - sound: true, - badge: false, - spaces: true, - mentions: true, - messages: true, - events: true, - reminder_minutes: [5], - }, -}) +export const notificationSettings = withGetter( + synced({ + key: "notificationSettings", + storage: kv, + defaultValue: { + push: false, + sound: true, + badge: false, + spaces: true, + mentions: true, + messages: true, + events: true, + reminder_minutes: [5], + }, + }), +) export interface IPushAdapter { request: (prompt?: boolean) => Promise diff --git a/src/app/roles.ts b/src/app/roles.ts index 0d1ad4bb..a946b12a 100644 --- a/src/app/roles.ts +++ b/src/app/roles.ts @@ -1,5 +1,4 @@ import {derived} from "svelte/store" -import {matchTags, tagSpec} from "@welshman/util" import {relayMemberLists} from "@app/core" // Roles are rendered at a fixed saturation and lightness, chosen to read on both themes. @@ -8,16 +7,8 @@ export const roleColor = (hue: number) => `hsl(${hue}, 70%, 50%)` // A translucent tint of the role color for use as a background fill. export const roleColorSoft = (hue: number) => `hsl(${hue}, 70%, 50%, 0.15)` -// Map parsed from EXTRA values on ["member", pubkey, ...roleIds] tags export const deriveSpaceMemberRoles = (url: string) => - derived(relayMemberLists.get().forUrl(url), $members => { - const rolesByPubkey = new Map() - - for (const [, member, ...roleIds] of matchTags(tagSpec("member"), $members?.tags() ?? [])) { - if (member) { - rolesByPubkey.set(member, roleIds) - } - } - - return rolesByPubkey - }) + derived( + relayMemberLists.get().forUrl(url), + $members => $members?.rolesByPubkey() ?? new Map(), + ) diff --git a/src/app/rooms.ts b/src/app/rooms.ts index 357a83ac..2d5fc1ca 100644 --- a/src/app/rooms.ts +++ b/src/app/rooms.ts @@ -2,17 +2,7 @@ import * as nip19 from "nostr-tools/nip19" import {derived, get} from "svelte/store" import {addToMapKey, dec, formatTimestampAsDate, int, sortBy, uniq, MINUTE} from "@welshman/lib" import type {Maybe} from "@welshman/lib" -import { - ROOM_DELETE_EVENT, - hexTags, - outbox, - relay, - seen, - tagSpec, - tagValue, - tagValueMatcher, - toNostrURI, -} from "@welshman/util" +import {ROOM_DELETE_EVENT, outbox, relay, seen, tagSpec, tagValue, toNostrURI} from "@welshman/util" import type {EventContent, TrustedEvent} from "@welshman/util" import {Comment, Message} from "@welshman/domain" import type {BaseEventReader, EventWriter, Relay} from "@welshman/domain" @@ -226,11 +216,10 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) => // The room's admin list names the management kinds each admin may publish. export const deriveUserRoomPermissions = (url: string, h: string) => - derived([user, rooms.get().forRoom(url, h)], ([$user, $room]) => { - const tag = $room?.admins?.tags().find(tagValueMatcher(hexTags("p"), $user.pubkey)) - - return tag?.slice(2).map(Number) ?? [] - }) + derived( + [user, rooms.get().forRoom(url, h)], + ([$user, $room]) => $room?.admins?.rolesFor($user.pubkey).map(Number) ?? [], + ) // Deleting someone else's content goes one of two ways, and a space answers for either one. export enum AdminDelete { diff --git a/src/app/social.ts b/src/app/social.ts index 1cdff321..e86d15dc 100644 --- a/src/app/social.ts +++ b/src/app/social.ts @@ -15,7 +15,6 @@ import type {Maybe} from "@welshman/lib" import { COMMENT, NOTE, - addressTags, eventOutbox, fromNostrURI, getAddress, @@ -25,7 +24,6 @@ import { hexTags, relays as relaySelections, seen, - tagSpec, tagValues, topicTags, } from "@welshman/util" @@ -176,13 +174,12 @@ export const buildCommentTree = (root: TrustedEvent, comments: TrustedEvent[]) = export const isEventMuted = withGetter( derived([user, deriveUserItem(MuteLists)], ([$user, $muteList]) => { - const tags = $muteList?.tags() ?? [] - const mutedEvents = new Set(tagValues(hexTags("e"), tags)) - const mutedPubkeys = new Set(tagValues(hexTags("p"), tags)) - const mutedAddresses = new Set(tagValues(addressTags("a"), tags)) - const mutedTopics = new Set(tagValues(topicTags("t"), tags)) + const mutedEvents = new Set($muteList?.ids()) + const mutedPubkeys = new Set($muteList?.pubkeys()) + const mutedAddresses = new Set($muteList?.addresses()) + const mutedTopics = new Set($muteList?.topics()) // Words come from any client, so a blank or a regex character must not mute everyone or throw. - const mutedWords = tagValues(tagSpec("word"), tags) + const mutedWords = ($muteList?.words() ?? []) .map(word => word.toLowerCase().trim()) .filter(Boolean) .map(word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) diff --git a/src/app/storage.ts b/src/app/storage.ts index 16321597..c80434b0 100644 --- a/src/app/storage.ts +++ b/src/app/storage.ts @@ -42,8 +42,7 @@ import { verifiedSymbol, } from "@welshman/util" import type {Handle, TrustedEvent} from "@welshman/util" -import {synced, withGetter} from "@welshman/store" -import type {SyncedConfig} from "@welshman/store" +import {withGetter} from "@welshman/store" import type {RepositoryUpdate, WrapItem} from "@welshman/net" import {Relay, Zapper} from "@welshman/domain" import type {RelayInfo, ZapperValues} from "@welshman/domain" @@ -53,13 +52,6 @@ import {IDB} from "@lib/indexeddb" import {appPolicies} from "@app/core" import {DM_KINDS} from "@app/content" -// withGetter's type drops the ready promise synced adds -export const syncedWithGetter = (config: SyncedConfig) => { - const store = synced(config) - - return {...withGetter(store), ready: store.ready} -} - type StringStorage = { get: (key: string) => Promise> set: (key: string, value: string) => Promise diff --git a/src/app/sync.ts b/src/app/sync.ts index 389d08fd..a4b21c79 100644 --- a/src/app/sync.ts +++ b/src/app/sync.ts @@ -31,7 +31,7 @@ import { } from "@welshman/util" import type {Filter} from "@welshman/util" import type {FollowListReader} from "@welshman/domain" -import {merged, throttled} from "@welshman/store" +import {merged, synced, throttled, withGetter} from "@welshman/store" import {FollowLists, MessagingRelayLists, RelayLists, SearchRelayLists, Sync} from "@welshman/app" import { app, @@ -57,15 +57,17 @@ import {FEATURED_CONTENT_D} from "@app/featured" import {decodeRelay} from "@app/relays" import {activeSpaceUrls, userSpaceUrls} from "@app/rooms" import {Settings} from "@app/settings" -import {kv, syncedWithGetter} from "@app/storage" +import {kv} from "@app/storage" import {hasBlossomSupport} from "@app/uploads" // Unwrapping is unconditional, so this is what keeps a signer from decrypting the whole DM history. -export const shouldUnwrap = syncedWithGetter({ - key: "shouldUnwrap", - storage: kv, - defaultValue: false, -}) +export const shouldUnwrap = withGetter( + synced({ + key: "shouldUnwrap", + storage: kv, + defaultValue: false, + }), +) // Utils