Skip to content

Commit

Permalink
feat: add confirmation for opening external apps (RSSNext#1618)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
ericyzhu and Innei authored Nov 16, 2024
1 parent fdb87cf commit 08a1e76
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 3 deletions.
30 changes: 30 additions & 0 deletions apps/main/src/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]) {
Expand Down
46 changes: 45 additions & 1 deletion apps/renderer/src/components/ui/modal/stacked/hooks.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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) => {
Expand Down Expand Up @@ -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,
})
})
}),
}
}
11 changes: 11 additions & 0 deletions apps/renderer/src/components/ui/modal/stacked/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}
9 changes: 8 additions & 1 deletion apps/renderer/src/providers/extension-expose-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -71,5 +71,12 @@ export const ExtensionExposeProvider = () => {
zenMode: toggleZenMode,
})
}, [toggleZenMode])

const dialog = useDialog()
useEffect(() => {
registerGlobalContext({
dialog,
})
}, [dialog])
return null
}
3 changes: 3 additions & 0 deletions locales/native/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion packages/shared/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,6 +45,7 @@ interface RenderGlobalContext {

/// Utils
toast: typeof toast
dialog: typeof dialog
// URL
getWebUrl: () => string
getApiUrl: () => string
Expand Down Expand Up @@ -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]> &
Expand Down

0 comments on commit 08a1e76

Please sign in to comment.