diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 00000000..a98b8819 Binary files /dev/null and b/build/icon.png differ diff --git a/electron/main.cjs b/electron/main.cjs index f779da79..b32ae8dc 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,11 +1,23 @@ 'use strict' -const { app, BrowserWindow, shell } = require('electron') +const { app, BrowserWindow, ipcMain, shell } = require('electron') const path = require('path') /** True when running from source (`electron .`); false when packaged. */ 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() { const win = new BrowserWindow({ width: 1280, @@ -23,13 +35,7 @@ function createWindow() { win.once('ready-to-show', () => win.show()) - if (isDev) { - 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')) - } + loadRenderer(win) win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http:') || url.startsWith('https:')) { @@ -40,6 +46,13 @@ function createWindow() { } 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() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() diff --git a/electron/preload.cjs b/electron/preload.cjs index 8f9e83c7..6251fbe3 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -1,7 +1,8 @@ 'use strict' -const { contextBridge } = require('electron') +const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('jumbleElectron', { - isElectron: true + isElectron: true, + reloadApp: () => ipcRenderer.invoke('jumble:reload-app') }) diff --git a/package-lock.json b/package-lock.json index 33cb4d32..7bbbd80b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jumble-imwald", - "version": "21.3.1", + "version": "21.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jumble-imwald", - "version": "21.3.1", + "version": "21.3.2", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", @@ -65,6 +65,7 @@ "katex": "^0.16.25", "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", + "marked": "^17.0.5", "nostr-tools": "^2.17.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.3.0", @@ -11183,6 +11184,18 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", diff --git a/package.json b/package.json index 2aa6a821..b7172e93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "private": true, "type": "module", @@ -38,7 +38,6 @@ "@getalby/bitcoin-connect-react": "^3.10.0", "@getalby/lightning-tools": "^6.1.0", "@noble/hashes": "^1.6.1", - "@scure/base": "^2.0.0", "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-checkbox": "^1.3.3", @@ -56,6 +55,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", + "@scure/base": "^2.0.0", "@tailwindcss/typography": "^0.5.16", "@tiptap/core": "^2.12.0", "@tiptap/extension-document": "^2.12.0", @@ -69,7 +69,6 @@ "@tiptap/pm": "^2.12.0", "@tiptap/react": "^2.12.0", "@tiptap/suggestion": "^2.12.0", - "prosemirror-state": "^1.4.3", "@webbtc/webln-types": "^3.0.0", "blossom-client-sdk": "^4.1.0", "blurhash": "^2.0.5", @@ -87,9 +86,11 @@ "katex": "^0.16.25", "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", + "marked": "^17.0.5", "nostr-tools": "^2.17.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.3.0", + "prosemirror-state": "^1.4.3", "qr-code-styling": "^1.9.2", "qr-scanner": "^1.4.2", "react": "^18.3.1", @@ -113,6 +114,8 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "concurrently": "^9.1.2", + "cross-env": "^7.0.3", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", @@ -126,8 +129,6 @@ "vite": "^6.0.3", "vite-plugin-pwa": "^0.21.1", "vitest": "^4.0.8", - "concurrently": "^9.1.2", - "cross-env": "^7.0.3", "wait-on": "^8.0.1" }, "optionalDependencies": { @@ -147,9 +148,13 @@ "package.json" ], "linux": { - "target": ["AppImage", "deb"], + "target": [ + "AppImage", + "deb" + ], "category": "Network", - "maintainer": "Silberengel" + "maintainer": "Silberengel", + "icon": "build/icon.png" } }, "overrides": { diff --git a/src/components/AboutInfoDialog/index.tsx b/src/components/AboutInfoDialog/index.tsx index d786c8fc..8c84cff4 100644 --- a/src/components/AboutInfoDialog/index.tsx +++ b/src/components/AboutInfoDialog/index.tsx @@ -1,32 +1,27 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' 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 { useState, useEffect } from 'react' -import Username from '../Username' import { replaceableEventService } from '@/services/client.service' import { getProfileFromEvent } from '@/lib/event-metadata' import { kinds } from 'nostr-tools' +import { toProfile } from '@/lib/link' export default function AboutInfoDialog({ children }: { children: React.ReactNode }) { const { isSmallScreen } = useScreenSize() const [open, setOpen] = useState(false) - const [codyLightning, setCodyLightning] = useState(null) const [silberengelLightning, setSilberengelLightning] = useState(null) useEffect(() => { const fetchProfiles = async () => { - const [codyProfileEvent, silberengelProfileEvent] = await Promise.all([ - replaceableEventService.fetchReplaceableEvent(CODY_PUBKEY, kinds.Metadata), - replaceableEventService.fetchReplaceableEvent(SILBERENGEL_PUBKEY, kinds.Metadata) - ]) - const codyProfile = codyProfileEvent ? getProfileFromEvent(codyProfileEvent) : undefined + const silberengelProfileEvent = await replaceableEventService.fetchReplaceableEvent( + SILBERENGEL_PUBKEY, + kinds.Metadata + ) const silberengelProfile = silberengelProfileEvent ? getProfileFromEvent(silberengelProfileEvent) : undefined - - if (codyProfile?.lightningAddress) { - setCodyLightning(codyProfile.lightningAddress) - } - + if (silberengelProfile?.lightningAddress) { setSilberengelLightning(silberengelProfile.lightningAddress) } @@ -34,26 +29,37 @@ export default function AboutInfoDialog({ children }: { children: React.ReactNod 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 = ( <>
Jumble 🌲
A user-friendly Nostr client focused on relay feed browsing and relay discovery
+
+ Version: v{import.meta.env.APP_VERSION} +
-
-
Main developer:
-
- - {codyLightning && ( -
⚡ {codyLightning}
- )} -
-
Imwald branch:
- + {silberengelLightning && (
⚡ {silberengelLightning}
)} @@ -61,24 +67,10 @@ export default function AboutInfoDialog({ children }: { children: React.ReactNod
- Source code:{' '} - - Main repo - - {' · '} - +
Source code:
+
If you like Jumble, please consider giving it a star ⭐
diff --git a/src/components/BottomNavigationBar/NotificationsButton.tsx b/src/components/BottomNavigationBar/NotificationsButton.tsx index f2023c0c..8d2c46f5 100644 --- a/src/components/BottomNavigationBar/NotificationsButton.tsx +++ b/src/components/BottomNavigationBar/NotificationsButton.tsx @@ -1,6 +1,6 @@ import { usePrimaryPage } from '@/contexts/primary-page-context' import { useNostr } from '@/providers/NostrProvider' -import { Bell } from 'lucide-react' +import { Bell, Settings } from 'lucide-react' import BottomNavigationBarItem from './BottomNavigationBarItem' export default function NotificationsButton() { @@ -8,7 +8,16 @@ export default function NotificationsButton() { const { pubkey } = useNostr() const spell = (currentPageProps as { spell?: string } | undefined)?.spell - if (!pubkey) return null + if (!pubkey) { + return ( + navigate('settings')} + > + + + ) + } return ( - createFakeEvent({ - id: '0'.repeat(64), - pubkey: '0'.repeat(64), - content: readmeMarkdown, - created_at: 0, - kind: 1, - tags: [], - sig: '0'.repeat(128) - }), - [] - ) + const readmeHtml = useMemo(() => { + // README is local project content; render it as regular markdown (not note-content parsing). + const html = marked.parse(readmeMarkdown, { + gfm: true, + breaks: true + }) + const resolved = typeof html === 'string' ? html : '' + return resolved.replace(/ - -
+
) } diff --git a/src/components/Sidebar/NotificationButton.tsx b/src/components/Sidebar/NotificationButton.tsx index 2ed17ab6..a985cc6f 100644 --- a/src/components/Sidebar/NotificationButton.tsx +++ b/src/components/Sidebar/NotificationButton.tsx @@ -1,7 +1,7 @@ import { usePrimaryPage } from '@/contexts/primary-page-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { useNostr } from '@/providers/NostrProvider' -import { Bell } from 'lucide-react' +import { Bell, Settings } from 'lucide-react' import SidebarItem from './SidebarItem' export default function NotificationButton() { @@ -10,7 +10,17 @@ export default function NotificationButton() { const { pubkey } = useNostr() const spell = (currentPageProps as { spell?: string } | undefined)?.spell - if (!pubkey) return null + if (!pubkey) { + return ( + navigate('settings')} + active={display && current === 'settings' && primaryViewType === null} + > + + + ) + } return ( void }) { const { profile, isFetching } = useFetchProfile(userId) const { navigateToProfile } = useSmartProfileNavigationOptional() @@ -50,6 +52,7 @@ export default function Username({ style={{ verticalAlign: 'baseline', ...style }} onClick={(e) => { e.stopPropagation() + onNavigate?.() navigateToProfile(toProfile(profilePubkey)) }} > @@ -72,6 +75,7 @@ export default function Username({ style={{ verticalAlign: 'baseline', ...style }} onClick={(e) => { e.stopPropagation() + onNavigate?.() navigateToProfile(toProfile(pubkey)) }} > diff --git a/src/services/session-feed-snapshot.service.ts b/src/services/session-feed-snapshot.service.ts index 4baafbfc..453b8336 100644 --- a/src/services/session-feed-snapshot.service.ts +++ b/src/services/session-feed-snapshot.service.ts @@ -1,5 +1,6 @@ import type { Event } from 'nostr-tools' import logger from '@/lib/logger' +import { isJumbleElectron } from '@/lib/client-platform' /** Max events stored per feed key (matches typical initial timeline cap). */ const MAX_EVENTS_PER_FEED = 120 @@ -46,6 +47,10 @@ export function setSessionFeedSnapshot(key: string, events: readonly Event[]): v */ export function hardReloadPreservingFeedSnapshots(): void { persistSessionFeedSnapshotsForHardRefresh() + if (isJumbleElectron() && typeof window.jumbleElectron?.reloadApp === 'function') { + void window.jumbleElectron.reloadApp() + return + } window.location.reload() } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 8113f995..29936e35 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -10,6 +10,10 @@ declare global { interface Window { nostr?: TNip07 /** 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 + } } }