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()
|
||||
.filter(command => command.matches(target))
|
||||
|
||||
export const deriveCommandsForTarget = (target: CommandScopeTarget): Readable<CommandReader[]> =>
|
||||
derived(
|
||||
fromApp($app => $app.use(Commands).forUrl(target.url ?? "").$),
|
||||
$commands => $commands.filter(command => command.matches(target)),
|
||||
// A space's definitions are the same for everything rendered in it, so one store per url is
|
||||
// shared rather than every message deriving its own. Subscribing is also what pulls them, so
|
||||
// reading a space loads its definitions whether or not anything is being composed.
|
||||
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
|
||||
// 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 = (
|
||||
available: CommandReader[],
|
||||
invocation: CommandInvocation,
|
||||
invocation: Pick<CommandInvocation, "command" | "pubkey">,
|
||||
) =>
|
||||
available.filter(
|
||||
command =>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import {slide} from "@lib/transition"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileName from "@app/components/ProfileName.svelte"
|
||||
import {deriveCommandsForTarget, describeCommandDraft} from "@app/commands"
|
||||
import {deriveValidCommands, describeCommandDraft} from "@app/commands"
|
||||
|
||||
type Props = {
|
||||
target: CommandScopeTarget
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
const {target, content, insert}: Props = $props()
|
||||
|
||||
const available = deriveCommandsForTarget(target)
|
||||
const available = deriveValidCommands(target)
|
||||
|
||||
const draft = $derived(describeCommandDraft($available, content))
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
isEmoji,
|
||||
isTopic,
|
||||
isCode,
|
||||
isCommand,
|
||||
isCashu,
|
||||
isInvoice,
|
||||
isLink,
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
import ContentMention from "@app/components/ContentMention.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
import {deriveCommandsForTarget, describeCommandDraft} from "@app/commands"
|
||||
import {deriveValidCommands, getCommandsForInvocation} from "@app/commands"
|
||||
import {userSettingsValues} from "@app/settings"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -63,18 +64,16 @@
|
|||
}: Props = $props()
|
||||
|
||||
// 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
|
||||
// publishes — falls through and renders as the text it is.
|
||||
const available = deriveCommandsForTarget({
|
||||
// definitions the space it was written in publishes. Anything else — including a command
|
||||
// nobody here answers to — falls through and renders as the text it is.
|
||||
const available = deriveValidCommands({
|
||||
url,
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey,
|
||||
tags: event.tags,
|
||||
})
|
||||
|
||||
const draft = $derived(describeCommandDraft($available, event.content))
|
||||
|
||||
const fullContent = $derived(parse(draft ? {...event, content: draft.invocation.rest} : event))
|
||||
const fullContent = $derived(parse(event))
|
||||
|
||||
const expand = () => {
|
||||
showEntire = true
|
||||
|
|
@ -175,11 +174,15 @@
|
|||
<div
|
||||
class="overflow-hidden text-ellipsis wrap-break-word"
|
||||
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
||||
{#if draft}
|
||||
<ContentCommand command={draft.command} />
|
||||
{/if}
|
||||
{#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} />
|
||||
{:else if isTopic(parsed)}
|
||||
<ContentTopic value={parsed.value} />
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<script lang="ts">
|
||||
import type {ParsedCommandValue} from "@welshman/content"
|
||||
import type {CommandReader} from "@welshman/domain"
|
||||
import ProfileName from "@app/components/ProfileName.svelte"
|
||||
|
||||
type Props = {
|
||||
command: CommandReader
|
||||
value: ParsedCommandValue
|
||||
}
|
||||
|
||||
const {command}: Props = $props()
|
||||
const {command, value}: Props = $props()
|
||||
</script>
|
||||
|
||||
<span class="tiptap-object tip tip-top" data-tip={command.description()}>
|
||||
/{command.command()}
|
||||
/{command.command()}{#if value.pubkey}@<ProfileName pubkey={value.pubkey} />{/if}
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Reference in a new issue