mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Add new version of note create components
This commit is contained in:
parent
fb32bf8ccc
commit
f98fb6e84c
@ -203,13 +203,8 @@
|
|||||||
|
|
||||||
router.register("/", import("src/app/views/Home.svelte"))
|
router.register("/", import("src/app/views/Home.svelte"))
|
||||||
router.register("/notes", 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,
|
requireSigner: true,
|
||||||
serializers: {
|
|
||||||
pubkey: asPerson,
|
|
||||||
group: asNaddr("group"),
|
|
||||||
type: asString("type"),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
router.register("/notes/:entity", import("src/app/views/NoteDetail.svelte"), {
|
router.register("/notes/:entity", import("src/app/views/NoteDetail.svelte"), {
|
||||||
serializers: {
|
serializers: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Content from "@welshman/content"
|
||||||
import {slide, fly} from "src/util/transition"
|
import {slide, fly} from "src/util/transition"
|
||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
@ -6,7 +7,7 @@
|
|||||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||||
import {menuIsOpen, searchTerm} from "src/app/state"
|
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"
|
import {env, pubkey, canSign, hasNewNotifications, hasNewMessages} from "src/engine"
|
||||||
|
|
||||||
let innerWidth = 0
|
let innerWidth = 0
|
||||||
@ -32,18 +33,18 @@
|
|||||||
return router.at("/login").open()
|
return router.at("/login").open()
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {} as any
|
const draft = makeDraftNote()
|
||||||
const props = router.getProps($page) as any
|
const props = router.getProps($page) as any
|
||||||
|
|
||||||
if ($page.path.startsWith("/people") && props.pubkey) {
|
if ($page.path.startsWith("/people") && props.pubkey) {
|
||||||
params.pubkey = props.pubkey
|
draft.content = Content.parse(props.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($env.FORCE_GROUP) {
|
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>
|
</script>
|
||||||
|
|
||||||
@ -69,10 +70,13 @@
|
|||||||
<div
|
<div
|
||||||
on:mousedown|preventDefault
|
on:mousedown|preventDefault
|
||||||
out:fly|local={{y: 20, duration: 200}}
|
out:fly|local={{y: 20, duration: 200}}
|
||||||
class="absolute right-0 top-10 w-96 rounded shadow-2xl opacity-100 transition-colors">
|
class="absolute right-0 top-10 w-96 rounded opacity-100 shadow-2xl transition-colors">
|
||||||
<div class="max-h-[70vh] overflow-auto bg-tinted-700 rounded">
|
<div class="max-h-[70vh] overflow-auto rounded bg-tinted-700">
|
||||||
<SearchResults bind:searching term={searchTerm}>
|
<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"}
|
{#if result.type === "topic"}
|
||||||
#{result.topic.name}
|
#{result.topic.name}
|
||||||
{:else if result.type === "profile"}
|
{:else if result.type === "profile"}
|
||||||
|
35
src/app/shared/NoteCreateContent.svelte
Normal file
35
src/app/shared/NoteCreateContent.svelte
Normal 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>
|
63
src/app/shared/NoteCreateControls.svelte
Normal file
63
src/app/shared/NoteCreateControls.svelte
Normal 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>
|
52
src/app/shared/NoteCreateFields.svelte
Normal file
52
src/app/shared/NoteCreateFields.svelte
Normal 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}
|
35
src/app/shared/NoteCreateGroups.svelte
Normal file
35
src/app/shared/NoteCreateGroups.svelte
Normal 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>
|
34
src/app/shared/NoteCreateKind.svelte
Normal file
34
src/app/shared/NoteCreateKind.svelte
Normal 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>
|
19
src/app/shared/NoteCreateOptions.svelte
Normal file
19
src/app/shared/NoteCreateOptions.svelte
Normal 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>
|
37
src/app/shared/NoteCreateRelays.svelte
Normal file
37
src/app/shared/NoteCreateRelays.svelte
Normal 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>
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {append} from "ramda"
|
import {append} from "@welshman/lib"
|
||||||
import {updateIn} from "src/util/misc"
|
import {updateIn} from "src/util/misc"
|
||||||
import {slide} from "src/util/transition"
|
import {slide} from "src/util/transition"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
@ -9,7 +9,7 @@
|
|||||||
export let task
|
export let task
|
||||||
|
|
||||||
const hideTask = () =>
|
const hideTask = () =>
|
||||||
updateCurrentSession(updateIn("onboarding_tasks_completed", append(task)))
|
updateCurrentSession(updateIn("onboarding_tasks_completed", tasks => append(task, tasks)))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$session.onboarding_tasks_completed.includes(task)}
|
{#if !$session.onboarding_tasks_completed.includes(task)}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {deriveProfileDisplay, loadPubkeys} from "src/engine"
|
import {deriveProfileDisplay, loadPubkeys} from "src/engine"
|
||||||
@ -11,6 +12,6 @@
|
|||||||
loadPubkeys([pubkey])
|
loadPubkeys([pubkey])
|
||||||
</script>
|
</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>
|
@<span class="underline">{$display}</span>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
216
src/app/util/draft.ts
Normal file
216
src/app/util/draft.ts
Normal 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()
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
export * from "src/app/util/draft"
|
||||||
export * from "src/app/util/feeds"
|
export * from "src/app/util/feeds"
|
||||||
export * from "src/app/util/router"
|
export * from "src/app/util/router"
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
userIsGroupMember,
|
userIsGroupMember,
|
||||||
updateCurrentSession,
|
updateCurrentSession,
|
||||||
communityListsByAddress,
|
communityListsByAddress,
|
||||||
searchGroupMeta,
|
groupMetaSearch,
|
||||||
groupMeta,
|
groupMeta,
|
||||||
} from "src/engine"
|
} from "src/engine"
|
||||||
|
|
||||||
@ -34,7 +34,7 @@
|
|||||||
let limit = 20
|
let limit = 20
|
||||||
let element = null
|
let element = null
|
||||||
|
|
||||||
$: otherGroupMeta = reject(userIsMember, $searchGroupMeta(q)).slice(0, limit)
|
$: otherGroupMeta = reject(userIsMember, $groupMetaSearch.searchOptions(q)).slice(0, limit)
|
||||||
|
|
||||||
document.title = "Groups"
|
document.title = "Groups"
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
||||||
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {displayRelayUrl, displayGroupMeta} from "src/domain"
|
import {displayRelayUrl} from "src/domain"
|
||||||
import {hints, relaySearch, searchGroupMeta, groupMetaByAddress} from "src/engine"
|
import {hints, relaySearch, groupMetaSearch, displayGroupByAddress} from "src/engine"
|
||||||
|
|
||||||
export let initialPubkey = null
|
export let initialPubkey = null
|
||||||
export let initialGroupAddress = null
|
export let initialGroupAddress = null
|
||||||
@ -70,8 +70,6 @@
|
|||||||
groups = toSpliced(groups, i, 1)
|
groups = toSpliced(groups, i, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayGroupFromAddress = a => displayGroupMeta($groupMetaByAddress.get(a))
|
|
||||||
|
|
||||||
let relayInput, groupInput
|
let relayInput, groupInput
|
||||||
let sections = []
|
let sections = []
|
||||||
let pubkeys = []
|
let pubkeys = []
|
||||||
@ -185,7 +183,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{#each groups as group, i (group.address + i)}
|
{#each groups as group, i (group.address + i)}
|
||||||
<ListItem on:remove={() => removeGroup(i)}>
|
<ListItem on:remove={() => removeGroup(i)}>
|
||||||
<span slot="label">{displayGroupFromAddress(group.address)}</span>
|
<span slot="label">{displayGroupByAddress(group.address)}</span>
|
||||||
<span slot="data">
|
<span slot="data">
|
||||||
<Input bind:value={group.claim} placeholder="Invite code (optional)" />
|
<Input bind:value={group.claim} placeholder="Invite code (optional)" />
|
||||||
</span>
|
</span>
|
||||||
@ -194,8 +192,8 @@
|
|||||||
<SearchSelect
|
<SearchSelect
|
||||||
value={null}
|
value={null}
|
||||||
bind:this={groupInput}
|
bind:this={groupInput}
|
||||||
search={$searchGroupMeta}
|
search={$groupMetaSearch.searchOptions}
|
||||||
displayItem={displayGroupMeta}
|
displayItem={$groupMetaSearch.displayOption}
|
||||||
getKey={groupMeta => getAddress(groupMeta.event)}
|
getKey={groupMeta => getAddress(groupMeta.event)}
|
||||||
onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}>
|
onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}>
|
||||||
<i slot="before" class="fa fa-search" />
|
<i slot="before" class="fa fa-search" />
|
||||||
|
56
src/app/views/NoteCreate2.svelte
Normal file
56
src/app/views/NoteCreate2.svelte
Normal 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}
|
@ -721,7 +721,7 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
|
|||||||
|
|
||||||
const template = await encryptable.reconcile(encrypt)
|
const template = await encryptable.reconcile(encrypt)
|
||||||
|
|
||||||
if (window.location.origin.includes('localhost')) {
|
if (window.location.origin.includes("localhost")) {
|
||||||
if (kind === MUTES) {
|
if (kind === MUTES) {
|
||||||
alert("Publishing mutes")
|
alert("Publishing mutes")
|
||||||
console.trace(template)
|
console.trace(template)
|
||||||
@ -743,9 +743,9 @@ export const unfollowPerson = (pubkey: string) => {
|
|||||||
|
|
||||||
export const followPerson = (pubkey: string) => {
|
export const followPerson = (pubkey: string) => {
|
||||||
if (canSign.get()) {
|
if (canSign.get()) {
|
||||||
updateSingleton(FOLLOWS, tags => append(tags, mention(pubkey)))
|
updateSingleton(FOLLOWS, tags => append(mention(pubkey), tags))
|
||||||
} else {
|
} 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))
|
updateSingleton(MUTES, tags => reject(nthEq(1, pubkey), tags))
|
||||||
|
|
||||||
export const mutePerson = (pubkey: string) =>
|
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 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) =>
|
export const removeFeedFavorite = (address: string) =>
|
||||||
updateSingleton(FEEDS, tags => reject(nthEq(1, address), tags))
|
updateSingleton(FEEDS, tags => reject(nthEq(1, address), tags))
|
||||||
|
|
||||||
export const addFeedFavorite = (address: string) =>
|
export const addFeedFavorite = (address: string) =>
|
||||||
updateSingleton(FEEDS, tags => append(tags, ["a", address]))
|
updateSingleton(FEEDS, tags => append(["a", address], tags))
|
||||||
|
|
||||||
// Relays
|
// Relays
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ const getFiltersForKey = (key: string, authors: string[]) => {
|
|||||||
case "pubkey/relays":
|
case "pubkey/relays":
|
||||||
return [{authors, kinds: [RELAYS, INBOX_RELAYS]}]
|
return [{authors, kinds: [RELAYS, INBOX_RELAYS]}]
|
||||||
case "pubkey/profile":
|
case "pubkey/profile":
|
||||||
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
|
return [{authors, kinds: [PROFILE, FOLLOWS, MUTES, HANDLER_INFORMATION, COMMUNITIES]}]
|
||||||
case "pubkey/user":
|
case "pubkey/user":
|
||||||
return [
|
return [
|
||||||
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, FEEDS]},
|
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, FEEDS]},
|
||||||
|
@ -107,6 +107,7 @@ import {
|
|||||||
} from "src/util/nostr"
|
} from "src/util/nostr"
|
||||||
import logger from "src/util/logger"
|
import logger from "src/util/logger"
|
||||||
import type {
|
import type {
|
||||||
|
GroupMeta,
|
||||||
PublishedFeed,
|
PublishedFeed,
|
||||||
PublishedProfile,
|
PublishedProfile,
|
||||||
PublishedListFeed,
|
PublishedListFeed,
|
||||||
@ -141,6 +142,7 @@ import {
|
|||||||
filterRelaysByNip,
|
filterRelaysByNip,
|
||||||
displayRelayUrl,
|
displayRelayUrl,
|
||||||
readGroupMeta,
|
readGroupMeta,
|
||||||
|
displayGroupMeta,
|
||||||
} from "src/domain"
|
} from "src/domain"
|
||||||
import type {
|
import type {
|
||||||
Channel,
|
Channel,
|
||||||
@ -691,7 +693,32 @@ export const groupMetaByAddress = withGetter(
|
|||||||
export const deriveGroupMeta = (address: string) =>
|
export const deriveGroupMeta = (address: string) =>
|
||||||
derived(groupMetaByAddress, $m => $m.get(address))
|
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],
|
||||||
([$groupMeta, $communityListsByAddress, $userFollows]) => {
|
([$groupMeta, $communityListsByAddress, $userFollows]) => {
|
||||||
const options = $groupMeta.map(meta => {
|
const options = $groupMeta.map(meta => {
|
||||||
@ -702,19 +729,7 @@ export const searchGroupMeta = derived(
|
|||||||
return {...meta, score: followedMembers.length}
|
return {...meta, score: followedMembers.length}
|
||||||
})
|
})
|
||||||
|
|
||||||
const fuse = new Fuse(options, {
|
return new GroupSearch(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)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2149,19 +2164,19 @@ class IndexedDBAdapter {
|
|||||||
const removedRecords = prev.filter(r => !currentIds.has(r[key]))
|
const removedRecords = prev.filter(r => !currentIds.has(r[key]))
|
||||||
|
|
||||||
if (newRecords.length > 0) {
|
if (newRecords.length > 0) {
|
||||||
console.log("putting", name, newRecords.length, current.length)
|
|
||||||
await storage.bulkPut(name, newRecords)
|
await storage.bulkPut(name, newRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedRecords.length > 0) {
|
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)))
|
await storage.bulkDelete(name, removedRecords.map(prop(key)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have much more than our limit, prune our store. This will get persisted
|
// If we have much more than our limit, prune our store. This will get persisted
|
||||||
// the next time around.
|
// the next time around.
|
||||||
if (current.length > limit * 1.5) {
|
if (current.length > limit * 1.5) {
|
||||||
console.log("pruning", name, current.length)
|
|
||||||
set((sort ? sort(current) : current).slice(0, limit))
|
set((sort ? sort(current) : current).slice(0, limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,3 +457,26 @@ export function withGetter<T>(store: Readable<T> | Writable<T>) {
|
|||||||
|
|
||||||
export const throttled = <T>(delay: number, store: Readable<T>) =>
|
export const throttled = <T>(delay: number, store: Readable<T>) =>
|
||||||
custom(set => store.subscribe(throttle(delay, set)))
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user