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
This commit is contained in:
parent
0c160a162d
commit
0968e18e77
17 changed files with 99 additions and 133 deletions
|
|
@ -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`.
|
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})`,
|
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
|
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`). The store emits the default first and loads the stored value in the background, so
|
|
||||||
`.ready` resolves once it has arrived:
|
`.ready` resolves once it has arrived:
|
||||||
|
|
||||||
- `theme`, `flTheme` (`theme.ts`), `checked` (`notifications.ts`), `shouldUnwrap` (`sync.ts`),
|
- `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.
|
- **A controller per flow.** `Nip46Controller` (`nip46.ts`) is a class a component instantiates.
|
||||||
It holds the writables and actions for a multi-step flow.
|
It holds the writables and actions for a multi-step flow.
|
||||||
- **Module-owned values.** `wallet` in `lightning.ts` persists itself to `ss` with
|
- **Module-owned values.** `wallet` in `lightning.ts` persists itself to `ss` with
|
||||||
`syncedWithGetter`.
|
`withGetter(synced(...))`.
|
||||||
|
|
||||||
## Runes and stores
|
## Runes and stores
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ await thunk.waitForCompletion()
|
||||||
thunk.getError() // string | undefined
|
thunk.getError() // string | undefined
|
||||||
app.use(Thunks).history // writable<Thunk[]> — optimistic log
|
app.use(Thunks).history // writable<Thunk[]> — optimistic log
|
||||||
app.use(Thunks).retry(thunk)
|
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:
|
// Gift-wrapped (NIP-59): single recipient via `recipient`, or many via Wraps:
|
||||||
app.use(Thunks).publish({event, relays, recipient: theirPubkey})
|
app.use(Thunks).publish({event, relays, recipient: theirPubkey})
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ Each row: kind# — NIP — Reader / Writer.
|
||||||
| 10063 | Blossom BUD-03 | `BlossomServerListReader` / `BlossomServerListWriter` |
|
| 10063 | Blossom BUD-03 | `BlossomServerListReader` / `BlossomServerListWriter` |
|
||||||
| 30002 | NIP-51 | `RelaySetReader` / `RelaySetWriter` |
|
| 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)
|
### 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 |
|
| 9010 | NIP-29 | `RoomUpdatePinsReader` / `RoomUpdatePinsWriter` — the pin op |
|
||||||
| 39005 | NIP-29 | `RoomPinsReader` / `RoomPinsWriter` — the relay-signed pin snapshot |
|
| 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)
|
### 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` |
|
| 28935 | NIP-29 | `RelayInviteReader` / `RelayInviteWriter` |
|
||||||
| 28936 | Flotilla | `RelayLeaveReader` / `RelayLeaveWriter` |
|
| 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)
|
### 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`) |
|
| 30023 | NIP-23 | `ArticleReader` / `ArticleWriter` (factory `Article`) |
|
||||||
| 31992 | slash commands | `CommandReader` / `CommandWriter` (factory `Command`) |
|
| 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`.
|
`ArticleReader` (long-form, 30023): `title`, `summary`, `image`, `publishedAt`, `topics()`; writer `setTitle`/`setSummary`/`setImage`/`setPublishedAt`/`setTopics`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ interface StorageProvider {
|
||||||
| Export | Description |
|
| Export | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `getter<T>(store, options?)` | Returns `() => T`; auto-switches from `get()` to a subscription when call frequency exceeds `threshold` (default 10/s) |
|
| `getter<T>(store, options?)` | Returns `() => T`; auto-switches from `get()` to a subscription when call frequency exceeds `threshold` (default 10/s) |
|
||||||
| `withGetter<T>(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
|
### Derivation helpers
|
||||||
|
|
||||||
|
|
|
||||||
20
package.json
20
package.json
|
|
@ -97,16 +97,16 @@
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@vite-pwa/sveltekit": "^1.1.0",
|
"@vite-pwa/sveltekit": "^1.1.0",
|
||||||
"@welshman/app": "^0.11.2",
|
"@welshman/app": "^0.11.3",
|
||||||
"@welshman/content": "^0.11.2",
|
"@welshman/content": "^0.11.3",
|
||||||
"@welshman/domain": "^0.11.2",
|
"@welshman/domain": "^0.11.3",
|
||||||
"@welshman/editor": "^0.11.2",
|
"@welshman/editor": "^0.11.3",
|
||||||
"@welshman/feeds": "^0.11.2",
|
"@welshman/feeds": "^0.11.3",
|
||||||
"@welshman/lib": "^0.11.2",
|
"@welshman/lib": "^0.11.3",
|
||||||
"@welshman/net": "^0.11.2",
|
"@welshman/net": "^0.11.3",
|
||||||
"@welshman/signer": "^0.11.2",
|
"@welshman/signer": "^0.11.3",
|
||||||
"@welshman/store": "^0.11.2",
|
"@welshman/store": "^0.11.3",
|
||||||
"@welshman/util": "^0.11.2",
|
"@welshman/util": "^0.11.3",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"compressorjs-next": "^1.1.2",
|
"compressorjs-next": "^1.1.2",
|
||||||
"dompurify": "^3.4.13",
|
"dompurify": "^3.4.13",
|
||||||
|
|
|
||||||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -7,7 +7,6 @@ import {
|
||||||
pushToMapKey,
|
pushToMapKey,
|
||||||
secondsToDate,
|
secondsToDate,
|
||||||
sortBy,
|
sortBy,
|
||||||
spec,
|
|
||||||
uniqBy,
|
uniqBy,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {EVENT_RSVP, getAddress, tagSpec, tagValue} from "@welshman/util"
|
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(
|
const eventWriter = await toSpace(
|
||||||
writer(CalendarRsvp)
|
writer(CalendarRsvp)
|
||||||
.setIdentifier(getAddress(event))
|
.setIdentifier(getAddress(event))
|
||||||
.setCalendarEventFromEvent(event)
|
.setCalendarEventFromEvent(event, url)
|
||||||
.setStatus(status)
|
.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]),
|
|
||||||
url,
|
url,
|
||||||
reader(TimeEvent)(event).room(),
|
reader(TimeEvent)(event).room(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,16 @@
|
||||||
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0)
|
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0)
|
||||||
|
|
||||||
const retry = (url: string) => {
|
const retry = (url: string) => {
|
||||||
for (const child of $thunks.flatten([thunk])) {
|
const retried = $thunks.retry(thunk, [url])
|
||||||
if (child.options.relays.includes(url)) {
|
|
||||||
const retried = $thunks.publish({
|
|
||||||
...child.options,
|
|
||||||
event: child.options.event,
|
|
||||||
relays: [url],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (showToastOnRetry) {
|
if (showToastOnRetry) {
|
||||||
pushToast({
|
pushToast({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
children: {
|
children: {
|
||||||
component: ThunkToast,
|
component: ThunkToast,
|
||||||
props: {thunk: retried},
|
props: {thunk: retried},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {randomId} from "@welshman/lib"
|
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()}))
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import {bech32ToHex, displayUrl, tryCatch} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {isNWCWallet, isWebLNWallet, toMsats} from "@welshman/util"
|
import {isNWCWallet, isWebLNWallet, toMsats} from "@welshman/util"
|
||||||
import type {Wallet} 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. */
|
/** Both lud06 and lud16 normalize into the bech32 lnurl zapping needs, so decode it before display. */
|
||||||
export const displayLnurl = (lnurl: string) => {
|
export const displayLnurl = (lnurl: string) => {
|
||||||
|
|
@ -19,11 +20,13 @@ export const displayLnurl = (lnurl: string) => {
|
||||||
return url ? displayUrl(url) : lnurl
|
return url ? displayUrl(url) : lnurl
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wallet = syncedWithGetter<Maybe<Wallet>>({
|
export const wallet = withGetter(
|
||||||
key: "wallet",
|
synced<Maybe<Wallet>>({
|
||||||
storage: ss,
|
key: "wallet",
|
||||||
defaultValue: undefined,
|
storage: ss,
|
||||||
})
|
defaultValue: undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const getWebLn = () => (window as any).webln
|
export const getWebLn = () => (window as any).webln
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {getIdOrAddress, sortEventsDesc, tagSpec, tagValue, COMMENT, MESSAGE} from "@welshman/util"
|
import {getIdOrAddress, sortEventsDesc, tagSpec, tagValue, COMMENT, MESSAGE} from "@welshman/util"
|
||||||
import {Comment} from "@welshman/domain"
|
import {Comment} from "@welshman/domain"
|
||||||
import {throttled} from "@welshman/store"
|
import {synced, throttled, withGetter} from "@welshman/store"
|
||||||
import {Events, Relays} from "@welshman/app"
|
import {Events, Relays} from "@welshman/app"
|
||||||
import {app, fromApp, reader} from "@app/core"
|
import {app, fromApp, reader} from "@app/core"
|
||||||
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
|
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 {isGroupSpace} from "@app/relays"
|
||||||
import {activeSpaceUrls} from "@app/rooms"
|
import {activeSpaceUrls} from "@app/rooms"
|
||||||
import {getHttpAuthHeader} from "@app/signer"
|
import {getHttpAuthHeader} from "@app/signer"
|
||||||
import {kv, syncedWithGetter} from "@app/storage"
|
import {kv} from "@app/storage"
|
||||||
import {notificationSettings} from "@app/push/adapters/common"
|
import {notificationSettings} from "@app/push/adapters/common"
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
export const checked = syncedWithGetter<Record<string, number>>({
|
export const checked = withGetter(
|
||||||
key: "checked",
|
synced<Record<string, number>>({
|
||||||
defaultValue: {},
|
key: "checked",
|
||||||
storage: kv,
|
defaultValue: {},
|
||||||
})
|
storage: kv,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const getChecked = (key: string) => checked.get()[key]
|
export const getChecked = (key: string) => checked.get()[key]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ import {
|
||||||
type Filter,
|
type Filter,
|
||||||
type TrustedEvent,
|
type TrustedEvent,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {merged} from "@welshman/store"
|
import {merged, synced, withGetter} from "@welshman/store"
|
||||||
import {User} from "@welshman/app"
|
import {User} from "@welshman/app"
|
||||||
import {navigate} from "@app/modal"
|
import {navigate} from "@app/modal"
|
||||||
import {app, messagingRelayLists, network, roomLists} from "@app/core"
|
import {app, messagingRelayLists, network, roomLists} from "@app/core"
|
||||||
import {DM_KINDS} from "@app/content"
|
import {DM_KINDS} from "@app/content"
|
||||||
import {getMutedRooms, shouldNotify, userSettingsValues} from "@app/settings"
|
import {getMutedRooms, shouldNotify, userSettingsValues} from "@app/settings"
|
||||||
import {makeEventPath, goToChat, goToSpace} from "@app/routes"
|
import {makeEventPath, goToChat, goToSpace} from "@app/routes"
|
||||||
import {kv, syncedWithGetter} from "@app/storage"
|
import {kv} from "@app/storage"
|
||||||
|
|
||||||
export type PushSubscription = {
|
export type PushSubscription = {
|
||||||
key: string
|
key: string
|
||||||
|
|
@ -38,26 +38,30 @@ export type PushState = {
|
||||||
export const requiresFallback =
|
export const requiresFallback =
|
||||||
Capacitor.getPlatform() === "android" && !Capacitor.isPluginAvailable("PushNotifications")
|
Capacitor.getPlatform() === "android" && !Capacitor.isPluginAvailable("PushNotifications")
|
||||||
|
|
||||||
export const pushState = syncedWithGetter<PushState>({
|
export const pushState = withGetter(
|
||||||
key: "notificationState",
|
synced<PushState>({
|
||||||
storage: kv,
|
key: "notificationState",
|
||||||
defaultValue: {useFallback: requiresFallback},
|
storage: kv,
|
||||||
})
|
defaultValue: {useFallback: requiresFallback},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const notificationSettings = syncedWithGetter({
|
export const notificationSettings = withGetter(
|
||||||
key: "notificationSettings",
|
synced({
|
||||||
storage: kv,
|
key: "notificationSettings",
|
||||||
defaultValue: {
|
storage: kv,
|
||||||
push: false,
|
defaultValue: {
|
||||||
sound: true,
|
push: false,
|
||||||
badge: false,
|
sound: true,
|
||||||
spaces: true,
|
badge: false,
|
||||||
mentions: true,
|
spaces: true,
|
||||||
messages: true,
|
mentions: true,
|
||||||
events: true,
|
messages: true,
|
||||||
reminder_minutes: [5],
|
events: true,
|
||||||
},
|
reminder_minutes: [5],
|
||||||
})
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export interface IPushAdapter {
|
export interface IPushAdapter {
|
||||||
request: (prompt?: boolean) => Promise<string>
|
request: (prompt?: boolean) => Promise<string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {matchTags, tagSpec} from "@welshman/util"
|
|
||||||
import {relayMemberLists} from "@app/core"
|
import {relayMemberLists} from "@app/core"
|
||||||
|
|
||||||
// Roles are rendered at a fixed saturation and lightness, chosen to read on both themes.
|
// 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.
|
// A translucent tint of the role color for use as a background fill.
|
||||||
export const roleColorSoft = (hue: number) => `hsl(${hue}, 70%, 50%, 0.15)`
|
export const roleColorSoft = (hue: number) => `hsl(${hue}, 70%, 50%, 0.15)`
|
||||||
|
|
||||||
// Map<pubkey, roleId[]> parsed from EXTRA values on ["member", pubkey, ...roleIds] tags
|
|
||||||
export const deriveSpaceMemberRoles = (url: string) =>
|
export const deriveSpaceMemberRoles = (url: string) =>
|
||||||
derived(relayMemberLists.get().forUrl(url), $members => {
|
derived(
|
||||||
const rolesByPubkey = new Map<string, string[]>()
|
relayMemberLists.get().forUrl(url),
|
||||||
|
$members => $members?.rolesByPubkey() ?? new Map<string, string[]>(),
|
||||||
for (const [, member, ...roleIds] of matchTags(tagSpec("member"), $members?.tags() ?? [])) {
|
)
|
||||||
if (member) {
|
|
||||||
rolesByPubkey.set(member, roleIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rolesByPubkey
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,7 @@ import * as nip19 from "nostr-tools/nip19"
|
||||||
import {derived, get} from "svelte/store"
|
import {derived, get} from "svelte/store"
|
||||||
import {addToMapKey, dec, formatTimestampAsDate, int, sortBy, uniq, MINUTE} from "@welshman/lib"
|
import {addToMapKey, dec, formatTimestampAsDate, int, sortBy, uniq, MINUTE} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {
|
import {ROOM_DELETE_EVENT, outbox, relay, seen, tagSpec, tagValue, toNostrURI} from "@welshman/util"
|
||||||
ROOM_DELETE_EVENT,
|
|
||||||
hexTags,
|
|
||||||
outbox,
|
|
||||||
relay,
|
|
||||||
seen,
|
|
||||||
tagSpec,
|
|
||||||
tagValue,
|
|
||||||
tagValueMatcher,
|
|
||||||
toNostrURI,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import type {EventContent, TrustedEvent} from "@welshman/util"
|
import type {EventContent, TrustedEvent} from "@welshman/util"
|
||||||
import {Comment, Message} from "@welshman/domain"
|
import {Comment, Message} from "@welshman/domain"
|
||||||
import type {BaseEventReader, EventWriter, Relay} 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.
|
// The room's admin list names the management kinds each admin may publish.
|
||||||
export const deriveUserRoomPermissions = (url: string, h: string) =>
|
export const deriveUserRoomPermissions = (url: string, h: string) =>
|
||||||
derived([user, rooms.get().forRoom(url, h)], ([$user, $room]) => {
|
derived(
|
||||||
const tag = $room?.admins?.tags().find(tagValueMatcher(hexTags("p"), $user.pubkey))
|
[user, rooms.get().forRoom(url, h)],
|
||||||
|
([$user, $room]) => $room?.admins?.rolesFor($user.pubkey).map(Number) ?? [],
|
||||||
return tag?.slice(2).map(Number) ?? []
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// Deleting someone else's content goes one of two ways, and a space answers for either one.
|
// Deleting someone else's content goes one of two ways, and a space answers for either one.
|
||||||
export enum AdminDelete {
|
export enum AdminDelete {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import type {Maybe} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
COMMENT,
|
COMMENT,
|
||||||
NOTE,
|
NOTE,
|
||||||
addressTags,
|
|
||||||
eventOutbox,
|
eventOutbox,
|
||||||
fromNostrURI,
|
fromNostrURI,
|
||||||
getAddress,
|
getAddress,
|
||||||
|
|
@ -25,7 +24,6 @@ import {
|
||||||
hexTags,
|
hexTags,
|
||||||
relays as relaySelections,
|
relays as relaySelections,
|
||||||
seen,
|
seen,
|
||||||
tagSpec,
|
|
||||||
tagValues,
|
tagValues,
|
||||||
topicTags,
|
topicTags,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
|
|
@ -176,13 +174,12 @@ export const buildCommentTree = (root: TrustedEvent, comments: TrustedEvent[]) =
|
||||||
|
|
||||||
export const isEventMuted = withGetter(
|
export const isEventMuted = withGetter(
|
||||||
derived([user, deriveUserItem(MuteLists)], ([$user, $muteList]) => {
|
derived([user, deriveUserItem(MuteLists)], ([$user, $muteList]) => {
|
||||||
const tags = $muteList?.tags() ?? []
|
const mutedEvents = new Set($muteList?.ids())
|
||||||
const mutedEvents = new Set(tagValues(hexTags("e"), tags))
|
const mutedPubkeys = new Set($muteList?.pubkeys())
|
||||||
const mutedPubkeys = new Set(tagValues(hexTags("p"), tags))
|
const mutedAddresses = new Set($muteList?.addresses())
|
||||||
const mutedAddresses = new Set(tagValues(addressTags("a"), tags))
|
const mutedTopics = new Set($muteList?.topics())
|
||||||
const mutedTopics = new Set(tagValues(topicTags("t"), tags))
|
|
||||||
// Words come from any client, so a blank or a regex character must not mute everyone or throw.
|
// 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())
|
.map(word => word.toLowerCase().trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ import {
|
||||||
verifiedSymbol,
|
verifiedSymbol,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Handle, TrustedEvent} from "@welshman/util"
|
import type {Handle, TrustedEvent} from "@welshman/util"
|
||||||
import {synced, withGetter} from "@welshman/store"
|
import {withGetter} from "@welshman/store"
|
||||||
import type {SyncedConfig} from "@welshman/store"
|
|
||||||
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
|
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
|
||||||
import {Relay, Zapper} from "@welshman/domain"
|
import {Relay, Zapper} from "@welshman/domain"
|
||||||
import type {RelayInfo, ZapperValues} 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 {appPolicies} from "@app/core"
|
||||||
import {DM_KINDS} from "@app/content"
|
import {DM_KINDS} from "@app/content"
|
||||||
|
|
||||||
// withGetter's type drops the ready promise synced adds
|
|
||||||
export const syncedWithGetter = <T>(config: SyncedConfig<T>) => {
|
|
||||||
const store = synced(config)
|
|
||||||
|
|
||||||
return {...withGetter(store), ready: store.ready}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StringStorage = {
|
type StringStorage = {
|
||||||
get: (key: string) => Promise<Maybe<string>>
|
get: (key: string) => Promise<Maybe<string>>
|
||||||
set: (key: string, value: string) => Promise<void>
|
set: (key: string, value: string) => Promise<void>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import type {FollowListReader} from "@welshman/domain"
|
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 {FollowLists, MessagingRelayLists, RelayLists, SearchRelayLists, Sync} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
|
|
@ -57,15 +57,17 @@ import {FEATURED_CONTENT_D} from "@app/featured"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {activeSpaceUrls, userSpaceUrls} from "@app/rooms"
|
import {activeSpaceUrls, userSpaceUrls} from "@app/rooms"
|
||||||
import {Settings} from "@app/settings"
|
import {Settings} from "@app/settings"
|
||||||
import {kv, syncedWithGetter} from "@app/storage"
|
import {kv} from "@app/storage"
|
||||||
import {hasBlossomSupport} from "@app/uploads"
|
import {hasBlossomSupport} from "@app/uploads"
|
||||||
|
|
||||||
// Unwrapping is unconditional, so this is what keeps a signer from decrypting the whole DM history.
|
// Unwrapping is unconditional, so this is what keeps a signer from decrypting the whole DM history.
|
||||||
export const shouldUnwrap = syncedWithGetter({
|
export const shouldUnwrap = withGetter(
|
||||||
key: "shouldUnwrap",
|
synced({
|
||||||
storage: kv,
|
key: "shouldUnwrap",
|
||||||
defaultValue: false,
|
storage: kv,
|
||||||
})
|
defaultValue: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue