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 f38b7b30..3d4d1e94 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ 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