|
|
@@ -1251,21 +1251,54 @@ export class FilePreviewComponent {
|
|
|
/**
|
|
|
* 在新标签页打开。
|
|
|
*
|
|
|
- * 修过的坑(2026-11-01):
|
|
|
- * ① `window.open(u,'_blank','noopener,noreferrer')` 带 noopener 时返回 null,
|
|
|
- * 被误判成"弹窗被拦"→ 退化为下载。
|
|
|
- * ② 附件地址是 Matrix 的 `/_matrix/media/v3/download/...`,服务端**强制**
|
|
|
- * `Content-Disposition: attachment`(curl 实测),所以无论怎么打开都是下载。
|
|
|
- * 页面内直接 fetch 该地址会被 CORS 拦(不带 token 时)。
|
|
|
+ * ## 两种地址,走两条路(2026-11-13 修正)
|
|
|
*
|
|
|
- * 现在:先同步开窗(保住用户手势),再用**带 access_token** 的地址 fetch 成 blob
|
|
|
- * (该接口返回 `access-control-allow-origin: *`,实测可跨域读取),
|
|
|
- * 把 blob 地址塞进新标签页 —— blob 由浏览器内联渲染,绕过 attachment 头。
|
|
|
- * fetch 失败再退回带 token 的原地址(至少能下载到正确文件)。
|
|
|
+ * ① **公网直链**(`s3.fmode.cn` / `file-cloud.fmode.cn` 等):
|
|
|
+ * 直接 `window.open(url)` 即可。原实现也走 blob 中转,
|
|
|
+ * 结果手机上长时间停在 `about:blank`(fetch 整个文件太慢),
|
|
|
+ * 用户看到就是"空页面"(真机实测反馈)。
|
|
|
+ *
|
|
|
+ * ② **Matrix 附件**(`/_matrix/media/...`):服务端强制
|
|
|
+ * `Content-Disposition: attachment`,直接开只会下载、不会内联显示。
|
|
|
+ * 才需要「先同步开窗保住用户手势 → fetch 成 blob → 塞进新标签页」。
|
|
|
+ *
|
|
|
+ * 判断依据:是否与 Matrix homeserver 同源。
|
|
|
*/
|
|
|
async openInTab(): Promise<void> {
|
|
|
const base = this.safeUrl();
|
|
|
if (!base) return;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * ① 公网直链:直接打开,不做任何中转。
|
|
|
+ *
|
|
|
+ * 🔴 不要用 `window.open(u, '_blank', 'noopener,noreferrer')`(2026-11-13 真机实测)。
|
|
|
+ *
|
|
|
+ * 带 `noopener` 特性字符串时,**iOS Safari 会返回 null**
|
|
|
+ * (规范允许实现这么干),原代码据此判断"弹窗被拦"→ 走 `download()`,
|
|
|
+ * 用户看到的就是「点了新标签没反应,文件也没下下来」。
|
|
|
+ *
|
|
|
+ * 正确做法:先**不带特性**同步开窗(保住用户手势、拿到 window 引用),
|
|
|
+ * 再手动把 `opener` 置空 —— 效果等同 noopener,且不会被误判。
|
|
|
+ */
|
|
|
+ if (!isMatrixMediaUrl(base)) {
|
|
|
+ const direct = window.open(base, '_blank');
|
|
|
+ if (direct) {
|
|
|
+ try {
|
|
|
+ direct.opener = null;
|
|
|
+ } catch {
|
|
|
+ /* 跨域时不可写,仍有 href 层面的保护 */
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ /*
|
|
|
+ * 真的被拦了(比如用户开了弹窗阻止):退回本地下载,
|
|
|
+ * 不要留一个 about:blank 空页。
|
|
|
+ */
|
|
|
+ void this.download();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ② Matrix 附件:必须走 blob 中转(见下方原有注释) */
|
|
|
const authed = this.mx.withAccessToken(base);
|
|
|
const w = window.open('about:blank', '_blank');
|
|
|
try {
|
|
|
@@ -1357,6 +1390,19 @@ export class FilePreviewComponent {
|
|
|
private async prepareBlob(url: string): Promise<void> {
|
|
|
this.releaseBlob();
|
|
|
if (!needsBlobPreview(this.name() || url)) return;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 🔴 公网直链直接给 iframe,不做 blob 中转(2026-11-13 真机实测修正)。
|
|
|
+ *
|
|
|
+ * 原实现对**所有** html 都先 fetch 成 blob。问题是:
|
|
|
+ * · 移动端要先把整个文件下完才渲染 → 白屏很久(用户以为打不开);
|
|
|
+ * · Safari 对 `blob:` + iframe sandbox 的处理与 Chrome 不同,
|
|
|
+ * 真机上会出现「预览空白」而电脑上正常;
|
|
|
+ * · 大文件还会二次占用内存(下载一次 + blob 再读一次)。
|
|
|
+ *
|
|
|
+ * 只有需要带 token 才能读的地址(Matrix 媒体接口)才必须走 blob。
|
|
|
+ */
|
|
|
+ if (!needsAuthToRead(url)) return;
|
|
|
try {
|
|
|
const authed = this.mx.withAccessToken(url);
|
|
|
const r = await toBlobUrl(authed, 'text/html');
|
|
|
@@ -1494,3 +1540,30 @@ function findScroller(root: HTMLElement): HTMLElement { const candidates: HTMLE
|
|
|
// 都不可滚(或没有内部滚动容器)→ 用外层
|
|
|
return root;
|
|
|
}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 该地址是不是 Matrix 媒体接口(需要 token + blob 中转才能内联打开)。
|
|
|
+ *
|
|
|
+ * Matrix 的 `/_matrix/media/...` 服务端强制 `Content-Disposition: attachment`,
|
|
|
+ * 直接开只会下载;而 s3/CDN 的公网直链可以直接开。
|
|
|
+ *
|
|
|
+ * 判定:同源且路径含 `/_matrix/media`。
|
|
|
+ */
|
|
|
+function isMatrixMediaUrl(url: string): boolean {
|
|
|
+ try {
|
|
|
+ const u = new URL(url, location.origin);
|
|
|
+ return u.origin === location.origin && u.pathname.includes('/_matrix/media');
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 该地址是否**必须带 token** 才能读取(只有这类才需要 blob 中转)。
|
|
|
+ *
|
|
|
+ * 判断依据:Matrix 媒体接口(同源 `/_matrix/media`)需要 access_token,
|
|
|
+ * 而 s3/CDN 的公网直链是公开可读的 —— 直接给 iframe 就能渲染。
|
|
|
+ */
|
|
|
+function needsAuthToRead(url: string): boolean {
|
|
|
+ return isMatrixMediaUrl(url);
|
|
|
+}
|