代码提交
This commit is contained in:
1 parent
e70bb4aa6f
commit
5bf3955b26
20 files changed
+636
-61
No files matched your search
@@ -39,6 +39,8 @@ export interface EmbedSession {
|
||||
theme: EmbedTheme;
|
||||
/** 隐藏深链底部「快捷跳转」栏(TaskJumpBar:底部按钮 + 右侧悬浮菜单)。 */
|
||||
hideJump: boolean;
|
||||
/** Only hide the global Header while retaining PageSidebar and normal app behavior. */
|
||||
hideHeader: boolean;
|
||||
/**
|
||||
* 仅去全局 chrome(Header 导航栏 + PageSidebar 侧边栏),**不**强制主题、**不**隔离鉴权。
|
||||
* 区别于 `embed`:嵌入态会额外强制主题 + 走 sessionStorage 隔离登录身份;`hideChrome` 只想要
|
||||
@@ -75,6 +77,11 @@ export function readHideJumpParam(search: string): FlagParam {
|
||||
return readFlagParam(search, "hideJump");
|
||||
}
|
||||
|
||||
/** Read the URL's `hideHeader` three-state flag (hide only the global Header). */
|
||||
export function readHideHeaderParam(search: string): FlagParam {
|
||||
return readFlagParam(search, "hideHeader");
|
||||
}
|
||||
|
||||
/** 读 URL 里的 `hideChrome` 三态(仅去全局导航栏 + 侧边栏)。 */
|
||||
export function readHideChromeParam(search: string): FlagParam {
|
||||
return readFlagParam(search, "hideChrome");
|
||||
@@ -110,6 +117,7 @@ export function readEmbedSession(): EmbedSession | null {
|
||||
embed: p.embed === true,
|
||||
theme: p.theme === "dark" ? "dark" : "light",
|
||||
hideJump: p.hideJump === true,
|
||||
hideHeader: p.hideHeader === true,
|
||||
hideChrome: p.hideChrome === true,
|
||||
isolate: p.isolate === true,
|
||||
};
|
||||
@@ -121,7 +129,7 @@ export function readEmbedSession(): EmbedSession | null {
|
||||
/** 写会话;无任何开关置位时直接清除(保持 sessionStorage 干净)。隐私模式/quota 异常静默失败。 */
|
||||
function writeEmbedSession(next: EmbedSession): void {
|
||||
try {
|
||||
if (!next.embed && !next.hideJump && !next.hideChrome && !next.isolate) {
|
||||
if (!next.embed && !next.hideJump && !next.hideHeader && !next.hideChrome && !next.isolate) {
|
||||
sessionStorage.removeItem(EMBED_SESSION_KEY);
|
||||
return;
|
||||
}
|
||||
@@ -141,6 +149,16 @@ export function isJumpBarHidden(): boolean {
|
||||
return readEmbedSession()?.hideJump === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this tab should hide its global Header.
|
||||
* `embed` and `hideChrome` also hide the Header, while `hideHeader` alone
|
||||
* deliberately leaves PageSidebar visible.
|
||||
*/
|
||||
export function isHeaderHidden(): boolean {
|
||||
const session = readEmbedSession();
|
||||
return session?.embed === true || session?.hideChrome === true || session?.hideHeader === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本标签页是否要去全局 chrome(Header 导航栏 + PageSidebar 侧边栏)。
|
||||
* `embed` 已隐含去 chrome,故这里取「embed || hideChrome」之并——任一置位都隐藏。
|
||||
@@ -172,7 +190,14 @@ export function isIsolatedSession(): boolean {
|
||||
export function syncEmbedSessionFromSearch(search: string): void {
|
||||
const cur =
|
||||
readEmbedSession() ??
|
||||
{ embed: false, theme: "light" as EmbedTheme, hideJump: false, hideChrome: false, isolate: false };
|
||||
{
|
||||
embed: false,
|
||||
theme: "light" as EmbedTheme,
|
||||
hideJump: false,
|
||||
hideHeader: false,
|
||||
hideChrome: false,
|
||||
isolate: false,
|
||||
};
|
||||
const next: EmbedSession = { ...cur };
|
||||
|
||||
const embedParam = readEmbedParam(search);
|
||||
@@ -187,6 +212,10 @@ export function syncEmbedSessionFromSearch(search: string): void {
|
||||
if (hideJumpParam === "on") next.hideJump = true;
|
||||
else if (hideJumpParam === "off") next.hideJump = false;
|
||||
|
||||
const hideHeaderParam = readHideHeaderParam(search);
|
||||
if (hideHeaderParam === "on") next.hideHeader = true;
|
||||
else if (hideHeaderParam === "off") next.hideHeader = false;
|
||||
|
||||
const hideChromeParam = readHideChromeParam(search);
|
||||
if (hideChromeParam === "on") next.hideChrome = true;
|
||||
else if (hideChromeParam === "off") next.hideChrome = false;
|
||||
@@ -238,6 +267,13 @@ export function resolveForcedEmbedTheme(search: string): EmbedTheme | undefined
|
||||
export function setIsolatedSession(on: boolean): void {
|
||||
const cur =
|
||||
readEmbedSession() ??
|
||||
{ embed: false, theme: "light" as EmbedTheme, hideJump: false, hideChrome: false, isolate: false };
|
||||
{
|
||||
embed: false,
|
||||
theme: "light" as EmbedTheme,
|
||||
hideJump: false,
|
||||
hideHeader: false,
|
||||
hideChrome: false,
|
||||
isolate: false,
|
||||
};
|
||||
writeEmbedSession({ ...cur, isolate: on });
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
isExplicitBrainyIframeMode,
|
||||
isChatsIframeWorkspacePath,
|
||||
resolveForcedThemeForPath,
|
||||
resolveStandardChatFallback,
|
||||
} from "./chats-iframe-route.ts";
|
||||
|
||||
test("matches only workspace chats_iframe routes", () => {
|
||||
@@ -31,3 +33,39 @@ test("forces light theme for landing and chats_iframe routes only", () => {
|
||||
);
|
||||
assert.equal(resolveForcedThemeForPath("/page/workspace/chats/new"), undefined);
|
||||
});
|
||||
|
||||
test("requires an explicit chatMode=iframe opt-in for the Brainy page", () => {
|
||||
assert.equal(isExplicitBrainyIframeMode(""), false);
|
||||
assert.equal(isExplicitBrainyIframeMode("?from=iframe"), false);
|
||||
assert.equal(isExplicitBrainyIframeMode("?chatMode=full"), false);
|
||||
assert.equal(isExplicitBrainyIframeMode("?chatMode=iframe"), true);
|
||||
assert.equal(isExplicitBrainyIframeMode("chatMode=IFRAME"), true);
|
||||
});
|
||||
|
||||
test("redirects accidental chats_iframe routes to standard full chat", () => {
|
||||
assert.equal(
|
||||
resolveStandardChatFallback(
|
||||
"/page/workspace/chats_iframe/new",
|
||||
"?from=iframe&hideChrome=1",
|
||||
),
|
||||
"/page/workspace/chats/new?hideChrome=1&chatMode=full",
|
||||
);
|
||||
assert.equal(
|
||||
resolveStandardChatFallback(
|
||||
"/page/workspace/chats_iframe/thread-123",
|
||||
"?hideChrome=1",
|
||||
),
|
||||
"/page/workspace/chats/thread-123?hideChrome=1&chatMode=full",
|
||||
);
|
||||
assert.equal(
|
||||
resolveStandardChatFallback(
|
||||
"/page/workspace/chats_iframe/new",
|
||||
"?chatMode=iframe",
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
resolveStandardChatFallback("/page/workspace/chats/new", ""),
|
||||
null,
|
||||
);
|
||||
});
|
||||
@@ -5,6 +5,53 @@ export function isChatsIframeWorkspacePath(pathname: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The lightweight iframe page is a private transport for the Brainy
|
||||
* postMessage protocol, not a general-purpose embedded chat page.
|
||||
*
|
||||
* Requiring an explicit mode prevents legacy `from=iframe` links, ordinary
|
||||
* iframes and micro-frontend containers from accidentally landing on the
|
||||
* protocol-only page.
|
||||
*/
|
||||
export function isExplicitBrainyIframeMode(search: string): boolean {
|
||||
const normalizedSearch = search.startsWith("?") ? search.slice(1) : search;
|
||||
return (
|
||||
new URLSearchParams(normalizedSearch)
|
||||
.get("chatMode")
|
||||
?.trim()
|
||||
.toLowerCase() === "iframe"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an accidental/legacy `chats_iframe` URL to the equivalent standard
|
||||
* chat URL. Existing thread ids and unrelated query parameters are retained.
|
||||
*/
|
||||
export function resolveStandardChatFallback(
|
||||
pathname: string,
|
||||
search: string,
|
||||
): string | null {
|
||||
if (
|
||||
!isChatsIframeWorkspacePath(pathname) ||
|
||||
isExplicitBrainyIframeMode(search)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const routePrefix = "/page/workspace/chats_iframe/";
|
||||
const routeSuffix = pathname.slice(routePrefix.length);
|
||||
const targetPath =
|
||||
routeSuffix && routeSuffix !== "new"
|
||||
? `/page/workspace/chats/${routeSuffix}`
|
||||
: "/page/workspace/chats/new";
|
||||
const normalizedSearch = search.startsWith("?") ? search.slice(1) : search;
|
||||
const params = new URLSearchParams(normalizedSearch);
|
||||
params.delete("from");
|
||||
params.set("chatMode", "full");
|
||||
const targetSearch = params.toString();
|
||||
return targetSearch ? `${targetPath}?${targetSearch}` : targetPath;
|
||||
}
|
||||
|
||||
export function resolveForcedThemeForPath(
|
||||
pathname: string,
|
||||
): "light" | undefined {
|
||||
|
||||
Reference in new issue
Block a user