@jant/core 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-CyysIxj_.js → app-B-wKZB8f.js} +88 -4
- package/dist/{app-BX2XKxq0.js → app-qwMcaTML.js} +1 -1
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/{client-CTrEFM5W.js → client-B1XjvRqE.js} +1 -1
- package/dist/client/_assets/{client-auth-LBSZxqNC.js → client-auth-B9T2QFl2.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/node.js +2 -2
- package/package.json +1 -1
- package/src/app.tsx +7 -0
- package/src/middleware/__tests__/cache-control.test.ts +50 -0
- package/src/middleware/cache-control.ts +60 -0
- package/src/ui/feed/ThreadPreview.tsx +17 -9
- package/src/ui/feed/__tests__/thread-preview.test.ts +131 -6
- package/src/ui/feed/thread-preview-state.ts +43 -0
|
@@ -3419,9 +3419,9 @@ function normalizeThemeColorForMeta(color) {
|
|
|
3419
3419
|
* internal paths (e.g. `/_assets/client-HASH.js`) embedded by the Worker build
|
|
3420
3420
|
* from the Vite client manifest. Used only in production (IS_VITE_DEV=false).
|
|
3421
3421
|
*/ var IS_VITE_DEV = typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
3422
|
-
var CORE_VERSION = "0.6.
|
|
3423
|
-
var CLIENT_JS_FILE = "/_assets/client-
|
|
3424
|
-
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-
|
|
3422
|
+
var CORE_VERSION = "0.6.4-619b1a90eba61395";
|
|
3423
|
+
var CLIENT_JS_FILE = "/_assets/client-B1XjvRqE.js";
|
|
3424
|
+
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-B9T2QFl2.js";
|
|
3425
3425
|
var CLIENT_CSS_FILE = "/_assets/client-BMPMuwvV.css";
|
|
3426
3426
|
var CLIENT_CJK_CSS_FILE = "/_assets/client-cjk-B7Z0snDu.css";
|
|
3427
3427
|
var CLIENT_CJK_TC_CSS_FILE = "/_assets/client-cjk-tc-BesJYrb2.css";
|
|
@@ -3740,7 +3740,7 @@ var IconSprite = () => {
|
|
|
3740
3740
|
const cjkSerifFont = appConfig?.cjkSerifFont ?? "off";
|
|
3741
3741
|
const cjkStylesheetPath = cjkSerifFont === "zh-Hans" ? IS_VITE_DEV ? assetPath("/src/style-cjk.css") : toPublicAssetPath(CLIENT_CJK_CSS_FILE, assetBasePath) : cjkSerifFont === "zh-Hant" ? IS_VITE_DEV ? assetPath("/src/style-cjk-tc.css") : toPublicAssetPath(CLIENT_CJK_TC_CSS_FILE, assetBasePath) : cjkSerifFont === "ja" ? IS_VITE_DEV ? assetPath("/src/style-cjk-jp.css") : toPublicAssetPath(CLIENT_CJK_JP_CSS_FILE, assetBasePath) : cjkSerifFont === "ko" ? IS_VITE_DEV ? assetPath("/src/style-cjk-kr.css") : toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath) : null;
|
|
3742
3742
|
const clientScriptPath = IS_VITE_DEV ? resolvedClientBundle === "full" ? assetPath("/src/client-auth.ts") : assetPath("/src/client.ts") : toPublicAssetPath(resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE, assetBasePath);
|
|
3743
|
-
const faviconAssetVersion = resolvedFaviconVersion || "0.6.
|
|
3743
|
+
const faviconAssetVersion = resolvedFaviconVersion || "0.6.4-619b1a90eba61395";
|
|
3744
3744
|
const resolvedFaviconHref = faviconHref ?? (faviconAssetVersion ? toPublicPath(`/favicon.ico?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/favicon.ico", sitePathPrefix));
|
|
3745
3745
|
const resolvedAppleTouchHref = appleTouchHref ?? (faviconAssetVersion ? toPublicPath(`/apple-touch-icon.png?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/apple-touch-icon.png", sitePathPrefix));
|
|
3746
3746
|
const socialImageHref = resolvedSocialImagePath ? toAbsoluteAssetUrl(resolvedSocialImagePath, appConfig?.siteUrl || "", sitePathPrefix) : "";
|
|
@@ -10112,6 +10112,35 @@ function getThreadPreviewState({ secondReply, penultimateReply, latestReply, tot
|
|
|
10112
10112
|
].filter((post) => post !== void 0).map((post) => post.id));
|
|
10113
10113
|
return { hiddenCount: Math.max(0, totalReplyCount - visibleReplyIds.size) };
|
|
10114
10114
|
}
|
|
10115
|
+
/**
|
|
10116
|
+
* A lone root post shorter than this (plain-text code points) comfortably
|
|
10117
|
+
* fits the thread-context height cap. Pairs with the
|
|
10118
|
+
* `--site-thread-context-max-height` token (160px) in tokens.css — revisit
|
|
10119
|
+
* both together if that cap changes.
|
|
10120
|
+
*/ var SHORT_ROOT_CHAR_LIMIT = 120;
|
|
10121
|
+
/**
|
|
10122
|
+
* First-paint guess for whether the thread-context shell will overflow its
|
|
10123
|
+
* height cap, used to pick the initial "Show more" toggle visibility.
|
|
10124
|
+
*
|
|
10125
|
+
* The client (thread-context.ts) always re-measures and corrects this, so a
|
|
10126
|
+
* wrong guess costs at most a one-time flash on load — never a wrong final
|
|
10127
|
+
* state. Kept deliberately coarse:
|
|
10128
|
+
*
|
|
10129
|
+
* - 3+ post threads stack 2+ cards in the shell — effectively always overflow.
|
|
10130
|
+
* - A lone root carrying media is tall regardless of its text length.
|
|
10131
|
+
* - Otherwise fall back to a plain-text length threshold. Only clearly short
|
|
10132
|
+
* roots flip to "fits"; anything at or above the limit keeps the historical
|
|
10133
|
+
* "assume overflow" default, so this can only remove flashes, never add them.
|
|
10134
|
+
*
|
|
10135
|
+
* @param rootPost - the thread's root post (the only post in a 2-post thread's
|
|
10136
|
+
* shell)
|
|
10137
|
+
* @param totalReplyCount - replies in the thread; `>= 2` means 3+ posts total
|
|
10138
|
+
* @returns whether to render the toggle visible on first paint
|
|
10139
|
+
*/ function threadContextAssumesOverflow({ rootPost, totalReplyCount }) {
|
|
10140
|
+
if (totalReplyCount >= 2) return true;
|
|
10141
|
+
if (rootPost.media.length > 0 || Boolean(rootPost.previewImageUrl)) return true;
|
|
10142
|
+
return [...rootPost.summary ?? ""].length >= SHORT_ROOT_CHAR_LIMIT;
|
|
10143
|
+
}
|
|
10115
10144
|
//#endregion
|
|
10116
10145
|
//#region src/ui/feed/ThreadPreview.tsx
|
|
10117
10146
|
/**
|
|
@@ -10133,6 +10162,10 @@ var ThreadPreview = ({ rootPost, secondReply, penultimateReply, latestReply, tot
|
|
|
10133
10162
|
latestReply,
|
|
10134
10163
|
totalReplyCount
|
|
10135
10164
|
});
|
|
10165
|
+
const assumeOverflow = threadContextAssumesOverflow({
|
|
10166
|
+
rootPost,
|
|
10167
|
+
totalReplyCount
|
|
10168
|
+
});
|
|
10136
10169
|
const hiddenPostsLabel = i18n._({ id: "oO0hKx" }, { count: hiddenCount });
|
|
10137
10170
|
const showMoreLabel = i18n._({ id: "fMPkxb" });
|
|
10138
10171
|
const showLessLabel = i18n._({ id: "6lGV3K" });
|
|
@@ -10192,6 +10225,7 @@ var ThreadPreview = ({ rootPost, secondReply, penultimateReply, latestReply, tot
|
|
|
10192
10225
|
"data-label-more": showMoreLabel,
|
|
10193
10226
|
"data-label-less": showLessLabel,
|
|
10194
10227
|
"aria-expanded": "false",
|
|
10228
|
+
hidden: !assumeOverflow,
|
|
10195
10229
|
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
10196
10230
|
class: "thread-context-toggle-label",
|
|
10197
10231
|
children: showMoreLabel
|
|
@@ -27844,6 +27878,55 @@ manifestRoutes.get("/manifest.webmanifest", (c) => {
|
|
|
27844
27878
|
};
|
|
27845
27879
|
}
|
|
27846
27880
|
//#endregion
|
|
27881
|
+
//#region src/middleware/cache-control.ts
|
|
27882
|
+
/**
|
|
27883
|
+
* Cache-Control Middleware
|
|
27884
|
+
*
|
|
27885
|
+
* Sets a safe default `Cache-Control` on responses that don't declare one.
|
|
27886
|
+
*
|
|
27887
|
+
* Almost every Jant page is auth-variant: the same URL renders differently
|
|
27888
|
+
* for the signed-in author (nav, the "more" menu, edit affordances) than for
|
|
27889
|
+
* an anonymous visitor. A shared/CDN cache keyed only by URL must therefore
|
|
27890
|
+
* never store these pages — otherwise it serves a stale or wrong-audience
|
|
27891
|
+
* snapshot, which both breaks the UI ("you still look signed out", "your edit
|
|
27892
|
+
* didn't take effect") and can leak the authenticated dashboard to the public.
|
|
27893
|
+
*
|
|
27894
|
+
* Jant is self-hosted software that runs behind whatever reverse proxy or CDN
|
|
27895
|
+
* the operator chooses, so it cannot rely on infrastructure config to get
|
|
27896
|
+
* this right — it must declare its own cache policy. The critical mistake is
|
|
27897
|
+
* emitting `Cache-Control: public`: that word is an explicit invitation for
|
|
27898
|
+
* any shared cache to store the response.
|
|
27899
|
+
*
|
|
27900
|
+
* Routes that serve genuinely public, auth-invariant resources (media, feeds,
|
|
27901
|
+
* sitemaps, favicons, manifests, static assets) set their own `Cache-Control`
|
|
27902
|
+
* explicitly; this middleware leaves those untouched and only fills in the
|
|
27903
|
+
* default for the un-annotated dynamic responses.
|
|
27904
|
+
*/ /**
|
|
27905
|
+
* Default cache directive for dynamic, potentially auth-variant responses.
|
|
27906
|
+
* `private` forbids shared/CDN caches from storing the response; `no-store`
|
|
27907
|
+
* prevents any cache (including the browser) from keeping a copy.
|
|
27908
|
+
*/ var DEFAULT_CACHE_CONTROL = "private, no-store";
|
|
27909
|
+
/**
|
|
27910
|
+
* Middleware that defaults a missing `Cache-Control` header to
|
|
27911
|
+
* `private, no-store`.
|
|
27912
|
+
*
|
|
27913
|
+
* Runs after the route handler: if the handler (or an inner middleware)
|
|
27914
|
+
* already set `Cache-Control`, that explicit value wins. Only responses that
|
|
27915
|
+
* declare nothing receive the safe default.
|
|
27916
|
+
*
|
|
27917
|
+
* @returns Hono middleware enforcing the default cache policy.
|
|
27918
|
+
*
|
|
27919
|
+
* @example
|
|
27920
|
+
* ```ts
|
|
27921
|
+
* app.use("*", defaultCacheControl());
|
|
27922
|
+
* ```
|
|
27923
|
+
*/ function defaultCacheControl() {
|
|
27924
|
+
return async (c, next) => {
|
|
27925
|
+
await next();
|
|
27926
|
+
if (!c.res.headers.has("Cache-Control")) c.res.headers.set("Cache-Control", DEFAULT_CACHE_CONTROL);
|
|
27927
|
+
};
|
|
27928
|
+
}
|
|
27929
|
+
//#endregion
|
|
27847
27930
|
//#region src/middleware/onboarding.ts
|
|
27848
27931
|
/**
|
|
27849
27932
|
* Onboarding Middleware
|
|
@@ -34129,6 +34212,7 @@ async function servePublicStorage(c) {
|
|
|
34129
34212
|
await next();
|
|
34130
34213
|
});
|
|
34131
34214
|
app.use("*", attachSession());
|
|
34215
|
+
app.use("*", defaultCacheControl());
|
|
34132
34216
|
app.use("*", async (c, next) => {
|
|
34133
34217
|
const redirectUrl = await getHostedCanonicalRedirect({
|
|
34134
34218
|
currentSite: c.var.currentSite,
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"name": "url"
|
|
147
147
|
},
|
|
148
148
|
"src/client-auth.ts": {
|
|
149
|
-
"file": "_assets/client-auth-
|
|
149
|
+
"file": "_assets/client-auth-B9T2QFl2.js",
|
|
150
150
|
"name": "client-auth",
|
|
151
151
|
"src": "src/client-auth.ts",
|
|
152
152
|
"isEntry": true,
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
]
|
|
164
164
|
},
|
|
165
165
|
"src/client.ts": {
|
|
166
|
-
"file": "_assets/client-
|
|
166
|
+
"file": "_assets/client-B1XjvRqE.js",
|
|
167
167
|
"name": "client",
|
|
168
168
|
"src": "src/client.ts",
|
|
169
169
|
"isEntry": true,
|
|
@@ -272,4 +272,4 @@ ${c}`:c;break;case`event`:r.event=c;break;case`id`:e(r.id=c);break;case`retry`:{
|
|
|
272
272
|
</div>`}
|
|
273
273
|
</div>
|
|
274
274
|
</dialog>
|
|
275
|
-
`:J}};customElements.define(`jant-text-preview`,Xi);var Zi=8;function Qi(e,t){let n=Number.parseFloat(e);return Number.isFinite(n)?n:t}function $i(e){return Qi(getComputedStyle(e).getPropertyValue(`--site-thread-context-max-height`).trim(),240)}function ea(e){if(e.dataset.threadContextToggleBound===`1`)return;e.dataset.threadContextToggleBound=`1`;let t=e.previousElementSibling;if(!(t instanceof HTMLElement)||t.dataset.threadContext===void 0)return;let n=e.querySelector(`.thread-context-toggle-label`),r=e.dataset.labelMore??`Show more`,i=e.dataset.labelLess??`Show less`,a=!1,o=t=>{e.setAttribute(`aria-expanded`,t?`true`:`false`),n&&(n.textContent=t?i:r)},s=()=>Array.from(t.querySelectorAll(`img`)).every(e=>e.complete),c=()=>{if(a)return;let n=$i(t),r=t.scrollHeight>n+Zi;t.dataset.collapsed===void 0&&(t.dataset.collapsed=``),r?(e.hidden=!1,o(!1)):s()&&(e.hidden=!0)};if(c(),t.querySelectorAll(`img`).forEach(e=>{e.complete||(e.addEventListener(`load`,c,{once:!0}),e.addEventListener(`error`,c,{once:!0}))}),`ResizeObserver`in globalThis){let e=0;new globalThis.ResizeObserver(()=>{cancelAnimationFrame(e),e=requestAnimationFrame(c)}).observe(t)}let l=()=>{let e=!1,n=r=>{e||r&&r.propertyName!==`max-height`||(e=!0,t.removeEventListener(`transitionend`,n),t.dataset.collapsed===void 0&&(t.style.maxHeight=``))};t.addEventListener(`transitionend`,n),window.setTimeout(n,600)};e.addEventListener(`click`,()=>{a=!0,t.dataset.collapsed===void 0?(t.style.maxHeight=`${t.scrollHeight}px`,t.offsetHeight,requestAnimationFrame(()=>{t.dataset.collapsed=``,t.style.maxHeight=``}),o(!1)):(t.style.maxHeight=`${t.scrollHeight}px`,t.offsetHeight,delete t.dataset.collapsed,o(!0)),l()})}function ta(e=document){e.querySelectorAll(`[data-thread-context-toggle]`).forEach(ea)}function na(e){let t=e.closest(`.thread-group-detail`);return t?t.querySelector(`.thread-detail-item`)===e:!1}function ra(){return globalThis.location.hash===`#continue`}function ia(e=document){let t=e.querySelector(`[data-post-current]`);if(!(t instanceof HTMLElement))return;let n=ra();if(globalThis.location.hash&&!n)return;let r=n?`auto`:`smooth`,i=na(t);requestAnimationFrame(()=>{i&&!n||t.scrollIntoView({behavior:r,block:`start`})})}document.addEventListener(`DOMContentLoaded`,()=>{ta(document),ia(document)}),document.querySelectorAll(`.archive-chip-dropdown`).forEach(e=>{let t=e.querySelector(`:scope > button`),n=e.querySelector(`:scope > [data-popover]`),r=n?n.querySelector(`[role="listbox"]`):null;if(!t||!n||!r)return;let i=Array.from(r.querySelectorAll(`[role="option"]`)),a=e.classList.contains(`archive-chip-media`),o=e.dataset.filterKey,s=new Set;a&&i.forEach(e=>{!e.dataset.navigate&&e.getAttribute(`aria-selected`)===`true`&&s.add(e.dataset.value)});let c=new Set(s),l=()=>{document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:e}})),n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),a&&(c=new Set(s))},u=(e=!0)=>{if(n.getAttribute(`aria-hidden`)!==`true`&&(n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus(),a&&o&&(s.size!==c.size||[...s].some(e=>!c.has(e))))){let e=new URL(window.location.href),t=[...s];t.length>0?e.searchParams.set(o,t.join(`,`)):e.searchParams.delete(o),e.searchParams.delete(`hasMedia`),e.searchParams.delete(`page`),window.location.href=e.pathname+(e.search||``)}};t.addEventListener(`click`,e=>{e.target.closest(`.archive-chip-clear`)||(t.getAttribute(`aria-expanded`)===`true`?u():l())}),document.addEventListener(`click`,t=>{e.contains(t.target)||u(!1)}),document.addEventListener(`basecoat:popover`,t=>{t.detail.source!==e&&u(!1)}),r.addEventListener(`click`,e=>{let t=e.target.closest(`[role="option"]`);if(t){if(t.dataset.navigate){let e=t.dataset.value;typeof e==`string`&&e.startsWith(`/`)&&(window.location.href=e);return}if(a){let e=t.dataset.value;s.has(e)?(s.delete(e),t.removeAttribute(`aria-selected`)):(s.add(e),t.setAttribute(`aria-selected`,`true`))}else{let e=t.dataset.value;typeof e==`string`&&e.startsWith(`/`)&&(window.location.href=e)}}});let d=-1,f=e=>{d>-1&&i[d]&&i[d].classList.remove(`active`),d=e,d>-1&&i[d]&&(i[d].classList.add(`active`),i[d].scrollIntoView({block:`nearest`}))};r.addEventListener(`mousemove`,e=>{let t=e.target.closest(`[role="option"]`);if(t){let e=i.indexOf(t);e!==-1&&e!==d&&f(e)}}),r.addEventListener(`mouseleave`,()=>{f(-1)}),t.addEventListener(`keydown`,e=>{let t=n.getAttribute(`aria-hidden`)===`false`;if(e.key===`Escape`){t&&(e.preventDefault(),u());return}if(!t&&[`ArrowDown`,`ArrowUp`].includes(e.key)){e.preventDefault(),l();return}t&&(e.preventDefault(),e.key===`ArrowDown`?f(Math.min(d+1,i.length-1)):e.key===`ArrowUp`?f(Math.max(d-1,0)):e.key===`Enter`&&d>-1&&(i[d].click(),!a&&!i[d].dataset.navigate&&u()))})});function aa(e){let t=e.querySelector(`.site-header-more-btn`),n=e.querySelector(`.site-header-more-popover`);if(!t||!n||t.dataset.moreInitialized===`true`)return;t.dataset.moreInitialized=`true`;function r(){n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:t.parentElement}}))}function i(e=!1){n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus()}t.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),t.getAttribute(`aria-expanded`)===`true`?i():r()}),document.addEventListener(`click`,e=>{e.target instanceof Node&&(t.parentElement?.contains(e.target)||i())}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.getAttribute(`aria-hidden`)===`false`&&i(!0)}),document.addEventListener(`basecoat:popover`,e=>{e.detail?.source!==t.parentElement&&i()})}var oa=`jant:nav-fresh-visits`;function sa(){let e=document.createElement(`span`);return e.className=`site-header-link-fresh`,e.setAttribute(`aria-hidden`,`true`),e.textContent=`*`,e}function ca(e){try{let t=JSON.parse(localStorage.getItem(oa)||`{}`),n=location.pathname,r=e.querySelectorAll(`[data-fresh-at]`);for(let e of r){let r=new URL(e.href).pathname,i=parseInt(e.dataset.freshAt,10);if(r===n)t[r]=Math.floor(Date.now()/1e3);else{let n=t[r];if(!n||n<i){let t=e.querySelector(`svg`);e.insertBefore(sa(),t)}}}localStorage.setItem(oa,JSON.stringify(t))}catch{}}function la(e=document){let t=e.querySelector(`.site-header-hamburger`),n=e.querySelector(`#site-nav-drawer`),r=e.querySelector(`.site-nav-drawer-backdrop`),i=n?.querySelector(`.site-nav-drawer-close`);if(ca(e),aa(e),!t||!n||!r||t.dataset.drawerInitialized===`true`)return;t.dataset.drawerInitialized=`true`;function a(){n.setAttribute(`aria-hidden`,`false`),n.removeAttribute(`inert`),r.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.documentElement.classList.add(`drawer-open`);let e=n.querySelector(`.site-nav-drawer-close`)??n.querySelector(`a[href], button`);e&&e.focus()}function o(e=!0){n.setAttribute(`aria-hidden`,`true`),r.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),document.documentElement.classList.remove(`drawer-open`),n.addEventListener(`transitionend`,()=>{n.getAttribute(`aria-hidden`)===`true`&&n.setAttribute(`inert`,``)},{once:!0}),e&&t.focus()}t.addEventListener(`click`,()=>{t.getAttribute(`aria-expanded`)===`true`?o():a()}),i?.addEventListener(`click`,()=>o()),r.addEventListener(`click`,()=>o()),n.addEventListener(`click`,e=>{e.target instanceof Element&&e.target.closest(`a[href]`)&&o(!1)}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),o())})}la();function ua(e=document){e.querySelectorAll(`.collection-sort-menu`).forEach(e=>{if(e.dataset.collectionSortMenuInitialized===`true`)return;let t=e.querySelector(`:scope > .collection-sort-trigger`),n=e.querySelector(`:scope > [data-popover]`),r=n?.querySelector(`[data-collection-sort-options]`);if(!(t instanceof HTMLButtonElement)||!n||!r)return;let i=(e=!1)=>{n.getAttribute(`aria-hidden`)!==`true`&&(n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus())},a=()=>{document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:e}})),n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`)};t.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),t.getAttribute(`aria-expanded`)===`true`?i(!1):a()}),t.addEventListener(`keydown`,e=>{if(e.key===`Escape`){e.preventDefault(),i(!0);return}if(e.key===`ArrowDown`){e.preventDefault(),t.getAttribute(`aria-expanded`)!==`true`&&a();let n=r.querySelector(`a[href]`);n instanceof HTMLElement&&n.focus()}}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),i(!0))}),document.addEventListener(`click`,t=>{t.target instanceof Node&&(e.contains(t.target)||i(!1))}),document.addEventListener(`basecoat:popover`,t=>{t.detail?.source!==e&&i(!1)}),e.dataset.collectionSortMenuInitialized=`true`})}ua(),rn(),document.documentElement.dataset.jantVersion=`0.6.
|
|
275
|
+
`:J}};customElements.define(`jant-text-preview`,Xi);var Zi=8;function Qi(e,t){let n=Number.parseFloat(e);return Number.isFinite(n)?n:t}function $i(e){return Qi(getComputedStyle(e).getPropertyValue(`--site-thread-context-max-height`).trim(),240)}function ea(e){if(e.dataset.threadContextToggleBound===`1`)return;e.dataset.threadContextToggleBound=`1`;let t=e.previousElementSibling;if(!(t instanceof HTMLElement)||t.dataset.threadContext===void 0)return;let n=e.querySelector(`.thread-context-toggle-label`),r=e.dataset.labelMore??`Show more`,i=e.dataset.labelLess??`Show less`,a=!1,o=t=>{e.setAttribute(`aria-expanded`,t?`true`:`false`),n&&(n.textContent=t?i:r)},s=()=>Array.from(t.querySelectorAll(`img`)).every(e=>e.complete),c=()=>{if(a)return;let n=$i(t),r=t.scrollHeight>n+Zi;t.dataset.collapsed===void 0&&(t.dataset.collapsed=``),r?(e.hidden=!1,o(!1)):s()&&(e.hidden=!0)};if(c(),t.querySelectorAll(`img`).forEach(e=>{e.complete||(e.addEventListener(`load`,c,{once:!0}),e.addEventListener(`error`,c,{once:!0}))}),`ResizeObserver`in globalThis){let e=0;new globalThis.ResizeObserver(()=>{cancelAnimationFrame(e),e=requestAnimationFrame(c)}).observe(t)}let l=()=>{let e=!1,n=r=>{e||r&&r.propertyName!==`max-height`||(e=!0,t.removeEventListener(`transitionend`,n),t.dataset.collapsed===void 0&&(t.style.maxHeight=``))};t.addEventListener(`transitionend`,n),window.setTimeout(n,600)};e.addEventListener(`click`,()=>{a=!0,t.dataset.collapsed===void 0?(t.style.maxHeight=`${t.scrollHeight}px`,t.offsetHeight,requestAnimationFrame(()=>{t.dataset.collapsed=``,t.style.maxHeight=``}),o(!1)):(t.style.maxHeight=`${t.scrollHeight}px`,t.offsetHeight,delete t.dataset.collapsed,o(!0)),l()})}function ta(e=document){e.querySelectorAll(`[data-thread-context-toggle]`).forEach(ea)}function na(e){let t=e.closest(`.thread-group-detail`);return t?t.querySelector(`.thread-detail-item`)===e:!1}function ra(){return globalThis.location.hash===`#continue`}function ia(e=document){let t=e.querySelector(`[data-post-current]`);if(!(t instanceof HTMLElement))return;let n=ra();if(globalThis.location.hash&&!n)return;let r=n?`auto`:`smooth`,i=na(t);requestAnimationFrame(()=>{i&&!n||t.scrollIntoView({behavior:r,block:`start`})})}document.addEventListener(`DOMContentLoaded`,()=>{ta(document),ia(document)}),document.querySelectorAll(`.archive-chip-dropdown`).forEach(e=>{let t=e.querySelector(`:scope > button`),n=e.querySelector(`:scope > [data-popover]`),r=n?n.querySelector(`[role="listbox"]`):null;if(!t||!n||!r)return;let i=Array.from(r.querySelectorAll(`[role="option"]`)),a=e.classList.contains(`archive-chip-media`),o=e.dataset.filterKey,s=new Set;a&&i.forEach(e=>{!e.dataset.navigate&&e.getAttribute(`aria-selected`)===`true`&&s.add(e.dataset.value)});let c=new Set(s),l=()=>{document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:e}})),n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),a&&(c=new Set(s))},u=(e=!0)=>{if(n.getAttribute(`aria-hidden`)!==`true`&&(n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus(),a&&o&&(s.size!==c.size||[...s].some(e=>!c.has(e))))){let e=new URL(window.location.href),t=[...s];t.length>0?e.searchParams.set(o,t.join(`,`)):e.searchParams.delete(o),e.searchParams.delete(`hasMedia`),e.searchParams.delete(`page`),window.location.href=e.pathname+(e.search||``)}};t.addEventListener(`click`,e=>{e.target.closest(`.archive-chip-clear`)||(t.getAttribute(`aria-expanded`)===`true`?u():l())}),document.addEventListener(`click`,t=>{e.contains(t.target)||u(!1)}),document.addEventListener(`basecoat:popover`,t=>{t.detail.source!==e&&u(!1)}),r.addEventListener(`click`,e=>{let t=e.target.closest(`[role="option"]`);if(t){if(t.dataset.navigate){let e=t.dataset.value;typeof e==`string`&&e.startsWith(`/`)&&(window.location.href=e);return}if(a){let e=t.dataset.value;s.has(e)?(s.delete(e),t.removeAttribute(`aria-selected`)):(s.add(e),t.setAttribute(`aria-selected`,`true`))}else{let e=t.dataset.value;typeof e==`string`&&e.startsWith(`/`)&&(window.location.href=e)}}});let d=-1,f=e=>{d>-1&&i[d]&&i[d].classList.remove(`active`),d=e,d>-1&&i[d]&&(i[d].classList.add(`active`),i[d].scrollIntoView({block:`nearest`}))};r.addEventListener(`mousemove`,e=>{let t=e.target.closest(`[role="option"]`);if(t){let e=i.indexOf(t);e!==-1&&e!==d&&f(e)}}),r.addEventListener(`mouseleave`,()=>{f(-1)}),t.addEventListener(`keydown`,e=>{let t=n.getAttribute(`aria-hidden`)===`false`;if(e.key===`Escape`){t&&(e.preventDefault(),u());return}if(!t&&[`ArrowDown`,`ArrowUp`].includes(e.key)){e.preventDefault(),l();return}t&&(e.preventDefault(),e.key===`ArrowDown`?f(Math.min(d+1,i.length-1)):e.key===`ArrowUp`?f(Math.max(d-1,0)):e.key===`Enter`&&d>-1&&(i[d].click(),!a&&!i[d].dataset.navigate&&u()))})});function aa(e){let t=e.querySelector(`.site-header-more-btn`),n=e.querySelector(`.site-header-more-popover`);if(!t||!n||t.dataset.moreInitialized===`true`)return;t.dataset.moreInitialized=`true`;function r(){n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:t.parentElement}}))}function i(e=!1){n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus()}t.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),t.getAttribute(`aria-expanded`)===`true`?i():r()}),document.addEventListener(`click`,e=>{e.target instanceof Node&&(t.parentElement?.contains(e.target)||i())}),document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.getAttribute(`aria-hidden`)===`false`&&i(!0)}),document.addEventListener(`basecoat:popover`,e=>{e.detail?.source!==t.parentElement&&i()})}var oa=`jant:nav-fresh-visits`;function sa(){let e=document.createElement(`span`);return e.className=`site-header-link-fresh`,e.setAttribute(`aria-hidden`,`true`),e.textContent=`*`,e}function ca(e){try{let t=JSON.parse(localStorage.getItem(oa)||`{}`),n=location.pathname,r=e.querySelectorAll(`[data-fresh-at]`);for(let e of r){let r=new URL(e.href).pathname,i=parseInt(e.dataset.freshAt,10);if(r===n)t[r]=Math.floor(Date.now()/1e3);else{let n=t[r];if(!n||n<i){let t=e.querySelector(`svg`);e.insertBefore(sa(),t)}}}localStorage.setItem(oa,JSON.stringify(t))}catch{}}function la(e=document){let t=e.querySelector(`.site-header-hamburger`),n=e.querySelector(`#site-nav-drawer`),r=e.querySelector(`.site-nav-drawer-backdrop`),i=n?.querySelector(`.site-nav-drawer-close`);if(ca(e),aa(e),!t||!n||!r||t.dataset.drawerInitialized===`true`)return;t.dataset.drawerInitialized=`true`;function a(){n.setAttribute(`aria-hidden`,`false`),n.removeAttribute(`inert`),r.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`),document.documentElement.classList.add(`drawer-open`);let e=n.querySelector(`.site-nav-drawer-close`)??n.querySelector(`a[href], button`);e&&e.focus()}function o(e=!0){n.setAttribute(`aria-hidden`,`true`),r.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),document.documentElement.classList.remove(`drawer-open`),n.addEventListener(`transitionend`,()=>{n.getAttribute(`aria-hidden`)===`true`&&n.setAttribute(`inert`,``)},{once:!0}),e&&t.focus()}t.addEventListener(`click`,()=>{t.getAttribute(`aria-expanded`)===`true`?o():a()}),i?.addEventListener(`click`,()=>o()),r.addEventListener(`click`,()=>o()),n.addEventListener(`click`,e=>{e.target instanceof Element&&e.target.closest(`a[href]`)&&o(!1)}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),o())})}la();function ua(e=document){e.querySelectorAll(`.collection-sort-menu`).forEach(e=>{if(e.dataset.collectionSortMenuInitialized===`true`)return;let t=e.querySelector(`:scope > .collection-sort-trigger`),n=e.querySelector(`:scope > [data-popover]`),r=n?.querySelector(`[data-collection-sort-options]`);if(!(t instanceof HTMLButtonElement)||!n||!r)return;let i=(e=!1)=>{n.getAttribute(`aria-hidden`)!==`true`&&(n.setAttribute(`aria-hidden`,`true`),t.setAttribute(`aria-expanded`,`false`),e&&t.focus())},a=()=>{document.dispatchEvent(new CustomEvent(`basecoat:popover`,{detail:{source:e}})),n.setAttribute(`aria-hidden`,`false`),t.setAttribute(`aria-expanded`,`true`)};t.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),t.getAttribute(`aria-expanded`)===`true`?i(!1):a()}),t.addEventListener(`keydown`,e=>{if(e.key===`Escape`){e.preventDefault(),i(!0);return}if(e.key===`ArrowDown`){e.preventDefault(),t.getAttribute(`aria-expanded`)!==`true`&&a();let n=r.querySelector(`a[href]`);n instanceof HTMLElement&&n.focus()}}),n.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),i(!0))}),document.addEventListener(`click`,t=>{t.target instanceof Node&&(e.contains(t.target)||i(!1))}),document.addEventListener(`basecoat:popover`,t=>{t.detail?.source!==e&&i(!1)}),e.dataset.collectionSortMenuInitialized=`true`})}ua(),rn(),document.documentElement.dataset.jantVersion=`0.6.4-619b1a90eba61395`;export{tn as _,Gi as a,Ai as c,hi as d,Z as f,ui as g,K as h,$ as i,ji as l,q as m,Ki as n,Mi as o,J as p,Wi as r,Ni as s,qi as t,ki as u};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as e,n as t,o as n,t as r}from"./chunks/url-pLre2DM_.js";import{_ as i,a,c as o,d as s,f as c,g as l,h as u,i as d,l as f,m as p,n as m,o as h,p as g,r as _,s as v,t as y,u as b}from"./client-
|
|
1
|
+
import{a as e,n as t,o as n,t as r}from"./chunks/url-pLre2DM_.js";import{_ as i,a,c as o,d as s,f as c,g as l,h as u,i as d,l as f,m as p,n as m,o as h,p as g,r as _,s as v,t as y,u as b}from"./client-B1XjvRqE.js";var x={maxShortSide:1920,maxLongSide:8192,quality:.85,mimeType:`image/webp`};function S(e){return new Promise((t,n)=>{let r=new Image;r.onload=()=>{URL.revokeObjectURL(r.src),t(r)},r.onerror=()=>n(Error(`Failed to load image`)),r.src=URL.createObjectURL(e)})}function C(e,t,n){if(Math.max(e,t)>n.maxLongSide)return{passthrough:!0,width:e,height:t};let r=Math.min(e,t);if(r<=n.maxShortSide)return{passthrough:!1,width:e,height:t};let i=n.maxShortSide/r;return{passthrough:!1,width:Math.round(e*i),height:Math.round(t*i)}}async function ee(e,t,n){let r=await new Promise((r,i)=>{e.toBlob(e=>e?r(e):i(Error(`Failed to create blob`)),t,n)});return t!==`image/png`&&r.type===`image/png`?new Promise((t,r)=>{e.toBlob(e=>e?t(e):r(Error(`Failed to create blob`)),`image/jpeg`,n)}):r}async function te(e,t={}){let n={...x,...t},r=await S(e),i=C(r.width,r.height,n);if(i.passthrough)return{blob:e,width:i.width,height:i.height,processed:!1};let a=document.createElement(`canvas`);a.width=i.width,a.height=i.height;let o=a.getContext(`2d`);if(!o)throw Error(`Failed to get canvas context`);return o.drawImage(r,0,0,i.width,i.height),{blob:await ee(a,n.mimeType,n.quality),width:i.width,height:i.height,processed:!0}}async function ne(e,t={}){let n=await te(e,t);if(!n.processed)return{file:e,width:n.width,height:n.height};let r={"image/webp":`webp`,"image/jpeg":`jpg`,"image/png":`png`}[n.blob.type]??`png`,i=`${e.name.replace(/\.[^.]+$/,``)}.${r}`;return{file:new File([n.blob],i,{type:n.blob.type}),width:n.width,height:n.height}}var re={process:te,processToFile:ne},ie={ICO_16:16,ICO_32:32,APPLE_TOUCH:512};function ae(e){let t=e.length*16,n=6+t,r=new ArrayBuffer(6+t),i=new DataView(r);i.setUint16(0,0,!0),i.setUint16(2,1,!0),i.setUint16(4,e.length,!0);let a=[];for(let t=0;t<e.length;t++){let r=e[t],o=6+t*16;i.setUint8(o+0,r.size<256?r.size:0),i.setUint8(o+1,r.size<256?r.size:0),i.setUint8(o+2,0),i.setUint8(o+3,0),i.setUint16(o+4,1,!0),i.setUint16(o+6,32,!0),i.setUint32(o+8,r.png.byteLength,!0),i.setUint32(o+12,n,!0),n+=r.png.byteLength,a.push(r.png)}return new Blob([r,...a],{type:`image/x-icon`})}function oe(e){return typeof e==`object`&&!!e}function w(e,t){if(!oe(e))return;let n=e[t];return typeof n==`string`?n:void 0}function se(e,t){if(!oe(e))return;let n=e[t];return typeof n==`boolean`?n:void 0}function ce(e,t){if(!oe(e))return;let n=e[t];return typeof n==`number`?n:void 0}async function le(e){let t=await e.json();return oe(t)?t:{}}var ue=180;function de(e){return new Promise((t,n)=>{let r=new Image;r.onload=()=>{URL.revokeObjectURL(r.src),t(r)},r.onerror=()=>n(Error(`Failed to load image`)),r.src=URL.createObjectURL(e)})}function fe(e,t){let n=document.createElement(`canvas`);n.width=t,n.height=t;let r=n.getContext(`2d`);if(!r)throw Error(`Failed to get canvas context`);r.imageSmoothingEnabled=!0,r.imageSmoothingQuality=`high`;let i=Math.max(t/e.width,t/e.height),a=t/i,o=t/i,s=(e.width-a)/2,c=(e.height-o)/2;return r.drawImage(e,s,c,a,o,0,0,t,t),new Promise((e,t)=>{n.toBlob(n=>{n?e(n):t(Error(`Failed to create PNG blob`))},`image/png`)})}function pe(e,t){return e.type===`image/png`&&t.width===t.height&&t.width>=ue&&t.height>=ue}async function me(e,t){let n=e.closest(`form`)?.querySelector(`label`),r=n?.textContent??``;try{n&&(n.textContent=e.dataset.textProcessing||`Processing...`);let r=await de(t),a=t,o=t.name;if(t.type!==`image/svg+xml`){let e=await fe(r,512);a=new File([e],t.name.replace(/\.[^.]+$/,`.png`),{type:`image/png`}),o=a.name}let s=pe(t,r)?Promise.resolve(t):fe(r,ie.APPLE_TOUCH),[c,l,u]=await Promise.all([fe(r,16),fe(r,32),s]),[d,f]=await Promise.all([c.arrayBuffer(),l.arrayBuffer()]),p=ae([{size:16,png:d},{size:32,png:f}]);n&&(n.textContent=e.dataset.textUploading||`Uploading...`);let m=new FormData;m.append(`file`,a,o),m.append(`favicon`,p,`favicon.ico`),m.append(`appleTouch`,u,`apple-touch-icon.png`);let h=await fetch(i(`/settings/avatar`),{method:`POST`,headers:{Accept:`application/json`},body:m});if(!h.ok){let t=e.dataset.textError||`Upload failed. Please try again.`;try{let e=await le(h);t=w(e,`error`)??w(e,`message`)??t}catch{}throw Error(t)}let g=await le(h),_=w(g,`status`),v=w(g,`url`);if(_===`redirect`&&v){window.location.href=v;return}window.location.href=i(`/settings/avatar?saved`)}catch(t){n&&(n.textContent=r),d(t instanceof Error&&t.message?t.message:e.dataset.textError||`Upload failed. Please try again.`,`error`)}e.value=``}function he(){document.addEventListener(`change`,e=>{let t=e.target.closest(`[data-avatar-upload]`);t?.files?.[0]&&(e.stopPropagation(),me(t,t.files[0]))})}he();var ge=`default`,_e=class extends c{static properties={_open:{state:!0},_title:{state:!0},_message:{state:!0},_confirmLabel:{state:!0},_cancelLabel:{state:!0},_tone:{state:!0}};#e=[];#t=null;createRenderRoot(){return this.innerHTML=``,this}constructor(){super(),this._open=!1,this._title=``,this._message=``,this._confirmLabel=``,this._cancelLabel=``,this._tone=ge}disconnectedCallback(){super.disconnectedCallback();let e=this.#t;this.#t=null,e?.resolve(!1);for(let e of this.#e.splice(0))e.resolve(!1)}async confirm(e){return await new Promise(t=>{this.#e.push({...e,tone:e.tone??ge,resolve:t}),this.#t||this.#n()})}async#n(){if(this.#t||this.#e.length===0)return;let e=this.#e.shift();if(!e)return;this.#t=e,this._title=e.title??``,this._message=e.message,this._confirmLabel=e.confirmLabel,this._cancelLabel=e.cancelLabel,this._tone=e.tone??ge,this._open=!0,await this.updateComplete;let t=this.querySelector(`.confirm-dialog`);t&&(t.open||t.showModal(),t.querySelector(`.confirm-dialog-panel`)?.focus())}#r(e){let t=this.#t;if(!t)return;this.#t=null;let n=this.querySelector(`.confirm-dialog`);n?.open&&n.close(),this._open=!1,this._title=``,this._message=``,this._confirmLabel=``,this._cancelLabel=``,this._tone=ge,t.resolve(e),queueMicrotask(()=>void this.#n())}#i=e=>{e.preventDefault(),this.#r(!1)};#a=e=>{e.target===e.currentTarget&&this.#r(!1)};#o=e=>{if(e.key===`Escape`){e.preventDefault(),e.stopPropagation(),this.#r(!1);return}let t=e.target,n=t instanceof HTMLButtonElement||t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLAnchorElement;e.key===`Enter`&&!e.defaultPrevented&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.shiftKey&&!n&&(e.preventDefault(),this.#r(!0))};#s(){if(this._title)return{title:this._title,message:this._message};let e=this._message.match(/^(.+?[??])\s+(.+)$/u);return e?{title:e[1],message:e[2]}:{title:this._message,message:``}}render(){if(!this._open)return g;let{title:e,message:t}=this.#s(),n=this._tone===`danger`?`btn-destructive`:`btn`;return u`
|
|
2
2
|
<dialog
|
|
3
3
|
class="dialog confirm-dialog"
|
|
4
4
|
@cancel=${this.#i}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v as url_exports } from "./url-XF0GbKGO.js";
|
|
2
|
-
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, h as defaultFeedRenderer, j as MAX_PINNED_POSTS, k as FORMATS, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-
|
|
2
|
+
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, h as defaultFeedRenderer, j as MAX_PINNED_POSTS, k as FORMATS, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-B-wKZB8f.js";
|
|
3
3
|
import { T as time_exports, a as markdown_exports } from "./export-CzuQyg5h.js";
|
|
4
4
|
import "./env-CoSe-1y4.js";
|
|
5
5
|
import "./github-sync-Dbrb1DS5.js";
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./url-XF0GbKGO.js";
|
|
2
|
-
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as resolveConfig, d as setWebhook, f as BUILTIN_FONT_THEMES, g as pgSchemaBundle, i as createSiteService, l as getWebhookUrl, m as getFontThemeCssVariables, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getCjkSerifCssVariables, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as setMyCommands, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-
|
|
2
|
+
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as resolveConfig, d as setWebhook, f as BUILTIN_FONT_THEMES, g as pgSchemaBundle, i as createSiteService, l as getWebhookUrl, m as getFontThemeCssVariables, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getCjkSerifCssVariables, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as setMyCommands, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-B-wKZB8f.js";
|
|
3
3
|
import { t as createExportService } from "./export-CzuQyg5h.js";
|
|
4
4
|
import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-CoSe-1y4.js";
|
|
5
5
|
import "./github-sync-Dbrb1DS5.js";
|
|
@@ -529,7 +529,7 @@ async function createNodeRequestHandler(options) {
|
|
|
529
529
|
async function start(env = process.env, app) {
|
|
530
530
|
const handler = await createNodeRequestHandler({
|
|
531
531
|
env,
|
|
532
|
-
app: async () => app ?? (await import("./app-
|
|
532
|
+
app: async () => app ?? (await import("./app-qwMcaTML.js")).createApp()
|
|
533
533
|
});
|
|
534
534
|
const hostname = resolveHost(env);
|
|
535
535
|
const port = resolvePort(env);
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -69,6 +69,7 @@ import { manifestRoutes } from "./routes/feed/manifest.js";
|
|
|
69
69
|
// Middleware
|
|
70
70
|
import { requireAuth } from "./middleware/auth.js";
|
|
71
71
|
import { attachSession } from "./middleware/session.js";
|
|
72
|
+
import { defaultCacheControl } from "./middleware/cache-control.js";
|
|
72
73
|
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
73
74
|
import { errorHandler } from "./middleware/error-handler.js";
|
|
74
75
|
import { withConfig } from "./middleware/config.js";
|
|
@@ -320,6 +321,12 @@ export function createApp(): App {
|
|
|
320
321
|
// downstream handlers don't each call auth.api.getSession themselves.
|
|
321
322
|
app.use("*", attachSession());
|
|
322
323
|
|
|
324
|
+
// Default every response without an explicit Cache-Control to
|
|
325
|
+
// `private, no-store`. Jant pages are auth-variant, so a shared/CDN cache
|
|
326
|
+
// must never store them; routes serving genuinely public resources (media,
|
|
327
|
+
// feeds, sitemaps, favicons) set their own Cache-Control and are untouched.
|
|
328
|
+
app.use("*", defaultCacheControl());
|
|
329
|
+
|
|
323
330
|
app.use("*", async (c, next) => {
|
|
324
331
|
const redirectUrl = await getHostedCanonicalRedirect({
|
|
325
332
|
currentSite: c.var.currentSite,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import type { Bindings } from "../../types.js";
|
|
4
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
5
|
+
import { defaultCacheControl } from "../cache-control.js";
|
|
6
|
+
|
|
7
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
8
|
+
|
|
9
|
+
function buildApp(): Hono<Env> {
|
|
10
|
+
const app = new Hono<Env>();
|
|
11
|
+
app.use("*", defaultCacheControl());
|
|
12
|
+
|
|
13
|
+
// Un-annotated dynamic page — the common case.
|
|
14
|
+
app.get("/", (c) => c.html("<h1>home</h1>"));
|
|
15
|
+
|
|
16
|
+
// Route that declares its own public cache policy (e.g. a feed).
|
|
17
|
+
app.get("/feed", (c) =>
|
|
18
|
+
c.body("<feed/>", 200, { "Cache-Control": "public, max-age=180" }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Route that already opts out explicitly.
|
|
22
|
+
app.get("/api/thing", (c) =>
|
|
23
|
+
c.json({ ok: true }, 200, { "Cache-Control": "no-store" }),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return app;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("defaultCacheControl", () => {
|
|
30
|
+
it("defaults un-annotated responses to private, no-store", async () => {
|
|
31
|
+
const response = await buildApp().request("/");
|
|
32
|
+
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("leaves an explicit public cache policy untouched", async () => {
|
|
36
|
+
const response = await buildApp().request("/feed");
|
|
37
|
+
expect(response.headers.get("Cache-Control")).toBe("public, max-age=180");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("leaves an explicit opt-out untouched", async () => {
|
|
41
|
+
const response = await buildApp().request("/api/thing");
|
|
42
|
+
expect(response.headers.get("Cache-Control")).toBe("no-store");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("defaults not-found responses too", async () => {
|
|
46
|
+
const response = await buildApp().request("/missing");
|
|
47
|
+
expect(response.status).toBe(404);
|
|
48
|
+
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-Control Middleware
|
|
3
|
+
*
|
|
4
|
+
* Sets a safe default `Cache-Control` on responses that don't declare one.
|
|
5
|
+
*
|
|
6
|
+
* Almost every Jant page is auth-variant: the same URL renders differently
|
|
7
|
+
* for the signed-in author (nav, the "more" menu, edit affordances) than for
|
|
8
|
+
* an anonymous visitor. A shared/CDN cache keyed only by URL must therefore
|
|
9
|
+
* never store these pages — otherwise it serves a stale or wrong-audience
|
|
10
|
+
* snapshot, which both breaks the UI ("you still look signed out", "your edit
|
|
11
|
+
* didn't take effect") and can leak the authenticated dashboard to the public.
|
|
12
|
+
*
|
|
13
|
+
* Jant is self-hosted software that runs behind whatever reverse proxy or CDN
|
|
14
|
+
* the operator chooses, so it cannot rely on infrastructure config to get
|
|
15
|
+
* this right — it must declare its own cache policy. The critical mistake is
|
|
16
|
+
* emitting `Cache-Control: public`: that word is an explicit invitation for
|
|
17
|
+
* any shared cache to store the response.
|
|
18
|
+
*
|
|
19
|
+
* Routes that serve genuinely public, auth-invariant resources (media, feeds,
|
|
20
|
+
* sitemaps, favicons, manifests, static assets) set their own `Cache-Control`
|
|
21
|
+
* explicitly; this middleware leaves those untouched and only fills in the
|
|
22
|
+
* default for the un-annotated dynamic responses.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { MiddlewareHandler } from "hono";
|
|
26
|
+
import type { Bindings } from "../types.js";
|
|
27
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
28
|
+
|
|
29
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default cache directive for dynamic, potentially auth-variant responses.
|
|
33
|
+
* `private` forbids shared/CDN caches from storing the response; `no-store`
|
|
34
|
+
* prevents any cache (including the browser) from keeping a copy.
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_CACHE_CONTROL = "private, no-store";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Middleware that defaults a missing `Cache-Control` header to
|
|
40
|
+
* `private, no-store`.
|
|
41
|
+
*
|
|
42
|
+
* Runs after the route handler: if the handler (or an inner middleware)
|
|
43
|
+
* already set `Cache-Control`, that explicit value wins. Only responses that
|
|
44
|
+
* declare nothing receive the safe default.
|
|
45
|
+
*
|
|
46
|
+
* @returns Hono middleware enforcing the default cache policy.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* app.use("*", defaultCacheControl());
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function defaultCacheControl(): MiddlewareHandler<Env> {
|
|
54
|
+
return async (c, next) => {
|
|
55
|
+
await next();
|
|
56
|
+
if (!c.res.headers.has("Cache-Control")) {
|
|
57
|
+
c.res.headers.set("Cache-Control", DEFAULT_CACHE_CONTROL);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -11,7 +11,10 @@ import { useLingui } from "../../i18n/context.js";
|
|
|
11
11
|
import type { ThreadPreviewProps } from "../../types.js";
|
|
12
12
|
import { TimelineItem } from "./TimelineItem.js";
|
|
13
13
|
import { TimelineItemFromPost } from "./TimelineItem.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getThreadPreviewState,
|
|
16
|
+
threadContextAssumesOverflow,
|
|
17
|
+
} from "./thread-preview-state.js";
|
|
15
18
|
|
|
16
19
|
const ROOT_CONTEXT_DISPLAY = {
|
|
17
20
|
footer: {
|
|
@@ -42,6 +45,10 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
42
45
|
latestReply,
|
|
43
46
|
totalReplyCount,
|
|
44
47
|
});
|
|
48
|
+
const assumeOverflow = threadContextAssumesOverflow({
|
|
49
|
+
rootPost,
|
|
50
|
+
totalReplyCount,
|
|
51
|
+
});
|
|
45
52
|
const hiddenPostsLabel = i18n._(
|
|
46
53
|
msg({
|
|
47
54
|
message: "{count, plural, one {# more post} other {# more posts}}",
|
|
@@ -75,14 +82,14 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
75
82
|
: undefined;
|
|
76
83
|
const gapHref = renderedSecondReply?.permalink ?? latestReply.permalink;
|
|
77
84
|
|
|
78
|
-
// Always render the collapsible shell
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
85
|
+
// Always render the collapsible shell + toggle: the cap and fade are a
|
|
86
|
+
// constant "this is context" affordance. The toggle's *initial* visibility
|
|
87
|
+
// is a server-side guess (threadContextAssumesOverflow) — the real rendered
|
|
88
|
+
// height is unknown until the client measures. Guessing matters because a
|
|
89
|
+
// 2-post thread's shell holds only the root, and a short root genuinely
|
|
90
|
+
// fits the cap; rendering the toggle visible anyway makes it flash in then
|
|
91
|
+
// out on load. Client-side measurement (thread-context.ts) re-measures and
|
|
92
|
+
// corrects, so the guess only ever affects that first paint.
|
|
86
93
|
|
|
87
94
|
const rootItem = (
|
|
88
95
|
<div class="thread-item thread-item-context">
|
|
@@ -138,6 +145,7 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
138
145
|
data-label-more={showMoreLabel}
|
|
139
146
|
data-label-less={showLessLabel}
|
|
140
147
|
aria-expanded="false"
|
|
148
|
+
hidden={!assumeOverflow}
|
|
141
149
|
>
|
|
142
150
|
<span class="thread-context-toggle-label">{showMoreLabel}</span>
|
|
143
151
|
<svg
|
|
@@ -7,7 +7,10 @@ import { createI18n } from "../../../i18n/i18n.js";
|
|
|
7
7
|
import type { PostView, TimelineItemView } from "../../../types.js";
|
|
8
8
|
import { CuratedThreadPreview } from "../CuratedThreadPreview.js";
|
|
9
9
|
import { ThreadPreview } from "../ThreadPreview.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getThreadPreviewState,
|
|
12
|
+
threadContextAssumesOverflow,
|
|
13
|
+
} from "../thread-preview-state.js";
|
|
11
14
|
|
|
12
15
|
function createPostView(overrides: Partial<PostView> = {}): PostView {
|
|
13
16
|
return {
|
|
@@ -48,6 +51,13 @@ function renderWithI18n(
|
|
|
48
51
|
return renderToString(render());
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** The opening tag of the show-more toggle button, for attribute assertions. */
|
|
55
|
+
function toggleTag(html: string): string {
|
|
56
|
+
return (
|
|
57
|
+
html.match(/<button[^>]*\bdata-thread-context-toggle\b[^>]*>/)?.[0] ?? ""
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
describe("getThreadPreviewState", () => {
|
|
52
62
|
it("has no hidden posts for a 2-post thread", () => {
|
|
53
63
|
const latestReply = createPostView({
|
|
@@ -236,13 +246,17 @@ describe("getThreadPreviewState", () => {
|
|
|
236
246
|
expect(html).toMatch(/data-label-less="[^"]+"/);
|
|
237
247
|
});
|
|
238
248
|
|
|
239
|
-
it("
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
249
|
+
it("hides the show-more toggle on first paint for a short lone root that fits the cap", () => {
|
|
250
|
+
// 2-post thread: the shell holds only the root. A short root genuinely
|
|
251
|
+
// fits the height cap, so the toggle is rendered hidden to avoid flashing
|
|
252
|
+
// it in then out. Client-side measurement (thread-context.ts) re-reveals
|
|
253
|
+
// it if the rendered height actually overflows.
|
|
243
254
|
const html = renderWithI18n(() =>
|
|
244
255
|
ThreadPreview({
|
|
245
|
-
rootPost: createPostView({
|
|
256
|
+
rootPost: createPostView({
|
|
257
|
+
bodyHtml: "<p>Root</p>",
|
|
258
|
+
summary: "Root",
|
|
259
|
+
}),
|
|
246
260
|
latestReply: createPostView({
|
|
247
261
|
id: "post-2",
|
|
248
262
|
permalink: "/post-2",
|
|
@@ -257,10 +271,35 @@ describe("getThreadPreviewState", () => {
|
|
|
257
271
|
expect(html).toContain("thread-context-shell");
|
|
258
272
|
expect(html).toContain("data-collapsed");
|
|
259
273
|
expect(html).toContain("data-thread-context-toggle");
|
|
274
|
+
expect(toggleTag(html)).toContain("hidden");
|
|
260
275
|
expect(html).toContain("<p>Root</p>");
|
|
261
276
|
expect(html).toContain("<p>Latest</p>");
|
|
262
277
|
});
|
|
263
278
|
|
|
279
|
+
it("shows the show-more toggle on first paint for a long lone root", () => {
|
|
280
|
+
// 2-post thread with a long root — the shell almost certainly overflows
|
|
281
|
+
// the cap, so the toggle is rendered visible immediately (no flash).
|
|
282
|
+
const html = renderWithI18n(() =>
|
|
283
|
+
ThreadPreview({
|
|
284
|
+
rootPost: createPostView({
|
|
285
|
+
bodyHtml: "<p>Root</p>",
|
|
286
|
+
summary: "word ".repeat(60),
|
|
287
|
+
}),
|
|
288
|
+
latestReply: createPostView({
|
|
289
|
+
id: "post-2",
|
|
290
|
+
permalink: "/post-2",
|
|
291
|
+
slug: "post-2",
|
|
292
|
+
bodyHtml: "<p>Latest</p>",
|
|
293
|
+
isLastInThread: true,
|
|
294
|
+
}),
|
|
295
|
+
totalReplyCount: 1,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(html).toContain("data-thread-context-toggle");
|
|
300
|
+
expect(toggleTag(html)).not.toContain("hidden");
|
|
301
|
+
});
|
|
302
|
+
|
|
264
303
|
it("renders the collapsible shell even when just one extra item precedes the latest reply", () => {
|
|
265
304
|
// root + secondReply + hero — one extra context item. The server can't
|
|
266
305
|
// measure actual height, so it renders the shell assuming overflow (the
|
|
@@ -292,6 +331,9 @@ describe("getThreadPreviewState", () => {
|
|
|
292
331
|
expect(html).toContain("<p>Root</p>");
|
|
293
332
|
expect(html).toContain("<p>Second</p>");
|
|
294
333
|
expect(html).toContain("<p>Latest</p>");
|
|
334
|
+
// 3-post thread (totalReplyCount 2): the shell stacks 2 cards, so the
|
|
335
|
+
// toggle is rendered visible on first paint.
|
|
336
|
+
expect(toggleTag(html)).not.toContain("hidden");
|
|
295
337
|
});
|
|
296
338
|
|
|
297
339
|
it("points the hidden-posts gap link to the second reply so the detail page opens just above the hidden range", () => {
|
|
@@ -352,6 +394,22 @@ describe("getThreadPreviewState", () => {
|
|
|
352
394
|
);
|
|
353
395
|
});
|
|
354
396
|
|
|
397
|
+
it("renders curated thread previews without a collapsible context shell", () => {
|
|
398
|
+
// Curated previews render each segment in flow — no shell, no toggle —
|
|
399
|
+
// so the overflow heuristic never applies to them.
|
|
400
|
+
const post = createPostView({ summary: "Curated note" });
|
|
401
|
+
const html = renderWithI18n(() =>
|
|
402
|
+
CuratedThreadPreview({
|
|
403
|
+
curatedThread: {
|
|
404
|
+
rootPost: post,
|
|
405
|
+
segments: [{ post, hiddenBeforeCount: 0, highlighted: true }],
|
|
406
|
+
},
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(html).not.toContain("data-thread-context-toggle");
|
|
411
|
+
});
|
|
412
|
+
|
|
355
413
|
it("renders article summaries in curated thread previews", () => {
|
|
356
414
|
const articlePost = createPostView({
|
|
357
415
|
title: "Curated article",
|
|
@@ -381,3 +439,70 @@ describe("getThreadPreviewState", () => {
|
|
|
381
439
|
expect(html).not.toContain('id="continue"');
|
|
382
440
|
});
|
|
383
441
|
});
|
|
442
|
+
|
|
443
|
+
describe("threadContextAssumesOverflow", () => {
|
|
444
|
+
it("assumes overflow for 3+ post threads (the shell stacks 2+ cards)", () => {
|
|
445
|
+
expect(
|
|
446
|
+
threadContextAssumesOverflow({
|
|
447
|
+
rootPost: createPostView({ summary: "short" }),
|
|
448
|
+
totalReplyCount: 2,
|
|
449
|
+
}),
|
|
450
|
+
).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("assumes a short lone root fits the cap (2-post thread)", () => {
|
|
454
|
+
expect(
|
|
455
|
+
threadContextAssumesOverflow({
|
|
456
|
+
rootPost: createPostView({
|
|
457
|
+
summary: "Took the long way home because the light was good.",
|
|
458
|
+
}),
|
|
459
|
+
totalReplyCount: 1,
|
|
460
|
+
}),
|
|
461
|
+
).toBe(false);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("assumes overflow for a long lone root", () => {
|
|
465
|
+
expect(
|
|
466
|
+
threadContextAssumesOverflow({
|
|
467
|
+
rootPost: createPostView({ summary: "x".repeat(200) }),
|
|
468
|
+
totalReplyCount: 1,
|
|
469
|
+
}),
|
|
470
|
+
).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("assumes overflow for a short lone root that carries media", () => {
|
|
474
|
+
expect(
|
|
475
|
+
threadContextAssumesOverflow({
|
|
476
|
+
rootPost: createPostView({
|
|
477
|
+
summary: "short",
|
|
478
|
+
media: [{} as PostView["media"][number]],
|
|
479
|
+
}),
|
|
480
|
+
totalReplyCount: 1,
|
|
481
|
+
}),
|
|
482
|
+
).toBe(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("assumes overflow for a short lone root with a link preview image", () => {
|
|
486
|
+
expect(
|
|
487
|
+
threadContextAssumesOverflow({
|
|
488
|
+
rootPost: createPostView({
|
|
489
|
+
format: "link",
|
|
490
|
+
summary: "short",
|
|
491
|
+
previewImageUrl: "https://example.com/cover.jpg",
|
|
492
|
+
}),
|
|
493
|
+
totalReplyCount: 1,
|
|
494
|
+
}),
|
|
495
|
+
).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("counts plain-text code points, not UTF-16 units, against the limit", () => {
|
|
499
|
+
// 130 CJK code points — comfortably over the 120 limit even though each
|
|
500
|
+
// is a single BMP character. Confirms the threshold reads real length.
|
|
501
|
+
expect(
|
|
502
|
+
threadContextAssumesOverflow({
|
|
503
|
+
rootPost: createPostView({ summary: "字".repeat(130) }),
|
|
504
|
+
totalReplyCount: 1,
|
|
505
|
+
}),
|
|
506
|
+
).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
@@ -22,3 +22,46 @@ export function getThreadPreviewState({
|
|
|
22
22
|
hiddenCount,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A lone root post shorter than this (plain-text code points) comfortably
|
|
28
|
+
* fits the thread-context height cap. Pairs with the
|
|
29
|
+
* `--site-thread-context-max-height` token (160px) in tokens.css — revisit
|
|
30
|
+
* both together if that cap changes.
|
|
31
|
+
*/
|
|
32
|
+
const SHORT_ROOT_CHAR_LIMIT = 120;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* First-paint guess for whether the thread-context shell will overflow its
|
|
36
|
+
* height cap, used to pick the initial "Show more" toggle visibility.
|
|
37
|
+
*
|
|
38
|
+
* The client (thread-context.ts) always re-measures and corrects this, so a
|
|
39
|
+
* wrong guess costs at most a one-time flash on load — never a wrong final
|
|
40
|
+
* state. Kept deliberately coarse:
|
|
41
|
+
*
|
|
42
|
+
* - 3+ post threads stack 2+ cards in the shell — effectively always overflow.
|
|
43
|
+
* - A lone root carrying media is tall regardless of its text length.
|
|
44
|
+
* - Otherwise fall back to a plain-text length threshold. Only clearly short
|
|
45
|
+
* roots flip to "fits"; anything at or above the limit keeps the historical
|
|
46
|
+
* "assume overflow" default, so this can only remove flashes, never add them.
|
|
47
|
+
*
|
|
48
|
+
* @param rootPost - the thread's root post (the only post in a 2-post thread's
|
|
49
|
+
* shell)
|
|
50
|
+
* @param totalReplyCount - replies in the thread; `>= 2` means 3+ posts total
|
|
51
|
+
* @returns whether to render the toggle visible on first paint
|
|
52
|
+
*/
|
|
53
|
+
export function threadContextAssumesOverflow({
|
|
54
|
+
rootPost,
|
|
55
|
+
totalReplyCount,
|
|
56
|
+
}: {
|
|
57
|
+
rootPost: PostView;
|
|
58
|
+
totalReplyCount: number;
|
|
59
|
+
}): boolean {
|
|
60
|
+
if (totalReplyCount >= 2) return true;
|
|
61
|
+
if (rootPost.media.length > 0 || Boolean(rootPost.previewImageUrl)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// `summary` is format-normalized at the viewmodel layer: quote text for
|
|
65
|
+
// quotes, body text for notes, the URL for bare links.
|
|
66
|
+
return [...(rootPost.summary ?? "")].length >= SHORT_ROOT_CHAR_LIMIT;
|
|
67
|
+
}
|