forked from coracle/flotilla
Use welshman/app
This commit is contained in:
Generated
+11
@@ -23,6 +23,7 @@
|
||||
"@tiptap/extension-text": "^2.6.6",
|
||||
"@tiptap/suggestion": "^2.6.4",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/app": "^0.0.1",
|
||||
"@welshman/lib": "^0.0.15",
|
||||
"@welshman/net": "^0.0.20",
|
||||
"@welshman/signer": "^0.0.4",
|
||||
@@ -1652,6 +1653,16 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/app": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.1.tgz",
|
||||
"integrity": "sha512-3vvbfOwMhY6iSyswhTlmL1TCkHVF7nlpxUVvbNpgS4KD2gIs4YFr9WqAlSL4x1Ebjdm0EE3KakCr3YodP5kIOg==",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "0.0.15",
|
||||
"@welshman/util": "0.0.28",
|
||||
"svelte": "^4.2.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/lib": {
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.15.tgz",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"@tiptap/extension-text": "^2.6.6",
|
||||
"@tiptap/suggestion": "^2.6.4",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/app": "^0.0.1",
|
||||
"@welshman/lib": "^0.0.15",
|
||||
"@welshman/net": "^0.0.20",
|
||||
"@welshman/signer": "^0.0.4",
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {memoize, assoc} from "@welshman/lib"
|
||||
import type {TrustedEvent, HashedEvent} from "@welshman/util"
|
||||
import {Repository, createEvent, Relay, REACTION, ZAP_RESPONSE} from "@welshman/util"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {NetworkContext, Tracker} from "@welshman/net"
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer"
|
||||
import {synced} from "@lib/util"
|
||||
import type {Session} from "@app/types"
|
||||
|
||||
export const DEFAULT_RELAYS = [
|
||||
"wss://groups.fiatjaf.com/",
|
||||
"wss://relay29.galaxoidlabs.com/",
|
||||
"wss://devrelay.highlighter.com/",
|
||||
"wss://relay.groups.nip29.com/",
|
||||
]
|
||||
|
||||
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
|
||||
|
||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||
|
||||
export const repository = new Repository<HashedEvent>()
|
||||
|
||||
export const relay = new Relay(repository)
|
||||
|
||||
export const tracker = new Tracker()
|
||||
|
||||
export const pk = withGetter(synced<string | null>("pk", null))
|
||||
|
||||
export const sessions = withGetter(synced<Record<string, Session>>("sessions", {}))
|
||||
|
||||
export const session = withGetter(
|
||||
derived([pk, sessions], ([$pk, $sessions]) => ($pk ? $sessions[$pk] : null)),
|
||||
)
|
||||
|
||||
export const getSession = (pubkey: string) => sessions.get()[pubkey]
|
||||
|
||||
export const addSession = (session: Session) => {
|
||||
sessions.update(assoc(session.pubkey, session))
|
||||
pk.set(session.pubkey)
|
||||
}
|
||||
|
||||
export const nip46Perms = "sign_event:22242,nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt"
|
||||
|
||||
export const getSigner = memoize((session: Session) => {
|
||||
switch (session?.method) {
|
||||
case "extension":
|
||||
return new Nip07Signer()
|
||||
case "privkey":
|
||||
return new Nip01Signer(session.secret!)
|
||||
case "nip46":
|
||||
return new Nip46Signer(Nip46Broker.get(session.pubkey, session.secret!, session.handler!))
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const signer = withGetter(derived(session, getSigner))
|
||||
|
||||
const seenChallenges = new Set()
|
||||
|
||||
Object.assign(NetworkContext, {
|
||||
onEvent: (url: string, event: TrustedEvent) => tracker.track(event.id, url),
|
||||
isDeleted: (url: string, event: TrustedEvent) => repository.isDeleted(event),
|
||||
onAuth: async (url: string, challenge: string) => {
|
||||
if (seenChallenges.has(challenge) || !signer.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
seenChallenges.add(challenge)
|
||||
|
||||
const event = await signer.get()!.sign(
|
||||
createEvent(22242, {
|
||||
tags: [
|
||||
["relay", url],
|
||||
["challenge", challenge],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
NetworkContext.pool.get(url).send(["AUTH", event])
|
||||
|
||||
return event
|
||||
},
|
||||
})
|
||||
+17
-17
@@ -8,26 +8,26 @@ import {
|
||||
displayProfile,
|
||||
} from "@welshman/util"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {pk, repository, INDEXER_RELAYS} from "@app/base"
|
||||
import {
|
||||
pubkey,
|
||||
repository,
|
||||
loadOne,
|
||||
getWriteRelayUrls,
|
||||
loadGroup,
|
||||
loadGroupMembership,
|
||||
loadProfile,
|
||||
loadFollows,
|
||||
loadMutes,
|
||||
getRelaySelectionsByPubkey,
|
||||
loadRelaySelections,
|
||||
makeThunk,
|
||||
publishThunk,
|
||||
getProfilesByPubkey,
|
||||
} from "@app/state"
|
||||
loadProfile,
|
||||
profilesByPubkey,
|
||||
relaySelectionsByPubkey,
|
||||
loadRelaySelections,
|
||||
getWriteRelayUrls,
|
||||
loadFollows,
|
||||
loadMutes,
|
||||
} from "@welshman/app"
|
||||
import {loadGroup, loadGroupMembership, INDEXER_RELAYS} from "@app/state"
|
||||
|
||||
// Utils
|
||||
|
||||
export const getPubkeyHints = (pubkey: string) => {
|
||||
const selections = getRelaySelectionsByPubkey().get(pubkey)
|
||||
const selections = relaySelectionsByPubkey.get().get(pubkey)
|
||||
const relays = selections ? getWriteRelayUrls(selections) : []
|
||||
const hints = relays.length ? relays : INDEXER_RELAYS
|
||||
|
||||
@@ -35,7 +35,7 @@ export const getPubkeyHints = (pubkey: string) => {
|
||||
}
|
||||
|
||||
export const getPubkeyPetname = (pubkey: string) => {
|
||||
const profile = getProfilesByPubkey().get(pubkey)
|
||||
const profile = profilesByPubkey.get().get(pubkey)
|
||||
const display = displayProfile(profile)
|
||||
|
||||
return display
|
||||
@@ -82,9 +82,9 @@ export const loadUserData = async (pubkey: string, hints: string[] = []) => {
|
||||
export type ModifyTags = (tags: string[][]) => string[][]
|
||||
|
||||
export const updateList = async (kind: number, modifyTags: ModifyTags) => {
|
||||
const $pk = pk.get()!
|
||||
const [prev] = repository.query([{kinds: [kind], authors: [$pk]}])
|
||||
const relays = getWriteRelayUrls(getRelaySelectionsByPubkey().get($pk))
|
||||
const $pubkey = pubkey.get()!
|
||||
const [prev] = repository.query([{kinds: [kind], authors: [$pubkey]}])
|
||||
const relays = getWriteRelayUrls(relaySelectionsByPubkey.get().get($pubkey))
|
||||
|
||||
// Preserve content if we have it
|
||||
const event = prev
|
||||
@@ -102,7 +102,7 @@ export const removeGroupMemberships = (noms: string[]) =>
|
||||
|
||||
export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => {
|
||||
const relays = [url]
|
||||
const filters = [{kinds: [9000], "#h": [nom], "#p": [pk.get()!], since: now() - 30}]
|
||||
const filters = [{kinds: [9000], "#h": [nom], "#p": [pubkey.get()!], since: now() - 30}]
|
||||
|
||||
const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]})
|
||||
const statusData = await publishThunk(makeThunk({event, relays}))
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||
import {NProfileExtension, TagExtension as TopicExtension, ImageExtension} from "nostr-editor"
|
||||
import {createEvent, CHAT_MESSAGE} from "@welshman/util"
|
||||
import {publishThunk, makeThunk} from "@welshman/app"
|
||||
import {findNodes} from "@lib/tiptap"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {publishThunk, makeThunk, userRelayUrlsByNom} from "@app/state"
|
||||
import {userRelayUrlsByNom} from "@app/state"
|
||||
import {makeMention, makeIMeta} from "@app/commands"
|
||||
import {getChatEditorOptions, addFile} from "@app/editor"
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import {displayProfile} from "@welshman/util"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import {deriveProfile} from "@app/state"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
export let selected: NodeViewProps["selected"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {deriveProfileDisplay} from "@app/state"
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
|
||||
export let value
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {
|
||||
publishStatusData,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
formatTimestampAsTime,
|
||||
} from "@welshman/app"
|
||||
import type {PublishStatusData} from "@welshman/app"
|
||||
import {
|
||||
GROUP_REPLY,
|
||||
REACTION,
|
||||
@@ -15,14 +22,12 @@
|
||||
displayRelayUrl,
|
||||
getAncestorTags,
|
||||
} from "@welshman/util"
|
||||
import {repository} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import {formatTimestampAsTime} from "@lib/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {repository} from "@app/base"
|
||||
import type {PublishStatusData} from "@app/state"
|
||||
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
|
||||
import {deriveEvent} from "@app/state"
|
||||
import {getChatViewOptions} from "@app/editor"
|
||||
|
||||
export let event: TrustedEvent
|
||||
@@ -73,8 +78,8 @@
|
||||
let editor: Readable<Editor>
|
||||
|
||||
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
|
||||
$: parentProfile = deriveProfile(parentPubkey)
|
||||
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
|
||||
$: parentProfile = deriveProfile(parentPubkey || "")
|
||||
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey || "")
|
||||
$: isPublished = findStatus($ps, [PublishStatus.Success])
|
||||
$: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
|
||||
$: failure =
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {DEFAULT_RELAYS} from "@app/base"
|
||||
import {DEFAULT_RELAYS} from "@app/state"
|
||||
import {clip} from "@app/toast"
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {makeSecret, getNip07, Nip46Broker} from "@welshman/signer"
|
||||
import {addSession, loadHandle, nip46Perms, type Session} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SignUp from "@app/components/SignUp.svelte"
|
||||
import InfoNostr from "@app/components/LogIn.svelte"
|
||||
import type {Session} from "@app/types"
|
||||
import {pushModal, clearModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {addSession, nip46Perms} from "@app/base"
|
||||
import {loadHandle} from "@app/state"
|
||||
import {loadUserData} from "@app/commands"
|
||||
|
||||
const signUp = () => pushModal(SignUp)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {clearStorage} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import {clearStorage} from "@app/storage"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
||||
import {addSession, nip46Perms, loadHandle} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -8,8 +9,6 @@
|
||||
import InfoNostr from "@app/components/LogIn.svelte"
|
||||
import {pushModal, clearModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {addSession, nip46Perms} from "@app/base"
|
||||
import {loadHandle} from "@app/state"
|
||||
|
||||
const login = () => pushModal(LogIn)
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {loadRelay} from "@welshman/app"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {splitGroupId, loadRelay, loadGroup} from "@app/state"
|
||||
import {splitGroupId, loadGroup} from "@app/state"
|
||||
import {addGroupMemberships} from "@app/commands"
|
||||
|
||||
const back = () => history.back()
|
||||
@@ -24,7 +25,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (!relay.supported_nips?.includes(29)) {
|
||||
if (!relay.profile?.supported_nips?.includes(29)) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, it looks like that relay doesn't support nostr spaces.",
|
||||
|
||||
+6
-8
@@ -1,4 +1,3 @@
|
||||
import cx from 'classnames'
|
||||
import type {Writable} from "svelte/store"
|
||||
import {nprofileEncode} from "nostr-tools/nip19"
|
||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
TagExtension as TopicExtension,
|
||||
} from "nostr-editor"
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
import {signer, topicSearch, profileSearch} from "@welshman/app"
|
||||
import {LinkExtension, asInline, createSuggestions} from "@lib/tiptap"
|
||||
import GroupComposeMention from "@app/components/GroupComposeMention.svelte"
|
||||
import GroupComposeTopic from "@app/components/GroupComposeTopic.svelte"
|
||||
@@ -34,8 +34,6 @@ import GroupComposeLink from "@app/components/GroupComposeLink.svelte"
|
||||
import GroupComposeSuggestions from "@app/components/GroupComposeSuggestions.svelte"
|
||||
import GroupComposeTopicSuggestion from "@app/components/GroupComposeTopicSuggestion.svelte"
|
||||
import GroupComposeProfileSuggestion from "@app/components/GroupComposeProfileSuggestion.svelte"
|
||||
import {signer} from "@app/base"
|
||||
import {searchProfiles, searchTopics} from "@app/state"
|
||||
import {getPubkeyHints} from "@app/commands"
|
||||
|
||||
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
|
||||
@@ -90,7 +88,7 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor
|
||||
char: "@",
|
||||
name: "nprofile",
|
||||
editor: this.editor,
|
||||
search: searchProfiles,
|
||||
search: profileSearch,
|
||||
select: (pubkey: string, props: any) => {
|
||||
const relays = getPubkeyHints(pubkey)
|
||||
const nprofile = nprofileEncode({pubkey, relays})
|
||||
@@ -119,13 +117,13 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor
|
||||
const attrs = {
|
||||
...mark.attrs,
|
||||
...HTMLAttributes,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferer',
|
||||
target: "_blank",
|
||||
rel: "noopener noreferer",
|
||||
href: `https://coracle.social/topics/${mark.attrs.tag.toLowerCase()}`,
|
||||
class: "underline",
|
||||
}
|
||||
|
||||
return ['a', attrs, 0]
|
||||
return ["a", attrs, 0]
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
@@ -133,7 +131,7 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor
|
||||
char: "#",
|
||||
name: "topic",
|
||||
editor: this.editor,
|
||||
search: searchTopics,
|
||||
search: topicSearch,
|
||||
select: (name: string, props: any) => props.command({name}),
|
||||
allowCreate: true,
|
||||
suggestionComponent: GroupComposeTopicSuggestion,
|
||||
|
||||
+41
-511
@@ -1,123 +1,52 @@
|
||||
import {throttle} from "throttle-debounce"
|
||||
import type {Readable} from "svelte/store"
|
||||
import type {FuseResult} from "fuse.js"
|
||||
import {get, writable, readable, derived} from "svelte/store"
|
||||
import {get, derived} from "svelte/store"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {
|
||||
max,
|
||||
first,
|
||||
between,
|
||||
uniqBy,
|
||||
groupBy,
|
||||
pushToMapKey,
|
||||
nthEq,
|
||||
batcher,
|
||||
postJson,
|
||||
stripProtocol,
|
||||
assoc,
|
||||
indexBy,
|
||||
now,
|
||||
Worker,
|
||||
inc,
|
||||
} from "@welshman/lib"
|
||||
import {max, between, groupBy, pushToMapKey, nthEq, stripProtocol, indexBy} from "@welshman/lib"
|
||||
import {
|
||||
getIdFilters,
|
||||
getIdentifier,
|
||||
getRelayTags,
|
||||
normalizeRelayUrl,
|
||||
GROUP_META,
|
||||
PROFILE,
|
||||
RELAYS,
|
||||
FOLLOWS,
|
||||
MUTES,
|
||||
GROUPS,
|
||||
getGroupTags,
|
||||
readProfile,
|
||||
readList,
|
||||
asDecryptedEvent,
|
||||
displayProfile,
|
||||
displayPubkey,
|
||||
GROUP_JOIN,
|
||||
GROUP_ADD_USER,
|
||||
REACTION,
|
||||
ZAP_RESPONSE,
|
||||
} from "@welshman/util"
|
||||
import type {
|
||||
SignedEvent,
|
||||
HashedEvent,
|
||||
EventTemplate,
|
||||
TrustedEvent,
|
||||
PublishedProfile,
|
||||
PublishedList,
|
||||
} from "@welshman/util"
|
||||
import type {SubscribeRequest} from "@welshman/net"
|
||||
import {publish as basePublish, subscribe as baseSubscribe, PublishStatus} from "@welshman/net"
|
||||
import {decrypt, stamp, own, hash} from "@welshman/signer"
|
||||
import {custom, deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
|
||||
import {createSearch} from "@lib/util"
|
||||
import type {Handle, Relay} from "@app/types"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
INDEXER_RELAYS,
|
||||
DUFFLEPUD_URL,
|
||||
env,
|
||||
pubkey,
|
||||
repository,
|
||||
pk,
|
||||
getSession,
|
||||
getSigner,
|
||||
REACTION_KINDS,
|
||||
} from "@app/base"
|
||||
|
||||
// Utils
|
||||
|
||||
export const createCollection = <T>({
|
||||
name,
|
||||
store,
|
||||
getKey,
|
||||
createSearch,
|
||||
load,
|
||||
}: {
|
||||
name: string
|
||||
store: Readable<T[]>
|
||||
getKey: (item: T) => string
|
||||
load: (key: string, ...args: any) => Promise<any>
|
||||
}) => {
|
||||
const indexStore = derived(store, $items => indexBy(getKey, $items))
|
||||
const getIndex = getter(indexStore)
|
||||
const getItem = (key: string) => getIndex().get(key)
|
||||
const pending = new Map<string, Promise<Maybe<T>>>()
|
||||
collection,
|
||||
loadRelay,
|
||||
relaysByPubkey,
|
||||
loadProfile,
|
||||
profilesByPubkey,
|
||||
loadRelaySelections,
|
||||
getWriteRelayUrls,
|
||||
} from "@welshman/app"
|
||||
import type {Relay} from "@welshman/app"
|
||||
import type {SubscribeRequest} from "@welshman/net"
|
||||
import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store"
|
||||
|
||||
const loadItem = async (key: string, ...args: any[]) => {
|
||||
if (getFreshness(name, key) > now() - 3600) {
|
||||
return getIndex().get(key)
|
||||
}
|
||||
export const DEFAULT_RELAYS = [
|
||||
"wss://groups.fiatjaf.com/",
|
||||
"wss://relay29.galaxoidlabs.com/",
|
||||
"wss://devrelay.highlighter.com/",
|
||||
"wss://relay.groups.nip29.com/",
|
||||
]
|
||||
|
||||
if (pending.has(key)) {
|
||||
await pending.get(key)
|
||||
} else {
|
||||
setFreshness(name, key, now())
|
||||
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
|
||||
|
||||
const promise = load(key, ...args)
|
||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
pending.set(key, promise)
|
||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||
|
||||
await promise
|
||||
|
||||
pending.delete(key)
|
||||
}
|
||||
|
||||
return getIndex().get(key)
|
||||
}
|
||||
|
||||
const deriveItem = (key: Maybe<string>, ...args: any[]) => {
|
||||
if (!key) {
|
||||
return readable(undefined)
|
||||
}
|
||||
|
||||
// If we don't yet have the item, or it's stale, trigger a request for it. The derived
|
||||
// store will update when it arrives
|
||||
load(key, ...args)
|
||||
|
||||
return derived(indexStore, $index => $index.get(key))
|
||||
}
|
||||
|
||||
return {indexStore, getIndex, deriveItem, loadItem, getItem}
|
||||
}
|
||||
Object.assign(env, {DUFFLEPUD_URL})
|
||||
|
||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
let attempted = false
|
||||
@@ -138,402 +67,6 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Publish
|
||||
|
||||
export type Thunk = {
|
||||
event: HashedEvent
|
||||
relays: string[]
|
||||
}
|
||||
|
||||
export type ThunkWithResolve = Thunk & {
|
||||
resolve: (data: PublishStatusDataByUrl) => void
|
||||
}
|
||||
|
||||
export const thunkWorker = new Worker<ThunkWithResolve>()
|
||||
|
||||
thunkWorker.addGlobalHandler(async ({event, relays, resolve}: ThunkWithResolve) => {
|
||||
const session = getSession(event.pubkey)
|
||||
|
||||
if (!session) {
|
||||
return console.warn(`No session found for ${event.pubkey}`)
|
||||
}
|
||||
|
||||
const signedEvent = await getSigner(session)!.sign(event)
|
||||
const pub = basePublish({event: signedEvent, relays})
|
||||
|
||||
// Copy the signature over since we had deferred it
|
||||
;(repository.getEvent(signedEvent.id) as SignedEvent).sig = signedEvent.sig
|
||||
|
||||
// Track publish success
|
||||
const {id} = event
|
||||
const statusByUrl: PublishStatusDataByUrl = {}
|
||||
|
||||
pub.emitter.on("*", (status: PublishStatus, url: string, message: string) => {
|
||||
publishStatusData.update(
|
||||
assoc(id, Object.assign(statusByUrl, {[url]: {id, url, status, message}})),
|
||||
)
|
||||
|
||||
if (
|
||||
Object.values(statusByUrl).filter(s => s.status !== PublishStatus.Pending).length ===
|
||||
relays.length
|
||||
) {
|
||||
resolve(statusByUrl)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export type ThunkParams = {
|
||||
event: EventTemplate
|
||||
relays: string[]
|
||||
}
|
||||
|
||||
export const makeThunk = ({event, relays}: ThunkParams) => {
|
||||
const $pk = get(pk)
|
||||
|
||||
if (!$pk) {
|
||||
throw new Error("Unable to make thunk if no user is logged in")
|
||||
}
|
||||
|
||||
return {event: hash(own(stamp(event), $pk)), relays}
|
||||
}
|
||||
|
||||
export const publishThunk = (thunk: Thunk) =>
|
||||
new Promise<PublishStatusDataByUrl>(resolve => {
|
||||
thunkWorker.push({...thunk, resolve})
|
||||
repository.publish(thunk.event)
|
||||
})
|
||||
|
||||
// Subscribe
|
||||
|
||||
export const subscribe = (request: SubscribeRequest) => {
|
||||
const sub = baseSubscribe({delay: 50, authTimeout: 3000, ...request})
|
||||
|
||||
sub.emitter.on("event", (url: string, e: SignedEvent) => {
|
||||
repository.publish(e)
|
||||
})
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
export const load = (request: SubscribeRequest) =>
|
||||
new Promise<TrustedEvent[]>(resolve => {
|
||||
const sub = subscribe({closeOnEose: true, timeout: 3000, ...request})
|
||||
const events: TrustedEvent[] = []
|
||||
|
||||
sub.emitter.on("event", (url: string, e: SignedEvent) => events.push(e))
|
||||
sub.emitter.on("complete", () => resolve(events))
|
||||
})
|
||||
|
||||
export const loadOne = async (request: SubscribeRequest) => first(await load(request))
|
||||
|
||||
// Publish status
|
||||
|
||||
export type PublishStatusData = {
|
||||
id: string
|
||||
url: string
|
||||
message: string
|
||||
status: PublishStatus
|
||||
}
|
||||
|
||||
export type PublishStatusDataByUrl = Record<string, PublishStatusData>
|
||||
|
||||
export type PublishStatusDataByUrlById = Record<string, PublishStatusDataByUrl>
|
||||
|
||||
export const publishStatusData = writable<PublishStatusDataByUrlById>({})
|
||||
|
||||
// Freshness
|
||||
|
||||
export const freshness = withGetter(writable<Record<string, number>>({}))
|
||||
|
||||
export const getFreshnessKey = (ns: string, key: string) => `${ns}:${key}`
|
||||
|
||||
export const getFreshness = (ns: string, key: string) =>
|
||||
freshness.get()[getFreshnessKey(ns, key)] || 0
|
||||
|
||||
export const setFreshness = (ns: string, key: string, ts: number) =>
|
||||
freshness.update(assoc(getFreshnessKey(ns, key), ts))
|
||||
|
||||
export const setFreshnessBulk = (ns: string, updates: Record<string, number>) =>
|
||||
freshness.update($freshness => {
|
||||
for (const [key, ts] of Object.entries(updates)) {
|
||||
$freshness[key] = ts
|
||||
}
|
||||
|
||||
return $freshness
|
||||
})
|
||||
|
||||
// Plaintext
|
||||
|
||||
export const plaintext = withGetter(writable<Record<string, string>>({}))
|
||||
|
||||
export const getPlaintext = (e: TrustedEvent) => plaintext.get()[e.id]
|
||||
|
||||
export const setPlaintext = (e: TrustedEvent, content: string) =>
|
||||
plaintext.update(assoc(e.id, content))
|
||||
|
||||
export const ensurePlaintext = async (e: TrustedEvent) => {
|
||||
if (e.content && !getPlaintext(e)) {
|
||||
const $signer = getSigner(getSession(e.pubkey))
|
||||
|
||||
if ($signer) {
|
||||
setPlaintext(e, await decrypt($signer, e.pubkey, e.content))
|
||||
}
|
||||
}
|
||||
|
||||
return getPlaintext(e)
|
||||
}
|
||||
|
||||
// Topics
|
||||
|
||||
export type Topic = {
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export const topics = custom<Topic[]>(setter => {
|
||||
const getTopics = () => {
|
||||
const topics = new Map<string, number>()
|
||||
for (const tagString of repository.eventsByTag.keys()) {
|
||||
if (tagString.startsWith("t:")) {
|
||||
const topic = tagString.slice(2).toLowerCase()
|
||||
|
||||
topics.set(topic, inc(topics.get(topic)))
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(topics.entries()).map(([name, count]) => ({name, count}))
|
||||
}
|
||||
|
||||
setter(getTopics())
|
||||
|
||||
const onUpdate = throttle(3000, () => setter(getTopics()))
|
||||
|
||||
repository.on("update", onUpdate)
|
||||
|
||||
return () => repository.off("update", onUpdate)
|
||||
})
|
||||
|
||||
export const searchTopics = derived(topics, $topics =>
|
||||
createSearch($topics, {
|
||||
getValue: (topic: Topic) => topic.name,
|
||||
fuseOptions: {keys: ["name"]},
|
||||
}),
|
||||
)
|
||||
|
||||
// Relay info
|
||||
|
||||
export const relays = writable<Relay[]>([])
|
||||
|
||||
export const relaysByPubkey = derived(relays, $relays =>
|
||||
groupBy(
|
||||
($relay: Relay) => $relay.pubkey,
|
||||
$relays.filter(r => r.pubkey),
|
||||
),
|
||||
)
|
||||
|
||||
export const {
|
||||
indexStore: relaysByUrl,
|
||||
getIndex: getRelaysByUrl,
|
||||
deriveItem: deriveRelay,
|
||||
loadItem: loadRelay,
|
||||
} = createCollection({
|
||||
name: "relays",
|
||||
store: relays,
|
||||
getKey: (relay: Relay) => relay.url,
|
||||
load: batcher(800, async (urls: string[]) => {
|
||||
const urlSet = new Set(urls)
|
||||
const res = await postJson(`${DUFFLEPUD_URL}/relay/info`, {urls: Array.from(urlSet)})
|
||||
const index = indexBy((item: any) => item.url, res?.data || [])
|
||||
const items: Relay[] = urls.map(url => {
|
||||
const {info = {}} = index.get(url) || {}
|
||||
|
||||
return {...info, url}
|
||||
})
|
||||
|
||||
relays.update($relays => uniqBy($relay => $relay.url, [...$relays, ...items]))
|
||||
|
||||
return items
|
||||
}),
|
||||
})
|
||||
|
||||
export const searchRelays = derived(relays, $relays =>
|
||||
createSearch($relays, {
|
||||
getValue: (relay: Relay) => relay.url,
|
||||
fuseOptions: {
|
||||
keys: ["url", "name", {name: "description", weight: 0.3}],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// Handles
|
||||
|
||||
export const handles = writable<Handle[]>([])
|
||||
|
||||
export const {
|
||||
indexStore: handlesByNip05,
|
||||
getIndex: getHandlesByNip05,
|
||||
deriveItem: deriveHandle,
|
||||
loadItem: loadHandle,
|
||||
} = createCollection({
|
||||
name: "handles",
|
||||
store: handles,
|
||||
getKey: (handle: Handle) => handle.nip05,
|
||||
load: batcher(800, async (nip05s: string[]) => {
|
||||
const nip05Set = new Set(nip05s)
|
||||
const res = await postJson(`${DUFFLEPUD_URL}/handle/info`, {handles: Array.from(nip05Set)})
|
||||
const index = indexBy((item: any) => item.handle, res?.data || [])
|
||||
const items: Handle[] = nip05s.map(nip05 => {
|
||||
const {info = {}} = index.get(nip05) || {}
|
||||
|
||||
return {...info, nip05}
|
||||
})
|
||||
|
||||
handles.update($handles => uniqBy($handle => $handle.nip05, [...$handles, ...items]))
|
||||
|
||||
return items
|
||||
}),
|
||||
})
|
||||
|
||||
// Profiles
|
||||
|
||||
export const profiles = deriveEventsMapped<PublishedProfile>(repository, {
|
||||
filters: [{kinds: [PROFILE]}],
|
||||
eventToItem: readProfile,
|
||||
itemToEvent: item => item.event,
|
||||
})
|
||||
|
||||
export const {
|
||||
indexStore: profilesByPubkey,
|
||||
getIndex: getProfilesByPubkey,
|
||||
deriveItem: deriveProfile,
|
||||
loadItem: loadProfile,
|
||||
} = createCollection({
|
||||
name: "profiles",
|
||||
store: profiles,
|
||||
getKey: profile => profile.event.pubkey,
|
||||
load: async (pubkey: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
const relays = getWriteRelayUrls(await loadRelaySelections(pubkey))
|
||||
|
||||
return load({
|
||||
...request,
|
||||
relays: [...relays, ...hints, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [PROFILE], authors: [pubkey]}],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const searchProfiles = derived(profiles, $profiles =>
|
||||
createSearch($profiles, {
|
||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||
fuseOptions: {
|
||||
keys: ["name", "display_name", {name: "about", weight: 0.3}],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export const displayProfileByPubkey = (pubkey: string, profile?: PublishedProfile) =>
|
||||
displayProfile(profile, pubkey ? displayPubkey(pubkey) : undefined)
|
||||
|
||||
export const deriveProfileDisplay = (pubkey: string) =>
|
||||
derived(deriveProfile(pubkey), $profile => displayProfileByPubkey(pubkey, $profile))
|
||||
|
||||
// Relay selections
|
||||
|
||||
export const getReadRelayUrls = (event?: TrustedEvent): string[] =>
|
||||
getRelayTags(event?.tags || [])
|
||||
.filter((t: string[]) => !t[2] || t[2] === "read")
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1]))
|
||||
|
||||
export const getWriteRelayUrls = (event?: TrustedEvent): string[] =>
|
||||
getRelayTags(event?.tags || [])
|
||||
.filter((t: string[]) => !t[2] || t[2] === "write")
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1]))
|
||||
|
||||
export const relaySelections = deriveEvents(repository, {filters: [{kinds: [RELAYS]}]})
|
||||
|
||||
export const {
|
||||
indexStore: relaySelectionsByPubkey,
|
||||
getIndex: getRelaySelectionsByPubkey,
|
||||
deriveItem: deriveRelaySelections,
|
||||
loadItem: loadRelaySelections,
|
||||
} = createCollection({
|
||||
name: "relaySelections",
|
||||
store: relaySelections,
|
||||
getKey: relaySelections => relaySelections.pubkey,
|
||||
load: (pubkey: string, hints = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
load({
|
||||
...request,
|
||||
relays: [...hints, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [RELAYS], authors: [pubkey]}],
|
||||
}),
|
||||
})
|
||||
|
||||
// Follows
|
||||
|
||||
export const follows = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [FOLLOWS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async (event: TrustedEvent) =>
|
||||
readList(
|
||||
asDecryptedEvent(event, {
|
||||
content: await ensurePlaintext(event),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const {
|
||||
indexStore: followsByPubkey,
|
||||
getIndex: getFollowsByPubkey,
|
||||
deriveItem: deriveFollows,
|
||||
loadItem: loadFollows,
|
||||
} = createCollection({
|
||||
name: "follows",
|
||||
store: follows,
|
||||
getKey: follows => follows.event.pubkey,
|
||||
load: async (pubkey: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
const relays = getWriteRelayUrls(await loadRelaySelections(pubkey, hints))
|
||||
|
||||
return load({
|
||||
...request,
|
||||
relays: [...relays, ...hints, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [FOLLOWS], authors: [pubkey]}],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Mutes
|
||||
|
||||
export const mutes = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [MUTES]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: async (event: TrustedEvent) =>
|
||||
readList(
|
||||
asDecryptedEvent(event, {
|
||||
content: await ensurePlaintext(event),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const {
|
||||
indexStore: mutesByPubkey,
|
||||
getIndex: getMutesByPubkey,
|
||||
deriveItem: deriveMutes,
|
||||
loadItem: loadMutes,
|
||||
} = createCollection({
|
||||
name: "mutes",
|
||||
store: mutes,
|
||||
getKey: mute => mute.event.pubkey,
|
||||
load: async (pubkey: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
const relays = getWriteRelayUrls(await loadRelaySelections(pubkey, hints))
|
||||
|
||||
return load({
|
||||
...request,
|
||||
relays: [...relays, ...hints, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [MUTES], authors: [pubkey]}],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Groups
|
||||
|
||||
export const GROUP_DELIMITER = `'`
|
||||
@@ -586,10 +119,9 @@ export const groups = deriveEventsMapped<PublishedGroup>(repository, {
|
||||
|
||||
export const {
|
||||
indexStore: groupsByNom,
|
||||
getIndex: getGroupsByNom,
|
||||
deriveItem: deriveGroup,
|
||||
loadItem: loadGroup,
|
||||
} = createCollection({
|
||||
} = collection({
|
||||
name: "groups",
|
||||
store: groups,
|
||||
getKey: (group: PublishedGroup) => group.nom,
|
||||
@@ -694,10 +226,9 @@ export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>(rep
|
||||
|
||||
export const {
|
||||
indexStore: groupMembershipByPubkey,
|
||||
getIndex: getGroupMembershipsByPubkey,
|
||||
deriveItem: deriveGroupMembership,
|
||||
loadItem: loadGroupMembership,
|
||||
} = createCollection({
|
||||
} = collection({
|
||||
name: "groupMemberships",
|
||||
store: groupMemberships,
|
||||
getKey: groupMembership => groupMembership.event.pubkey,
|
||||
@@ -754,10 +285,9 @@ export const groupChats = derived(groupMessages, $groupMessages => {
|
||||
|
||||
export const {
|
||||
indexStore: groupChatByNom,
|
||||
getIndex: getGroupChatsByNom,
|
||||
deriveItem: deriveGroupChat,
|
||||
loadItem: loadGroupChat,
|
||||
} = createCollection({
|
||||
} = collection({
|
||||
name: "groupChats",
|
||||
store: groupChats,
|
||||
getKey: groupChat => groupChat.nom,
|
||||
@@ -773,22 +303,22 @@ export const {
|
||||
|
||||
// User stuff
|
||||
|
||||
export const userProfile = derived([pk, profilesByPubkey], ([$pk, $profilesByPubkey]) => {
|
||||
if (!$pk) return null
|
||||
export const userProfile = derived([pubkey, profilesByPubkey], ([$pubkey, $profilesByPubkey]) => {
|
||||
if (!$pubkey) return null
|
||||
|
||||
loadProfile($pk)
|
||||
loadProfile($pubkey)
|
||||
|
||||
return $profilesByPubkey.get($pk)
|
||||
return $profilesByPubkey.get($pubkey)
|
||||
})
|
||||
|
||||
export const userMembership = derived(
|
||||
[pk, groupMembershipByPubkey],
|
||||
([$pk, $groupMembershipByPubkey]) => {
|
||||
if (!$pk) return null
|
||||
[pubkey, groupMembershipByPubkey],
|
||||
([$pubkey, $groupMembershipByPubkey]) => {
|
||||
if (!$pubkey) return null
|
||||
|
||||
loadGroupMembership($pk)
|
||||
loadGroupMembership($pubkey)
|
||||
|
||||
return $groupMembershipByPubkey.get($pk)
|
||||
return $groupMembershipByPubkey.get($pubkey)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import {openDB, deleteDB} from "idb"
|
||||
import type {IDBPDatabase} from "idb"
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {writable} from "svelte/store"
|
||||
import type {Unsubscriber, Writable} from "svelte/store"
|
||||
import {randomInt} from "@welshman/lib"
|
||||
import {withGetter} from "@welshman/store"
|
||||
|
||||
export type Item = Record<string, any>
|
||||
|
||||
export type IndexedDbAdapter = {
|
||||
keyPath: string
|
||||
store: Writable<Item[]>
|
||||
}
|
||||
|
||||
export let db: IDBPDatabase
|
||||
|
||||
export const dead = withGetter(writable(false))
|
||||
|
||||
export const subs: Unsubscriber[] = []
|
||||
|
||||
export const DB_NAME = "flotilla"
|
||||
|
||||
export const getAll = async (name: string) => {
|
||||
const tx = db.transaction(name, "readwrite")
|
||||
const store = tx.objectStore(name)
|
||||
const result = await store.getAll()
|
||||
|
||||
await tx.done
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const bulkPut = async (name: string, data: any[]) => {
|
||||
const tx = db.transaction(name, "readwrite")
|
||||
const store = tx.objectStore(name)
|
||||
|
||||
await Promise.all(data.map(item => store.put(item)))
|
||||
await tx.done
|
||||
}
|
||||
|
||||
export const bulkDelete = async (name: string, ids: string[]) => {
|
||||
const tx = db.transaction(name, "readwrite")
|
||||
const store = tx.objectStore(name)
|
||||
|
||||
await Promise.all(ids.map(id => store.delete(id)))
|
||||
await tx.done
|
||||
}
|
||||
|
||||
export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapter) => {
|
||||
let copy = await getAll(name)
|
||||
|
||||
adapter.store.set(copy)
|
||||
|
||||
adapter.store.subscribe(
|
||||
throttle(randomInt(3000, 5000), async (data: Item[]) => {
|
||||
if (dead.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
const prevIds = new Set(copy.map(item => item[adapter.keyPath]))
|
||||
const currentIds = new Set(data.map(item => item[adapter.keyPath]))
|
||||
const newRecords = data.filter(r => !prevIds.has(r[adapter.keyPath]))
|
||||
const removedRecords = copy.filter(r => !currentIds.has(r[adapter.keyPath]))
|
||||
|
||||
copy = data
|
||||
|
||||
if (newRecords.length > 0) {
|
||||
await bulkPut(name, newRecords)
|
||||
}
|
||||
|
||||
if (removedRecords.length > 0) {
|
||||
await bulkDelete(
|
||||
name,
|
||||
removedRecords.map(item => item[adapter.keyPath]),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const initStorage = async (version: number, adapters: Record<string, IndexedDbAdapter>) => {
|
||||
if (!window.indexedDB) return
|
||||
|
||||
window.addEventListener("beforeunload", () => closeStorage())
|
||||
|
||||
db = await openDB(DB_NAME, version, {
|
||||
upgrade(db: IDBPDatabase) {
|
||||
const names = Object.keys(adapters)
|
||||
|
||||
for (const name of db.objectStoreNames) {
|
||||
if (!names.includes(name)) {
|
||||
db.deleteObjectStore(name)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, {keyPath}] of Object.entries(adapters)) {
|
||||
try {
|
||||
db.createObjectStore(name, {keyPath})
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(adapters).map(([name, config]) => initIndexedDbAdapter(name, config)),
|
||||
)
|
||||
}
|
||||
|
||||
export const closeStorage = async () => {
|
||||
dead.set(true)
|
||||
subs.forEach(unsub => unsub())
|
||||
await db?.close()
|
||||
}
|
||||
|
||||
export const clearStorage = async () => {
|
||||
await closeStorage()
|
||||
await deleteDB(DB_NAME)
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import {synced} from "@lib/util"
|
||||
import {synced} from "@welshman/store"
|
||||
|
||||
export const theme = synced<string>("theme", "dark")
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type {Nip46Handler} from "@welshman/signer"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
|
||||
export type Session = {
|
||||
method: string
|
||||
pubkey: string
|
||||
secret?: string
|
||||
handler?: Nip46Handler
|
||||
}
|
||||
|
||||
export type Relay = RelayProfile
|
||||
|
||||
export type Handle = {
|
||||
pubkey: string
|
||||
nip05: string
|
||||
nip46: string[]
|
||||
relays: string[]
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import tippy, {type Instance} from "tippy.js"
|
||||
import type {Editor} from "@tiptap/core"
|
||||
import {PluginKey} from "@tiptap/pm/state"
|
||||
import Suggestion from "@tiptap/suggestion"
|
||||
import type {Search} from "@lib/util"
|
||||
import type {Search} from "@welshman/app"
|
||||
|
||||
export type SuggestionsOptions = {
|
||||
char: string
|
||||
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
import Fuse from "fuse.js"
|
||||
import type {IFuseOptions, FuseResult} from "fuse.js"
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {writable} from "svelte/store"
|
||||
import {sortBy} from "@welshman/lib"
|
||||
import {browser} from "$app/environment"
|
||||
|
||||
export const parseJson = (json: string) => {
|
||||
if (!json) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const getJson = (k: string) => (browser ? parseJson(localStorage.getItem(k) || "") : null)
|
||||
|
||||
export const setJson = (k: string, v: any) => {
|
||||
if (browser) {
|
||||
localStorage.setItem(k, JSON.stringify(v))
|
||||
}
|
||||
}
|
||||
|
||||
export const synced = <T>(key: string, defaultValue: T, delay = 300) => {
|
||||
const init = getJson(key)
|
||||
const store = writable<T>(init === null ? defaultValue : init)
|
||||
|
||||
store.subscribe(throttle(delay, (value: T) => setJson(key, value)))
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export type SearchOptions<V, T> = {
|
||||
getValue: (item: T) => V
|
||||
fuseOptions?: IFuseOptions<T>
|
||||
sortFn?: (items: FuseResult<T>) => any
|
||||
}
|
||||
|
||||
export type Search<V, T> = {
|
||||
getValue: (item: T) => V
|
||||
getOption: (value: V) => T | undefined
|
||||
searchOptions: (term: string) => T[]
|
||||
searchValues: (term: string) => V[]
|
||||
}
|
||||
|
||||
export const createSearch = <V, T>(data: T[], opts: SearchOptions<V, T>): Search<V, T> => {
|
||||
const fuse = new Fuse(data, {...opts.fuseOptions, includeScore: true})
|
||||
const map = new Map<V, T>(data.map(item => [opts.getValue(item), item]))
|
||||
|
||||
const search = (term: string) => {
|
||||
let results = term ? fuse.search(term) : data.map(item => ({item, score: 1}) as FuseResult<T>)
|
||||
|
||||
if (opts.sortFn) {
|
||||
results = sortBy(opts.sortFn, results)
|
||||
}
|
||||
|
||||
return results.map(result => result.item)
|
||||
}
|
||||
|
||||
return {
|
||||
getValue: opts.getValue,
|
||||
getOption: (value: V) => map.get(value),
|
||||
searchOptions: (term: string) => search(term),
|
||||
searchValues: (term: string) => search(term).map(opts.getValue),
|
||||
}
|
||||
}
|
||||
|
||||
export const secondsToDate = (ts: number) => new Date(ts * 1000)
|
||||
|
||||
export const dateToSeconds = (date: Date) => Math.round(date.valueOf() / 1000)
|
||||
|
||||
export const getTimeZone = () => new Date().toString().match(/GMT[^\s]+/)
|
||||
|
||||
export const createLocalDate = (dateString: any) => new Date(`${dateString} ${getTimeZone()}`)
|
||||
|
||||
export const getLocale = () => new Intl.DateTimeFormat().resolvedOptions().locale
|
||||
|
||||
export const formatTimestamp = (ts: number) => {
|
||||
const formatter = new Intl.DateTimeFormat(getLocale(), {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
})
|
||||
|
||||
return formatter.format(secondsToDate(ts))
|
||||
}
|
||||
|
||||
export const formatTimestampAsDate = (ts: number) => {
|
||||
const formatter = new Intl.DateTimeFormat(getLocale(), {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
|
||||
return formatter.format(secondsToDate(ts))
|
||||
}
|
||||
|
||||
export const formatTimestampAsTime = (ts: number) => {
|
||||
const formatter = new Intl.DateTimeFormat(getLocale(), {
|
||||
timeStyle: "short",
|
||||
})
|
||||
|
||||
return formatter.format(secondsToDate(ts))
|
||||
}
|
||||
@@ -6,6 +6,19 @@
|
||||
import {goto} from "$app/navigation"
|
||||
import {browser} from "$app/environment"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {
|
||||
relays,
|
||||
handles,
|
||||
loadRelay,
|
||||
initStorage,
|
||||
repository,
|
||||
session,
|
||||
pubkey,
|
||||
publishStatusData,
|
||||
plaintext,
|
||||
freshness,
|
||||
} from "@welshman/app"
|
||||
import type {PublishStatusData, PublishStatusDataByUrlById} from "@welshman/app"
|
||||
import {createEventStore, adapter} from "@welshman/store"
|
||||
import ModalBox from "@lib/components/ModalBox.svelte"
|
||||
import Toast from "@app/components/Toast.svelte"
|
||||
@@ -13,12 +26,8 @@
|
||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||
import {modals, clearModal} from "@app/modal"
|
||||
import {theme} from "@app/theme"
|
||||
import {pk, session, repository, DEFAULT_RELAYS} from "@app/base"
|
||||
import type {PublishStatusData, PublishStatusDataByUrlById} from "@app/state"
|
||||
import {relays, freshness, plaintext, handles, loadRelay, publishStatusData} from "@app/state"
|
||||
import {initStorage} from "@app/storage"
|
||||
import {DEFAULT_RELAYS} from "@app/state"
|
||||
import {loadUserData} from "@app/commands"
|
||||
import * as base from "@app/base"
|
||||
import * as state from "@app/state"
|
||||
|
||||
let ready: Promise<unknown>
|
||||
@@ -48,7 +57,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
Object.assign(window, {get, base, state})
|
||||
Object.assign(window, {get, state})
|
||||
|
||||
ready = initStorage(3, {
|
||||
events: {
|
||||
@@ -137,8 +146,8 @@
|
||||
loadRelay(url)
|
||||
}
|
||||
|
||||
if ($pk) {
|
||||
loadUserData($pk)
|
||||
if ($pubkey) {
|
||||
loadUserData($pubkey)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const prerender = true
|
||||
export const ssr = false
|
||||
@@ -2,16 +2,15 @@
|
||||
import {onMount} from "svelte"
|
||||
import Masonry from "svelte-bricks"
|
||||
import {GROUP_META, displayRelayUrl} from "@welshman/util"
|
||||
import {load, relays} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {DEFAULT_RELAYS} from "@app/base"
|
||||
import {
|
||||
load,
|
||||
displayGroup,
|
||||
relays,
|
||||
searchGroups,
|
||||
relayUrlsByNom,
|
||||
userMembership,
|
||||
DEFAULT_RELAYS,
|
||||
} from "@app/state"
|
||||
|
||||
let term = ""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import themes from "daisyui/src/theming/themes"
|
||||
import {identity} from "@welshman/lib"
|
||||
import {createSearch} from "@lib/util"
|
||||
import {createSearch} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {theme} from "@app/theme"
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import {readable} from "svelte/store"
|
||||
import {displayRelayUrl, isShareableRelayUrl} from "@welshman/util"
|
||||
import type {SignedEvent} from "@welshman/util"
|
||||
import {subscribe, loadRelay, relaySearch} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/base"
|
||||
import {searchRelays, subscribe, loadRelay} from "@app/state"
|
||||
import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/state"
|
||||
|
||||
const relays = readable(DEFAULT_RELAYS)
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<Icon icon="magnifer" />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search for relays..." />
|
||||
</label>
|
||||
{#each $searchRelays.searchValues(term).filter(url => !$relays.includes(url)) as url (url)}
|
||||
{#each $relaySearch.searchValues(term).filter(url => !$relays.includes(url)) as url (url)}
|
||||
<div class="card2 card2-sm flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="remote-controller-minimalistic" />
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, now} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {formatTimestampAsDate} from "@lib/util"
|
||||
import {subscribe, formatTimestampAsDate} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import GroupNote from "@app/components/GroupNote.svelte"
|
||||
import GroupCompose from "@app/components/GroupCompose.svelte"
|
||||
import {subscribe, deriveGroupChat, userRelayUrlsByNom} from "@app/state"
|
||||
import {deriveGroupChat, userRelayUrlsByNom} from "@app/state"
|
||||
|
||||
const {nom} = $page.params
|
||||
const chat = deriveGroupChat(nom)
|
||||
|
||||
Reference in New Issue
Block a user