diff --git a/src/application/slate-yjs/command/index.ts b/src/application/slate-yjs/command/index.ts index cd8ef5426..a6a39e983 100644 --- a/src/application/slate-yjs/command/index.ts +++ b/src/application/slate-yjs/command/index.ts @@ -50,6 +50,7 @@ import { YjsEditorKey, } from '@/application/types'; import { EditorInlineAttributes } from '@/slate-editor'; +import { Log } from '@/utils/log'; import { renderDate } from '@/utils/time'; export const CustomEditor = { @@ -114,7 +115,7 @@ export const CustomEditor = { const entry = findSlateEntryByBlockId(editor, blockId); if (!entry) { - console.error('Block not found'); + Log.error('Block not found'); return; } @@ -149,7 +150,7 @@ export const CustomEditor = { }, deleteBlockBackward(editor: YjsEditor, at?: BaseRange) { - console.trace('deleteBlockBackward', editor.selection, at); + Log.trace('deleteBlockBackward', editor.selection, at); const sharedRoot = getSharedRoot(editor); const newAt = getSelectionOrThrow(editor, at); @@ -162,7 +163,7 @@ export const CustomEditor = { const blockEntry = getBlockEntry(editor, point); if (!blockEntry) { - console.warn('Block not found', point); + Log.warn('Block not found', point); return; } @@ -208,7 +209,7 @@ export const CustomEditor = { const blockEntry = getBlockEntry(editor, point); if (!blockEntry) { - console.warn('Block not found', point); + Log.warn('Block not found', point); return; } @@ -327,7 +328,7 @@ export const CustomEditor = { const endResult = blockResults.find((r) => r.isEnd); if (!startResult?.newId || !endResult?.newId) { - console.warn('Failed to get new block IDs after tab operation'); + Log.warn('Failed to get new block IDs after tab operation'); return; } @@ -335,7 +336,7 @@ export const CustomEditor = { const newEndBlockEntry = findSlateEntryByBlockId(editor, endResult.newId); if (!newStartBlockEntry || !newEndBlockEntry) { - console.warn('Failed to find new block entries after tab operation'); + Log.warn('Failed to find new block entries after tab operation'); // Try to restore selection using original selection const fallbackSelection = findNearestValidSelection(editor, originalSelection); @@ -380,23 +381,23 @@ export const CustomEditor = { }; } } catch (error) { - console.warn('Error constructing new selection paths:', error); + Log.warn('Error constructing new selection paths:', error); newSelection = null; } // Try to apply the new selection, with multiple fallback strategies if (newSelection && isValidSelection(editor, newSelection)) { - console.debug('✅ Using calculated selection:', newSelection); + Log.debug('✅ Using calculated selection:', newSelection); Transforms.select(editor, newSelection); } else { - console.warn('⚠️ Calculated selection invalid, trying fallback strategies'); + Log.warn('⚠️ Calculated selection invalid, trying fallback strategies'); // Strategy 1: Try to find nearest valid selection from our calculated selection if (newSelection) { const nearestFromCalculated = findNearestValidSelection(editor, newSelection); if (nearestFromCalculated) { - console.debug('✅ Using nearest from calculated:', nearestFromCalculated); + Log.debug('✅ Using nearest from calculated:', nearestFromCalculated); Transforms.select(editor, nearestFromCalculated); return; } @@ -406,7 +407,7 @@ export const CustomEditor = { const nearestFromOriginal = findNearestValidSelection(editor, originalSelection); if (nearestFromOriginal) { - console.debug('✅ Using nearest from original:', nearestFromOriginal); + Log.debug('✅ Using nearest from original:', nearestFromOriginal); Transforms.select(editor, nearestFromOriginal); return; } @@ -417,12 +418,12 @@ export const CustomEditor = { const startOfBlock = Editor.start(editor, newStartBlockPath); if (isValidSelection(editor, { anchor: startOfBlock, focus: startOfBlock })) { - console.debug('✅ Using start of block:', startOfBlock); + Log.debug('✅ Using start of block:', startOfBlock); Transforms.select(editor, startOfBlock); return; } } catch (error) { - console.warn('Failed to select start of block:', error); + Log.warn('Failed to select start of block:', error); } } @@ -430,10 +431,10 @@ export const CustomEditor = { const documentSelection = findNearestValidSelection(editor, null); if (documentSelection) { - console.debug('✅ Using document fallback:', documentSelection); + Log.debug('✅ Using document fallback:', documentSelection); Transforms.select(editor, documentSelection); } else { - console.warn('❌ Could not establish any valid selection after tab operation'); + Log.warn('❌ Could not establish any valid selection after tab operation'); } } }, @@ -455,7 +456,7 @@ export const CustomEditor = { const blockEntry = getBlockEntry(editor, point); if (!blockEntry) { - console.warn('Block not found', point); + Log.warn('Block not found', point); return; } @@ -615,7 +616,7 @@ export const CustomEditor = { return Boolean((attributes as Record)[key]); }); } catch (error) { - console.warn('Error checking mark in expanded selection:', error); + Log.warn('Error checking mark in expanded selection:', error); return false; } } @@ -625,7 +626,7 @@ export const CustomEditor = { return marks ? !!marks[key] : false; } catch (error) { - console.warn('Error checking mark at collapsed selection:', error); + Log.warn('Error checking mark at collapsed selection:', error); return false; } }, @@ -650,7 +651,7 @@ export const CustomEditor = { return attributes as EditorInlineAttributes; }); } catch (error) { - console.warn('Error getting all marks:', error); + Log.warn('Error getting all marks:', error); return []; } } @@ -660,7 +661,7 @@ export const CustomEditor = { return [marks]; } catch (error) { - console.warn('Error getting marks at collapsed selection:', error); + Log.warn('Error getting marks at collapsed selection:', error); return []; } }, @@ -697,7 +698,7 @@ export const CustomEditor = { const parent = getBlock(blockId, sharedRoot); if (!parent) { - console.warn('Parent block not found'); + Log.warn('Parent block not found'); return; } @@ -712,7 +713,7 @@ export const CustomEditor = { ); if (!newBlockId) { - console.warn('Failed to add block'); + Log.warn('Failed to add block'); return; } @@ -731,7 +732,7 @@ export const CustomEditor = { return newBlockId; } } catch (e) { - console.error(e); + Log.error(e); } }, @@ -778,7 +779,7 @@ export const CustomEditor = { return newBlockId; } } catch (e) { - console.error(e); + Log.error(e); } }, @@ -795,7 +796,7 @@ export const CustomEditor = { const parent = getParent(blockId, sharedRoot); if (!parent) { - console.warn('Parent block not found'); + Log.warn('Parent block not found'); return; } @@ -872,7 +873,7 @@ export const CustomEditor = { const prevIndex = getBlockIndex(prevId || blockId, sharedRoot); if (!parent) { - console.warn('Parent block not found'); + Log.warn('Parent block not found'); return; } @@ -885,7 +886,7 @@ export const CustomEditor = { newBlockId = deepCopyBlock(sharedRoot, block); if (!newBlockId) { - console.warn('Copied block not found'); + Log.warn('Copied block not found'); return; } diff --git a/src/application/slate-yjs/plugins/withYjs.ts b/src/application/slate-yjs/plugins/withYjs.ts index b220c5e9a..ff52050d2 100644 --- a/src/application/slate-yjs/plugins/withYjs.ts +++ b/src/application/slate-yjs/plugins/withYjs.ts @@ -85,7 +85,6 @@ export function withYjs( } ): T & YjsEditor { const { - id, uploadFile, localOrigin = CollabOrigin.Local, readSummary, @@ -127,7 +126,6 @@ export function withYjs( } onContentChange?.(content.children); - console.debug('===initializeDocumentContent', e.children); Editor.normalize(e, { force: true }); }; @@ -181,7 +179,6 @@ export function withYjs( throw new Error('Already connected'); } - console.debug('===connect', id); initializeDocumentContent(); e.sharedRoot.observeDeep(handleYEvents); connectSet.add(e); diff --git a/src/application/slate-yjs/utils/applyToSlate.ts b/src/application/slate-yjs/utils/applyToSlate.ts index 7224e1818..b17e8580e 100644 --- a/src/application/slate-yjs/utils/applyToSlate.ts +++ b/src/application/slate-yjs/utils/applyToSlate.ts @@ -9,6 +9,7 @@ import { blockToSlateNode, deltaInsertToSlateNode } from '@/application/slate-yj import { findSlateEntryByBlockId } from '@/application/slate-yjs/utils/editor'; import { dataStringTOJson, getBlock, getChildrenArray, getPageId, getText } from '@/application/slate-yjs/utils/yjs'; import { YBlock, YjsEditorKey } from '@/application/types'; +import { Log } from '@/utils/log'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type BlockMapEvent = YMapEvent; @@ -137,7 +138,7 @@ function applyBlocksYEvent(editor: YjsEditor, event: BlockMapEvent) { const value = keys.get(key); if (!value) { - console.warn(`⚠️ No value found for key: ${key}`); + Log.warn(`⚠️ No value found for key: ${key}`); return; } @@ -152,20 +153,20 @@ function applyBlocksYEvent(editor: YjsEditor, event: BlockMapEvent) { }); updates.forEach(({ key, action, value }, index) => { - console.debug(`📋 Processing block change ${index + 1}/${updates.length}:`, { + Log.debug(`📋 Processing block change ${index + 1}/${updates.length}:`, { key, action, oldValue: value.oldValue, }); if (action === 'add') { - console.debug(`➕ Adding new block: ${key}`); + Log.debug(`➕ Adding new block: ${key}`); handleNewBlock(editor, key, keyPath); } else if (action === 'delete') { - console.debug(`🗑️ Deleting block: ${key}`); + Log.debug(`🗑️ Deleting block: ${key}`); handleDeleteNode(editor, key); } else if (action === 'update') { - console.debug(`🔄 Updating block: ${key}`); + Log.debug(`🔄 Updating block: ${key}`); // TODO: Implement block update logic } }); @@ -185,14 +186,14 @@ function handleNewBlock(editor: YjsEditor, key: string, keyPath: Record !Editor.isEditor(node) && Element.isElement(node) && node.blockId) @@ -209,7 +210,7 @@ function handleNewBlock(editor: YjsEditor, key: string, keyPath: Record !Editor.isEditor(node) && Element.isElement(node) && node.blockId) @@ -271,17 +272,17 @@ function handleNewBlock(editor: YjsEditor, key: string, keyPath: Record !Editor.isEditor(node) && Element.isElement(node) && node.blockId) .map(([node]) => (node as Element).blockId), @@ -325,7 +326,7 @@ function handleDeleteNode(editor: YjsEditor, key: string) { const [node, path] = entry; - console.debug(`🗑️ Deleting block: ${key}`, { + Log.debug(`🗑️ Deleting block: ${key}`, { path, nodeType: (node as Element).type, childrenCount: (node as Element).children.length, @@ -337,5 +338,5 @@ function handleDeleteNode(editor: YjsEditor, key: string) { node, }); - console.debug(`✅ Block deleted successfully: ${key}`); + Log.debug(`✅ Block deleted successfully: ${key}`); } diff --git a/src/components/main/AppConfig.tsx b/src/components/main/AppConfig.tsx index d5775ae90..a6aff7661 100644 --- a/src/components/main/AppConfig.tsx +++ b/src/components/main/AppConfig.tsx @@ -16,6 +16,7 @@ import { AFConfigContext, defaultConfig } from '@/components/main/app.hooks'; import { useUserTimezone } from '@/components/main/hooks/useUserTimezone'; import { useAppLanguage } from '@/components/main/useAppLanguage'; import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; +import { Log } from '@/utils/log'; function AppConfig({ children }: { children: React.ReactNode }) { const [appConfig] = useState(defaultConfig); @@ -41,7 +42,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { try { await db.users.put(user, user.uuid); } catch (e) { - console.error(e); + Log.error(e); } }, [service, userId] @@ -68,7 +69,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { try { await service.getCurrentUser(); } catch (e) { - console.error(e); + Log.error(e); } })(); }, [isAuthenticated, service]); @@ -98,7 +99,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { useEffect(() => { const hasToken = isTokenValid(); - console.debug('[AppConfig] sync check', { + Log.debug('[AppConfig] sync check', { hasToken, isAuthenticated, willSync: hasToken && !isAuthenticated, @@ -106,12 +107,12 @@ function AppConfig({ children }: { children: React.ReactNode }) { // If token exists but state says not authenticated, sync the state if (hasToken && !isAuthenticated) { - console.debug('[AppConfig] syncing authentication state - token exists but state is false'); + Log.debug('[AppConfig] syncing authentication state - token exists but state is false'); setIsAuthenticated(true); } // If no token but state says authenticated, invalidate the session else if (!hasToken && isAuthenticated) { - console.debug('[AppConfig] token removed but state still authenticated - invalidating'); + Log.debug('[AppConfig] token removed but state still authenticated - invalidating'); setIsAuthenticated(false); } }, [isAuthenticated]); @@ -121,13 +122,13 @@ function AppConfig({ children }: { children: React.ReactNode }) { const timeoutId = setTimeout(() => { const hasToken = isTokenValid(); - console.debug('[AppConfig] mount sync check', { + Log.debug('[AppConfig] mount sync check', { hasToken, isAuthenticated, }); if (hasToken && !isAuthenticated) { - console.debug('[AppConfig] mount sync - forcing authentication state to true'); + Log.debug('[AppConfig] mount sync - forcing authentication state to true'); setIsAuthenticated(true); } }, 100); // Small delay to allow all initialization to complete @@ -169,14 +170,14 @@ function AppConfig({ children }: { children: React.ReactNode }) { }; await service.updateUserProfile(metadata); - console.debug('Initial timezone set in user profile:', timezoneData); + Log.debug('Initial timezone set in user profile:', timezoneData); } else { - console.debug('User timezone already set, skipping update:', existingTimezone); + Log.debug('User timezone already set, skipping update:', existingTimezone); } setHasCheckedTimezone(true); } catch (e) { - console.error('Failed to check/update timezone:', e); + Log.error('Failed to check/update timezone:', e); // Still mark as checked to avoid repeated attempts setHasCheckedTimezone(true); } diff --git a/src/components/ws/useAppflowyWebSocket.ts b/src/components/ws/useAppflowyWebSocket.ts index 8404d2c23..2d716081c 100644 --- a/src/components/ws/useAppflowyWebSocket.ts +++ b/src/components/ws/useAppflowyWebSocket.ts @@ -5,6 +5,7 @@ import useWebSocket from 'react-use-websocket'; import { getTokenParsed } from '@/application/session/token'; import { messages } from '@/proto/messages'; import { getConfigValue } from '@/utils/runtime-config'; +import { Log } from '@/utils/log'; const wsURL = getConfigValue('APPFLOWY_WS_BASE_URL', 'ws://localhost:8000/ws/v2'); @@ -116,23 +117,23 @@ export const useAppflowyWebSocket = (options: Options): AppflowyWebSocketType => }, // Reconnect configuration shouldReconnect: (closeEvent) => { - console.info('Connection closed, code:', closeEvent.code, 'reason:', closeEvent.reason); + Log.info('Connection closed, code:', closeEvent.code, 'reason:', closeEvent.reason); // Determine if reconnect is needed based on the close code if (closeEvent.code === CloseCode.NormalClose) { // Normal close, no reconnect - console.debug('✅ Normal close, no reconnect'); + Log.debug('✅ Normal close, no reconnect'); return false; } if (closeEvent.code === CloseCode.EndpointLeft) { // Endpoint left, reconnect - console.debug('✅ Endpoint left, reconnect'); + Log.debug('✅ Endpoint left, reconnect'); return true; } if (closeEvent.code >= CloseCode.ProtocolError && closeEvent.code <= CloseCode.TLSHandshakeFailed) { - console.debug('✅ Protocol error, reconnect'); + Log.debug('✅ Protocol error, reconnect'); // Protocol error, reconnect return true; } @@ -150,7 +151,7 @@ export const useAppflowyWebSocket = (options: Options): AppflowyWebSocketType => if (attemptNumber === 0) { const firstDelay = 5000 + Math.random() * FIRST_ATTEMPT_MAX_DELAY; - console.info(`Reconnect attempt ${attemptNumber}, first attempt delay ${Math.round(firstDelay)}ms`); + Log.info(`Reconnect attempt ${attemptNumber}, first attempt delay ${Math.round(firstDelay)}ms`); return firstDelay; } @@ -163,13 +164,13 @@ export const useAppflowyWebSocket = (options: Options): AppflowyWebSocketType => const jitter = cappedDelay * JITTER_FACTOR * (Math.random() * 2 - 1); const finalDelay = Math.max(0, cappedDelay + jitter); - console.info(`Reconnect attempt ${attemptNumber}, delay ${Math.round(finalDelay)}ms (base: ${cappedDelay}ms)`); + Log.info(`Reconnect attempt ${attemptNumber}, delay ${Math.round(finalDelay)}ms (base: ${cappedDelay}ms)`); return finalDelay; }, // Connection event callback onOpen: () => { - console.info('✅ WebSocket connection opened'); + Log.info('✅ WebSocket connection opened'); setReconnectAttempt(0); const websocket = getWebSocket() as WebSocket | null; @@ -179,21 +180,21 @@ export const useAppflowyWebSocket = (options: Options): AppflowyWebSocketType => }, onClose: (event) => { - console.info('❌ WebSocket connection closed', event); + Log.info('❌ WebSocket connection closed', event); }, onError: (event) => { - console.error('❌ WebSocket error', { event, deviceId: options.deviceId }); + Log.error('❌ WebSocket error', { event, deviceId: options.deviceId }); }, onReconnectStop: (numAttempts) => { - console.info('❌ Reconnect stopped, attempt number:', numAttempts); + Log.info('❌ Reconnect stopped, attempt number:', numAttempts); }, }); const sendProtobufMessage = useCallback( (message: messages.IMessage, keep = true): void => { - console.debug('sending sync message:', message); + Log.debug('sending sync message:', message); const protobufMessage = messages.Message.encode(message).finish(); @@ -203,7 +204,7 @@ export const useAppflowyWebSocket = (options: Options): AppflowyWebSocketType => ); const manualReconnect = useCallback(() => { - console.debug('Manual reconnect triggered'); + Log.debug('Manual reconnect triggered'); window.location.reload(); }, []); const lastProtobufMessage = useMemo( diff --git a/src/utils/image.ts b/src/utils/image.ts index d2efe8cad..152495d75 100644 --- a/src/utils/image.ts +++ b/src/utils/image.ts @@ -1,5 +1,6 @@ import { getTokenParsed } from '@/application/session/token'; import { isAppFlowyFileStorageUrl } from '@/utils/file-storage-url'; +import { Log } from '@/utils/log'; import { getConfigValue } from '@/utils/runtime-config'; const resolveImageUrl = (url: string): string => { @@ -55,6 +56,32 @@ const validateImageLoad = (imageUrl: string): Promise => { }); }; +const validateImageBlob = async (blob: Blob, url?: string): Promise => { + // Check if the response is actually JSON (e.g. error message with 200 status) + if (blob.type === 'application/json') { + try { + const text = await blob.text(); + + Log.error('Image fetch returned JSON instead of image:', text); + } catch (e) { + Log.error('Image fetch returned JSON blob'); + } + + return null; + } + + // If the blob type is generic or missing, try to infer from URL + if ((!blob.type || blob.type === 'application/octet-stream') && url) { + const inferredType = getMimeTypeFromUrl(url); + + if (inferredType) { + return blob.slice(0, blob.size, inferredType); + } + } + + return blob; +}; + export const checkImage = async (url: string): Promise => { // If it's an AppFlowy file storage URL, try authenticated fetch first if (isAppFlowyFileStorageUrl(url)) { @@ -71,7 +98,18 @@ export const checkImage = async (url: string): Promise => { if (response.ok) { const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); + const validatedBlob = await validateImageBlob(blob, url); + + if (!validatedBlob) { + return { + ok: false, + status: 406, // Not Acceptable + statusText: 'Not Acceptable', + error: 'Image fetch returned JSON instead of image', + }; + } + + const blobUrl = URL.createObjectURL(validatedBlob); return { ok: true, @@ -97,11 +135,11 @@ export const checkImage = async (url: string): Promise => { export const fetchImageBlob = async (url: string): Promise => { if (isAppFlowyFileStorageUrl(url)) { - console.debug("fetch appflowy image blob", url); + Log.debug("fetch appflowy image blob", url); const token = getTokenParsed(); if (!token) { - console.error('No authentication token available for image fetch'); + Log.error('No authentication token available for image fetch'); return null; } @@ -114,21 +152,10 @@ export const fetchImageBlob = async (url: string): Promise => { }, }); - console.debug("fetch image blob response", response); - if (response.ok) { const blob = await response.blob(); - // If the blob type is generic or missing, try to infer from URL - if ((!blob.type || blob.type === 'application/octet-stream') && url) { - const inferredType = getMimeTypeFromUrl(url); - - if (inferredType) { - return blob.slice(0, blob.size, inferredType); - } - } - - return blob; + return validateImageBlob(blob, url); } } catch (error) { return null; diff --git a/src/utils/log.ts b/src/utils/log.ts index daccf21d0..9da5e6e90 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -7,11 +7,15 @@ export class Log { } static debug(...msg: unknown[]) { - console.debug(...msg); + if (process.env.NODE_ENV !== 'production') { + console.debug(...msg); + } } static trace(...msg: unknown[]) { - console.trace(...msg); + if (process.env.NODE_ENV !== 'production') { + console.trace(...msg); + } } static warn(...msg: unknown[]) {