* fix(ui): open app deep links from chat after confirmation DOMPurify's default URI policy stripped href from anchors with custom application schemes (obsidian://, vscode://, ...), so every app link rendered in chat was dead across web, desktop, VS Code, and mobile. - Classify safe app-link schemes in lib/url.ts (browser-handled, scriptable, webview-internal, network, and self-deep-link schemes stay excluded) and let openExternalUrl accept them - Keep app-link hrefs through the markdown sanitize hook - Intercept app-link clicks in the markdown renderer and route them through a confirmation dialog (Trust and open / Open once, dismiss to cancel) mounted in the desktop/web app root and the mobile shell - Persist per-device trusted schemes in a zustand store; trusted schemes open without asking again * feat(settings): manage trusted app link schemes in General Add an App links section to Settings > General listing the application schemes trusted on this device with a delete action; removing a scheme restores the confirmation dialog for it. Register the section in settings search. * fix(ui): enforce app link confirmation * fix(ui): handle app links by runtime * fix(vscode): keep app links unsupported * fix(settings): clarify trusted app links --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
72 lines
2.0 KiB
TypeScript
72 lines
2.0 KiB
TypeScript
import { useAppLinkTrustStore } from '@/stores/appLinkTrustStore';
|
|
import { getUrlScheme, openConfirmedAppLinkUrl } from '@/lib/url';
|
|
|
|
export type AppLinkConfirmationChoice = 'open' | 'trust' | 'cancel';
|
|
|
|
type PendingAppLinkRequest = {
|
|
url: string;
|
|
resolve: (choice: AppLinkConfirmationChoice) => void;
|
|
};
|
|
|
|
let pendingRequest: PendingAppLinkRequest | null = null;
|
|
const listeners = new Set<() => void>();
|
|
|
|
const emitChange = (): void => {
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
};
|
|
|
|
const getSnapshot = (): PendingAppLinkRequest | null => pendingRequest;
|
|
|
|
const subscribe = (listener: () => void): (() => void) => {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Ask the user (via the app-level confirmation dialog) whether an application
|
|
* deep link may be opened. Resolves immediately when the scheme was trusted
|
|
* earlier. Only one request is active at a time; a new request cancels the
|
|
* pending one.
|
|
*/
|
|
export const openAppLinkWithConfirmation = (url: string): Promise<void> => {
|
|
const scheme = getUrlScheme(url);
|
|
if (!scheme) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const trustStore = useAppLinkTrustStore.getState();
|
|
if (trustStore.isSchemeTrusted(scheme)) {
|
|
return openConfirmedAppLinkUrl(url).then(() => undefined);
|
|
}
|
|
|
|
if (pendingRequest) {
|
|
pendingRequest.resolve('cancel');
|
|
}
|
|
|
|
return new Promise<AppLinkConfirmationChoice>((resolve) => {
|
|
pendingRequest = { url, resolve };
|
|
emitChange();
|
|
}).then((choice) => {
|
|
if (choice === 'trust') {
|
|
useAppLinkTrustStore.getState().trustScheme(scheme);
|
|
}
|
|
if (choice === 'open' || choice === 'trust') {
|
|
return openConfirmedAppLinkUrl(url).then(() => undefined);
|
|
}
|
|
});
|
|
};
|
|
|
|
export const settleAppLinkConfirmation = (choice: AppLinkConfirmationChoice): void => {
|
|
const request = pendingRequest;
|
|
pendingRequest = null;
|
|
emitChange();
|
|
request?.resolve(choice);
|
|
};
|
|
|
|
export const subscribeAppLinkConfirmation = subscribe;
|
|
export const getAppLinkConfirmationSnapshot = getSnapshot;
|