Render command invocations from parsed content
This commit is contained in:
parent
7eb1c8c141
commit
4992b05ef1
4 changed files with 56 additions and 21 deletions
|
|
@ -78,17 +78,46 @@ export const getCommandsForTarget = (target: CommandScopeTarget) =>
|
||||||
.get()
|
.get()
|
||||||
.filter(command => command.matches(target))
|
.filter(command => command.matches(target))
|
||||||
|
|
||||||
export const deriveCommandsForTarget = (target: CommandScopeTarget): Readable<CommandReader[]> =>
|
// A space's definitions are the same for everything rendered in it, so one store per url is
|
||||||
derived(
|
// shared rather than every message deriving its own. Subscribing is also what pulls them, so
|
||||||
fromApp($app => $app.use(Commands).forUrl(target.url ?? "").$),
|
// reading a space loads its definitions whether or not anything is being composed.
|
||||||
$commands => $commands.filter(command => command.matches(target)),
|
const commandsByUrl = new Map<string, Readable<CommandReader[]>>()
|
||||||
|
|
||||||
|
const deriveCommandsForUrl = (url: string): Readable<CommandReader[]> => {
|
||||||
|
const cached = commandsByUrl.get(url)
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = fromApp($app => {
|
||||||
|
const plugin = $app.use(Commands)
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
plugin.ensureLoaded(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.forUrl(url).$
|
||||||
|
})
|
||||||
|
|
||||||
|
commandsByUrl.set(url, store)
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
// The definitions a piece of content is read against: the ones its space publishes that also
|
||||||
|
// scope to it. Content outside a space has no url, and so no commands.
|
||||||
|
export const deriveValidCommands = (target: CommandScopeTarget): Readable<CommandReader[]> =>
|
||||||
|
derived(deriveCommandsForUrl(target.url ?? ""), $commands =>
|
||||||
|
$commands.filter(command => command.matches(target)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Without a qualifier an invocation targets every definition whose trigger matches, so more
|
// Without a qualifier an invocation targets every definition whose trigger matches, so more
|
||||||
// than one here is what tells a composer to disambiguate.
|
// than one here is what tells a composer to disambiguate. Takes an invocation from either
|
||||||
|
// side: the one a composer is typing, or the one a message was parsed into.
|
||||||
export const getCommandsForInvocation = (
|
export const getCommandsForInvocation = (
|
||||||
available: CommandReader[],
|
available: CommandReader[],
|
||||||
invocation: CommandInvocation,
|
invocation: Pick<CommandInvocation, "command" | "pubkey">,
|
||||||
) =>
|
) =>
|
||||||
available.filter(
|
available.filter(
|
||||||
command =>
|
command =>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import {slide} from "@lib/transition"
|
import {slide} from "@lib/transition"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {deriveCommandsForTarget, describeCommandDraft} from "@app/commands"
|
import {deriveValidCommands, describeCommandDraft} from "@app/commands"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
target: CommandScopeTarget
|
target: CommandScopeTarget
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
const {target, content, insert}: Props = $props()
|
const {target, content, insert}: Props = $props()
|
||||||
|
|
||||||
const available = deriveCommandsForTarget(target)
|
const available = deriveValidCommands(target)
|
||||||
|
|
||||||
const draft = $derived(describeCommandDraft($available, content))
|
const draft = $derived(describeCommandDraft($available, content))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
isEmoji,
|
isEmoji,
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
|
isCommand,
|
||||||
isCashu,
|
isCashu,
|
||||||
isInvoice,
|
isInvoice,
|
||||||
isLink,
|
isLink,
|
||||||
|
|
@ -39,7 +40,7 @@
|
||||||
import ContentMention from "@app/components/ContentMention.svelte"
|
import ContentMention from "@app/components/ContentMention.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
import {deriveCommandsForTarget, describeCommandDraft} from "@app/commands"
|
import {deriveValidCommands, getCommandsForInvocation} from "@app/commands"
|
||||||
import {userSettingsValues} from "@app/settings"
|
import {userSettingsValues} from "@app/settings"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -63,18 +64,16 @@
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
// An invocation is plain text and carries no tags, so it's only recognizable against the
|
// An invocation is plain text and carries no tags, so it's only recognizable against the
|
||||||
// definitions loaded for this space. Anything else — including a command nobody here
|
// definitions the space it was written in publishes. Anything else — including a command
|
||||||
// publishes — falls through and renders as the text it is.
|
// nobody here answers to — falls through and renders as the text it is.
|
||||||
const available = deriveCommandsForTarget({
|
const available = deriveValidCommands({
|
||||||
url,
|
url,
|
||||||
kind: event.kind,
|
kind: event.kind,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
tags: event.tags,
|
tags: event.tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
const draft = $derived(describeCommandDraft($available, event.content))
|
const fullContent = $derived(parse(event))
|
||||||
|
|
||||||
const fullContent = $derived(parse(draft ? {...event, content: draft.invocation.rest} : event))
|
|
||||||
|
|
||||||
const expand = () => {
|
const expand = () => {
|
||||||
showEntire = true
|
showEntire = true
|
||||||
|
|
@ -175,11 +174,15 @@
|
||||||
<div
|
<div
|
||||||
class="overflow-hidden text-ellipsis wrap-break-word"
|
class="overflow-hidden text-ellipsis wrap-break-word"
|
||||||
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
||||||
{#if draft}
|
|
||||||
<ContentCommand command={draft.command} />
|
|
||||||
{/if}
|
|
||||||
{#each shortContent as parsed, i (i)}
|
{#each shortContent as parsed, i (i)}
|
||||||
{#if isNewline(parsed) && !isBlock(i - 1)}
|
{#if isCommand(parsed)}
|
||||||
|
{@const command = getCommandsForInvocation($available, parsed.value)[0]}
|
||||||
|
{#if command}
|
||||||
|
<ContentCommand {command} value={parsed.value} />
|
||||||
|
{:else}
|
||||||
|
{@html renderAsHtml(parsed)}
|
||||||
|
{/if}
|
||||||
|
{:else if isNewline(parsed) && !isBlock(i - 1)}
|
||||||
<ContentNewline value={parsed.value} />
|
<ContentNewline value={parsed.value} />
|
||||||
{:else if isTopic(parsed)}
|
{:else if isTopic(parsed)}
|
||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {ParsedCommandValue} from "@welshman/content"
|
||||||
import type {CommandReader} from "@welshman/domain"
|
import type {CommandReader} from "@welshman/domain"
|
||||||
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
command: CommandReader
|
command: CommandReader
|
||||||
|
value: ParsedCommandValue
|
||||||
}
|
}
|
||||||
|
|
||||||
const {command}: Props = $props()
|
const {command, value}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="tiptap-object tip tip-top" data-tip={command.description()}>
|
<span class="tiptap-object tip tip-top" data-tip={command.description()}>
|
||||||
/{command.command()}
|
/{command.command()}{#if value.pubkey}@<ProfileName pubkey={value.pubkey} />{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue