Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add deeplink support #2883

Merged
merged 2 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add deeplink support
  • Loading branch information
James committed May 9, 2024
commit 0888839882e0836eb654a0d0ce4d0a6a230e20b1
3 changes: 3 additions & 0 deletions core/src/types/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 19,7 @@ export enum NativeRoute {
showMainWindow = 'showMainWindow',

quickAskSizeUpdated = 'quickAskSizeUpdated',
ackDeepLink = 'ackDeepLink',
}

/**
Expand All @@ -45,6 46,8 @@ export enum AppEvent {

onUserSubmitQuickAsk = 'onUserSubmitQuickAsk',
onSelectedText = 'onSelectedText',

onDeepLink = 'onDeepLink',
}

export enum DownloadRoute {
Expand Down
4 changes: 4 additions & 0 deletions electron/handlers/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 151,8 @@ export function handleAppIPCs() {
async (_event, heightOffset: number): Promise<void> =>
windowManager.expandQuickAskWindow(heightOffset)
)

ipcMain.handle(NativeRoute.ackDeepLink, async (_event): Promise<void> => {
windowManager.ackDeepLink()
})
}
41 changes: 33 additions & 8 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 1,6 @@
import { app, BrowserWindow } from 'electron'

import { join } from 'path'
import { join, resolve } from 'path'
/**
* Managers
**/
Expand Down Expand Up @@ -39,15 39,40 @@ const quickAskUrl = `${mainUrl}/search`

const gotTheLock = app.requestSingleInstanceLock()

if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('jan', process.execPath, [
resolve(process.argv[1]),
])
}
} else {
app.setAsDefaultProtocolClient('jan')
}

const createMainWindow = () => {
const startUrl = app.isPackaged ? `file://${mainPath}` : mainUrl
windowManager.createMainWindow(preloadPath, startUrl)
}

app
.whenReady()
.then(() => {
if (!gotTheLock) {
app.quit()
throw new Error('Another instance of the app is already running')
} else {
if (process.platform === 'win32' || process.platform === 'linux') {
// this is for handling deeplink on windows and linux
// since those OS will emit second-instance instead of open-url
app.on('second-instance', (_event, commandLine, _workingDirectory) => {
const url = commandLine.pop()
if (url) {
windowManager.sendMainAppDeepLink(url)
}
})
}
}
})
.then(setupReactDevTool)
.then(setupCore)
.then(createUserSpace)
.then(migrateExtensions)
Expand All @@ -60,6 85,7 @@ app
.then(registerGlobalShortcuts)
.then(() => {
if (!app.isPackaged) {
setupReactDevTool()
windowManager.mainWindow?.webContents.openDevTools()
}
})
Expand All @@ -75,11 101,15 @@ app
})
})

app.on('open-url', (_event, url) => {
windowManager.sendMainAppDeepLink(url)
})

app.on('second-instance', (_event, _commandLine, _workingDirectory) => {
windowManager.showMainWindow()
})

app.on('before-quit', function (evt) {
app.on('before-quit', function (_event) {
trayManager.destroyCurrentTray()
})

Expand All @@ -104,11 134,6 @@ function createQuickAskWindow() {
windowManager.createQuickAskWindow(preloadPath, startUrl)
}

function createMainWindow() {
const startUrl = app.isPackaged ? `file://${mainPath}` : mainUrl
windowManager.createMainWindow(preloadPath, startUrl)
}

/**
* Handles various IPC messages from the renderer process.
*/
Expand Down
24 changes: 24 additions & 0 deletions electron/managers/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 14,7 @@ class WindowManager {
private _quickAskWindowVisible = false
private _mainWindowVisible = false

private deeplink: string | undefined
/**
* Creates a new window instance.
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
Expand Down Expand Up @@ -123,6 124,22 @@ class WindowManager {
)
}

/**
* Try to send the deep link to the main app.
*/
sendMainAppDeepLink(url: string): void {
this.deeplink = url
const interval = setInterval(() => {
if (!this.deeplink) clearInterval(interval)
const mainWindow = this.mainWindow
if (mainWindow) {
mainWindow.webContents.send(AppEvent.onDeepLink, this.deeplink)
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
}, 500)
}

cleanUp(): void {
if (!this.mainWindow?.isDestroyed()) {
this.mainWindow?.close()
Expand All @@ -137,6 154,13 @@ class WindowManager {
this._quickAskWindowVisible = false
}
}

/**
* Acknowledges that the window has received a deep link. We can remove it.
*/
ackDeepLink() {
this.deeplink = undefined
}
}

export const windowManager = new WindowManager()
8 changes: 8 additions & 0 deletions electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 61,14 @@
"include": "scripts/uninstaller.nsh",
"deleteAppDataOnUninstall": true
},
"protocols": [
{
"name": "Jan",
"schemes": [
"jan"
]
}
],
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
},
"scripts": {
Expand Down
24 changes: 10 additions & 14 deletions electron/utils/dev.ts
Original file line number Diff line number Diff line change
@@ -1,17 1,13 @@
import { app } from 'electron'

export const setupReactDevTool = async () => {
if (!app.isPackaged) {
// Which means you're running from source code
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
'electron-devtools-installer'
) // Don't use import on top level, since the installer package is dev-only
try {
const name = await installExtension(REACT_DEVELOPER_TOOLS)
console.debug(`Added Extension: ${name}`)
} catch (err) {
console.error('An error occurred while installing devtools:', err)
// Only log the error and don't throw it because it's not critical
}
// Which means you're running from source code
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
'electron-devtools-installer'
) // Don't use import on top level, since the installer package is dev-only
try {
const name = await installExtension(REACT_DEVELOPER_TOOLS)
console.debug(`Added Extension: ${name}`)
} catch (err) {
console.error('An error occurred while installing devtools:', err)
// Only log the error and don't throw it because it's not critical
}
}
3 changes: 3 additions & 0 deletions web/containers/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 25,8 @@ import ImportModelOptionModal from '@/screens/Settings/ImportModelOptionModal'
import ImportingModelModal from '@/screens/Settings/ImportingModelModal'
import SelectingModelModal from '@/screens/Settings/SelectingModelModal'

import LoadingModal from '../LoadingModal'

import MainViewContainer from '../MainViewContainer'

import InstallingExtensionModal from './BottomBar/InstallingExtension/InstallingExtensionModal'
Expand Down Expand Up @@ -69,6 71,7 @@ const BaseLayout = () => {
<BottomBar />
</div>
</div>
<LoadingModal />
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
Expand Down
28 changes: 28 additions & 0 deletions web/containers/LoadingModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 1,28 @@
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai'

export type LoadingInfo = {
title: string
message: string
}

export const loadingModalVisibilityAtom = atom<LoadingInfo | undefined>(
undefined
)

const ResettingModal: React.FC = () => {
const loadingInfo = useAtomValue(loadingModalVisibilityAtom)

return (
<Modal open={loadingInfo != null}>
<ModalContent>
<ModalHeader>
<ModalTitle>{loadingInfo?.title}</ModalTitle>
</ModalHeader>
<p className="text-muted-foreground">{loadingInfo?.message}</p>
</ModalContent>
</Modal>
)
}

export default ResettingModal
72 changes: 72 additions & 0 deletions web/containers/Providers/DeepLinkListener.tsx
Original file line number Diff line number Diff line change
@@ -0,0 1,72 @@
import { Fragment, ReactNode } from 'react'

import { useSetAtom } from 'jotai'

import { useDebouncedCallback } from 'use-debounce'

import { useGetHFRepoData } from '@/hooks/useGetHFRepoData'

import { loadingModalVisibilityAtom as loadingModalInfoAtom } from '../LoadingModal'
import { toaster } from '../Toast'

import {
importHuggingFaceModelStageAtom,
importingHuggingFaceRepoDataAtom,
} from '@/helpers/atoms/HuggingFace.atom'
type Props = {
children: ReactNode
}

const DeepLinkListener: React.FC<Props> = ({ children }) => {
const { getHfRepoData } = useGetHFRepoData()
const setLoadingInfo = useSetAtom(loadingModalInfoAtom)
const setImportingHuggingFaceRepoData = useSetAtom(
importingHuggingFaceRepoDataAtom
)
const setImportHuggingFaceModelStage = useSetAtom(
importHuggingFaceModelStageAtom
)

const debounced = useDebouncedCallback(async (searchText) => {
if (searchText.indexOf('/') === -1) {
toaster({
title: 'Failed to get Hugging Face models',
description: 'Invalid Hugging Face model URL',
type: 'error',
})
return
}

try {
setLoadingInfo({
title: 'Getting Hugging Face models',
message: 'Please wait..',
})
const data = await getHfRepoData(searchText)
setImportingHuggingFaceRepoData(data)
setImportHuggingFaceModelStage('REPO_DETAIL')
setLoadingInfo(undefined)
} catch (err) {
setLoadingInfo(undefined)
let errMessage = 'Unexpected Error'
if (err instanceof Error) {
errMessage = err.message
}
toaster({
title: 'Failed to get Hugging Face models',
description: errMessage,
type: 'error',
})
console.error(err)
}
}, 300)
window.electronAPI?.onDeepLink((_event: string, input: string) => {
window.core?.api?.ackDeepLink()
const url = input.replaceAll('jan://', '')
debounced(url)
})

return <Fragment>{children}</Fragment>
}

export default DeepLinkListener
5 changes: 4 additions & 1 deletion web/containers/Providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 22,7 @@ import Loader from '../Loader'

import DataLoader from './DataLoader'

import DeepLinkListener from './DeepLinkListener'
import KeyListener from './KeyListener'

import { extensionManager } from '@/extension'
Expand Down Expand Up @@ -78,7 79,9 @@ const Providers = ({ children }: PropsWithChildren) => {
<KeyListener>
<EventListenerWrapper>
<TooltipProvider delayDuration={0}>
<DataLoader>{children}</DataLoader>
<DataLoader>
<DeepLinkListener>{children}</DeepLinkListener>
</DataLoader>
</TooltipProvider>
</EventListenerWrapper>
<Toaster />
Expand Down
Loading