Skip to content

Commit 40c0a57

Browse files
authored
fix(files): stop the file-viewer image reshift on open (#6390)
* fix(files): reserve embedded-image space on direct file-view loads An embedded image in a markdown file reshifted on every open: it loaded ~2.3s in with no reserved box and shoved everything below it down (CLS ~0.17). The intrinsic dimensions ARE stored server-side, but the image node view never read them at render. useWorkspaceImageDimensionsAdapter read the active files list via queryClient.getQueryData — non-reactively — so on a cold file-view load it returned null at first render and, because the adapter identity was stable, never re-checked when the list later resolved. Read the list via a reactive useWorkspaceFiles subscription instead: the adapter re-runs the image node view's memoized dimension read when the list resolves, so it reserves the box from the stored dimensions before the (slower) image download finishes. The query key is shared, so it dedupes with surrounding views. Gate it behind `enabled` (driven by the absence of a caller-supplied contentSource) so the public share page — which passes a share token as workspaceId — doesn't fire a 404. Verified in a CLS harness: dims present -> 0 shift; dims absent -> 0.20. * fix(files): stop the duplicate 'mention' extension-name warning The @-mention menu extension and the mention node were both named 'mention', so TipTap logged "Duplicate extension names found: ['mention']" on every editor (twice under the collaborative placeholder + live pair). Rename the menu extension to 'mentionMenu' (the node keeps 'mention', its persisted doc-node type) and move its editor.storage.mention -> editor.storage.mentionMenu.
1 parent 9b80fdd commit 40c0a57

6 files changed

Lines changed: 35 additions & 24 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ interface FileViewerProps {
131131

132132
export function FileViewer(props: FileViewerProps) {
133133
const { contentSource, workspaceId } = props
134-
const imageDimensions = useWorkspaceImageDimensionsAdapter(workspaceId)
134+
// A caller-supplied contentSource means the adapter is unused (and its `workspaceId` may be a share token).
135+
const imageDimensions = useWorkspaceImageDimensionsAdapter(workspaceId, {
136+
enabled: !contentSource,
137+
})
135138
const source = useMemo(
136139
() => contentSource ?? createWorkspaceFileContentSource(workspaceId, imageDimensions),
137140
[contentSource, workspaceId, imageDimensions]

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function fakeNode(attrs: Record<string, unknown>) {
2626
}
2727

2828
function fakeEditor(): Editor {
29-
return { storage: { mention: { navigable: false } } } as unknown as Editor
29+
return { storage: { mentionMenu: { navigable: false } } } as unknown as Editor
3030
}
3131

3232
let container: HTMLDivElement | null = null

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function MentionChipView({ node, editor }: ReactNodeViewProps) {
3838
const { kind, id, label } = node.attrs as MentionAttrs
3939
const Icon = mentionIcon(kind, id, label) as StyleableIcon | undefined
4040
const iconStyle = Icon ? getBareIconStyle(Icon) : undefined
41-
const navigable = editor.storage.mention?.navigable === true
41+
const navigable = editor.storage.mentionMenu?.navigable === true
4242
const workspaceId = typeof params.workspaceId === 'string' ? params.workspaceId : undefined
4343
const path = navigable && workspaceId ? simLinkPath(workspaceId, kind, id) : null
4444

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface MentionStorage {
2525

2626
declare module '@tiptap/core' {
2727
interface Storage {
28-
mention: MentionStorage
28+
mentionMenu: MentionStorage
2929
}
3030
}
3131

@@ -35,10 +35,10 @@ declare module '@tiptap/core' {
3535
* entity inserts it as a portable `sim:<kind>/<id>` markdown link (same wire format as the chat
3636
* composer's `chip-clipboard-codec`), so it round-trips natively through the editor's link + markdown
3737
* machinery. The plugin's `items` is an empty gate; the real list is sourced reactively from the store
38-
* inside {@link MentionList}, populated by the host via the extension's `mention` storage.
38+
* inside {@link MentionList}, populated by the host via the extension's `mentionMenu` storage.
3939
*/
4040
export const Mention = Extension.create<Record<string, never>, MentionStorage>({
41-
name: 'mention',
41+
name: 'mentionMenu',
4242

4343
addStorage() {
4444
return { store: createMentionStore(), onOpen: null, enabled: true, navigable: false }
@@ -53,7 +53,7 @@ export const Mention = Extension.create<Record<string, never>, MentionStorage>({
5353
allowSpaces: false,
5454
startOfLine: false,
5555
allow: ({ editor, range }) => {
56-
if (!editor.storage.mention.enabled) return false
56+
if (!editor.storage.mentionMenu.enabled) return false
5757
if (editor.isActive('codeBlock') || editor.isActive('link') || editor.isActive('code')) {
5858
return false
5959
}
@@ -78,10 +78,10 @@ export const Mention = Extension.create<Record<string, never>, MentionStorage>({
7878
mapProps: (props) => ({
7979
query: props.query,
8080
command: props.command,
81-
store: props.editor.storage.mention.store,
81+
store: props.editor.storage.mentionMenu.store,
8282
editor: props.editor,
8383
}),
84-
onOpen: (props) => props.editor.storage.mention?.onOpen?.(),
84+
onOpen: (props) => props.editor.storage.mentionMenu?.onOpen?.(),
8585
}),
8686
}),
8787
]

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/use-editor-mentions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ export function useEditorMentions(
2727
useEffect(() => {
2828
if (!editor) return
2929
const taggingOn = Boolean(workspaceId) && !disableTagging
30-
editor.storage.mention.enabled = taggingOn
31-
editor.storage.mention.navigable = navigable
32-
editor.storage.mention.onOpen = taggingOn ? () => setActive(true) : null
30+
editor.storage.mentionMenu.enabled = taggingOn
31+
editor.storage.mentionMenu.navigable = navigable
32+
editor.storage.mentionMenu.onOpen = taggingOn ? () => setActive(true) : null
3333
return () => {
34-
editor.storage.mention.onOpen = null
34+
editor.storage.mentionMenu.onOpen = null
3535
}
3636
}, [editor, workspaceId, navigable, disableTagging])
3737

3838
useEffect(() => {
39-
editor?.storage.mention.store.set(items)
39+
editor?.storage.mentionMenu.store.set(items)
4040
}, [editor, items])
4141
}

apps/sim/hooks/queries/workspace-files.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,28 @@ export function useWorkspaceFiles(
142142
}
143143

144144
/**
145-
* Back the file content source's image-dimension capability with workspace file metadata. Reads intrinsic
146-
* dimensions synchronously from the already-loaded active file list (so a stored image reserves its box on
147-
* the first render), and persists the browser's measured dimensions when they're absent or disagree with
148-
* what's stored — an overwrite, so a stale value (left over after a content swap, or a non-EXIF-corrected
149-
* one) self-corrects rather than sticking. The write is fire-and-forget and de-duped (an exact-match cache
150-
* check plus mismatch-only reporting from the caller), so it never storms, never blocks render, and never
151-
* touches the collaborative document.
145+
* Back the file content source's image-dimension capability with workspace file metadata. Subscribes to
146+
* the active file list ({@link useWorkspaceFiles}) and reads each image's stored intrinsic dimensions from
147+
* it, so a stored image reserves its box before it downloads. A reactive read (not a one-shot
148+
* `getQueryData`), so it also works on a cold direct file-view load where the list isn't cached until after
149+
* the image first renders: the subscription re-runs the node view's dimension read once the list resolves.
150+
* Persists the browser's measured dimensions when they're absent or disagree with what's stored — an
151+
* overwrite, so a stale value (left over after a content swap, or a non-EXIF-corrected one) self-corrects
152+
* rather than sticking. The write is fire-and-forget and de-duped (an exact-match cache check plus
153+
* mismatch-only reporting from the caller), so it never storms, never blocks render, and never touches the
154+
* collaborative document. `options.enabled` turns the subscription off for callers that supply their own
155+
* content source (the public share page, whose `workspaceId` is a share token that would 404).
152156
*/
153-
export function useWorkspaceImageDimensionsAdapter(workspaceId: string): ImageDimensionsSource {
157+
export function useWorkspaceImageDimensionsAdapter(
158+
workspaceId: string,
159+
options?: { enabled?: boolean }
160+
): ImageDimensionsSource {
154161
const queryClient = useQueryClient()
162+
const { data: files } = useWorkspaceFiles(workspaceId, 'active', options)
155163
return useMemo<ImageDimensionsSource>(() => {
156164
const listKey = workspaceFilesKeys.list(workspaceId, 'active')
157165
const findRecord = (src: string | undefined): WorkspaceFileRecord | undefined =>
158-
findWorkspaceFileBySrc(queryClient.getQueryData<WorkspaceFileRecord[]>(listKey), src)
166+
findWorkspaceFileBySrc(files, src)
159167
return {
160168
getImageDimensions: (src) => {
161169
const record = findRecord(src)
@@ -194,7 +202,7 @@ export function useWorkspaceImageDimensionsAdapter(workspaceId: string): ImageDi
194202
.catch(() => {})
195203
},
196204
}
197-
}, [queryClient, workspaceId])
205+
}, [files, queryClient, workspaceId])
198206
}
199207

200208
/**

0 commit comments

Comments
 (0)