fix: use refs for suggestion search functions and reduce PostViewer spacing

NostrEditor:
- Use refs for suggestion configs instead of capturing at mount time
- This ensures search functions are always fresh when TipTap calls them
- Fixes autocomplete filtering not updating as user types

PostViewer:
- Reduce padding from p-4 to p-3
- Reduce minLines from 10 to 6
- Reduce button bar spacing from pt-4 mt-4 to pt-2 mt-2
This commit is contained in:
Claude
2026-01-20 14:55:30 +00:00
parent 67b6441547
commit 39667dea68
2 changed files with 55 additions and 71 deletions

View File

@@ -184,7 +184,7 @@ export function PostViewer() {
}
return (
<div className="h-full flex flex-col p-4">
<div className="h-full flex flex-col p-3">
<div className="flex-1 min-h-0">
<NostrEditor
ref={editorRef}
@@ -192,14 +192,14 @@ export function PostViewer() {
variant="full"
submitBehavior="button-only"
blobPreview="gallery"
minLines={10}
minLines={6}
suggestions={suggestions}
onChange={handleChange}
autoFocus
/>
</div>
<div className="flex items-center justify-between pt-4 border-t mt-4">
<div className="flex items-center justify-between pt-2 border-t mt-2">
<Button
type="button"
variant="ghost"

View File

@@ -311,16 +311,21 @@ function createBlobAttachmentNode(previewStyle: BlobPreviewStyle) {
/**
* Create a TipTap suggestion configuration from our SuggestionConfig
* Uses a ref to always get the latest search function (since TipTap captures at mount)
*/
function createSuggestionConfig<T>(
config: SuggestionConfig<T>,
configRef: React.MutableRefObject<SuggestionConfig<T> | undefined>,
triggerChar: string,
handleSubmitRef: React.MutableRefObject<(editor: unknown) => void>,
): Omit<SuggestionOptions<T>, "editor"> {
return {
char: config.char,
allowSpaces: config.allowSpaces ?? false,
allow: config.allow,
char: triggerChar,
allowSpaces: configRef.current?.allowSpaces ?? false,
allow: configRef.current?.allow,
items: async ({ query }) => {
// Always use the current config from ref to get fresh search function
const config = configRef.current;
if (!config) return [];
return await config.search(query);
},
render: () => {
@@ -330,6 +335,9 @@ function createSuggestionConfig<T>(
return {
onStart: (props) => {
const config = configRef.current;
if (!config) return;
editorRef = props.editor;
component = new ReactRenderer(config.component as never, {
props: {
@@ -416,6 +424,23 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
) => {
const handleSubmitRef = useRef<(editor: unknown) => void>(() => {});
// Refs for suggestion configs - TipTap captures these at mount, so we use refs
// to always get the latest search functions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mentionConfigRef = useRef<SuggestionConfig<any> | undefined>(
undefined,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const emojiConfigRef = useRef<SuggestionConfig<any> | undefined>(undefined);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const slashConfigRef = useRef<SuggestionConfig<any> | undefined>(undefined);
// Initialize refs immediately (for first render) and update when suggestions change
// This ensures the refs have values before extensions are created
mentionConfigRef.current = suggestions.find((s) => s.char === "@");
emojiConfigRef.current = suggestions.find((s) => s.char === ":");
slashConfigRef.current = suggestions.find((s) => s.char === "/");
// Helper function to serialize editor content
const serializeContent = useCallback(
(editorInstance: {
@@ -573,14 +598,13 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
createBlobAttachmentNode(blobPreview),
];
// Add mention extension for @ mentions (find it in suggestions)
const mentionConfig = suggestions.find((s) => s.char === "@");
if (mentionConfig) {
// Add mention extension for @ mentions (uses ref for latest config)
if (mentionConfigRef.current) {
exts.push(
Mention.configure({
HTMLAttributes: { class: "mention" },
suggestion: {
...createSuggestionConfig(mentionConfig, handleSubmitRef),
...createSuggestionConfig(mentionConfigRef, "@", handleSubmitRef),
command: ({
editor,
range,
@@ -590,7 +614,9 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
range: unknown;
props: unknown;
}) => {
const result = mentionConfig.onSelect(props as never);
const config = mentionConfigRef.current;
if (!config) return;
const result = config.onSelect(props as never);
const ed = editor as {
chain: () => {
focus: () => {
@@ -617,14 +643,13 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
);
}
// Add emoji extension (find it in suggestions)
const emojiConfig = suggestions.find((s) => s.char === ":");
if (emojiConfig) {
// Add emoji extension (uses ref for latest config)
if (emojiConfigRef.current) {
exts.push(
EmojiMention.configure({
HTMLAttributes: { class: "emoji" },
suggestion: {
...createSuggestionConfig(emojiConfig, handleSubmitRef),
...createSuggestionConfig(emojiConfigRef, ":", handleSubmitRef),
command: ({
editor,
range,
@@ -634,7 +659,9 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
range: unknown;
props: unknown;
}) => {
const result = emojiConfig.onSelect(props as never);
const config = emojiConfigRef.current;
if (!config) return;
const result = config.onSelect(props as never);
const ed = editor as {
chain: () => {
focus: () => {
@@ -658,15 +685,14 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
);
}
// Add slash command extension (find it in suggestions)
const slashConfig = suggestions.find((s) => s.char === "/");
if (slashConfig) {
// Add slash command extension (uses ref for latest config)
if (slashConfigRef.current) {
const SlashCommand = Mention.extend({ name: "slashCommand" });
exts.push(
SlashCommand.configure({
HTMLAttributes: { class: "slash-command" },
suggestion: {
...createSuggestionConfig(slashConfig, handleSubmitRef),
...createSuggestionConfig(slashConfigRef, "/", handleSubmitRef),
command: ({
editor,
props,
@@ -674,12 +700,14 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
editor: unknown;
props: unknown;
}) => {
const config = slashConfigRef.current;
if (!config) return;
const ed = editor as { commands: { clearContent: () => void } };
if (slashConfig.clearOnSelect !== false) {
if (config.clearOnSelect !== false) {
ed.commands.clearContent();
}
if (slashConfig.onExecute) {
slashConfig.onExecute(props as never).catch((error) => {
if (config.onExecute) {
config.onExecute(props as never).catch((error) => {
console.error(
"[NostrEditor] Command execution failed:",
error,
@@ -695,54 +723,10 @@ export const NostrEditor = forwardRef<NostrEditorHandle, NostrEditorProps>(
);
}
// Add any additional custom suggestions (not @, :, or /)
const customSuggestions = suggestions.filter(
(s) => !["@", ":", "/"].includes(s.char),
);
for (const config of customSuggestions) {
const CustomMention = Mention.extend({
name: `suggestion-${config.char}`,
});
exts.push(
CustomMention.configure({
HTMLAttributes: { class: `suggestion-${config.char}` },
suggestion: {
...createSuggestionConfig(config, handleSubmitRef),
command: ({
editor,
range,
props,
}: {
editor: unknown;
range: unknown;
props: unknown;
}) => {
const result = config.onSelect(props as never);
const ed = editor as {
chain: () => {
focus: () => {
insertContentAt: (
range: unknown,
content: unknown[],
) => { run: () => void };
};
};
};
ed.chain()
.focus()
.insertContentAt(range, [
{ type: result.type, attrs: result.attrs },
{ type: "text", text: " " },
])
.run();
},
},
}),
);
}
return exts;
}, [suggestions, submitBehavior, placeholder, blobPreview]);
// Note: We don't depend on suggestions here - we use refs that are updated
// synchronously when suggestions change, ensuring the search functions are always fresh
}, [submitBehavior, placeholder, blobPreview]);
const editor = useEditor({
extensions,