chore: test deploy snapshot
This commit is contained in:
@@ -5171,9 +5171,11 @@ export function MainChatPanel({
|
||||
activePreview,
|
||||
isPreviewLoading,
|
||||
isPreviewModalOpen,
|
||||
previewModalTarget,
|
||||
previewContentType,
|
||||
previewError,
|
||||
previewText,
|
||||
handlePreviewModalAfterOpenChange,
|
||||
setActivePreviewId,
|
||||
setActivePreviewOverride,
|
||||
setIsPreviewModalOpen,
|
||||
@@ -9456,16 +9458,17 @@ export function MainChatPanel({
|
||||
</Space>
|
||||
</Modal>
|
||||
<FullscreenPreviewModal
|
||||
open={isPreviewModalOpen && Boolean(activePreview)}
|
||||
open={isPreviewModalOpen}
|
||||
afterOpenChange={handlePreviewModalAfterOpenChange}
|
||||
title={
|
||||
activePreview
|
||||
? `${activePreview.label} · ${activePreview.kind} · ${activePreview.source === 'context' ? '현재 화면' : '채팅 결과'}`
|
||||
previewModalTarget
|
||||
? `${previewModalTarget.label} · ${previewModalTarget.kind} · ${previewModalTarget.source === 'context' ? '현재 화면' : '채팅 결과'}`
|
||||
: 'preview'
|
||||
}
|
||||
meta={null}
|
||||
actions={
|
||||
<>
|
||||
{isActivePreviewHtml ? (
|
||||
{previewModalTarget && isHtmlPreviewItem(previewModalTarget) ? (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -9487,7 +9490,7 @@ export function MainChatPanel({
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{canSearchActivePreview ? (
|
||||
{previewModalTarget && canSearchActivePreview ? (
|
||||
<Button
|
||||
type="text"
|
||||
className="fullscreen-preview-modal__icon-button"
|
||||
@@ -9526,7 +9529,7 @@ export function MainChatPanel({
|
||||
}}
|
||||
className={`app-chat-panel__preview-modal${isHtmlPreviewFullscreen ? ' app-chat-panel__preview-modal--html-mobile' : ''}`}
|
||||
>
|
||||
{activePreview ? (
|
||||
{previewModalTarget ? (
|
||||
<div className="app-chat-panel__preview-modal-body">
|
||||
{canSearchActivePreview && isPreviewFindOpen ? (
|
||||
<div className="app-chat-panel__preview-modal-findbar">
|
||||
@@ -9570,7 +9573,7 @@ export function MainChatPanel({
|
||||
onPointerDownCapture={handlePreviewSearchRootPointerDown}
|
||||
>
|
||||
<ChatPreviewBody
|
||||
target={activePreview}
|
||||
target={previewModalTarget}
|
||||
previewText={previewText}
|
||||
isPreviewLoading={isPreviewLoading}
|
||||
previewError={previewError}
|
||||
|
||||
@@ -42,6 +42,7 @@ export function useConversationViewController({
|
||||
const [activePreviewId, setActivePreviewId] = useState<string | null>(null);
|
||||
const [activePreviewOverride, setActivePreviewOverride] = useState<PreviewItem | null>(null);
|
||||
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
||||
const [previewModalTarget, setPreviewModalTarget] = useState<PreviewItem | null>(null);
|
||||
const [previewText, setPreviewText] = useState('');
|
||||
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
||||
const [previewError, setPreviewError] = useState('');
|
||||
@@ -49,6 +50,14 @@ export function useConversationViewController({
|
||||
|
||||
const activePreview = activePreviewOverride ?? previewItems.find((item) => item.id === activePreviewId) ?? previewItems[0] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPreviewModalOpen || !activePreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewModalTarget(activePreview);
|
||||
}, [activePreview, isPreviewModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousSessionId = previousSessionIdRef.current;
|
||||
const hasSessionChanged = previousSessionId !== activeSessionId;
|
||||
@@ -98,7 +107,7 @@ export function useConversationViewController({
|
||||
}, [activePreviewId, activePreviewOverride, previewItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPreviewModalOpen || !activePreview) {
|
||||
if (!previewModalTarget) {
|
||||
setPreviewText('');
|
||||
setPreviewError('');
|
||||
setPreviewContentType('');
|
||||
@@ -106,11 +115,15 @@ export function useConversationViewController({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPreviewModalOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
activePreview.kind === 'image' ||
|
||||
activePreview.kind === 'video' ||
|
||||
activePreview.kind === 'pdf' ||
|
||||
activePreview.kind === 'file'
|
||||
previewModalTarget.kind === 'image' ||
|
||||
previewModalTarget.kind === 'video' ||
|
||||
previewModalTarget.kind === 'pdf' ||
|
||||
previewModalTarget.kind === 'file'
|
||||
) {
|
||||
setPreviewText('');
|
||||
setPreviewError('');
|
||||
@@ -124,7 +137,7 @@ export function useConversationViewController({
|
||||
setPreviewError('');
|
||||
setPreviewContentType('');
|
||||
|
||||
fetch(activePreview.url, {
|
||||
fetch(previewModalTarget.url, {
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
})
|
||||
@@ -155,7 +168,19 @@ export function useConversationViewController({
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [activePreview, isPreviewModalOpen]);
|
||||
}, [isPreviewModalOpen, previewModalTarget]);
|
||||
|
||||
const handlePreviewModalAfterOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewModalTarget(null);
|
||||
setPreviewText('');
|
||||
setPreviewError('');
|
||||
setPreviewContentType('');
|
||||
setIsPreviewLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const previousActiveView = previousActiveViewRef.current;
|
||||
@@ -216,9 +241,11 @@ export function useConversationViewController({
|
||||
activePreviewId,
|
||||
isPreviewLoading,
|
||||
isPreviewModalOpen,
|
||||
previewModalTarget,
|
||||
previewContentType,
|
||||
previewError,
|
||||
previewText,
|
||||
handlePreviewModalAfterOpenChange,
|
||||
setActivePreviewId,
|
||||
setActivePreviewOverride,
|
||||
setIsPreviewModalOpen,
|
||||
|
||||
@@ -1626,6 +1626,7 @@ async function requestChatApi<T>(
|
||||
init?: RequestInit,
|
||||
options?: {
|
||||
allowUnauthenticated?: boolean;
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
sharePin?: string | null;
|
||||
},
|
||||
@@ -1635,9 +1636,18 @@ async function requestChatApi<T>(
|
||||
const accessToken = getRegisteredAccessToken();
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
const controller = new AbortController();
|
||||
const externalAbortHandler = () => controller.abort();
|
||||
const timeoutMs = Number.isFinite(options?.timeoutMs) ? Math.max(1000, Number(options?.timeoutMs)) : 8000;
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
if (options?.signal) {
|
||||
if (options.signal.aborted) {
|
||||
window.clearTimeout(timeoutId);
|
||||
throw new DOMException('Aborted', 'AbortError');
|
||||
}
|
||||
options.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
||||
}
|
||||
|
||||
if (!allowUnauthenticated && !hasRegisteredAccessTokenAccess()) {
|
||||
window.clearTimeout(timeoutId);
|
||||
throw new Error('등록된 접근 토큰이 없어 채팅 요청을 보낼 수 없습니다.');
|
||||
@@ -1681,8 +1691,14 @@ async function requestChatApi<T>(
|
||||
});
|
||||
} catch (error) {
|
||||
window.clearTimeout(timeoutId);
|
||||
if (options?.signal) {
|
||||
options.signal.removeEventListener('abort', externalAbortHandler);
|
||||
}
|
||||
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
if (options?.signal?.aborted) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('채팅 서버 응답이 지연됩니다.');
|
||||
}
|
||||
|
||||
@@ -1690,6 +1706,9 @@ async function requestChatApi<T>(
|
||||
}
|
||||
|
||||
window.clearTimeout(timeoutId);
|
||||
if (options?.signal) {
|
||||
options.signal.removeEventListener('abort', externalAbortHandler);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
||||
@@ -2847,6 +2866,7 @@ export async function fetchChatShareSnapshot(
|
||||
options?: {
|
||||
sharePin?: string | null;
|
||||
sessionId?: string | null;
|
||||
signal?: AbortSignal;
|
||||
view?: 'full' | 'initial';
|
||||
},
|
||||
) {
|
||||
@@ -2882,6 +2902,7 @@ export async function fetchChatShareSnapshot(
|
||||
undefined,
|
||||
{
|
||||
allowUnauthenticated: true,
|
||||
signal: options?.signal,
|
||||
sharePin: options?.sharePin,
|
||||
timeoutMs: 20000,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AppstoreOutlined, CheckOutlined, CloseOutlined, CommentOutlined, CopyOutlined, DeleteOutlined, DownOutlined, EditOutlined, EyeInvisibleOutlined, EyeOutlined, FileTextOutlined, FilterOutlined, FullscreenOutlined, LeftOutlined, MinusOutlined, PlusOutlined, ReloadOutlined, RightOutlined, SearchOutlined, SendOutlined, SettingOutlined, ThunderboltOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { Alert, App, Button, Checkbox, Drawer, Dropdown, Input, Modal, Select, Spin, Tabs, Tag, Typography, type MenuProps } from 'antd';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ClipboardEvent, type CSSProperties, type FocusEvent, type KeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react';
|
||||
import { Suspense, lazy, startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ClipboardEvent, type CSSProperties, type FocusEvent, type KeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { FullscreenPreviewModal } from '../../../components/previewer';
|
||||
@@ -110,6 +110,8 @@ const SHARE_LAST_ROOM_STORAGE_KEY = 'codex-live-share-last-room-by-token';
|
||||
const SHARE_ROOM_SNAPSHOT_SESSION_INDEX_STORAGE_KEY = 'codex-live-share-room-snapshot-index:v1';
|
||||
const SHARE_ROOM_SNAPSHOT_SESSION_STORAGE_KEY_PREFIX = 'codex-live-share-room-snapshot:v1';
|
||||
const SHARE_ROOM_SNAPSHOT_SESSION_CACHE_LIMIT = 6;
|
||||
const SHARE_ROOM_SWITCH_CACHE_REQUEST_LIMIT = 12;
|
||||
const SHARE_ROOM_SWITCH_CACHE_MESSAGE_LIMIT = 32;
|
||||
const SHARE_IMMEDIATE_SEND_TOGGLE_HOLD_MS = 1000;
|
||||
const SHARE_EDGE_NAVIGATION_HOTZONE_PX = 28;
|
||||
const SHARE_APPS_EDGE_MIDDLE_BAND_RATIO = 0.2;
|
||||
@@ -140,6 +142,28 @@ type ShareMessageRenderPayload = ShareRenderedMessage & {
|
||||
previewItems: PreviewItem[];
|
||||
};
|
||||
|
||||
type ShareRoomSwitchPerfStep = {
|
||||
name: string;
|
||||
at: number;
|
||||
detailLevel?: ChatShareSnapshot['detailLevel'];
|
||||
requestCount?: number;
|
||||
messageCount?: number;
|
||||
activityLogCount?: number;
|
||||
hasCache?: boolean;
|
||||
};
|
||||
|
||||
type ShareRoomSwitchPerfRun = {
|
||||
id: string;
|
||||
fromSessionId: string;
|
||||
toSessionId: string;
|
||||
startedAt: number;
|
||||
steps: ShareRoomSwitchPerfStep[];
|
||||
};
|
||||
|
||||
type ShareWindowWithPerf = Window & {
|
||||
__chatShareRoomSwitchPerfRuns?: ShareRoomSwitchPerfRun[];
|
||||
};
|
||||
|
||||
type ShareExpandMode = 'latest' | 'pending' | 'all';
|
||||
type PendingSharePromptSelection = PromptDraftSelection & {
|
||||
status: 'draft' | 'submitted';
|
||||
@@ -593,7 +617,11 @@ function readStoredShareRoomSnapshot(token: string, sessionId: string) {
|
||||
}
|
||||
|
||||
const cachedSessionId = resolveShareSnapshotCacheSessionId(snapshot);
|
||||
return cachedSessionId === normalizedSessionId ? snapshot : null;
|
||||
if (cachedSessionId !== normalizedSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildShareRoomSwitchCacheSnapshot(snapshot);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -643,9 +671,10 @@ function clearStoredShareRoomSnapshotCache(token: string) {
|
||||
|
||||
function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot | null | undefined) {
|
||||
const normalizedToken = token.trim();
|
||||
const normalizedSessionId = resolveShareSnapshotCacheSessionId(snapshot);
|
||||
const storedSnapshot = buildShareRoomSwitchCacheSnapshot(snapshot ?? null);
|
||||
const normalizedSessionId = resolveShareSnapshotCacheSessionId(storedSnapshot);
|
||||
|
||||
if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId || !snapshot) {
|
||||
if (!canUseShareRoomSnapshotSessionStorage() || !normalizedToken || !normalizedSessionId || !storedSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -656,7 +685,7 @@ function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot
|
||||
buildShareRoomSnapshotSessionStorageKey(normalizedToken, normalizedSessionId),
|
||||
JSON.stringify({
|
||||
savedAt: nextSavedAt,
|
||||
snapshot,
|
||||
snapshot: storedSnapshot,
|
||||
}),
|
||||
);
|
||||
const nextIndex = readStoredShareRoomSnapshotSessionIndex();
|
||||
@@ -681,6 +710,91 @@ function writeStoredShareRoomSnapshot(token: string, snapshot: ChatShareSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
function buildShareRoomSwitchCacheSnapshot(snapshot: ChatShareSnapshot | null | undefined) {
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (snapshot.detailLevel === 'initial') {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const trimmedRequests = snapshot.requests.slice(-SHARE_ROOM_SWITCH_CACHE_REQUEST_LIMIT);
|
||||
const requestIdSet = new Set(trimmedRequests.map((request) => request.requestId));
|
||||
const trimmedMessages = snapshot.messages
|
||||
.filter((message) => {
|
||||
const requestId = message.requestId?.trim() ?? '';
|
||||
return !requestId || requestIdSet.has(requestId);
|
||||
})
|
||||
.slice(-SHARE_ROOM_SWITCH_CACHE_MESSAGE_LIMIT);
|
||||
const oldestLoadedMessageId =
|
||||
trimmedMessages.length > 0
|
||||
? trimmedMessages[0]?.id ?? snapshot.oldestLoadedMessageId
|
||||
: snapshot.oldestLoadedMessageId;
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
detailLevel: 'initial',
|
||||
requests: trimmedRequests,
|
||||
messages: trimmedMessages,
|
||||
activityLogs: [],
|
||||
oldestLoadedMessageId,
|
||||
hasOlderMessages: true,
|
||||
} satisfies ChatShareSnapshot;
|
||||
}
|
||||
|
||||
function startShareRoomSwitchPerfRun(fromSessionId: string, toSessionId: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const perfWindow = window as ShareWindowWithPerf;
|
||||
const nextRun: ShareRoomSwitchPerfRun = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
fromSessionId: fromSessionId.trim(),
|
||||
toSessionId: toSessionId.trim(),
|
||||
startedAt: performance.now(),
|
||||
steps: [],
|
||||
};
|
||||
const nextRuns = [...(perfWindow.__chatShareRoomSwitchPerfRuns ?? []).slice(-9), nextRun];
|
||||
perfWindow.__chatShareRoomSwitchPerfRuns = nextRuns;
|
||||
return nextRun.id;
|
||||
}
|
||||
|
||||
function recordShareRoomSwitchPerfStep(
|
||||
runId: string,
|
||||
name: string,
|
||||
snapshot?: ChatShareSnapshot | null,
|
||||
extra?: Pick<ShareRoomSwitchPerfStep, 'hasCache'>,
|
||||
) {
|
||||
if (typeof window === 'undefined' || !runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const perfWindow = window as ShareWindowWithPerf;
|
||||
const runs = perfWindow.__chatShareRoomSwitchPerfRuns;
|
||||
|
||||
if (!runs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRun = runs.find((run) => run.id === runId);
|
||||
|
||||
if (!targetRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetRun.steps.push({
|
||||
name,
|
||||
at: performance.now(),
|
||||
detailLevel: snapshot?.detailLevel,
|
||||
requestCount: snapshot?.requests.length,
|
||||
messageCount: snapshot?.messages.length,
|
||||
activityLogCount: snapshot?.activityLogs.length,
|
||||
hasCache: extra?.hasCache,
|
||||
});
|
||||
}
|
||||
|
||||
function readShareRoomSessionIdFromLocation() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
@@ -4074,6 +4188,7 @@ export function ChatSharePage() {
|
||||
typeof window === 'undefined' || !normalizedToken
|
||||
? null
|
||||
: readStoredShareRoomSnapshot(normalizedToken, initialRequestedRoomSessionId);
|
||||
const initialHasCachedSnapshot = initialCachedSnapshot != null;
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const pageRef = useRef<HTMLDivElement | null>(null);
|
||||
const requestAnchorRefs = useRef(new Map<string, HTMLElement>());
|
||||
@@ -4083,6 +4198,11 @@ export function ChatSharePage() {
|
||||
const deferredSnapshotRef = useRef<ChatShareSnapshot | null>(null);
|
||||
const liveRefreshTimerRef = useRef<number | null>(null);
|
||||
const snapshotRefreshPromiseRef = useRef<Promise<boolean> | null>(null);
|
||||
const snapshotRefreshAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const snapshotRefreshInFlightRoomSessionIdRef = useRef('');
|
||||
const snapshotRefreshInFlightViewRef = useRef<'initial' | 'full' | null>(null);
|
||||
const roomSwitchPerfRunIdRef = useRef('');
|
||||
const pendingSnapshotPerfStageRef = useRef<{ runId: string; stage: 'cache' | 'initial' | 'full' } | null>(null);
|
||||
const pendingSilentRefreshRef = useRef(false);
|
||||
const scrollSyncFrameRef = useRef<number | null>(null);
|
||||
const scrollIdleTimerRef = useRef<number | null>(null);
|
||||
@@ -4232,6 +4352,21 @@ export function ChatSharePage() {
|
||||
const isRoomSwitchingRef = useRef(isRoomSwitching);
|
||||
isRoomSwitchingRef.current = isRoomSwitching;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const pendingStage = pendingSnapshotPerfStageRef.current;
|
||||
|
||||
if (!pendingStage || !snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingSnapshotPerfStageRef.current = null;
|
||||
recordShareRoomSwitchPerfStep(pendingStage.runId, `${pendingStage.stage}-snapshot-commit`, snapshot);
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
recordShareRoomSwitchPerfStep(pendingStage.runId, `${pendingStage.stage}-first-frame`, snapshot);
|
||||
});
|
||||
}, [snapshot]);
|
||||
|
||||
const shareTokenSetting = snapshot?.share.tokenSetting ?? null;
|
||||
const shareAllowedAppIdSet = useMemo(
|
||||
() => buildShareAllowedAppIdSet(shareTokenSetting?.allowedAppIds, snapshot?.share.permissions),
|
||||
@@ -4858,35 +4993,65 @@ export function ChatSharePage() {
|
||||
|
||||
void refreshShareRuntime();
|
||||
}, [isRoomSettingsOpen, refreshShareRuntime, roomSettingsTabKey]);
|
||||
const refreshSnapshot = useCallback(async (options?: { initialLoad?: boolean; silent?: boolean; sharePin?: string | null }) => {
|
||||
const refreshSnapshot = useCallback(async (options?: {
|
||||
initialLoad?: boolean;
|
||||
silent?: boolean;
|
||||
sharePin?: string | null;
|
||||
view?: 'initial' | 'full';
|
||||
preserveVisibleSnapshot?: boolean;
|
||||
}) => {
|
||||
if (!normalizedToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialLoad = options?.initialLoad === true;
|
||||
const silent = options?.silent === true;
|
||||
const requestView = options?.view ?? (initialLoad ? 'initial' : 'full');
|
||||
const preserveVisibleSnapshot = options?.preserveVisibleSnapshot === true;
|
||||
const currentPerfRunId = roomSwitchPerfRunIdRef.current;
|
||||
const requestedSessionId = requestedRoomSessionIdRef.current.trim();
|
||||
|
||||
if (snapshotRefreshPromiseRef.current) {
|
||||
if (!initialLoad) {
|
||||
pendingSilentRefreshRef.current = pendingSilentRefreshRef.current || silent;
|
||||
const inFlightRoomSessionId = snapshotRefreshInFlightRoomSessionIdRef.current;
|
||||
const inFlightView = snapshotRefreshInFlightViewRef.current;
|
||||
const shouldReplaceInFlightRequest =
|
||||
requestView === 'initial'
|
||||
|| requestedSessionId !== inFlightRoomSessionId
|
||||
|| inFlightView !== requestView;
|
||||
|
||||
if (shouldReplaceInFlightRequest) {
|
||||
snapshotRefreshAbortControllerRef.current?.abort();
|
||||
} else {
|
||||
if (!initialLoad) {
|
||||
pendingSilentRefreshRef.current = pendingSilentRefreshRef.current || silent;
|
||||
}
|
||||
return snapshotRefreshPromiseRef.current;
|
||||
}
|
||||
return snapshotRefreshPromiseRef.current;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
snapshotRefreshAbortControllerRef.current = abortController;
|
||||
snapshotRefreshInFlightRoomSessionIdRef.current = requestedSessionId;
|
||||
snapshotRefreshInFlightViewRef.current = requestView;
|
||||
|
||||
if (options?.initialLoad) {
|
||||
setIsLoading(true);
|
||||
} else {
|
||||
if (!(preserveVisibleSnapshot && hasSnapshotRef.current)) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
} else if (!silent) {
|
||||
setIsRefreshing(true);
|
||||
}
|
||||
|
||||
const refreshTask = (async () => {
|
||||
const refreshTaskPromise = (async () => {
|
||||
try {
|
||||
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-fetch-start`);
|
||||
const nextSnapshot = await fetchChatShareSnapshot(normalizedToken, {
|
||||
sharePin: options?.sharePin,
|
||||
sessionId: requestedRoomSessionIdRef.current || undefined,
|
||||
view: initialLoad ? 'initial' : 'full',
|
||||
sessionId: requestedSessionId || undefined,
|
||||
signal: abortController.signal,
|
||||
view: requestView,
|
||||
});
|
||||
const requestedSessionId = requestedRoomSessionIdRef.current.trim();
|
||||
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-fetch-end`, nextSnapshot);
|
||||
const matchedRequestedRoom = doesShareSnapshotMatchRequestedRoom(nextSnapshot, requestedSessionId);
|
||||
|
||||
const shouldApplyImmediately =
|
||||
@@ -4899,12 +5064,19 @@ export function ChatSharePage() {
|
||||
if (!shouldApplyImmediately) {
|
||||
deferredSnapshotRef.current = nextSnapshot;
|
||||
} else {
|
||||
setSnapshot(nextSnapshot);
|
||||
pendingSnapshotPerfStageRef.current = currentPerfRunId
|
||||
? { runId: currentPerfRunId, stage: requestView }
|
||||
: null;
|
||||
if (!initialLoad && requestView === 'full' && hasSnapshotRef.current) {
|
||||
startTransition(() => {
|
||||
setSnapshot(nextSnapshot);
|
||||
});
|
||||
} else {
|
||||
setSnapshot(nextSnapshot);
|
||||
}
|
||||
deferredSnapshotRef.current = null;
|
||||
}
|
||||
if (nextSnapshot.detailLevel !== 'initial') {
|
||||
writeStoredShareRoomSnapshot(normalizedToken, nextSnapshot);
|
||||
}
|
||||
writeStoredShareRoomSnapshot(normalizedToken, nextSnapshot);
|
||||
if (nextSnapshot.share.hasAccessPin) {
|
||||
const resolvedPin = normalizeAccessPinInput(options?.sharePin ?? '');
|
||||
const persistedPin = resolvedPin || getStoredChatShareAccessPin(normalizedToken);
|
||||
@@ -4919,6 +5091,7 @@ export function ChatSharePage() {
|
||||
}
|
||||
|
||||
if (matchedRequestedRoom) {
|
||||
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-matched-requested-room`, nextSnapshot);
|
||||
setIsRoomSwitching(false);
|
||||
} else if (requestedSessionId) {
|
||||
pendingSilentRefreshRef.current = true;
|
||||
@@ -4930,6 +5103,10 @@ export function ChatSharePage() {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error instanceof ChatApiError && error.status === 401 && (error.code === 'share_pin_required' || error.code === 'share_pin_invalid')) {
|
||||
setStoredChatShareAccessPin(normalizedToken, null);
|
||||
clearStoredShareRoomSnapshotCache(normalizedToken);
|
||||
@@ -4950,7 +5127,16 @@ export function ChatSharePage() {
|
||||
setIsRoomSwitching(false);
|
||||
return false;
|
||||
} finally {
|
||||
snapshotRefreshPromiseRef.current = null;
|
||||
if (snapshotRefreshAbortControllerRef.current === abortController) {
|
||||
snapshotRefreshAbortControllerRef.current = null;
|
||||
}
|
||||
if (snapshotRefreshPromiseRef.current === refreshTaskPromise) {
|
||||
snapshotRefreshPromiseRef.current = null;
|
||||
}
|
||||
if (snapshotRefreshInFlightRoomSessionIdRef.current === requestedSessionId) {
|
||||
snapshotRefreshInFlightRoomSessionIdRef.current = '';
|
||||
snapshotRefreshInFlightViewRef.current = null;
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsRefreshing(false);
|
||||
|
||||
@@ -4963,9 +5149,40 @@ export function ChatSharePage() {
|
||||
}
|
||||
})();
|
||||
|
||||
snapshotRefreshPromiseRef.current = refreshTask;
|
||||
return refreshTask;
|
||||
snapshotRefreshPromiseRef.current = refreshTaskPromise;
|
||||
return refreshTaskPromise;
|
||||
}, [normalizedToken]);
|
||||
const ensureLatestShareRoomDetailLoaded = useCallback(
|
||||
async (requestId?: string | null) => {
|
||||
const normalizedRequestId = requestId?.trim() ?? '';
|
||||
|
||||
if (!normalizedToken || !snapshot?.conversation.sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedRequestId
|
||||
&& snapshot.activityLogs.some((item) => item.requestId.trim() === normalizedRequestId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingFullSnapshot(true);
|
||||
|
||||
try {
|
||||
const detailPage = await fetchChatShareConversationDetail(normalizedToken, {
|
||||
sessionId: snapshot.conversation.sessionId,
|
||||
limit: Math.max(SHARE_ROOM_SWITCH_CACHE_REQUEST_LIMIT, snapshot.requests.length || 0),
|
||||
sharePin: getStoredChatShareAccessPin(normalizedToken) || undefined,
|
||||
});
|
||||
|
||||
setSnapshot((current) => (current ? mergeChatShareSnapshotHistory(current, detailPage) : current));
|
||||
} finally {
|
||||
setIsLoadingFullSnapshot(false);
|
||||
}
|
||||
},
|
||||
[normalizedToken, snapshot],
|
||||
);
|
||||
const handleLoadOlderShareHistory = useCallback(async () => {
|
||||
if (!normalizedToken || !snapshot?.conversation.sessionId || !snapshot.hasOlderMessages || !snapshot.oldestLoadedMessageId) {
|
||||
return;
|
||||
@@ -5676,15 +5893,27 @@ export function ChatSharePage() {
|
||||
event.preventDefault();
|
||||
}, []);
|
||||
|
||||
const openProcessInspector = useCallback((requestId: string) => {
|
||||
if (!requestId.trim()) {
|
||||
const openProcessInspector = useCallback(async (requestId: string) => {
|
||||
const normalizedRequestId = requestId.trim();
|
||||
|
||||
if (!normalizedRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveProcessInspectorRequestId(requestId);
|
||||
const hasActivityLog = snapshot?.activityLogs.some((item) => item.requestId.trim() === normalizedRequestId) ?? false;
|
||||
|
||||
if (!hasActivityLog) {
|
||||
try {
|
||||
await ensureLatestShareRoomDetailLoaded(normalizedRequestId);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '상세 활동 로그를 불러오지 못했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
setActiveProcessInspectorRequestId(normalizedRequestId);
|
||||
setProcessInspectorMode('default');
|
||||
setProcessInspectorExpandedSection('checklist');
|
||||
}, []);
|
||||
}, [ensureLatestShareRoomDetailLoaded, message, snapshot?.activityLogs]);
|
||||
|
||||
const closeProcessInspector = useCallback(() => {
|
||||
setActiveProcessInspectorRequestId('');
|
||||
@@ -6083,9 +6312,13 @@ export function ChatSharePage() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
void refreshSnapshot({ initialLoad: true });
|
||||
void refreshSnapshot({
|
||||
initialLoad: true,
|
||||
view: 'initial',
|
||||
preserveVisibleSnapshot: initialHasCachedSnapshot,
|
||||
});
|
||||
return undefined;
|
||||
}, [normalizedToken, refreshSnapshot]);
|
||||
}, [initialHasCachedSnapshot, normalizedToken, refreshSnapshot]);
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -6094,7 +6327,6 @@ export function ChatSharePage() {
|
||||
const urlRoomSessionId = readShareRoomSessionIdFromLocation();
|
||||
const restoredRoomSessionId = urlRoomSessionId || readStoredShareLastRoomSessionId(normalizedToken);
|
||||
const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, restoredRoomSessionId);
|
||||
|
||||
requestedRoomSessionIdRef.current = restoredRoomSessionId;
|
||||
deferredSnapshotRef.current = null;
|
||||
setSnapshot(cachedSnapshot);
|
||||
@@ -6116,7 +6348,7 @@ export function ChatSharePage() {
|
||||
if (cachedSnapshot) {
|
||||
setSnapshot(cachedSnapshot);
|
||||
}
|
||||
setIsRoomSwitching(Boolean(nextRoomSessionId));
|
||||
setIsRoomSwitching(Boolean(nextRoomSessionId) && !cachedSnapshot);
|
||||
setRequestedRoomSessionId(nextRoomSessionId);
|
||||
setIsShareRoomListVisible(false);
|
||||
};
|
||||
@@ -6137,8 +6369,15 @@ export function ChatSharePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshSnapshot({ silent: true });
|
||||
}, [normalizedToken, refreshSnapshot, requestedRoomSessionId]);
|
||||
const requestedSessionId = requestedRoomSessionId.trim();
|
||||
const hasMatchingSnapshot = doesShareSnapshotMatchRequestedRoom(snapshot, requestedSessionId);
|
||||
|
||||
if (hasMatchingSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshSnapshot({ silent: true, view: 'initial' });
|
||||
}, [normalizedToken, refreshSnapshot, requestedRoomSessionId, snapshot]);
|
||||
useEffect(() => {
|
||||
if (!normalizedToken || !snapshot || snapshot.detailLevel === 'initial') {
|
||||
return;
|
||||
@@ -6688,9 +6927,13 @@ export function ChatSharePage() {
|
||||
}
|
||||
|
||||
const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, normalizedSessionId);
|
||||
const perfRunId = startShareRoomSwitchPerfRun(selectedShareRoomSessionId, normalizedSessionId);
|
||||
|
||||
deferredSnapshotRef.current = null;
|
||||
roomSwitchPerfRunIdRef.current = perfRunId;
|
||||
recordShareRoomSwitchPerfStep(perfRunId, 'room-select', undefined, { hasCache: Boolean(cachedSnapshot) });
|
||||
if (cachedSnapshot) {
|
||||
pendingSnapshotPerfStageRef.current = { runId: perfRunId, stage: 'cache' };
|
||||
setSnapshot(cachedSnapshot);
|
||||
}
|
||||
setIsRoomSwitching(!cachedSnapshot);
|
||||
@@ -8366,7 +8609,11 @@ export function ChatSharePage() {
|
||||
const normalizedRoomSessionId = restoreSnapshot.roomSessionId.trim();
|
||||
|
||||
if (normalizedRoomSessionId && normalizedRoomSessionId !== selectedShareRoomSessionId) {
|
||||
setIsRoomSwitching(true);
|
||||
const cachedSnapshot = readStoredShareRoomSnapshot(normalizedToken, normalizedRoomSessionId);
|
||||
if (cachedSnapshot) {
|
||||
setSnapshot(cachedSnapshot);
|
||||
}
|
||||
setIsRoomSwitching(!cachedSnapshot);
|
||||
requestedRoomSessionIdRef.current = normalizedRoomSessionId;
|
||||
writeStoredShareLastRoomSessionId(normalizedToken, normalizedRoomSessionId);
|
||||
writeShareRoomSessionIdToLocation(normalizedRoomSessionId, 'replace');
|
||||
|
||||
@@ -5,6 +5,7 @@ import './FullscreenPreviewModal.css';
|
||||
|
||||
type FullscreenPreviewModalProps = {
|
||||
open: boolean;
|
||||
afterOpenChange?: (open: boolean) => void;
|
||||
title?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
@@ -25,6 +26,7 @@ type FullscreenPreviewModalProps = {
|
||||
|
||||
export function FullscreenPreviewModal({
|
||||
open,
|
||||
afterOpenChange,
|
||||
title,
|
||||
meta,
|
||||
actions,
|
||||
@@ -45,6 +47,7 @@ export function FullscreenPreviewModal({
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
afterOpenChange={afterOpenChange}
|
||||
footer={null}
|
||||
title={null}
|
||||
width="100vw"
|
||||
|
||||
@@ -2056,11 +2056,6 @@ export function BaseballTicketBayPlayAppView({ onBack, launchContext = 'direct',
|
||||
<span>{alerts.length}건</span>
|
||||
</div>
|
||||
</div>
|
||||
{isViewingAllClients || isSharedTokenScope ? (
|
||||
<div className="baseball-ticket-bay-app__scope-note">
|
||||
{isSharedTokenScope ? '현재 공유 리소스 별명 기준으로 목록을 표시합니다.' : '등록 토큰으로 전체 기기 목록을 보고 있습니다. 리소스 별명으로 확인하고 수정과 삭제도 바로 할 수 있습니다.'}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="baseball-ticket-bay-app__items">
|
||||
{alerts.length ? (
|
||||
alerts.map((item) => {
|
||||
|
||||
Reference in New Issue
Block a user