From f18fb83a9220d5ced03e2e96eead9e9e66e89196 Mon Sep 17 00:00:00 2001 From: aits2026 Date: Thu, 5 Mar 2026 16:32:25 +0800 Subject: [PATCH] feat(chat): smooth standalone session window loading --- electron/main.ts | 25 +++++-- electron/preload.ts | 10 ++- src/App.tsx | 14 +++- src/pages/ChatPage.scss | 24 ++++++ src/pages/ChatPage.tsx | 161 ++++++++++++++++++++++++++++++++++++---- src/types/electron.d.ts | 3 + 6 files changed, 214 insertions(+), 23 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 7770856..207d3b6 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -126,21 +126,36 @@ interface AnnualReportYearsTaskState { interface OpenSessionChatWindowOptions { source?: 'chat' | 'export' + initialDisplayName?: string + initialAvatarUrl?: string + initialContactType?: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } const normalizeSessionChatWindowSource = (source: unknown): 'chat' | 'export' => { return String(source || '').trim().toLowerCase() === 'export' ? 'export' : 'chat' } +const normalizeSessionChatWindowOptionString = (value: unknown): string => { + return String(value || '').trim() +} + const loadSessionChatWindowContent = ( win: BrowserWindow, sessionId: string, - source: 'chat' | 'export' + source: 'chat' | 'export', + options?: OpenSessionChatWindowOptions ) => { - const query = new URLSearchParams({ + const queryParams = new URLSearchParams({ sessionId, source - }).toString() + }) + const initialDisplayName = normalizeSessionChatWindowOptionString(options?.initialDisplayName) + const initialAvatarUrl = normalizeSessionChatWindowOptionString(options?.initialAvatarUrl) + const initialContactType = normalizeSessionChatWindowOptionString(options?.initialContactType) + if (initialDisplayName) queryParams.set('initialDisplayName', initialDisplayName) + if (initialAvatarUrl) queryParams.set('initialAvatarUrl', initialAvatarUrl) + if (initialContactType) queryParams.set('initialContactType', initialContactType) + const query = queryParams.toString() if (process.env.VITE_DEV_SERVER_URL) { win.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/chat-window?${query}`) return @@ -724,7 +739,7 @@ function createSessionChatWindow(sessionId: string, options?: OpenSessionChatWin if (existing && !existing.isDestroyed()) { const trackedSource = sessionChatWindowSources.get(normalizedSessionId) || 'chat' if (trackedSource !== normalizedSource) { - loadSessionChatWindowContent(existing, normalizedSessionId, normalizedSource) + loadSessionChatWindowContent(existing, normalizedSessionId, normalizedSource, options) sessionChatWindowSources.set(normalizedSessionId, normalizedSource) } if (existing.isMinimized()) { @@ -763,7 +778,7 @@ function createSessionChatWindow(sessionId: string, options?: OpenSessionChatWin autoHideMenuBar: true }) - loadSessionChatWindowContent(win, normalizedSessionId, normalizedSource) + loadSessionChatWindowContent(win, normalizedSessionId, normalizedSource, options) if (process.env.VITE_DEV_SERVER_URL) { win.webContents.on('before-input-event', (event, input) => { diff --git a/electron/preload.ts b/electron/preload.ts index 3c51ee3..a323f4c 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -99,7 +99,15 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('window:openImageViewerWindow', imagePath, liveVideoPath), openChatHistoryWindow: (sessionId: string, messageId: number) => ipcRenderer.invoke('window:openChatHistoryWindow', sessionId, messageId), - openSessionChatWindow: (sessionId: string, options?: { source?: 'chat' | 'export' }) => + openSessionChatWindow: ( + sessionId: string, + options?: { + source?: 'chat' | 'export' + initialDisplayName?: string + initialAvatarUrl?: string + initialContactType?: 'friend' | 'group' | 'official' | 'former_friend' | 'other' + } + ) => ipcRenderer.invoke('window:openSessionChatWindow', sessionId, options) }, diff --git a/src/App.tsx b/src/App.tsx index 05d1990..a15bb1d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -405,7 +405,19 @@ function App() { const params = new URLSearchParams(location.search) const sessionId = params.get('sessionId') || '' const standaloneSource = params.get('source') - return + const standaloneInitialDisplayName = params.get('initialDisplayName') + const standaloneInitialAvatarUrl = params.get('initialAvatarUrl') + const standaloneInitialContactType = params.get('initialContactType') + return ( + + ) } // 独立通知窗口 diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index d987e11..a21cafb 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -1783,6 +1783,30 @@ z-index: 2; } +.standalone-phase-overlay { + position: absolute; + inset: 0; + z-index: 3; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + background: color-mix(in srgb, var(--bg-tertiary) 82%, transparent); + color: var(--text-secondary); + font-size: 14px; + pointer-events: none; + + .spin { + animation: spin 1s linear infinite; + } + + small { + color: var(--text-tertiary); + font-size: 12px; + } +} + .empty-chat-inline { display: flex; flex-direction: column; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 56d7ba5..23d5ea5 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -205,8 +205,12 @@ interface ChatPageProps { standaloneSessionWindow?: boolean initialSessionId?: string | null standaloneSource?: string | null + standaloneInitialDisplayName?: string | null + standaloneInitialAvatarUrl?: string | null + standaloneInitialContactType?: string | null } +type StandaloneLoadStage = 'idle' | 'connecting' | 'loading' | 'ready' interface SessionDetail { wxid: string @@ -409,9 +413,19 @@ const SessionItem = React.memo(function SessionItem({ function ChatPage(props: ChatPageProps) { - const { standaloneSessionWindow = false, initialSessionId = null, standaloneSource = null } = props + const { + standaloneSessionWindow = false, + initialSessionId = null, + standaloneSource = null, + standaloneInitialDisplayName = null, + standaloneInitialAvatarUrl = null, + standaloneInitialContactType = null + } = props const normalizedInitialSessionId = useMemo(() => String(initialSessionId || '').trim(), [initialSessionId]) const normalizedStandaloneSource = useMemo(() => String(standaloneSource || '').trim().toLowerCase(), [standaloneSource]) + const normalizedStandaloneInitialDisplayName = useMemo(() => String(standaloneInitialDisplayName || '').trim(), [standaloneInitialDisplayName]) + const normalizedStandaloneInitialAvatarUrl = useMemo(() => String(standaloneInitialAvatarUrl || '').trim(), [standaloneInitialAvatarUrl]) + const normalizedStandaloneInitialContactType = useMemo(() => String(standaloneInitialContactType || '').trim().toLowerCase(), [standaloneInitialContactType]) const shouldHideStandaloneDetailButton = standaloneSessionWindow && normalizedStandaloneSource === 'export' const navigate = useNavigate() @@ -496,7 +510,12 @@ function ChatPage(props: ChatPageProps) { const [hasInitialMessages, setHasInitialMessages] = useState(false) const [isSessionSwitching, setIsSessionSwitching] = useState(false) const [noMessageTable, setNoMessageTable] = useState(false) - const [fallbackDisplayName, setFallbackDisplayName] = useState(null) + const [fallbackDisplayName, setFallbackDisplayName] = useState(normalizedStandaloneInitialDisplayName || null) + const [fallbackAvatarUrl, setFallbackAvatarUrl] = useState(normalizedStandaloneInitialAvatarUrl || null) + const [standaloneLoadStage, setStandaloneLoadStage] = useState( + standaloneSessionWindow && normalizedInitialSessionId ? 'connecting' : 'idle' + ) + const [standaloneInitialLoadRequested, setStandaloneInitialLoadRequested] = useState(false) const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false) const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null) const [inProgressExportSessionIds, setInProgressExportSessionIds] = useState>(new Set()) @@ -2411,9 +2430,9 @@ function ChatPage(props: ChatPageProps) { }, [appendMessages, getMessageKey]) // 选择会话 - const selectSessionById = useCallback((sessionId: string) => { + const selectSessionById = useCallback((sessionId: string, options: { force?: boolean } = {}) => { const normalizedSessionId = String(sessionId || '').trim() - if (!normalizedSessionId || normalizedSessionId === currentSessionId) return + if (!normalizedSessionId || (!options.force && normalizedSessionId === currentSessionId)) return const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1 sessionSwitchRequestSeqRef.current = switchRequestSeq @@ -2737,7 +2756,7 @@ function ChatPage(props: ChatPageProps) { }, [currentSessionId, messages.length, isLoadingMessages]) useEffect(() => { - if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) { + if (currentSessionId && isConnected && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) { if (pendingSessionLoadRef.current === currentSessionId) return if (initialLoadRequestedSessionRef.current === currentSessionId) return initialLoadRequestedSessionRef.current = currentSessionId @@ -2748,7 +2767,7 @@ function ChatPage(props: ChatPageProps) { forceInitialLimit: 30 }) } - }, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable]) + }, [currentSessionId, isConnected, messages.length, isLoadingMessages, isLoadingMore, noMessageTable]) useEffect(() => { return () => { @@ -2909,7 +2928,21 @@ function ChatPage(props: ChatPageProps) { // 获取当前会话信息(从通讯录跳转时可能不在 sessions 列表中,构造 fallback) const currentSession = (() => { const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined - if (found || !currentSessionId) return found + if (found) { + if ( + standaloneSessionWindow && + normalizedInitialSessionId && + found.username === normalizedInitialSessionId + ) { + return { + ...found, + displayName: found.displayName || fallbackDisplayName || found.username, + avatarUrl: found.avatarUrl || fallbackAvatarUrl || undefined + } + } + return found + } + if (!currentSessionId) return found return { username: currentSessionId, type: 0, @@ -2919,6 +2952,7 @@ function ChatPage(props: ChatPageProps) { lastTimestamp: 0, lastMsgType: 0, displayName: fallbackDisplayName || currentSessionId, + avatarUrl: fallbackAvatarUrl || undefined, } as ChatSession })() const filteredGroupPanelMembers = useMemo(() => { @@ -2938,33 +2972,121 @@ function ChatPage(props: ChatPageProps) { }, [groupMemberSearchKeyword, groupPanelMembers]) const isCurrentSessionExporting = Boolean(currentSessionId && inProgressExportSessionIds.has(currentSessionId)) const isExportActionBusy = isCurrentSessionExporting || isPreparingExportDialog + const isCurrentSessionGroup = Boolean( + currentSession && ( + isGroupChatSession(currentSession.username) || + ( + standaloneSessionWindow && + currentSession.username === normalizedInitialSessionId && + normalizedStandaloneInitialContactType === 'group' + ) + ) + ) + + useEffect(() => { + if (!standaloneSessionWindow) return + setStandaloneInitialLoadRequested(false) + setStandaloneLoadStage(normalizedInitialSessionId ? 'connecting' : 'idle') + setFallbackDisplayName(normalizedStandaloneInitialDisplayName || null) + setFallbackAvatarUrl(normalizedStandaloneInitialAvatarUrl || null) + }, [ + standaloneSessionWindow, + normalizedInitialSessionId, + normalizedStandaloneInitialDisplayName, + normalizedStandaloneInitialAvatarUrl + ]) + + useEffect(() => { + if (!standaloneSessionWindow) return + if (!normalizedInitialSessionId) return + + if (normalizedStandaloneInitialDisplayName) { + setFallbackDisplayName(normalizedStandaloneInitialDisplayName) + } + if (normalizedStandaloneInitialAvatarUrl) { + setFallbackAvatarUrl(normalizedStandaloneInitialAvatarUrl) + } + + if (!currentSessionId) { + setCurrentSession(normalizedInitialSessionId, { preserveMessages: false }) + } + if (!isConnected || isConnecting) { + setStandaloneLoadStage('connecting') + } + }, [ + standaloneSessionWindow, + normalizedInitialSessionId, + normalizedStandaloneInitialDisplayName, + normalizedStandaloneInitialAvatarUrl, + currentSessionId, + isConnected, + isConnecting, + setCurrentSession + ]) useEffect(() => { if (!standaloneSessionWindow) return if (!normalizedInitialSessionId) return if (!isConnected || isConnecting) return - if (currentSessionId === normalizedInitialSessionId) return - selectSessionById(normalizedInitialSessionId) + if (currentSessionId === normalizedInitialSessionId && standaloneInitialLoadRequested) return + setStandaloneInitialLoadRequested(true) + setStandaloneLoadStage('loading') + selectSessionById(normalizedInitialSessionId, { + force: currentSessionId === normalizedInitialSessionId + }) }, [ standaloneSessionWindow, normalizedInitialSessionId, isConnected, isConnecting, currentSessionId, + standaloneInitialLoadRequested, selectSessionById ]) + useEffect(() => { + if (!standaloneSessionWindow || !normalizedInitialSessionId) return + if (!isConnected || isConnecting) { + setStandaloneLoadStage('connecting') + return + } + if (!standaloneInitialLoadRequested) { + setStandaloneLoadStage('loading') + return + } + if (currentSessionId !== normalizedInitialSessionId) { + setStandaloneLoadStage('loading') + return + } + if (isLoadingMessages || isSessionSwitching) { + setStandaloneLoadStage('loading') + return + } + setStandaloneLoadStage('ready') + }, [ + standaloneSessionWindow, + normalizedInitialSessionId, + isConnected, + isConnecting, + standaloneInitialLoadRequested, + currentSessionId, + isLoadingMessages, + isSessionSwitching + ]) + // 从通讯录跳转时,会话不在列表中,主动加载联系人显示名称 useEffect(() => { if (!currentSessionId) return const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined if (found) { - setFallbackDisplayName(null) + if (found.displayName) setFallbackDisplayName(found.displayName) + if (found.avatarUrl) setFallbackAvatarUrl(found.avatarUrl) return } loadContactInfoBatch([currentSessionId]).then(() => { const cached = senderAvatarCache.get(currentSessionId) if (cached?.displayName) setFallbackDisplayName(cached.displayName) + if (cached?.avatarUrl) setFallbackAvatarUrl(cached.avatarUrl) }) }, [currentSessionId, sessions]) @@ -3741,16 +3863,16 @@ function ChatPage(props: ChatPageProps) { src={currentSession.avatarUrl} name={currentSession.displayName || currentSession.username} size={40} - className={isGroupChatSession(currentSession.username) ? 'group session-avatar' : 'session-avatar'} + className={isCurrentSessionGroup ? 'group session-avatar' : 'session-avatar'} />

{currentSession.displayName || currentSession.username}

- {isGroupChatSession(currentSession.username) && ( + {isCurrentSessionGroup && (
群聊
)}
- {!standaloneSessionWindow && isGroupChatSession(currentSession.username) && ( + {!standaloneSessionWindow && isCurrentSessionGroup && ( )} - {isGroupChatSession(currentSession.username) && ( + {isCurrentSessionGroup && (