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:
parent
fdb87cf5cd
commit
08a1e76247
|
|
@ -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"]) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]> &
|
||||
|
|
|
|||
Loading…
Reference in New Issue