feat: add confirmation for opening external apps (#1618)

* feat: add confirmation for opening external apps

* update

* update

* update

* feat: add expose dialog

Signed-off-by: Innei <tukon479@gmail.com>

* fix: handle all external url

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Innei <tukon479@gmail.com>
This commit is contained in:
Eric Zhu 2024-11-16 18:39:43 +08:00 committed by GitHub
parent fdb87cf5cd
commit 08a1e76247
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 110 additions and 3 deletions

View File

@ -5,10 +5,12 @@ import { is } from "@electron-toolkit/utils"
import { callWindowExpose } from "@follow/shared/bridge"
import type { BrowserWindowConstructorOptions } from "electron"
import { app, BrowserWindow, screen, shell } from "electron"
import type { Event } from "electron/main"
import { START_IN_TRAY_ARGS } from "./constants/app"
import { isDev, isMacOS, isWindows, isWindows11 } from "./env"
import { getIconPath } from "./helper"
import { t } from "./lib/i18n"
import { store } from "./lib/store"
import { getTrayConfig } from "./lib/tray"
import { logger } from "./logger"
@ -109,6 +111,34 @@ export function createWindow(
return { action: "deny" }
})
const handleExternalProtocol = async (e: Event, url: string, window: BrowserWindow) => {
const { protocol } = new URL(url)
if (protocol === "http:" || protocol === "https:" || protocol === "follow:") {
return
}
e.preventDefault()
const caller = callWindowExpose(window)
const confirm = await caller.dialog.ask({
title: "Open External App?",
message: t("dialog.openExternalApp", { url, interpolation: { escapeValue: false } }),
confirmText: t("dialog.open"),
cancelText: t("dialog.cancel"),
})
if (!confirm) {
return
}
shell.openExternal(url)
}
// Handle main window external links
window.webContents.on("will-navigate", (e, url) => handleExternalProtocol(e, url, window))
// Handle webview external links
window.webContents.on("did-attach-webview", (_, webContents) => {
webContents.on("will-navigate", (e, url) => handleExternalProtocol(e, url, window))
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {

View File

@ -1,7 +1,9 @@
import { Button } from "@follow/components/ui/button/index.js"
import type { DragControls } from "framer-motion"
import type { ResizeCallback, ResizeStartCallback } from "re-resizable"
import { useCallback, useContext, useId, useRef, useState } from "react"
import { flushSync } from "react-dom"
import { useTranslation } from "react-i18next"
import { useContextSelector } from "use-context-selector"
import { useEventCallback } from "usehooks-ts"
@ -11,7 +13,7 @@ import { jotaiStore } from "~/lib/jotai"
import { modalStackAtom } from "./atom"
import { ModalEventBus } from "./bus"
import { CurrentModalContext, CurrentModalStateContext } from "./context"
import type { ModalProps, ModalStackOptions } from "./types"
import type { DialogInstance, ModalProps, ModalStackOptions } from "./types"
export const modalIdToPropsMap = {} as Record<string, ModalProps>
export const useModalStack = (options?: ModalStackOptions) => {
@ -158,3 +160,45 @@ export const useResizeableModal = (
}
export const useIsTopModal = () => useContextSelector(CurrentModalStateContext, (v) => v.isTop)
export const useDialog = (): DialogInstance => {
const { present } = useModalStack()
const { t } = useTranslation()
return {
ask: useEventCallback((options) => {
return new Promise<boolean>((resolve) => {
present({
title: options.title,
content: ({ dismiss }) => (
<div className="flex max-w-[75ch] flex-col gap-3">
{options.message}
<div className="flex items-center justify-end gap-3">
<Button
variant="outline"
onClick={() => {
options.onCancel?.()
resolve(false)
dismiss()
}}
>
{options.cancelText ?? t("cancel", { ns: "common" })}
</Button>
<Button
onClick={() => {
options.onConfirm?.()
resolve(true)
}}
>
{options.confirmText ?? t("confirm", { ns: "common" })}
</Button>
</div>
</div>
),
canClose: true,
clickOutsideToDismiss: false,
})
})
}),
}
}

View File

@ -35,3 +35,14 @@ export interface ModalProps {
export interface ModalStackOptions {
wrapper?: FC
}
export interface DialogInstance {
ask: (options: {
title: string
message: string
onConfirm?: () => void
onCancel?: () => void
confirmText?: string
cancelText?: string
}) => Promise<boolean>
}

View File

@ -6,7 +6,7 @@ import { toast } from "sonner"
import { getGeneralSettings } from "~/atoms/settings/general"
import { getUISettings, useToggleZenMode } from "~/atoms/settings/ui"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useDiscoverRSSHubRouteModal } from "~/hooks/biz/useDiscoverRSSHubRoute"
import { useFollow } from "~/hooks/biz/useFollow"
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
@ -71,5 +71,12 @@ export const ExtensionExposeProvider = () => {
zenMode: toggleZenMode,
})
}, [toggleZenMode])
const dialog = useDialog()
useEffect(() => {
registerGlobalContext({
dialog,
})
}, [dialog])
return null
}

View File

@ -19,8 +19,11 @@
"contextMenu.searchWithGoogle": "Search with Google",
"contextMenu.selectAll": "Select All",
"contextMenu.services": "Services",
"dialog.cancel": "Cancel",
"dialog.clearAllData": "Are you sure you want to clear all data?",
"dialog.no": "No",
"dialog.open": "Open",
"dialog.openExternalApp": "Are you sure you want to open \"{{url}}\" with other apps?",
"dialog.yes": "Yes",
"menu.about": "About {{name}}",
"menu.actualSize": "Actual Size",

View File

@ -6,6 +6,17 @@ import type { GeneralSettings, UISettings } from "./interface/settings"
const PREFIX = "__follow"
// eslint-disable-next-line unused-imports/no-unused-vars
declare const dialog: {
ask: (options: {
title: string
message: string
onConfirm?: () => void
onCancel?: () => void
confirmText?: string
cancelText?: string
}) => Promise<boolean>
}
interface RenderGlobalContext {
/// Access Settings
showSetting: (path?: string) => void
@ -34,6 +45,7 @@ interface RenderGlobalContext {
/// Utils
toast: typeof toast
dialog: typeof dialog
// URL
getWebUrl: () => string
getApiUrl: () => string
@ -87,7 +99,7 @@ type AddPromise<T> = T extends (...args: infer A) => Promise<infer R>
? (...args: A) => Promise<R>
: T extends (...args: infer A) => infer R
? (...args: A) => Promise<Awaited<R>>
: any
: unknown
type Fn<T> = {
[K in keyof T]: AddPromise<T[K]> &