Add new version of note create components

This commit is contained in:
Jon Staab 2024-06-25 16:58:15 -07:00
parent fb32bf8ccc
commit f98fb6e84c
20 changed files with 634 additions and 50 deletions

View File

@ -203,13 +203,8 @@
router.register("/", import("src/app/views/Home.svelte"))
router.register("/notes", import("src/app/views/Home.svelte"))
router.register("/notes/create", import("src/app/views/NoteCreate.svelte"), {
router.register("/notes/create", import("src/app/views/NoteCreate2.svelte"), {
requireSigner: true,
serializers: {
pubkey: asPerson,
group: asNaddr("group"),
type: asString("type"),
},
})
router.register("/notes/:entity", import("src/app/views/NoteDetail.svelte"), {
serializers: {

View File

@ -1,4 +1,5 @@
<script lang="ts">
import * as Content from "@welshman/content"
import {slide, fly} from "src/util/transition"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
@ -6,7 +7,7 @@
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {menuIsOpen, searchTerm} from "src/app/state"
import {router} from "src/app/util/router"
import {router, makeDraftNote} from "src/app/util"
import {env, pubkey, canSign, hasNewNotifications, hasNewMessages} from "src/engine"
let innerWidth = 0
@ -32,18 +33,18 @@
return router.at("/login").open()
}
const params = {} as any
const draft = makeDraftNote()
const props = router.getProps($page) as any
if ($page.path.startsWith("/people") && props.pubkey) {
params.pubkey = props.pubkey
draft.content = Content.parse(props.pubkey)
}
if ($env.FORCE_GROUP) {
params.group = $env.FORCE_GROUP
draft.groups = [$env.FORCE_GROUP]
}
router.at("notes/create").qp(params).open()
router.at("notes/create").cx({draft}).open()
}
</script>
@ -69,10 +70,13 @@
<div
on:mousedown|preventDefault
out:fly|local={{y: 20, duration: 200}}
class="absolute right-0 top-10 w-96 rounded shadow-2xl opacity-100 transition-colors">
<div class="max-h-[70vh] overflow-auto bg-tinted-700 rounded">
class="absolute right-0 top-10 w-96 rounded opacity-100 shadow-2xl transition-colors">
<div class="max-h-[70vh] overflow-auto rounded bg-tinted-700">
<SearchResults bind:searching term={searchTerm}>
<div slot="result" let:result class="px-4 py-2 transition-colors hover:bg-neutral-800 cursor-pointer">
<div
slot="result"
let:result
class="cursor-pointer px-4 py-2 transition-colors hover:bg-neutral-800">
{#if result.type === "topic"}
#{result.topic.name}
{:else if result.type === "profile"}

View File

@ -0,0 +1,35 @@
<script lang="ts">
import {commaFormat} from 'hurdak'
import {throttle} from 'throttle-debounce'
import Card from 'src/partials/Card.svelte'
import FlexColumn from 'src/partials/FlexColumn.svelte'
import Anchor from 'src/partials/Anchor.svelte'
export let ctrl
const updateCounts = throttle(300, () => {
ctrl.setContent("")
})
</script>
<Card class="!p-0">
<FlexColumn large>
<div class="p-6">
Todo: new version of compose that combines preview with input (wysiwyg)
</div>
<div class="flex justify-between items-center border-t border-solid border-neutral-600 py-2 px-6">
<Anchor button accent class="!text-sm !h-5">
Add an image
</Anchor>
<div class="flex items-center justify-end gap-2 text-neutral-200">
<small>
{commaFormat($ctrl.counts.chars)} characters
</small>
<span></span>
<small>
{commaFormat($ctrl.counts.words)} words
</small>
</div>
</div>
</FlexColumn>
</Card>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import cx from 'classnames'
import {slide} from 'src/util/transition'
import {remove, append} from '@welshman/lib'
import Card from 'src/partials/Card.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import FlexColumn from 'src/partials/FlexColumn.svelte'
import NoteCreateRelays from 'src/app/shared/NoteCreateRelays.svelte'
import NoteCreateGroups from 'src/app/shared/NoteCreateGroups.svelte'
import NoteCreateOptions from 'src/app/shared/NoteCreateOptions.svelte'
import {env} from 'src/engine'
export let ctrl
const toggleCard = card => {
cards = cards.includes(card) ? remove(card, cards) : append(card, cards)
}
const className = active => cx("!text-sm mr-2 mb-2 !inline-flex", {'opacity-50': active})
const handler = card => () => toggleCard(card)
let cards = []
</script>
{#each cards as card (card)}
<div transition:slide|local class="mb-4 mt-2">
<Card class="relative">
<span class="h-4 w-4 cursor-pointer absolute top-2 right-2" on:click={handler(card)}>
<i class="fa fa-times" />
</span>
<FlexColumn>
{#if card === "relays"}
<NoteCreateRelays {ctrl} />
{:else if card === "groups"}
<NoteCreateGroups {ctrl} />
{:else if card === "options"}
<NoteCreateOptions {ctrl} />
{/if}
</FlexColumn>
</Card>
</div>
{/each}
<div class="flex flex-col gap-4">
<div>
<Anchor button low class={className(cards.includes('relays'))} on:click={handler('relays')}>
{$ctrl.draft.relays.length} Relays
</Anchor>
{#if !$env.FORCE_GROUP}
<Anchor button low class={className(cards.includes('groups'))} on:click={handler('groups')}>
{$ctrl.draft.groups.length} Groups
</Anchor>
{/if}
<Anchor button low class={className(cards.includes('options'))} on:click={handler('options')}>
Options
</Anchor>
</div>
<div class="flex justify-end gap-3">
<Anchor button on:click={$ctrl.save}>Save as Draft</Anchor>
<Anchor button accent on:click={$ctrl.publish}>Send Note</Anchor>
</div>
</div>

View File

@ -0,0 +1,52 @@
<script lang="ts">
import {NOTE, EVENT_TIME, CLASSIFIED} from '@welshman/util'
import Field from 'src/partials/Field.svelte'
import Input from 'src/partials/Input.svelte'
import CurrencyInput from 'src/partials/CurrencyInput.svelte'
import CurrencySymbol from 'src/partials/CurrencySymbol.svelte'
import DateTimeInput from 'src/partials/DateTimeInput.svelte'
export let ctrl
</script>
{#if $ctrl.draft.kind !== NOTE}
<Field label="Title">
<Input bind:value={$ctrl.draft.extra.title} />
</Field>
{/if}
{#if $ctrl.draft.kind === CLASSIFIED}
<Field label="Summary">
<Input bind:value={$ctrl.draft.extra.summary} />
</Field>
<Field label="Price">
<div class="grid grid-cols-3 gap-2">
<div class="col-span-2">
<Input type="number" placeholder="0" bind:value={$ctrl.draft.extra.price}>
<span slot="before">
<CurrencySymbol code={$ctrl.draft.extra.currency?.code || "SAT"} />
</span>
</Input>
</div>
<div class="relative">
<CurrencyInput bind:value={$ctrl.draft.extra.currency} />
</div>
</div>
</Field>
{/if}
{#if $ctrl.draft.kind === EVENT_TIME}
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<strong>Start</strong>
<DateTimeInput bind:value={$ctrl.draft.extra.start} />
</div>
<div class="flex flex-col gap-1">
<strong>End</strong>
<DateTimeInput bind:value={$ctrl.draft.extra.end} />
</div>
</div>
{/if}
{#if $ctrl.draft.kind !== NOTE}
<Field label="Location (optional)">
<Input bind:value={$ctrl.draft.extra.location} />
</Field>
{/if}

View File

@ -0,0 +1,35 @@
<script lang="ts">
import {append, uniq} from '@welshman/lib'
import SelectButton from 'src/partials/SelectButton.svelte'
import SearchSelect from 'src/partials/SearchSelect.svelte'
import {session, groupMetaSearch, displayGroupByAddress} from "src/engine"
export let ctrl
const onChange = groups => {
$ctrl.draft.groups = groups
}
const addGroup = group => {
if (group) {
$ctrl.draft.groups = append(group, $ctrl.draft.groups)
groupInput.clear()
}
}
let groupInput
$: groupOptions = uniq([...groupOptions || [], ...$ctrl.draft.groups, ...Object.keys($session.groups)])
</script>
<p>Select which groups to publish to</p>
<SelectButton multiple value={$ctrl.draft.groups} onChange={onChange} options={groupOptions}>
<span slot="item" let:option>{displayGroupByAddress(option)}</span>
</SelectButton>
<SearchSelect
onChange={addGroup}
bind:this={groupInput}
search={$groupMetaSearch.searchValues}
placeholder="Search groups">
<span slot="item" let:item>{$groupMetaSearch.displayValue(item)}</span>
</SearchSelect>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import {NOTE, EVENT_TIME, CLASSIFIED} from '@welshman/util'
import Popover from 'src/partials/Popover.svelte'
import Chip from 'src/partials/Chip.svelte'
import Menu from 'src/partials/Menu.svelte'
import MenuItem from 'src/partials/MenuItem.svelte'
export let ctrl
</script>
<div class="flex gap-2">
<span class="text-2xl font-bold">Create a</span>
<Popover theme="transparent" placement="bottom" opts={{hideOnClick: true}}>
<div slot="trigger">
<Chip class="cursor-pointer">
{#if $ctrl.draft.kind === NOTE}
Note
{:else if $ctrl.draft.kind === EVENT_TIME}
Event
{:else if $ctrl.draft.kind === CLASSIFIED}
Listing
{/if}
<i class="fa fa-caret-down" />
</Chip>
</div>
<div slot="tooltip">
<Menu class="-mt-2 w-24">
<MenuItem on:click={() => ctrl.setKind(NOTE)}>Note</MenuItem>
<MenuItem on:click={() => ctrl.setKind(EVENT_TIME)}>Event</MenuItem>
<MenuItem on:click={() => ctrl.setKind(CLASSIFIED)}>Listing</MenuItem>
</Menu>
</div>
</Popover>
</div>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import Field from 'src/partials/Field.svelte'
import FieldInline from 'src/partials/FieldInline.svelte'
import Input from 'src/partials/Input.svelte'
import Toggle from 'src/partials/Toggle.svelte'
export let ctrl
</script>
<p>Control how your note is presented</p>
<Field icon="fa-warning" label="Content warnings">
<Input
bind:value={$ctrl.draft.warning}
placeholder="Why might people want to skip this post?" />
</Field>
<FieldInline icon="fa-user-secret" label="Post anonymously">
<Toggle bind:value={$ctrl.draft.anonymous} />
<p slot="info">Enable this to create an anonymous note.</p>
</FieldInline>

View File

@ -0,0 +1,37 @@
<script lang="ts">
import {append, uniq} from '@welshman/lib'
import SelectButton from 'src/partials/SelectButton.svelte'
import SearchSelect from 'src/partials/SearchSelect.svelte'
import {displayRelayUrl, normalizeRelayUrl} from 'src/domain'
import {userRelayPolicies, relaySearch} from 'src/engine'
export let ctrl
const onChange = relays => {
$ctrl.draft.relays = relays
}
const addRelay = relay => {
if (relay) {
$ctrl.draft.relays = append(relay, $ctrl.draft.relays)
relayInput.clear()
}
}
let relayInput
$: relayOptions = uniq([...relayOptions || [], ...$ctrl.draft.relays, ...$userRelayPolicies.map(p => p.url)])
</script>
<p>Select which relays to publish to</p>
<SelectButton multiple value={$ctrl.draft.relays} onChange={onChange} options={relayOptions}>
<span slot="item" let:option>{displayRelayUrl(option)}</span>
</SelectButton>
<SearchSelect
onChange={addRelay}
bind:this={relayInput}
termToItem={normalizeRelayUrl}
search={$relaySearch.searchValues}
placeholder="Add another relay">
<span slot="item" let:item>{$relaySearch.displayValue(item)}</span>
</SearchSelect>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {append} from "ramda"
import {append} from "@welshman/lib"
import {updateIn} from "src/util/misc"
import {slide} from "src/util/transition"
import Card from "src/partials/Card.svelte"
@ -9,7 +9,7 @@
export let task
const hideTask = () =>
updateCurrentSession(updateIn("onboarding_tasks_completed", append(task)))
updateCurrentSession(updateIn("onboarding_tasks_completed", tasks => append(task, tasks)))
</script>
{#if !$session.onboarding_tasks_completed.includes(task)}

View File

@ -1,4 +1,5 @@
<script lang="ts">
import cx from "classnames"
import Anchor from "src/partials/Anchor.svelte"
import {router} from "src/app/util/router"
import {deriveProfileDisplay, loadPubkeys} from "src/engine"
@ -11,6 +12,6 @@
loadPubkeys([pubkey])
</script>
<Anchor modal stopPropagation class={$$props.class} href={path}>
<Anchor modal stopPropagation class={cx("!no-underline", $$props.class)} href={path}>
@<span class="underline">{$display}</span>
</Anchor>

216
src/app/util/draft.ts Normal file
View File

@ -0,0 +1,216 @@
import {v4 as uuid} from "uuid"
import {now, identity} from "@welshman/lib"
import type {EventTemplate} from "@welshman/util"
import {NOTE, CLASSIFIED, EVENT_TIME} from '@welshman/util'
import * as Content from '@welshman/content'
import type {Parsed} from '@welshman/content'
import {currencyOptions} from "src/util/i18n"
import {SelfStore, dateToSeconds} from 'src/util/misc'
import {getClientTags, env, hints} from 'src/engine'
export enum DraftError {
EmptyContent = "empty_content",
EmptyCurrency = "empty_currency",
EmptyTitle = "empty_title",
EmptyTime = "empty_time",
InvalidPrice = "invalid_price",
HasNsec = "has_nsec",
}
export type DraftImage = {
url: string,
meta: Record<string, string>
}
export type Draft = {
kind: number
groups: string[]
relays: string[]
warning: string
anonymous: boolean
content: Parsed[]
images: DraftImage[]
extra: Record<string, any>
}
export const makeDraftNote = (draft: Partial<Draft> = {}): Draft => ({
kind: NOTE,
relays: hints.WriteRelays().getUrls(),
groups: [env.get().FORCE_GROUP].filter(identity),
warning: "",
anonymous: false,
content: [],
images: [],
extra: {},
...draft,
})
export const makeDraftListing = (draft: Partial<Draft> = {}): Draft => ({
...makeDraftNote(),
kind: CLASSIFIED,
extra: {
title: "",
summary: "",
price: "",
currency: currencyOptions.find(o => o.code === "SAT"),
location: "",
},
...draft,
})
export const makeDraftEvent = (draft: Partial<Draft> = {}): Draft => ({
...makeDraftNote(),
kind: EVENT_TIME,
extra: {
title: "",
location: "",
start: "",
end: "",
},
...draft,
})
export const validateDraft = ({kind, content, extra}: Draft) => {
const errors = []
if (content.length === 0) {
errors.push(DraftError.EmptyContent)
}
if (kind === EVENT_TIME) {
if (!extra.title) errors.push(DraftError.EmptyTitle)
if (!extra.start || !extra.end) errors.push(DraftError.EmptyTime)
}
if (kind === CLASSIFIED) {
if (!extra.title) errors.push(DraftError.EmptyTitle)
if (isNaN(parseFloat(extra.price))) DraftError.InvalidPrice
if (!extra.currency) DraftError.EmptyCurrency
}
return errors
}
export const createDraft = (draft: Draft): EventTemplate => {
let tags = getClientTags()
if (draft.warning) {
tags.push(["content-warning", draft.warning])
}
for (const {url, meta} of draft.images) {
tags.push(['imeta', ...Object.entries({...meta, url}).map(pair => pair.join(' '))])
}
for (const parsed of draft.content) {
if (Content.isTopic(parsed)) {
tags.push(["t", parsed.value])
} else if (Content.isEvent(parsed)) {
const {id, relays = [], author = ""} = parsed.value
tags.push(["q", id, relays[0] || "", "mention", author])
} else if (Content.isProfile(parsed)) {
const {pubkey, relays = []} = parsed.value
tags.push(["p", pubkey, relays[0] || "", ""])
} else if (Content.isAddress(parsed)) {
const {kind, pubkey, identifier, relays = []} = parsed.value
const address = [kind, pubkey, identifier].join(":")
tags.push(["a", address.toString(), relays[0] || ""])
}
}
if (draft.kind === EVENT_TIME) {
tags = [
...tags,
["d", uuid()],
["title", draft.extra.title],
["summary", draft.extra.summary || ""],
["location", draft.extra.location || ""],
["published_at", now().toString()],
["price", draft.extra.price, draft.extra.currency.code],
]
}
if (draft.kind === CLASSIFIED) {
tags = [
...tags,
["d", uuid()],
["title", draft.extra.title],
["location", draft.extra.location || ""],
["start", dateToSeconds(draft.extra.start).toString()],
["end", dateToSeconds(draft.extra.end).toString()],
]
}
return {
kind: draft.kind,
created_at: now(),
content: draft.content.map(p => p.raw).join(''),
tags,
}
}
export type DraftControllerOpts = {
publish: () => void
}
export class DraftController extends SelfStore {
nsecWarning = false
skipNsecWarning = false
content = ""
counts = {
words: 0,
chars: 0
}
constructor(readonly draft: Draft, readonly opts: DraftControllerOpts) {
super()
this.content = draft.content.map(p => p.raw).join(''),
this.notify()
}
set = draft => {
Object.assign(this, draft)
this.notify()
}
setKind = (kind: number) => {
this.draft.kind = kind
this.notify()
}
setContent = (content: string) => {
this.content = content
this.counts.chars = content.length || 0
this.counts.words = content.trim() ? (content.match(/\s+/g)?.length || 0) + 1 : 0
this.notify()
}
clearNsecWarning = () => {
this.nsecWarning = false
}
ignoreNsecWarning = () => {
this.nsecWarning = false
this.skipNsecWarning = true
}
validate = () => {
const errors = validateDraft(this.draft)
if (errors.includes(DraftError.HasNsec) && !this.skipNsecWarning) {
this.nsecWarning = Boolean(this.content.match(/\bnsec1.+/))
}
return errors
}
getDraft = () => ({...this.draft, content: Content.parse({content: this.content})})
getEvent = () => createDraft(this.getDraft())
publish = () => this.opts.publish()
}

View File

@ -1,2 +1,3 @@
export * from "src/app/util/draft"
export * from "src/app/util/feeds"
export * from "src/app/util/router"

View File

@ -18,7 +18,7 @@
userIsGroupMember,
updateCurrentSession,
communityListsByAddress,
searchGroupMeta,
groupMetaSearch,
groupMeta,
} from "src/engine"
@ -34,7 +34,7 @@
let limit = 20
let element = null
$: otherGroupMeta = reject(userIsMember, $searchGroupMeta(q)).slice(0, limit)
$: otherGroupMeta = reject(userIsMember, $groupMetaSearch.searchOptions(q)).slice(0, limit)
document.title = "Groups"

View File

@ -15,8 +15,8 @@
import GroupCircle from "src/app/shared/GroupCircle.svelte"
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import {router} from "src/app/util/router"
import {displayRelayUrl, displayGroupMeta} from "src/domain"
import {hints, relaySearch, searchGroupMeta, groupMetaByAddress} from "src/engine"
import {displayRelayUrl} from "src/domain"
import {hints, relaySearch, groupMetaSearch, displayGroupByAddress} from "src/engine"
export let initialPubkey = null
export let initialGroupAddress = null
@ -70,8 +70,6 @@
groups = toSpliced(groups, i, 1)
}
const displayGroupFromAddress = a => displayGroupMeta($groupMetaByAddress.get(a))
let relayInput, groupInput
let sections = []
let pubkeys = []
@ -185,7 +183,7 @@
</p>
{#each groups as group, i (group.address + i)}
<ListItem on:remove={() => removeGroup(i)}>
<span slot="label">{displayGroupFromAddress(group.address)}</span>
<span slot="label">{displayGroupByAddress(group.address)}</span>
<span slot="data">
<Input bind:value={group.claim} placeholder="Invite code (optional)" />
</span>
@ -194,8 +192,8 @@
<SearchSelect
value={null}
bind:this={groupInput}
search={$searchGroupMeta}
displayItem={displayGroupMeta}
search={$groupMetaSearch.searchOptions}
displayItem={$groupMetaSearch.displayOption}
getKey={groupMeta => getAddress(groupMeta.event)}
onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}>
<i slot="before" class="fa fa-search" />

View File

@ -0,0 +1,56 @@
<script lang="ts">
import {switcher} from "hurdak"
import {NOTE} from '@welshman/util'
import {showWarning, showPublishInfo} from "src/partials/Toast.svelte"
import Field from "src/partials/Field.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import NsecWarning from "src/app/shared/NsecWarning.svelte"
import NoteCreateKind from "src/app/shared/NoteCreateKind.svelte"
import NoteCreateFields from "src/app/shared/NoteCreateFields.svelte"
import NoteCreateContent from "src/app/shared/NoteCreateContent.svelte"
import NoteCreateControls from "src/app/shared/NoteCreateControls.svelte"
import {router, makeDraftNote, DraftError, DraftController} from "src/app/util"
import {publishToZeroOrMoreGroups} from "src/engine"
export let draft = makeDraftNote()
const ctrl = new DraftController(draft, {
publish: async () => {
for (const error of ctrl.validate()) {
const message = switcher(error, {
[DraftError.EmptyContent]: "Please provide a description.",
[DraftError.EmptyTitle]: "Please provide a title.",
[DraftError.EmptyCurrency]: "Please select a currency.",
[DraftError.EmptyTime]: "Please provide a start and end date and time.",
[DraftError.InvalidPrice]: "Please provide a valid price.",
default: null
})
if (message) {
return showWarning(error)
}
}
const {groups, anonymous} = ctrl.draft
const {pubs} = await publishToZeroOrMoreGroups(groups, ctrl.getEvent(), {anonymous})
showPublishInfo(pubs[0])
router.clearModals()
},
})
</script>
<FlexColumn>
<NoteCreateKind {ctrl} />
<div>
<NoteCreateFields {ctrl} />
<Field label={$ctrl.draft.kind === NOTE ? "What do you want to say?" : "Description"}>
<NoteCreateContent {ctrl} />
</Field>
<NoteCreateControls {ctrl} />
</div>
</FlexColumn>
{#if $ctrl.nsecWarning}
<NsecWarning onAbort={ctrl.clearNsecWarning} onBypass={ctrl.ignoreNsecWarning} />
{/if}

View File

@ -721,7 +721,7 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
const template = await encryptable.reconcile(encrypt)
if (window.location.origin.includes('localhost')) {
if (window.location.origin.includes("localhost")) {
if (kind === MUTES) {
alert("Publishing mutes")
console.trace(template)
@ -743,9 +743,9 @@ export const unfollowPerson = (pubkey: string) => {
export const followPerson = (pubkey: string) => {
if (canSign.get()) {
updateSingleton(FOLLOWS, tags => append(tags, mention(pubkey)))
updateSingleton(FOLLOWS, tags => append(mention(pubkey), tags))
} else {
anonymous.update($a => ({...$a, follows: append($a.follows, mention(pubkey))}))
anonymous.update($a => ({...$a, follows: append(mention(pubkey), $a.follows)}))
}
}
@ -753,17 +753,17 @@ export const unmutePerson = (pubkey: string) =>
updateSingleton(MUTES, tags => reject(nthEq(1, pubkey), tags))
export const mutePerson = (pubkey: string) =>
updateSingleton(MUTES, tags => append(tags, mention(pubkey)))
updateSingleton(MUTES, tags => append(mention(pubkey), tags))
export const unmuteNote = (id: string) => updateSingleton(MUTES, tags => reject(nthEq(1, id), tags))
export const muteNote = (id: string) => updateSingleton(MUTES, tags => append(tags, ["e", id]))
export const muteNote = (id: string) => updateSingleton(MUTES, tags => append(["e", id], tags))
export const removeFeedFavorite = (address: string) =>
updateSingleton(FEEDS, tags => reject(nthEq(1, address), tags))
export const addFeedFavorite = (address: string) =>
updateSingleton(FEEDS, tags => append(tags, ["a", address]))
updateSingleton(FEEDS, tags => append(["a", address], tags))
// Relays

View File

@ -54,7 +54,7 @@ const getFiltersForKey = (key: string, authors: string[]) => {
case "pubkey/relays":
return [{authors, kinds: [RELAYS, INBOX_RELAYS]}]
case "pubkey/profile":
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
return [{authors, kinds: [PROFILE, FOLLOWS, MUTES, HANDLER_INFORMATION, COMMUNITIES]}]
case "pubkey/user":
return [
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, FEEDS]},

View File

@ -107,6 +107,7 @@ import {
} from "src/util/nostr"
import logger from "src/util/logger"
import type {
GroupMeta,
PublishedFeed,
PublishedProfile,
PublishedListFeed,
@ -141,6 +142,7 @@ import {
filterRelaysByNip,
displayRelayUrl,
readGroupMeta,
displayGroupMeta,
} from "src/domain"
import type {
Channel,
@ -691,7 +693,32 @@ export const groupMetaByAddress = withGetter(
export const deriveGroupMeta = (address: string) =>
derived(groupMetaByAddress, $m => $m.get(address))
export const searchGroupMeta = derived(
export const displayGroupByAddress = a => displayGroupMeta(groupMetaByAddress.get().get(a))
export class GroupSearch extends SearchHelper<GroupMeta & {score: number}, string> {
config = {
keys: [{name: "identifier", weight: 0.2}, "name", {name: "about", weight: 0.5}],
threshold: 0.3,
shouldSort: false,
includeScore: true,
}
getSearch = () => {
const fuse = new Fuse(this.options, this.config)
const sortFn = (r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100)
return (term: string) =>
term
? sortBy(sortFn, fuse.search(term)).map((r: any) => r.item)
: sortBy(meta => -meta.score, this.options)
}
getValue = (option: GroupMeta) => getAddress(option.event)
displayValue = displayGroupByAddress
}
export const groupMetaSearch = derived(
[groupMeta, communityListsByAddress, userFollows],
([$groupMeta, $communityListsByAddress, $userFollows]) => {
const options = $groupMeta.map(meta => {
@ -702,19 +729,7 @@ export const searchGroupMeta = derived(
return {...meta, score: followedMembers.length}
})
const fuse = new Fuse(options, {
keys: [{name: "identifier", weight: 0.2}, "name", {name: "about", weight: 0.5}],
threshold: 0.3,
shouldSort: false,
includeScore: true,
})
const sortFn = (r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100)
return (term: string) =>
term
? sortBy(sortFn, fuse.search(term)).map((r: any) => r.item.meta)
: sortBy(meta => -meta.score, options)
return new GroupSearch(options)
},
)
@ -2149,19 +2164,19 @@ class IndexedDBAdapter {
const removedRecords = prev.filter(r => !currentIds.has(r[key]))
if (newRecords.length > 0) {
console.log("putting", name, newRecords.length, current.length)
await storage.bulkPut(name, newRecords)
}
if (removedRecords.length > 0) {
console.trace("deleting", name, removedRecords.length, current.length)
if (name === "repository") {
console.trace("deleting", removedRecords.length, current.length)
}
await storage.bulkDelete(name, removedRecords.map(prop(key)))
}
// If we have much more than our limit, prune our store. This will get persisted
// the next time around.
if (current.length > limit * 1.5) {
console.log("pruning", name, current.length)
set((sort ? sort(current) : current).slice(0, limit))
}

View File

@ -457,3 +457,26 @@ export function withGetter<T>(store: Readable<T> | Writable<T>) {
export const throttled = <T>(delay: number, store: Readable<T>) =>
custom(set => store.subscribe(throttle(delay, set)))
export class SelfStore {
subs: Sub<typeof this>[] = []
notify = () => {
for (const sub of this.subs) {
sub(this)
}
}
subscribe = (sub: Sub<typeof this>) => {
this.subs.push(sub)
sub(this)
return () => {
this.subs.splice(
this.subs.findIndex(s => s === sub),
1,
)
}
}
}