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("/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: {
|
||||
|
@ -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"}
|
||||
|
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">
|
||||
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)}
|
||||
|
@ -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
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/router"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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" />
|
||||
|
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)
|
||||
|
||||
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
|
||||
|
||||
|
@ -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]},
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user