feat(ios): improve share preview (#15538)

#### PR Dependency Tree

* **PR #15538** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added rich link previews to mobile and iOS sharing, including images,
metadata, transcripts, and selected text.
* Share imports can now create structured content blocks, embeds,
bookmarks, and transcript callouts.
* Added workspace-aware preview handling for cloud, self-hosted, and
signed-out modes.
* **Accessibility**
* Improved collapse/expand controls with semantic buttons and ARIA
relationships.
* **Bug Fixes**
  * Enhanced URL and error sanitization in server logs.
* Improved link-preview CORS support, validation, and request handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-28 03:32:26 +08:00
committed by GitHub
parent 329839e467
commit b6de0ad51b
36 changed files with 3152 additions and 372 deletions
@@ -1,12 +1,10 @@
import { app, clipboard, nativeImage, nativeTheme } from 'electron';
import { getLinkPreview } from 'link-preview-js';
import { map, shareReplay } from 'rxjs';
import { isMacOS } from '../../shared/utils';
import { persistentConfig } from '../config-storage/persist';
import { logger } from '../logger';
import { openExternalSafely } from '../security/open-external';
import { resolveAndValidateUrlForPreview } from '../security/url-safety';
import type { WorkbenchViewMeta } from '../shared-state-schema';
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
import { globalStateStorage } from '../shared-storage/storage';
@@ -39,13 +37,6 @@ import { getOrCreateCustomThemeWindow } from '../windows-manager/custom-theme-wi
import { getChallengeResponse } from './challenge';
import { uiSubjects } from './subject';
const EMPTY_OBJECT = Object.freeze({
title: undefined,
description: undefined,
icon: undefined,
image: undefined,
});
const TraySettingsState = {
$: globalStateStorage.watch<MenubarStateSchema>(MenubarStateKey).pipe(
map(v => MenubarStateSchema.parse(v ?? {})),
@@ -134,83 +125,6 @@ export const uiHandlers = {
logger.error('handleOpenMainApp', err);
}
},
getBookmarkDataByLink: async (_, link: string) => {
try {
// Basic validation up-front to prevent SSRF (including redirects).
await resolveAndValidateUrlForPreview(link);
} catch {
return EMPTY_OBJECT;
}
if (
(link.startsWith('https://x.com/') ||
link.startsWith('https://www.x.com/') ||
link.startsWith('https://www.twitter.com/') ||
link.startsWith('https://twitter.com/')) &&
link.includes('/status/')
) {
// use api.fxtwitter.com
const statusId = /\/status\/(\d+)/.exec(link)?.[1];
if (!statusId) return EMPTY_OBJECT;
link = `https://api.fxtwitter.com/status/${statusId}`;
try {
const { tweet } = (await fetch(link).then(res => res.json())) as any;
return {
title: tweet.author.name,
icon: tweet.author.avatar_url,
description: tweet.text,
image: tweet.media?.photos[0].url || tweet.author.banner_url,
};
} catch (err) {
logger.error('getBookmarkDataByLink', err);
return {
title: undefined,
description: undefined,
icon: undefined,
image: undefined,
};
}
} else {
const previewData = (await getLinkPreview(link, {
timeout: 6000,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0 Safari/537.36 Edg/120.0.0',
},
followRedirects: 'manual',
handleRedirects: (_baseUrl: string, forwardedUrl: string) => {
try {
// Only allow http(s) redirects and re-validate before following.
const u = new URL(forwardedUrl);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
},
resolveDNSHost: async (url: string) => {
const { address } = await resolveAndValidateUrlForPreview(url);
return address;
},
}).catch(() => {
return {
title: '',
siteName: '',
description: '',
images: [],
videos: [],
contentType: `text/html`,
favicons: [],
};
})) as any;
return {
title: previewData.title,
description: previewData.description,
icon: previewData.favicons[0],
image: previewData.images[0],
};
}
},
openExternal(_, url: string) {
return openExternalSafely(url);
},