Use welshman/app
This commit is contained in:
Generated
+11
@@ -23,6 +23,7 @@
|
|||||||
"@tiptap/extension-text": "^2.6.6",
|
"@tiptap/extension-text": "^2.6.6",
|
||||||
"@tiptap/suggestion": "^2.6.4",
|
"@tiptap/suggestion": "^2.6.4",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
|
"@welshman/app": "^0.0.1",
|
||||||
"@welshman/lib": "^0.0.15",
|
"@welshman/lib": "^0.0.15",
|
||||||
"@welshman/net": "^0.0.20",
|
"@welshman/net": "^0.0.20",
|
||||||
"@welshman/signer": "^0.0.4",
|
"@welshman/signer": "^0.0.4",
|
||||||
@@ -1652,6 +1653,16 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"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": {
|
"node_modules/@welshman/lib": {
|
||||||
"version": "0.0.15",
|
"version": "0.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.15.tgz",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"@tiptap/extension-text": "^2.6.6",
|
"@tiptap/extension-text": "^2.6.6",
|
||||||
"@tiptap/suggestion": "^2.6.4",
|
"@tiptap/suggestion": "^2.6.4",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
|
"@welshman/app": "^0.0.1",
|
||||||
"@welshman/lib": "^0.0.15",
|
"@welshman/lib": "^0.0.15",
|
||||||
"@welshman/net": "^0.0.20",
|
"@welshman/net": "^0.0.20",
|
||||||
"@welshman/signer": "^0.0.4",
|
"@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,
|
displayProfile,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {PublishStatus} from "@welshman/net"
|
import {PublishStatus} from "@welshman/net"
|
||||||
import {pk, repository, INDEXER_RELAYS} from "@app/base"
|
|
||||||
import {
|
import {
|
||||||
|
pubkey,
|
||||||
|
repository,
|
||||||
loadOne,
|
loadOne,
|
||||||
getWriteRelayUrls,
|
|
||||||
loadGroup,
|
|
||||||
loadGroupMembership,
|
|
||||||
loadProfile,
|
|
||||||
loadFollows,
|
|
||||||
loadMutes,
|
|
||||||
getRelaySelectionsByPubkey,
|
|
||||||
loadRelaySelections,
|
|
||||||
makeThunk,
|
makeThunk,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
getProfilesByPubkey,
|
loadProfile,
|
||||||
} from "@app/state"
|
profilesByPubkey,
|
||||||
|
relaySelectionsByPubkey,
|
||||||
|
loadRelaySelections,
|
||||||
|
getWriteRelayUrls,
|
||||||
|
loadFollows,
|
||||||
|
loadMutes,
|
||||||
|
} from "@welshman/app"
|
||||||
|
import {loadGroup, loadGroupMembership, INDEXER_RELAYS} from "@app/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
export const getPubkeyHints = (pubkey: string) => {
|
export const getPubkeyHints = (pubkey: string) => {
|
||||||
const selections = getRelaySelectionsByPubkey().get(pubkey)
|
const selections = relaySelectionsByPubkey.get().get(pubkey)
|
||||||
const relays = selections ? getWriteRelayUrls(selections) : []
|
const relays = selections ? getWriteRelayUrls(selections) : []
|
||||||
const hints = relays.length ? relays : INDEXER_RELAYS
|
const hints = relays.length ? relays : INDEXER_RELAYS
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export const getPubkeyHints = (pubkey: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getPubkeyPetname = (pubkey: string) => {
|
export const getPubkeyPetname = (pubkey: string) => {
|
||||||
const profile = getProfilesByPubkey().get(pubkey)
|
const profile = profilesByPubkey.get().get(pubkey)
|
||||||
const display = displayProfile(profile)
|
const display = displayProfile(profile)
|
||||||
|
|
||||||
return display
|
return display
|
||||||
@@ -82,9 +82,9 @@ export const loadUserData = async (pubkey: string, hints: string[] = []) => {
|
|||||||
export type ModifyTags = (tags: string[][]) => string[][]
|
export type ModifyTags = (tags: string[][]) => string[][]
|
||||||
|
|
||||||
export const updateList = async (kind: number, modifyTags: ModifyTags) => {
|
export const updateList = async (kind: number, modifyTags: ModifyTags) => {
|
||||||
const $pk = pk.get()!
|
const $pubkey = pubkey.get()!
|
||||||
const [prev] = repository.query([{kinds: [kind], authors: [$pk]}])
|
const [prev] = repository.query([{kinds: [kind], authors: [$pubkey]}])
|
||||||
const relays = getWriteRelayUrls(getRelaySelectionsByPubkey().get($pk))
|
const relays = getWriteRelayUrls(relaySelectionsByPubkey.get().get($pubkey))
|
||||||
|
|
||||||
// Preserve content if we have it
|
// Preserve content if we have it
|
||||||
const event = prev
|
const event = prev
|
||||||
@@ -102,7 +102,7 @@ export const removeGroupMemberships = (noms: string[]) =>
|
|||||||
|
|
||||||
export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => {
|
export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => {
|
||||||
const relays = [url]
|
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 event = createEvent(GROUP_JOIN, {tags: [["h", nom]]})
|
||||||
const statusData = await publishThunk(makeThunk({event, relays}))
|
const statusData = await publishThunk(makeThunk({event, relays}))
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||||
import {NProfileExtension, TagExtension as TopicExtension, ImageExtension} from "nostr-editor"
|
import {NProfileExtension, TagExtension as TopicExtension, ImageExtension} from "nostr-editor"
|
||||||
import {createEvent, CHAT_MESSAGE} from "@welshman/util"
|
import {createEvent, CHAT_MESSAGE} from "@welshman/util"
|
||||||
|
import {publishThunk, makeThunk} from "@welshman/app"
|
||||||
import {findNodes} from "@lib/tiptap"
|
import {findNodes} from "@lib/tiptap"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.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 {makeMention, makeIMeta} from "@app/commands"
|
||||||
import {getChatEditorOptions, addFile} from "@app/editor"
|
import {getChatEditorOptions, addFile} from "@app/editor"
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import type {NodeViewProps} from "@tiptap/core"
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
import {NodeViewWrapper} from "svelte-tiptap"
|
||||||
import {displayProfile} from "@welshman/util"
|
import {displayProfile} from "@welshman/util"
|
||||||
|
import {deriveProfile} from "@welshman/app"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import {deriveProfile} from "@app/state"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
export let node: NodeViewProps["node"]
|
||||||
export let selected: NodeViewProps["selected"]
|
export let selected: NodeViewProps["selected"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {deriveProfileDisplay} from "@app/state"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveEvents} from "@welshman/store"
|
||||||
import {PublishStatus} from "@welshman/net"
|
import {PublishStatus} from "@welshman/net"
|
||||||
|
import {
|
||||||
|
publishStatusData,
|
||||||
|
deriveProfile,
|
||||||
|
deriveProfileDisplay,
|
||||||
|
formatTimestampAsTime,
|
||||||
|
} from "@welshman/app"
|
||||||
|
import type {PublishStatusData} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
GROUP_REPLY,
|
GROUP_REPLY,
|
||||||
REACTION,
|
REACTION,
|
||||||
@@ -15,14 +22,12 @@
|
|||||||
displayRelayUrl,
|
displayRelayUrl,
|
||||||
getAncestorTags,
|
getAncestorTags,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
|
import {repository} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import {formatTimestampAsTime} from "@lib/util"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import {repository} from "@app/base"
|
import {deriveEvent} from "@app/state"
|
||||||
import type {PublishStatusData} from "@app/state"
|
|
||||||
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
|
|
||||||
import {getChatViewOptions} from "@app/editor"
|
import {getChatViewOptions} from "@app/editor"
|
||||||
|
|
||||||
export let event: TrustedEvent
|
export let event: TrustedEvent
|
||||||
@@ -73,8 +78,8 @@
|
|||||||
let editor: Readable<Editor>
|
let editor: Readable<Editor>
|
||||||
|
|
||||||
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
|
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
|
||||||
$: parentProfile = deriveProfile(parentPubkey)
|
$: parentProfile = deriveProfile(parentPubkey || "")
|
||||||
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
|
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey || "")
|
||||||
$: isPublished = findStatus($ps, [PublishStatus.Success])
|
$: isPublished = findStatus($ps, [PublishStatus.Success])
|
||||||
$: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
|
$: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
|
||||||
$: failure =
|
$: failure =
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Icon from "@lib/components/Icon.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"
|
import {clip} from "@app/toast"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {makeSecret, getNip07, Nip46Broker} from "@welshman/signer"
|
import {makeSecret, getNip07, Nip46Broker} from "@welshman/signer"
|
||||||
|
import {addSession, loadHandle, nip46Perms, type Session} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import InfoNostr from "@app/components/LogIn.svelte"
|
import InfoNostr from "@app/components/LogIn.svelte"
|
||||||
import type {Session} from "@app/types"
|
|
||||||
import {pushModal, clearModal} from "@app/modal"
|
import {pushModal, clearModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {addSession, nip46Perms} from "@app/base"
|
|
||||||
import {loadHandle} from "@app/state"
|
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/commands"
|
||||||
|
|
||||||
const signUp = () => pushModal(SignUp)
|
const signUp = () => pushModal(SignUp)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {clearStorage} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import {clearStorage} from "@app/storage"
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
||||||
|
import {addSession, nip46Perms, loadHandle} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -8,8 +9,6 @@
|
|||||||
import InfoNostr from "@app/components/LogIn.svelte"
|
import InfoNostr from "@app/components/LogIn.svelte"
|
||||||
import {pushModal, clearModal} from "@app/modal"
|
import {pushModal, clearModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {addSession, nip46Perms} from "@app/base"
|
|
||||||
import {loadHandle} from "@app/state"
|
|
||||||
|
|
||||||
const login = () => pushModal(LogIn)
|
const login = () => pushModal(LogIn)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
|
import {loadRelay} from "@welshman/app"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {splitGroupId, loadRelay, loadGroup} from "@app/state"
|
import {splitGroupId, loadGroup} from "@app/state"
|
||||||
import {addGroupMemberships} from "@app/commands"
|
import {addGroupMemberships} from "@app/commands"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relay.supported_nips?.includes(29)) {
|
if (!relay.profile?.supported_nips?.includes(29)) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, it looks like that relay doesn't support nostr spaces.",
|
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 type {Writable} from "svelte/store"
|
||||||
import {nprofileEncode} from "nostr-tools/nip19"
|
import {nprofileEncode} from "nostr-tools/nip19"
|
||||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
TagExtension as TopicExtension,
|
TagExtension as TopicExtension,
|
||||||
} from "nostr-editor"
|
} from "nostr-editor"
|
||||||
import type {StampedEvent} from "@welshman/util"
|
import type {StampedEvent} from "@welshman/util"
|
||||||
|
import {signer, topicSearch, profileSearch} from "@welshman/app"
|
||||||
import {LinkExtension, asInline, createSuggestions} from "@lib/tiptap"
|
import {LinkExtension, asInline, createSuggestions} from "@lib/tiptap"
|
||||||
import GroupComposeMention from "@app/components/GroupComposeMention.svelte"
|
import GroupComposeMention from "@app/components/GroupComposeMention.svelte"
|
||||||
import GroupComposeTopic from "@app/components/GroupComposeTopic.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 GroupComposeSuggestions from "@app/components/GroupComposeSuggestions.svelte"
|
||||||
import GroupComposeTopicSuggestion from "@app/components/GroupComposeTopicSuggestion.svelte"
|
import GroupComposeTopicSuggestion from "@app/components/GroupComposeTopicSuggestion.svelte"
|
||||||
import GroupComposeProfileSuggestion from "@app/components/GroupComposeProfileSuggestion.svelte"
|
import GroupComposeProfileSuggestion from "@app/components/GroupComposeProfileSuggestion.svelte"
|
||||||
import {signer} from "@app/base"
|
|
||||||
import {searchProfiles, searchTopics} from "@app/state"
|
|
||||||
import {getPubkeyHints} from "@app/commands"
|
import {getPubkeyHints} from "@app/commands"
|
||||||
|
|
||||||
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
|
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
|
||||||
@@ -90,7 +88,7 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor
|
|||||||
char: "@",
|
char: "@",
|
||||||
name: "nprofile",
|
name: "nprofile",
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
search: searchProfiles,
|
search: profileSearch,
|
||||||
select: (pubkey: string, props: any) => {
|
select: (pubkey: string, props: any) => {
|
||||||
const relays = getPubkeyHints(pubkey)
|
const relays = getPubkeyHints(pubkey)
|
||||||
const nprofile = nprofileEncode({pubkey, relays})
|
const nprofile = nprofileEncode({pubkey, relays})
|
||||||
@@ -119,13 +117,13 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor
|
|||||||
const attrs = {
|
const attrs = {
|
||||||
...mark.attrs,
|
...mark.attrs,
|
||||||
...HTMLAttributes,
|
...HTMLAttributes,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
rel: 'noopener noreferer',
|
rel: "noopener noreferer",
|
||||||
href: `https://coracle.social/topics/${mark.attrs.tag.toLowerCase()}`,
|
href: `https://coracle.social/topics/${mark.attrs.tag.toLowerCase()}`,
|
||||||
class: "underline",
|
class: "underline",
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['a', attrs, 0]
|
return ["a", attrs, 0]
|
||||||
},
|
},
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
@@ -133,7 +131,7 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor
|
|||||||
char: "#",
|
char: "#",
|
||||||
name: "topic",
|
name: "topic",
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
search: searchTopics,
|
search: topicSearch,
|
||||||
select: (name: string, props: any) => props.command({name}),
|
select: (name: string, props: any) => props.command({name}),
|
||||||
allowCreate: true,
|
allowCreate: true,
|
||||||
suggestionComponent: GroupComposeTopicSuggestion,
|
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 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 type {Maybe} from "@welshman/lib"
|
||||||
import {
|
import {max, between, groupBy, pushToMapKey, nthEq, stripProtocol, indexBy} from "@welshman/lib"
|
||||||
max,
|
|
||||||
first,
|
|
||||||
between,
|
|
||||||
uniqBy,
|
|
||||||
groupBy,
|
|
||||||
pushToMapKey,
|
|
||||||
nthEq,
|
|
||||||
batcher,
|
|
||||||
postJson,
|
|
||||||
stripProtocol,
|
|
||||||
assoc,
|
|
||||||
indexBy,
|
|
||||||
now,
|
|
||||||
Worker,
|
|
||||||
inc,
|
|
||||||
} from "@welshman/lib"
|
|
||||||
import {
|
import {
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
getIdentifier,
|
getIdentifier,
|
||||||
getRelayTags,
|
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
GROUP_META,
|
GROUP_META,
|
||||||
PROFILE,
|
|
||||||
RELAYS,
|
|
||||||
FOLLOWS,
|
|
||||||
MUTES,
|
|
||||||
GROUPS,
|
GROUPS,
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
readProfile,
|
|
||||||
readList,
|
|
||||||
asDecryptedEvent,
|
|
||||||
displayProfile,
|
|
||||||
displayPubkey,
|
|
||||||
GROUP_JOIN,
|
GROUP_JOIN,
|
||||||
GROUP_ADD_USER,
|
GROUP_ADD_USER,
|
||||||
|
REACTION,
|
||||||
|
ZAP_RESPONSE,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
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 {
|
import {
|
||||||
INDEXER_RELAYS,
|
env,
|
||||||
DUFFLEPUD_URL,
|
pubkey,
|
||||||
repository,
|
repository,
|
||||||
pk,
|
createSearch,
|
||||||
getSession,
|
|
||||||
getSigner,
|
|
||||||
REACTION_KINDS,
|
|
||||||
} from "@app/base"
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
|
|
||||||
export const createCollection = <T>({
|
|
||||||
name,
|
|
||||||
store,
|
|
||||||
getKey,
|
|
||||||
load,
|
load,
|
||||||
}: {
|
collection,
|
||||||
name: string
|
loadRelay,
|
||||||
store: Readable<T[]>
|
relaysByPubkey,
|
||||||
getKey: (item: T) => string
|
loadProfile,
|
||||||
load: (key: string, ...args: any) => Promise<any>
|
profilesByPubkey,
|
||||||
}) => {
|
loadRelaySelections,
|
||||||
const indexStore = derived(store, $items => indexBy(getKey, $items))
|
getWriteRelayUrls,
|
||||||
const getIndex = getter(indexStore)
|
} from "@welshman/app"
|
||||||
const getItem = (key: string) => getIndex().get(key)
|
import type {Relay} from "@welshman/app"
|
||||||
const pending = new Map<string, Promise<Maybe<T>>>()
|
import type {SubscribeRequest} from "@welshman/net"
|
||||||
|
import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store"
|
||||||
|
|
||||||
const loadItem = async (key: string, ...args: any[]) => {
|
export const DEFAULT_RELAYS = [
|
||||||
if (getFreshness(name, key) > now() - 3600) {
|
"wss://groups.fiatjaf.com/",
|
||||||
return getIndex().get(key)
|
"wss://relay29.galaxoidlabs.com/",
|
||||||
}
|
"wss://devrelay.highlighter.com/",
|
||||||
|
"wss://relay.groups.nip29.com/",
|
||||||
|
]
|
||||||
|
|
||||||
if (pending.has(key)) {
|
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
|
||||||
await pending.get(key)
|
|
||||||
} else {
|
|
||||||
setFreshness(name, key, now())
|
|
||||||
|
|
||||||
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
|
Object.assign(env, {DUFFLEPUD_URL})
|
||||||
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||||
let attempted = false
|
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
|
// Groups
|
||||||
|
|
||||||
export const GROUP_DELIMITER = `'`
|
export const GROUP_DELIMITER = `'`
|
||||||
@@ -586,10 +119,9 @@ export const groups = deriveEventsMapped<PublishedGroup>(repository, {
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
indexStore: groupsByNom,
|
indexStore: groupsByNom,
|
||||||
getIndex: getGroupsByNom,
|
|
||||||
deriveItem: deriveGroup,
|
deriveItem: deriveGroup,
|
||||||
loadItem: loadGroup,
|
loadItem: loadGroup,
|
||||||
} = createCollection({
|
} = collection({
|
||||||
name: "groups",
|
name: "groups",
|
||||||
store: groups,
|
store: groups,
|
||||||
getKey: (group: PublishedGroup) => group.nom,
|
getKey: (group: PublishedGroup) => group.nom,
|
||||||
@@ -694,10 +226,9 @@ export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>(rep
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
indexStore: groupMembershipByPubkey,
|
indexStore: groupMembershipByPubkey,
|
||||||
getIndex: getGroupMembershipsByPubkey,
|
|
||||||
deriveItem: deriveGroupMembership,
|
deriveItem: deriveGroupMembership,
|
||||||
loadItem: loadGroupMembership,
|
loadItem: loadGroupMembership,
|
||||||
} = createCollection({
|
} = collection({
|
||||||
name: "groupMemberships",
|
name: "groupMemberships",
|
||||||
store: groupMemberships,
|
store: groupMemberships,
|
||||||
getKey: groupMembership => groupMembership.event.pubkey,
|
getKey: groupMembership => groupMembership.event.pubkey,
|
||||||
@@ -754,10 +285,9 @@ export const groupChats = derived(groupMessages, $groupMessages => {
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
indexStore: groupChatByNom,
|
indexStore: groupChatByNom,
|
||||||
getIndex: getGroupChatsByNom,
|
|
||||||
deriveItem: deriveGroupChat,
|
deriveItem: deriveGroupChat,
|
||||||
loadItem: loadGroupChat,
|
loadItem: loadGroupChat,
|
||||||
} = createCollection({
|
} = collection({
|
||||||
name: "groupChats",
|
name: "groupChats",
|
||||||
store: groupChats,
|
store: groupChats,
|
||||||
getKey: groupChat => groupChat.nom,
|
getKey: groupChat => groupChat.nom,
|
||||||
@@ -773,22 +303,22 @@ export const {
|
|||||||
|
|
||||||
// User stuff
|
// User stuff
|
||||||
|
|
||||||
export const userProfile = derived([pk, profilesByPubkey], ([$pk, $profilesByPubkey]) => {
|
export const userProfile = derived([pubkey, profilesByPubkey], ([$pubkey, $profilesByPubkey]) => {
|
||||||
if (!$pk) return null
|
if (!$pubkey) return null
|
||||||
|
|
||||||
loadProfile($pk)
|
loadProfile($pubkey)
|
||||||
|
|
||||||
return $profilesByPubkey.get($pk)
|
return $profilesByPubkey.get($pubkey)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const userMembership = derived(
|
export const userMembership = derived(
|
||||||
[pk, groupMembershipByPubkey],
|
[pubkey, groupMembershipByPubkey],
|
||||||
([$pk, $groupMembershipByPubkey]) => {
|
([$pubkey, $groupMembershipByPubkey]) => {
|
||||||
if (!$pk) return null
|
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")
|
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 type {Editor} from "@tiptap/core"
|
||||||
import {PluginKey} from "@tiptap/pm/state"
|
import {PluginKey} from "@tiptap/pm/state"
|
||||||
import Suggestion from "@tiptap/suggestion"
|
import Suggestion from "@tiptap/suggestion"
|
||||||
import type {Search} from "@lib/util"
|
import type {Search} from "@welshman/app"
|
||||||
|
|
||||||
export type SuggestionsOptions = {
|
export type SuggestionsOptions = {
|
||||||
char: string
|
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 {goto} from "$app/navigation"
|
||||||
import {browser} from "$app/environment"
|
import {browser} from "$app/environment"
|
||||||
import {sleep} from "@welshman/lib"
|
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 {createEventStore, adapter} from "@welshman/store"
|
||||||
import ModalBox from "@lib/components/ModalBox.svelte"
|
import ModalBox from "@lib/components/ModalBox.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
@@ -13,12 +26,8 @@
|
|||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import {modals, clearModal} from "@app/modal"
|
import {modals, clearModal} from "@app/modal"
|
||||||
import {theme} from "@app/theme"
|
import {theme} from "@app/theme"
|
||||||
import {pk, session, repository, DEFAULT_RELAYS} from "@app/base"
|
import {DEFAULT_RELAYS} from "@app/state"
|
||||||
import type {PublishStatusData, PublishStatusDataByUrlById} from "@app/state"
|
|
||||||
import {relays, freshness, plaintext, handles, loadRelay, publishStatusData} from "@app/state"
|
|
||||||
import {initStorage} from "@app/storage"
|
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/commands"
|
||||||
import * as base from "@app/base"
|
|
||||||
import * as state from "@app/state"
|
import * as state from "@app/state"
|
||||||
|
|
||||||
let ready: Promise<unknown>
|
let ready: Promise<unknown>
|
||||||
@@ -48,7 +57,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
Object.assign(window, {get, base, state})
|
Object.assign(window, {get, state})
|
||||||
|
|
||||||
ready = initStorage(3, {
|
ready = initStorage(3, {
|
||||||
events: {
|
events: {
|
||||||
@@ -137,8 +146,8 @@
|
|||||||
loadRelay(url)
|
loadRelay(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pk) {
|
if ($pubkey) {
|
||||||
loadUserData($pk)
|
loadUserData($pubkey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const prerender = true
|
||||||
|
export const ssr = false
|
||||||
@@ -2,16 +2,15 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import Masonry from "svelte-bricks"
|
import Masonry from "svelte-bricks"
|
||||||
import {GROUP_META, displayRelayUrl} from "@welshman/util"
|
import {GROUP_META, displayRelayUrl} from "@welshman/util"
|
||||||
|
import {load, relays} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {DEFAULT_RELAYS} from "@app/base"
|
|
||||||
import {
|
import {
|
||||||
load,
|
|
||||||
displayGroup,
|
displayGroup,
|
||||||
relays,
|
|
||||||
searchGroups,
|
searchGroups,
|
||||||
relayUrlsByNom,
|
relayUrlsByNom,
|
||||||
userMembership,
|
userMembership,
|
||||||
|
DEFAULT_RELAYS,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
|
||||||
let term = ""
|
let term = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import themes from "daisyui/src/theming/themes"
|
import themes from "daisyui/src/theming/themes"
|
||||||
import {identity} from "@welshman/lib"
|
import {identity} from "@welshman/lib"
|
||||||
import {createSearch} from "@lib/util"
|
import {createSearch} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {theme} from "@app/theme"
|
import {theme} from "@app/theme"
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {displayRelayUrl, isShareableRelayUrl} from "@welshman/util"
|
import {displayRelayUrl, isShareableRelayUrl} from "@welshman/util"
|
||||||
import type {SignedEvent} from "@welshman/util"
|
import type {SignedEvent} from "@welshman/util"
|
||||||
|
import {subscribe, loadRelay, relaySearch} from "@welshman/app"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/base"
|
import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/state"
|
||||||
import {searchRelays, subscribe, loadRelay} from "@app/state"
|
|
||||||
|
|
||||||
const relays = readable(DEFAULT_RELAYS)
|
const relays = readable(DEFAULT_RELAYS)
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<Icon icon="magnifer" />
|
<Icon icon="magnifer" />
|
||||||
<input bind:value={term} class="grow" type="text" placeholder="Search for relays..." />
|
<input bind:value={term} class="grow" type="text" placeholder="Search for relays..." />
|
||||||
</label>
|
</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="card2 card2-sm flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon="remote-controller-minimalistic" />
|
<Icon icon="remote-controller-minimalistic" />
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {sortBy, now} from "@welshman/lib"
|
import {sortBy, now} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
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 Icon from "@lib/components/Icon.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import GroupNote from "@app/components/GroupNote.svelte"
|
import GroupNote from "@app/components/GroupNote.svelte"
|
||||||
import GroupCompose from "@app/components/GroupCompose.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 {nom} = $page.params
|
||||||
const chat = deriveGroupChat(nom)
|
const chat = deriveGroupChat(nom)
|
||||||
|
|||||||
Reference in New Issue
Block a user