Browse Source

fix electron refresh

fix about and readme pages
imwald
Silberengel 1 month ago
parent
commit
40862eeb2c
  1. BIN
      build/icon.png
  2. 29
      electron/main.cjs
  3. 5
      electron/preload.cjs
  4. 17
      package-lock.json
  5. 19
      package.json
  6. 72
      src/components/AboutInfoDialog/index.tsx
  7. 13
      src/components/BottomNavigationBar/NotificationsButton.tsx
  8. 39
      src/components/KeyboardShortcutsHelp/index.tsx
  9. 14
      src/components/Sidebar/NotificationButton.tsx
  10. 6
      src/components/Username/index.tsx
  11. 5
      src/services/session-feed-snapshot.service.ts
  12. 6
      src/vite-env.d.ts

BIN
build/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

29
electron/main.cjs

@ -1,11 +1,23 @@
'use strict' 'use strict'
const { app, BrowserWindow, shell } = require('electron') const { app, BrowserWindow, ipcMain, shell } = require('electron')
const path = require('path') const path = require('path')
/** True when running from source (`electron .`); false when packaged. */ /** True when running from source (`electron .`); false when packaged. */
const isDev = !app.isPackaged const isDev = !app.isPackaged
function loadRenderer(win) {
if (isDev) {
const devUrl = process.env.VITE_DEV_SERVER_URL || 'http://127.0.0.1:5173'
void win.loadURL(devUrl)
if (!win.webContents.isDevToolsOpened()) {
win.webContents.openDevTools({ mode: 'detach' })
}
return
}
void win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
}
function createWindow() { function createWindow() {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 1280, width: 1280,
@ -23,13 +35,7 @@ function createWindow() {
win.once('ready-to-show', () => win.show()) win.once('ready-to-show', () => win.show())
if (isDev) { loadRenderer(win)
const devUrl = process.env.VITE_DEV_SERVER_URL || 'http://127.0.0.1:5173'
win.loadURL(devUrl)
win.webContents.openDevTools({ mode: 'detach' })
} else {
win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
}
win.webContents.setWindowOpenHandler(({ url }) => { win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('http:') || url.startsWith('https:')) { if (url.startsWith('http:') || url.startsWith('https:')) {
@ -40,6 +46,13 @@ function createWindow() {
} }
app.whenReady().then(() => { app.whenReady().then(() => {
ipcMain.handle('jumble:reload-app', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win || win.isDestroyed()) return false
loadRenderer(win)
return true
})
createWindow() createWindow()
app.on('activate', () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow() if (BrowserWindow.getAllWindows().length === 0) createWindow()

5
electron/preload.cjs

@ -1,7 +1,8 @@
'use strict' 'use strict'
const { contextBridge } = require('electron') const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('jumbleElectron', { contextBridge.exposeInMainWorld('jumbleElectron', {
isElectron: true isElectron: true,
reloadApp: () => ipcRenderer.invoke('jumble:reload-app')
}) })

17
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.3.1", "version": "21.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.3.1", "version": "21.3.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",
@ -65,6 +65,7 @@
"katex": "^0.16.25", "katex": "^0.16.25",
"lru-cache": "^11.0.2", "lru-cache": "^11.0.2",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"marked": "^17.0.5",
"nostr-tools": "^2.17.0", "nostr-tools": "^2.17.0",
"nstart-modal": "^2.0.0", "nstart-modal": "^2.0.0",
"path-to-regexp": "^8.3.0", "path-to-regexp": "^8.3.0",
@ -11183,6 +11184,18 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/marked": {
"version": "17.0.5",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz",
"integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/matcher": { "node_modules/matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",

19
package.json

@ -1,6 +1,6 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.3.1", "version": "21.3.2",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true, "private": true,
"type": "module", "type": "module",
@ -38,7 +38,6 @@
"@getalby/bitcoin-connect-react": "^3.10.0", "@getalby/bitcoin-connect-react": "^3.10.0",
"@getalby/lightning-tools": "^6.1.0", "@getalby/lightning-tools": "^6.1.0",
"@noble/hashes": "^1.6.1", "@noble/hashes": "^1.6.1",
"@scure/base": "^2.0.0",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
@ -56,6 +55,7 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@scure/base": "^2.0.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tiptap/core": "^2.12.0", "@tiptap/core": "^2.12.0",
"@tiptap/extension-document": "^2.12.0", "@tiptap/extension-document": "^2.12.0",
@ -69,7 +69,6 @@
"@tiptap/pm": "^2.12.0", "@tiptap/pm": "^2.12.0",
"@tiptap/react": "^2.12.0", "@tiptap/react": "^2.12.0",
"@tiptap/suggestion": "^2.12.0", "@tiptap/suggestion": "^2.12.0",
"prosemirror-state": "^1.4.3",
"@webbtc/webln-types": "^3.0.0", "@webbtc/webln-types": "^3.0.0",
"blossom-client-sdk": "^4.1.0", "blossom-client-sdk": "^4.1.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
@ -87,9 +86,11 @@
"katex": "^0.16.25", "katex": "^0.16.25",
"lru-cache": "^11.0.2", "lru-cache": "^11.0.2",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"marked": "^17.0.5",
"nostr-tools": "^2.17.0", "nostr-tools": "^2.17.0",
"nstart-modal": "^2.0.0", "nstart-modal": "^2.0.0",
"path-to-regexp": "^8.3.0", "path-to-regexp": "^8.3.0",
"prosemirror-state": "^1.4.3",
"qr-code-styling": "^1.9.2", "qr-code-styling": "^1.9.2",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"react": "^18.3.1", "react": "^18.3.1",
@ -113,6 +114,8 @@
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.16",
@ -126,8 +129,6 @@
"vite": "^6.0.3", "vite": "^6.0.3",
"vite-plugin-pwa": "^0.21.1", "vite-plugin-pwa": "^0.21.1",
"vitest": "^4.0.8", "vitest": "^4.0.8",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"wait-on": "^8.0.1" "wait-on": "^8.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -147,9 +148,13 @@
"package.json" "package.json"
], ],
"linux": { "linux": {
"target": ["AppImage", "deb"], "target": [
"AppImage",
"deb"
],
"category": "Network", "category": "Network",
"maintainer": "Silberengel" "maintainer": "Silberengel",
"icon": "build/icon.png"
} }
}, },
"overrides": { "overrides": {

72
src/components/AboutInfoDialog/index.tsx

@ -1,32 +1,27 @@
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerTrigger } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerTrigger } from '@/components/ui/drawer'
import { CODY_PUBKEY, SILBERENGEL_PUBKEY } from '@/constants' import { Button } from '@/components/ui/button'
import { SILBERENGEL_PUBKEY } from '@/constants'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import Username from '../Username'
import { replaceableEventService } from '@/services/client.service' import { replaceableEventService } from '@/services/client.service'
import { getProfileFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent } from '@/lib/event-metadata'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { toProfile } from '@/lib/link'
export default function AboutInfoDialog({ children }: { children: React.ReactNode }) { export default function AboutInfoDialog({ children }: { children: React.ReactNode }) {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [codyLightning, setCodyLightning] = useState<string | null>(null)
const [silberengelLightning, setSilberengelLightning] = useState<string | null>(null) const [silberengelLightning, setSilberengelLightning] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const fetchProfiles = async () => { const fetchProfiles = async () => {
const [codyProfileEvent, silberengelProfileEvent] = await Promise.all([ const silberengelProfileEvent = await replaceableEventService.fetchReplaceableEvent(
replaceableEventService.fetchReplaceableEvent(CODY_PUBKEY, kinds.Metadata), SILBERENGEL_PUBKEY,
replaceableEventService.fetchReplaceableEvent(SILBERENGEL_PUBKEY, kinds.Metadata) kinds.Metadata
]) )
const codyProfile = codyProfileEvent ? getProfileFromEvent(codyProfileEvent) : undefined
const silberengelProfile = silberengelProfileEvent ? getProfileFromEvent(silberengelProfileEvent) : undefined const silberengelProfile = silberengelProfileEvent ? getProfileFromEvent(silberengelProfileEvent) : undefined
if (codyProfile?.lightningAddress) {
setCodyLightning(codyProfile.lightningAddress)
}
if (silberengelProfile?.lightningAddress) { if (silberengelProfile?.lightningAddress) {
setSilberengelLightning(silberengelProfile.lightningAddress) setSilberengelLightning(silberengelProfile.lightningAddress)
} }
@ -34,26 +29,37 @@ export default function AboutInfoDialog({ children }: { children: React.ReactNod
fetchProfiles() fetchProfiles()
}, []) }, [])
const openSilberengelProfile = () => {
setOpen(false)
window.location.assign(toProfile(SILBERENGEL_PUBKEY))
}
const openGithubFork = () => {
setOpen(false)
window.open('https://github.com/Silberengel/jumble', '_blank', 'noopener,noreferrer')
}
const content = ( const content = (
<> <>
<div className="text-xl font-semibold">Jumble 🌲</div> <div className="text-xl font-semibold">Jumble 🌲</div>
<div className="text-muted-foreground"> <div className="text-muted-foreground">
A user-friendly Nostr client focused on relay feed browsing and relay discovery A user-friendly Nostr client focused on relay feed browsing and relay discovery
</div> </div>
<div className="text-sm text-muted-foreground">
Version: v{import.meta.env.APP_VERSION}
</div>
<div className="space-y-2"> <div className="space-y-2">
<div>
<div className="font-medium">Main developer:</div>
<div className="ml-2">
<Username userId={CODY_PUBKEY} className="inline-block text-primary" showAt />
{codyLightning && (
<div className="text-sm text-muted-foreground"> {codyLightning}</div>
)}
</div>
</div>
<div> <div>
<div className="font-medium">Imwald branch:</div> <div className="font-medium">Imwald branch:</div>
<div className="ml-2"> <div className="ml-2">
<Username userId={SILBERENGEL_PUBKEY} className="inline-block text-primary" showAt /> <Button
type="button"
variant="link"
className="h-auto p-0 text-primary"
onClick={openSilberengelProfile}
>
@silberengel
</Button>
{silberengelLightning && ( {silberengelLightning && (
<div className="text-sm text-muted-foreground"> {silberengelLightning}</div> <div className="text-sm text-muted-foreground"> {silberengelLightning}</div>
)} )}
@ -61,24 +67,10 @@ export default function AboutInfoDialog({ children }: { children: React.ReactNod
</div> </div>
</div> </div>
<div> <div>
Source code:{' '} <div className="mb-1">Source code:</div>
<a <Button type="button" variant="link" className="h-auto p-0 text-primary" onClick={openGithubFork}>
href="https://github.com/CodyTseng/jumble"
target="_blank"
rel="noreferrer"
className="text-primary hover:underline"
>
Main repo
</a>
{' · '}
<a
href="https://github.com/Silberengel/jumble"
target="_blank"
rel="noreferrer"
className="text-primary hover:underline"
>
Imwald fork Imwald fork
</a> </Button>
<div className="text-sm text-muted-foreground mt-1"> <div className="text-sm text-muted-foreground mt-1">
If you like Jumble, please consider giving it a star If you like Jumble, please consider giving it a star
</div> </div>

13
src/components/BottomNavigationBar/NotificationsButton.tsx

@ -1,6 +1,6 @@
import { usePrimaryPage } from '@/contexts/primary-page-context' import { usePrimaryPage } from '@/contexts/primary-page-context'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Bell } from 'lucide-react' import { Bell, Settings } from 'lucide-react'
import BottomNavigationBarItem from './BottomNavigationBarItem' import BottomNavigationBarItem from './BottomNavigationBarItem'
export default function NotificationsButton() { export default function NotificationsButton() {
@ -8,7 +8,16 @@ export default function NotificationsButton() {
const { pubkey } = useNostr() const { pubkey } = useNostr()
const spell = (currentPageProps as { spell?: string } | undefined)?.spell const spell = (currentPageProps as { spell?: string } | undefined)?.spell
if (!pubkey) return null if (!pubkey) {
return (
<BottomNavigationBarItem
active={current === 'settings' && display}
onClick={() => navigate('settings')}
>
<Settings />
</BottomNavigationBarItem>
)
}
return ( return (
<BottomNavigationBarItem <BottomNavigationBarItem

39
src/components/KeyboardShortcutsHelp/index.tsx

@ -1,4 +1,3 @@
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, Dialog,
@ -8,7 +7,6 @@ import {
DialogTitle DialogTitle
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { createFakeEvent } from '@/lib/event'
import { import {
isRadixDialogOpen, isRadixDialogOpen,
OPEN_NEW_POST_SHORTCUT_KEY, OPEN_NEW_POST_SHORTCUT_KEY,
@ -18,6 +16,7 @@ import { cn } from '@/lib/utils'
import postEditorService from '@/services/post-editor.service' import postEditorService from '@/services/post-editor.service'
import { CircleHelp } from 'lucide-react' import { CircleHelp } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import { marked } from 'marked'
import { import {
KeyboardShortcutsHelpContext, KeyboardShortcutsHelpContext,
useKeyboardShortcutsHelp useKeyboardShortcutsHelp
@ -179,24 +178,28 @@ function ShortcutsPanel() {
} }
function ReadmeOverviewPanel({ className }: { className?: string }) { function ReadmeOverviewPanel({ className }: { className?: string }) {
const readmeEvent = useMemo( const readmeHtml = useMemo(() => {
() => // README is local project content; render it as regular markdown (not note-content parsing).
createFakeEvent({ const html = marked.parse(readmeMarkdown, {
id: '0'.repeat(64), gfm: true,
pubkey: '0'.repeat(64), breaks: true
content: readmeMarkdown, })
created_at: 0, const resolved = typeof html === 'string' ? html : ''
kind: 1, return resolved.replace(/<a\s+href=/g, '<a target="_blank" rel="noopener noreferrer" href=')
tags: [], }, [])
sig: '0'.repeat(128)
}),
[]
)
return ( return (
<div className={cn('min-w-0 pt-1', className)}> <div
<MarkdownArticle event={readmeEvent} hideMetadata className="text-sm" /> className={cn(
</div> 'min-w-0 pt-1 text-sm prose prose-sm dark:prose-invert max-w-none',
'[&_a]:text-green-600 [&_a]:dark:text-green-400 hover:[&_a]:underline',
'[&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded',
'[&_pre]:bg-muted [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-x-auto',
'[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-md',
className
)}
dangerouslySetInnerHTML={{ __html: readmeHtml }}
/>
) )
} }

14
src/components/Sidebar/NotificationButton.tsx

@ -1,7 +1,7 @@
import { usePrimaryPage } from '@/contexts/primary-page-context' import { usePrimaryPage } from '@/contexts/primary-page-context'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Bell } from 'lucide-react' import { Bell, Settings } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function NotificationButton() { export default function NotificationButton() {
@ -10,7 +10,17 @@ export default function NotificationButton() {
const { pubkey } = useNostr() const { pubkey } = useNostr()
const spell = (currentPageProps as { spell?: string } | undefined)?.spell const spell = (currentPageProps as { spell?: string } | undefined)?.spell
if (!pubkey) return null if (!pubkey) {
return (
<SidebarItem
title="Settings"
onClick={() => navigate('settings')}
active={display && current === 'settings' && primaryViewType === null}
>
<Settings strokeWidth={3} />
</SidebarItem>
)
}
return ( return (
<SidebarItem <SidebarItem

6
src/components/Username/index.tsx

@ -12,7 +12,8 @@ export default function Username({
className, className,
skeletonClassName, skeletonClassName,
withoutSkeleton = false, withoutSkeleton = false,
style style,
onNavigate
}: { }: {
userId: string userId: string
showAt?: boolean showAt?: boolean
@ -20,6 +21,7 @@ export default function Username({
skeletonClassName?: string skeletonClassName?: string
withoutSkeleton?: boolean withoutSkeleton?: boolean
style?: React.CSSProperties style?: React.CSSProperties
onNavigate?: () => void
}) { }) {
const { profile, isFetching } = useFetchProfile(userId) const { profile, isFetching } = useFetchProfile(userId)
const { navigateToProfile } = useSmartProfileNavigationOptional() const { navigateToProfile } = useSmartProfileNavigationOptional()
@ -50,6 +52,7 @@ export default function Username({
style={{ verticalAlign: 'baseline', ...style }} style={{ verticalAlign: 'baseline', ...style }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onNavigate?.()
navigateToProfile(toProfile(profilePubkey)) navigateToProfile(toProfile(profilePubkey))
}} }}
> >
@ -72,6 +75,7 @@ export default function Username({
style={{ verticalAlign: 'baseline', ...style }} style={{ verticalAlign: 'baseline', ...style }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onNavigate?.()
navigateToProfile(toProfile(pubkey)) navigateToProfile(toProfile(pubkey))
}} }}
> >

5
src/services/session-feed-snapshot.service.ts

@ -1,5 +1,6 @@
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { isJumbleElectron } from '@/lib/client-platform'
/** Max events stored per feed key (matches typical initial timeline cap). */ /** Max events stored per feed key (matches typical initial timeline cap). */
const MAX_EVENTS_PER_FEED = 120 const MAX_EVENTS_PER_FEED = 120
@ -46,6 +47,10 @@ export function setSessionFeedSnapshot(key: string, events: readonly Event[]): v
*/ */
export function hardReloadPreservingFeedSnapshots(): void { export function hardReloadPreservingFeedSnapshots(): void {
persistSessionFeedSnapshotsForHardRefresh() persistSessionFeedSnapshotsForHardRefresh()
if (isJumbleElectron() && typeof window.jumbleElectron?.reloadApp === 'function') {
void window.jumbleElectron.reloadApp()
return
}
window.location.reload() window.location.reload()
} }

6
src/vite-env.d.ts vendored

@ -10,6 +10,10 @@ declare global {
interface Window { interface Window {
nostr?: TNip07 nostr?: TNip07
/** Set by {@link electron/preload.cjs} when running inside Electron. */ /** Set by {@link electron/preload.cjs} when running inside Electron. */
jumbleElectron?: { isElectron: true } jumbleElectron?: {
isElectron: true
/** Ask Electron main to reload index safely (avoids file:// history path reload issues). */
reloadApp?: () => Promise<boolean>
}
} }
} }

Loading…
Cancel
Save