mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-02 22:59:56 +08:00
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:
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user