diff --git a/src/app/commands.ts b/src/app/commands.ts index 79634404..c3ca4cb6 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -78,17 +78,46 @@ export const getCommandsForTarget = (target: CommandScopeTarget) => .get() .filter(command => command.matches(target)) -export const deriveCommandsForTarget = (target: CommandScopeTarget): Readable => - 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>() + +const deriveCommandsForUrl = (url: string): Readable => { + 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 => + 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, ) => available.filter( command => diff --git a/src/app/components/CommandArgBar.svelte b/src/app/components/CommandArgBar.svelte index fe4863c6..7b5d5df4 100644 --- a/src/app/components/CommandArgBar.svelte +++ b/src/app/components/CommandArgBar.svelte @@ -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)) diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte index afe95c6b..3b3a2fc6 100644 --- a/src/app/components/Content.svelte +++ b/src/app/components/Content.svelte @@ -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 @@
- {#if draft} - - {/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} + + {:else} + {@html renderAsHtml(parsed)} + {/if} + {:else if isNewline(parsed) && !isBlock(i - 1)} {:else if isTopic(parsed)} diff --git a/src/app/components/ContentCommand.svelte b/src/app/components/ContentCommand.svelte index 6b43c0ac..1067eb5b 100644 --- a/src/app/components/ContentCommand.svelte +++ b/src/app/components/ContentCommand.svelte @@ -1,13 +1,16 @@ - /{command.command()} + /{command.command()}{#if value.pubkey}@{/if}