feat: update main chat and system chat UI
This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
import { getRegisteredAccessToken } from '../tokenAccess';
|
||||
import { buildPreviewRuntimeUrl, resolvePreviewAppOrigin } from '../previewRuntime';
|
||||
|
||||
const CHAT_EXTERNAL_LINK_OPENED_AT_KEY = 'ai-code-app.chat.external-link-opened-at';
|
||||
const CHAT_EXTERNAL_LINK_TTL_MS = 15_000;
|
||||
const EXTERNAL_WINDOW_TARGET_PREFIX = 'ai-code-app.external-window';
|
||||
const UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS = 600;
|
||||
|
||||
type LinkNavigationEvent = {
|
||||
preventDefault?: () => void;
|
||||
stopPropagation?: () => void;
|
||||
};
|
||||
|
||||
type OpenExternalLinkOptions = {
|
||||
event?: LinkNavigationEvent;
|
||||
onUnsupportedStandalone?: (url: string) => void;
|
||||
};
|
||||
|
||||
function canUseSessionStorage() {
|
||||
return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';
|
||||
}
|
||||
@@ -26,6 +36,165 @@ function clearExternalLinkOpenTimestamp() {
|
||||
window.sessionStorage.removeItem(CHAT_EXTERNAL_LINK_OPENED_AT_KEY);
|
||||
}
|
||||
|
||||
function isStandaloneDisplayMode() {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
window.matchMedia?.('(display-mode: standalone)').matches === true ||
|
||||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true
|
||||
);
|
||||
}
|
||||
|
||||
function isAppleMobileStandaloneMode() {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const navigatorValue = window.navigator as Navigator & { standalone?: boolean };
|
||||
const userAgent = navigatorValue.userAgent ?? '';
|
||||
const platform = navigatorValue.platform ?? '';
|
||||
const maxTouchPoints = typeof navigatorValue.maxTouchPoints === 'number' ? navigatorValue.maxTouchPoints : 0;
|
||||
const isAppleMobileUserAgent = /iPhone|iPad|iPod/iu.test(userAgent);
|
||||
const isTouchMac = platform === 'MacIntel' && maxTouchPoints > 1;
|
||||
|
||||
return isStandaloneDisplayMode() && (isAppleMobileUserAgent || isTouchMac);
|
||||
}
|
||||
|
||||
function buildExternalWindowTarget() {
|
||||
return `${EXTERNAL_WINDOW_TARGET_PREFIX}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function clickExternalAnchor(url: string, target: string) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.target = target;
|
||||
anchor.rel = 'noopener noreferrer';
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
}
|
||||
|
||||
function buildPreviewRuntimeFallbackUrl(url: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetUrl = new URL(url, window.location.origin);
|
||||
const previewOrigin = resolvePreviewAppOrigin();
|
||||
|
||||
if (targetUrl.origin !== window.location.origin && targetUrl.origin !== previewOrigin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previewRuntimeUrl = buildPreviewRuntimeUrl(
|
||||
targetUrl.pathname,
|
||||
targetUrl.search,
|
||||
getRegisteredAccessToken(),
|
||||
null,
|
||||
'mobile',
|
||||
);
|
||||
|
||||
if (!targetUrl.hash) {
|
||||
return previewRuntimeUrl;
|
||||
}
|
||||
|
||||
const previewUrl = new URL(previewRuntimeUrl);
|
||||
previewUrl.hash = targetUrl.hash;
|
||||
return previewUrl.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function canFallbackToSameTab(url: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetUrl = new URL(url, window.location.origin);
|
||||
const previewOrigin = resolvePreviewAppOrigin();
|
||||
return targetUrl.origin === window.location.origin || targetUrl.origin === previewOrigin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSameTabFallback(url: string) {
|
||||
if (typeof window === 'undefined' || !canFallbackToSameTab(url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.location.assign(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
function openPreviewRuntimeFallback(url: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const previewRuntimeUrl = buildPreviewRuntimeFallbackUrl(url);
|
||||
|
||||
if (!previewRuntimeUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.location.assign(previewRuntimeUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
function scheduleUnsupportedStandaloneFallback(url: string, callback?: (url: string) => void) {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined' || !isAppleMobileStandaloneMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
const pageStillVisible = document.visibilityState !== 'hidden';
|
||||
const pageStillFocused = typeof document.hasFocus !== 'function' || document.hasFocus();
|
||||
|
||||
if (pageStillVisible && pageStillFocused) {
|
||||
if (openPreviewRuntimeFallback(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (openSameTabFallback(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback?.(url);
|
||||
}
|
||||
}, UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS);
|
||||
}
|
||||
|
||||
function schedulePopupBlockedFallback(url: string, callback?: (url: string) => void) {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
const pageStillVisible = document.visibilityState !== 'hidden';
|
||||
const pageStillFocused = typeof document.hasFocus !== 'function' || document.hasFocus();
|
||||
|
||||
if (!pageStillVisible || !pageStillFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (openSameTabFallback(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback?.(url);
|
||||
}, UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS);
|
||||
}
|
||||
|
||||
export function shouldSkipForegroundResyncAfterExternalLink() {
|
||||
if (!canUseSessionStorage()) {
|
||||
return false;
|
||||
@@ -42,24 +211,29 @@ export function shouldSkipForegroundResyncAfterExternalLink() {
|
||||
return Number.isFinite(openedAt) && Date.now() - openedAt <= CHAT_EXTERNAL_LINK_TTL_MS;
|
||||
}
|
||||
|
||||
export function openChatExternalLink(url: string, event?: LinkNavigationEvent) {
|
||||
event?.preventDefault?.();
|
||||
event?.stopPropagation?.();
|
||||
export function openExternalLinkInNewWindow(url: string, options: OpenExternalLinkOptions = {}) {
|
||||
options.event?.preventDefault?.();
|
||||
options.event?.stopPropagation?.();
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
persistExternalLinkOpenTimestamp(Date.now());
|
||||
const openedWindow = window.open(url, '_blank', 'noopener,noreferrer');
|
||||
const target = buildExternalWindowTarget();
|
||||
const openedWindow = window.open(url, target, 'noopener,noreferrer');
|
||||
|
||||
if (openedWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.target = '_blank';
|
||||
anchor.rel = 'noopener noreferrer';
|
||||
anchor.click();
|
||||
clickExternalAnchor(url, target);
|
||||
scheduleUnsupportedStandaloneFallback(url, options.onUnsupportedStandalone);
|
||||
schedulePopupBlockedFallback(url, options.onUnsupportedStandalone);
|
||||
}
|
||||
|
||||
export function openChatExternalLink(url: string, event?: LinkNavigationEvent) {
|
||||
openExternalLinkInNewWindow(url, {
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user