diff --git a/electron/main.cjs b/electron/main.cjs index 57926655..1bf1645a 100755 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,18 +1,28 @@ +// Import log.cjs require('./log.cjs'); -const { app, BrowserWindow, Menu, MenuItem, Tray, shell, dialog, nativeTheme, ipcMain, Notification } = require('electron'); +// Import Electron components using CommonJS require +const { app, BrowserWindow, Menu, MenuItem, Tray, shell, dialog, nativeTheme, ipcMain, Notification, systemPreferences } = require('electron'); +// Import Node.js built-ins using CommonJS const path = require('path'); const fs = require('fs'); -const { URL } = require('url'); +const { URL, fileURLToPath } = require('url'); +const util = require('util'); +// Don't require EnvPaths directly, it will be dynamically imported later +// const EnvPaths = require('env-paths'); const FormData = require('form-data'); +const fetch = require('node-fetch'); const contextMenu = require('electron-context-menu'); +// Import local modules using CommonJS require('./start-ipfs.cjs'); require('./start-plebbit-rpc.cjs'); +// Load package.json using CommonJS const packageJson = require('../package.json'); +// Set ELECTRON_IS_DEV for other modules to use process.env.ELECTRON_IS_DEV = app.isPackaged ? '0' : '1'; // Since we're in CommonJS, we can't use import.meta.url @@ -76,6 +86,96 @@ startIpfs.onError = (error) => { } }); + // Handle request for notification permission status + ipcMain.handle('get-notification-permission-status', async () => { + try { + // Check Notification API support + if (!Notification.isSupported()) { + console.log('[Electron Main] Notification API not supported.'); + return 'not-supported'; + } + // On macOS, prefer getNotificationSettings() + if (process.platform === 'darwin') { + if (typeof systemPreferences.getNotificationSettings === 'function') { + const settings = systemPreferences.getNotificationSettings(); + const auth = settings.authorizationStatus; // 'authorized'|'denied'|'not-determined' + console.log('[Electron Main] macOS systemPreferences.getNotificationSettings() returned:', auth); + if (auth === 'denied') return 'denied'; + if (auth === 'authorized') return 'granted'; + return 'not-determined'; + } + // Fallback to old API if present + if (typeof systemPreferences.getNotificationPermissionStatus === 'function') { + const status = systemPreferences.getNotificationPermissionStatus(); + console.log('[Electron Main] macOS systemPreferences.getNotificationPermissionStatus() returned:', status); + return status; + } + console.warn('[Electron Main] No macOS notification permission API available; assuming granted.'); + return 'granted'; + } + // For Windows/Linux, assume granted if API supported + console.log('[Electron Main] Assuming notification permission granted on non-macOS platform.'); + return 'granted'; + } catch (error) { + console.error('[Electron Main] Error getting notification permission status:', error); + return 'unknown'; + } + }); + + // Handle request for the current platform + ipcMain.handle('get-platform', async () => { + return process.platform; // Returns 'darwin', 'win32', 'linux', etc. + }); + + // Handle request to test notification permission (by sending one) + ipcMain.handle('test-notification-permission', async () => { + if (!Notification.isSupported()) { + console.warn('[Electron Main] Test notification requested, but not supported.'); + return { success: false, reason: 'not-supported' }; + } + try { + // Check status *before* trying to send, using the platform-aware logic + let status = 'unknown'; + if (process.platform === 'darwin') { + // Explicitly check if the function exists before calling it + if (typeof systemPreferences.getNotificationPermissionStatus === 'function') { + status = systemPreferences.getNotificationPermissionStatus(); + } else { + console.warn('[Electron Main Test] systemPreferences.getNotificationPermissionStatus is NOT a function. Falling back.'); + status = Notification.isSupported() ? 'granted' : 'not-supported'; // Fallback for macOS + } + } else { + // Assume granted on other platforms if supported + status = Notification.isSupported() ? 'granted' : 'not-supported'; + } + + console.log('[Electron Main Test] Determined status before sending:', status); + + if (status === 'denied') { + console.warn('[Electron Main Test] notification requested, but status is denied.'); + return { success: false, reason: 'denied' }; + } + if (status === 'not-supported') { + console.warn('[Electron Main Test] notification requested, but not supported.'); + return { success: false, reason: 'not-supported' }; + } + + // Sending a notification is the standard way to trigger the 'not-determined' prompt on macOS + const testNotification = new Notification({ + title: 'Seedit Test', + body: 'Testing if notifications are allowed.', + }); + testNotification.show(); + // We can't easily *confirm* it showed, but if no error and not denied/not-supported, assume success for now. + // The user will see (or not see) the notification. + console.log('[Electron Main Test] notification shown (or attempted). Status was:', status); + return { success: true }; + } catch (error) { + console.error('[Electron Main Test] Error sending test notification:', error); + return { success: false, reason: 'error' }; + } + }); + // add right click menu contextMenu({ // prepend custom buttons to top @@ -119,7 +219,7 @@ startIpfs.onError = (error) => { nodeIntegration: false, contextIsolation: true, devTools: !app.isPackaged, // Use app.isPackaged to determine if devTools should be enabled - preload: path.join(dirname, 'preload.cjs'), + preload: path.join(dirname, 'preload.cjs'), // Updated to use preload.cjs // sandbox: false, // sandbox:false is generally discouraged for security unless strictly necessary. Re-evaluate if needed. }, }); @@ -209,8 +309,8 @@ startIpfs.onError = (error) => { const validatedUrl = new URL(originalUrl); let serializedUrl = ''; - // make an exception for ipfs stats - if (validatedUrl.toString() === 'http://localhost:50019/webui/') { + // make an exception for ipfs stats (allow proxy port 50019 in dev) + if (validatedUrl.toString() === 'http://localhost:50019/webui/') { serializedUrl = validatedUrl.toString(); } else if (validatedUrl.protocol === 'https:') { // open serialized url to prevent remote execution @@ -239,8 +339,8 @@ startIpfs.onError = (error) => { const validatedUrl = new URL(originalUrl); let serializedUrl = ''; - // make an exception for ipfs stats - if (validatedUrl.toString() === 'http://localhost:50019/webui/') { + // make an exception for ipfs stats (allow proxy port 50019 in dev) + if (validatedUrl.toString() === 'http://localhost:50019/webui/') { serializedUrl = validatedUrl.toString(); } else if (validatedUrl.protocol === 'https:') { // open serialized url to prevent remote execution @@ -470,7 +570,3 @@ ipcMain.handle('plugin:file-uploader:pickMedia', async (event) => { throw error; } }); - -ipcMain.handle('get-platform', () => { - return process.platform; // 'darwin', 'win32', 'linux', etc. -}); diff --git a/electron/preload.cjs b/electron/preload.cjs index 33a0ee3d..c0ab29b5 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -2,9 +2,31 @@ const { contextBridge, ipcRenderer } = require('electron'); console.log('Preload script loaded.'); +// Expose core IPC methods under 'electron' +contextBridge.exposeInMainWorld('electron', { + invoke: (channel, ...args) => { + const validChannels = [ + 'get-notification-permission-status', + 'get-platform', + 'test-notification-permission', + 'show-notification' + ]; + if (validChannels.includes(channel)) { + return ipcRenderer.invoke(channel, ...args); + } + throw new Error(`Unauthorized IPC channel: ${channel}`); + }, + sendNotification: (notification) => { + ipcRenderer.send('show-notification', notification); + } +}); + +// Expose higher-level API under 'electronApi' contextBridge.exposeInMainWorld('electronApi', { isElectron: true, + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), getNotificationStatus: () => ipcRenderer.invoke('get-notification-permission-status'), getPlatform: () => ipcRenderer.invoke('get-platform'), - testNotification: () => ipcRenderer.invoke('test-notification-permission') + testNotification: () => ipcRenderer.invoke('test-notification-permission'), + showNotification: (notification) => ipcRenderer.send('show-notification', notification) }); \ No newline at end of file diff --git a/electron/src/main.ts b/electron/src/main.ts index dedeee0d..83b0ff72 100755 --- a/electron/src/main.ts +++ b/electron/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, ipcMain, Notification, systemPreferences } from 'electron'; import * as path from 'path'; import { setupFileUploaderPlugin } from './plugins/file-uploader'; @@ -16,18 +16,20 @@ function createWindow() { }); // Prevent file drops on the window - mainWindow.webContents.on('will-navigate', (e, url) => { - if (url !== mainWindow.webContents.getURL()) { + mainWindow!.webContents.on('will-navigate', (e, url) => { + if (url !== mainWindow!.webContents.getURL()) { e.preventDefault(); } }); // Prevent default file drop behavior - mainWindow.webContents.on('drop', (e) => { + // @ts-ignore: drop event not in WebContents type definitions + (mainWindow!.webContents as any).on('drop', (e: any) => { e.preventDefault(); }); - mainWindow.webContents.on('dragover', (e) => { + // @ts-ignore: dragover event not in WebContents type definitions + (mainWindow!.webContents as any).on('dragover', (e: any) => { e.preventDefault(); }); @@ -44,6 +46,71 @@ function createWindow() { } app.whenReady().then(() => { + // --- notification IPC handlers --- + ipcMain.handle('get-notification-permission-status', async () => { + try { + // First check if the Notification API itself is supported + if (!Notification.isSupported()) { + console.log('[Electron Main] Notification API not supported.'); + return 'not-supported'; + } + + // Platform-specific checks using Electron built-ins + if (process.platform === 'darwin') { + // macOS + if (typeof (systemPreferences as any).getNotificationSettings === 'function') { + // Use systemPreferences only if available (avoids electron internal warnings on older builds) + // @ts-ignore: getNotificationSettings may not be in TS definitions + const settings = (systemPreferences as any).getNotificationSettings(); + const auth = settings.authorizationStatus as string; // 'authorized'|'denied'|'not-determined' + console.log('[Electron Main] macOS getNotificationSettings returned:', auth); + if (auth === 'denied') return 'denied'; + if (auth === 'authorized') return 'granted'; + return 'not-determined'; + } else if (typeof (systemPreferences as any).getNotificationPermissionStatus === 'function') { + // Fallback to older API + // @ts-ignore + const status = (systemPreferences as any).getNotificationPermissionStatus(); + console.log('[Electron Main] macOS getNotificationPermissionStatus returned:', status); + return status; + } else { + console.warn('[Electron Main] No macOS notification permission API available; assuming granted.'); + return 'granted'; + } + } + + // Windows/Linux/Other: Assume granted if Notification API is supported + console.log('[Electron Main] Assuming notification permission granted on non-macOS platform.'); + return 'granted'; + } catch (error) { + console.error('[Electron Main] Error getting notification permission status:', error); + return 'unknown'; + } + }); + ipcMain.handle('get-platform', () => { + return process.platform as NodeJS.Platform; + }); + // Changed from handle to on as it doesn't need to return a value + ipcMain.on('show-notification', (_evt, notificationData) => { + if (!Notification.isSupported()) { + console.log('Notifications not supported on this system.'); + return; // Don't try to show if not supported + } + // Use the data passed from the renderer + const { title, body } = notificationData; + if (title && body) { + new Notification({ title, body }).show(); + } else { + console.error('Invalid notification data received:', notificationData); + } + }); + // Placeholder for test-notification if you implement it later + // ipcMain.handle('test-notification', async () => { + // // Implement test logic + // return { success: true }; + // }); + // --------------------------------- + createWindow(); app.on('activate', () => { diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 1c023799..b5245be2 100755 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -5,10 +5,34 @@ import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('electron', { invoke: (channel: string, ...args: any[]) => { // whitelist channels - const validChannels = ['plugin:file-uploader:pickAndUploadMedia', 'plugin:file-uploader:uploadMedia', 'plugin:file-uploader:pickMedia']; + const validChannels = [ + 'plugin:file-uploader:pickAndUploadMedia', + 'plugin:file-uploader:uploadMedia', + 'plugin:file-uploader:pickMedia', + 'get-notification-permission-status', // actual channel + 'get-platform', + 'test-notification-permission', // correct test channel in main + 'show-notification', + ]; if (validChannels.includes(channel)) { return ipcRenderer.invoke(channel, ...args); } throw new Error(`Unauthorized IPC channel: ${channel}`); }, + // Direct send for notifications + sendNotification: (notificationData: { title: string; body: string }) => { + ipcRenderer.send('show-notification', notificationData); + }, +}); + +// Expose a dedicated "electronApi" for your UI code +contextBridge.exposeInMainWorld('electronApi', { + isElectron: true, + invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args), + getNotificationStatus: () => ipcRenderer.invoke('get-notification-permission-status'), + getPlatform: () => ipcRenderer.invoke('get-platform'), + testNotification: () => ipcRenderer.invoke('test-notification'), // Note: show-notification uses the other API + showNotification: (notificationData: { title: string; body: string }) => { + ipcRenderer.send('show-notification', notificationData); + }, }); diff --git a/electron/start-ipfs.cjs b/electron/start-ipfs.cjs index 74bbad18..de687936 100755 --- a/electron/start-ipfs.cjs +++ b/electron/start-ipfs.cjs @@ -5,96 +5,115 @@ const ps = require('node:process'); const tcpPortUsed = require('tcp-port-used'); const proxyServer = require('./proxy-server.cjs'); -// Use app.isPackaged defined in main.js via process.env +// Instead of using electron-is-dev, use app.isPackaged +// (we already made this change in main.js) const isDev = process.env.ELECTRON_IS_DEV === '1'; -// CommonJS equivalent of __dirname +// Use __dirname directly instead of fileURLToPath(import.meta.url) const dirname = __dirname; +// Flag to ensure proxy is started only once let proxyStarted = false; -// Start a debugging proxy for the IPFS API only in development mode. -// This allows connecting dev tools (like the IPFS Web UI) to the standard -// port 50019 while the actual daemon runs on 50029. +// Function to start the proxy if in dev mode and not already started function startDevProxyOnce() { if (isDev && !proxyStarted) { - const proxyPort = 50019; - const targetPort = 50029; + const proxyPort = 50019; + const targetPort = 50029; // Actual IPFS API port in dev try { console.log(`Attempting to start development proxy server on port ${proxyPort}...`); proxyServer.start({ proxyPort, targetPort }); - proxyStarted = true; + proxyStarted = true; // Set flag only after successful start console.log(`Development proxy server started successfully on port ${proxyPort}, forwarding to ${targetPort}.`); } catch (e) { + // Log the error but don't necessarily stop everything, + // as the proxy is mainly for debugging. console.error(`Failed to start development proxy server on port ${proxyPort}:`, e); + // Consider if proxyStarted should be set to true even on failure to prevent retries + // proxyStarted = true; } } } -// Handles IPFS setup: finding binary, initializing repo, configuring ports, and starting daemon. +// Make the main logic async to handle dynamic import async function initializeIpfs() { + // Dynamically import env-paths const EnvPaths = (await import('env-paths')).default; const envPaths = EnvPaths('plebbit', { suffix: false }); const ipfsFileName = process.platform == 'win32' ? 'ipfs.exe' : 'ipfs'; - let ipfsPath = path.join(process.resourcesPath, 'bin', ipfsFileName); // Packaged app path - let ipfsDataPath = path.join(envPaths.data, 'ipfs'); // Standard data path + let ipfsPath = path.join(process.resourcesPath, 'bin', ipfsFileName); + let ipfsDataPath = path.join(envPaths.data, 'ipfs'); - // Override paths for development mode + // test launching the ipfs binary in dev mode + // they must be downloaded first using `yarn electron:build` if (isDev) { let binFolderName = 'win'; - if (process.platform === 'linux') binFolderName = 'linux'; - if (process.platform === 'darwin') binFolderName = 'mac'; + if (process.platform === 'linux') { + binFolderName = 'linux'; + } + if (process.platform === 'darwin') { + binFolderName = 'mac'; + } ipfsPath = path.join(dirname, '..', 'bin', binFolderName, ipfsFileName); ipfsDataPath = path.join(dirname, '..', '.plebbit', 'ipfs'); } if (!fs.existsSync(ipfsPath)) { - throw Error(`ipfs binary '${ipfsPath}' doesn't exist. Run 'yarn electron:build' or 'yarn electron:before:download-ipfs' first?`); + throw Error(`ipfs binary '${ipfsPath}' doesn't exist`); } console.log({ ipfsPath, ipfsDataPath }); fs.ensureDirSync(ipfsDataPath); const env = { IPFS_PATH: ipfsDataPath }; + // init ipfs client on first launch + try { + await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true }); + } catch (e) {} - // Attempt to initialize the IPFS repo; ignore errors if already initialized. - try { await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true }); } catch (e) {} + // make sure repo is migrated + try { + await spawnAsync(ipfsPath, ['repo', 'migrate'], { env, hideWindows: true }); + } catch (e) {} - // Ensure the repo is migrated to the latest version. - try { await spawnAsync(ipfsPath, ['repo', 'migrate'], { env, hideWindows: true }); } catch (e) {} + // dont use 8080 port because it's too common + await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', '"/ip4/127.0.0.1/tcp/6473"'], { + env, + hideWindows: true, + }); - // Configure IPFS Gateway port (avoiding common port 8080). - await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', '"/ip4/127.0.0.1/tcp/6473"'], { env, hideWindows: true }); - - // Configure IPFS API port. Use a different port in dev (50029) vs prod (50019). + // use different port with proxy for debugging during env let apiAddress = '/ip4/127.0.0.1/tcp/50019'; if (isDev) { apiAddress = apiAddress.replace('50019', '50029'); + // Do NOT start proxy server here + // proxyServer.start({ proxyPort: 50039, targetPort: 50029 }); } await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true }); await startIpfsDaemon(ipfsPath, env); - // Attempt to start the dev proxy *after* trying to start the daemon (increases chance target port is ready). - startDevProxyOnce(); + // Attempt to start proxy AFTER daemon reports ready (or at least after start attempt) + // This increases chances the target port 50029 is actually listening. + startDevProxyOnce(); } -// Wrapper around child_process.spawn for better logging and Promise interface. +// use this custom function instead of spawnSync for better logging +// also spawnSync might have been causing crash on start on windows const spawnAsync = (...args) => new Promise((resolve, reject) => { - const spawnedProcess = spawn(...args); - spawnedProcess.on('exit', (exitCode, signal) => { + const spawedProcess = spawn(...args); + spawedProcess.on('exit', (exitCode, signal) => { if (exitCode === 0) resolve(); - else reject(Error(`spawnAsync process '${spawnedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`)); + else reject(Error(`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`)); }); - spawnedProcess.stderr.on('data', (data) => console.error(data.toString())); - spawnedProcess.stdin.on('data', (data) => console.log(data.toString())); - spawnedProcess.stdout.on('data', (data) => console.log(data.toString())); - spawnedProcess.on('error', (data) => console.error(data.toString())); + spawedProcess.stderr.on('data', (data) => console.error(data.toString())); + spawedProcess.stdin.on('data', (data) => console.log(data.toString())); + spawedProcess.stdout.on('data', (data) => console.log(data.toString())); + spawedProcess.on('error', (data) => console.error(data.toString())); }); -// Starts the IPFS daemon process and resolves when it's ready. const startIpfsDaemon = (ipfsPath, env) => new Promise((resolve, reject) => { const ipfsProcess = spawn(ipfsPath, ['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'], { env, hideWindows: true }); @@ -117,15 +136,14 @@ const startIpfsDaemon = (ipfsPath, env) => console.error(`ipfs process with pid ${ipfsProcess.pid} exited`); reject(Error(lastError)); }); - - // Ensure IPFS daemon is killed when the Electron app exits. + // Restore the exit handler to cleanly kill the daemon on Electron exit process.on('exit', () => { try { console.log(`Attempting to kill IPFS daemon (pid: ${ipfsProcess.pid}) on Electron exit...`); ps.kill(ipfsProcess.pid); console.log(`Successfully sent kill signal to IPFS daemon (pid: ${ipfsProcess.pid}).`); } catch (e) { - // Ignore error if process doesn't exist (ESRCH). + // Ignore ESRCH errors (process already gone) if (e.code !== 'ESRCH') { console.warn(`Warn: Failed to kill IPFS daemon (pid: ${ipfsProcess.pid}) on exit:`, e.message); } @@ -133,39 +151,45 @@ const startIpfsDaemon = (ipfsPath, env) => }); }); -// Export object for other modules to attach an onError handler. +// Export object for error handling const DefaultExport = {}; -// Checks periodically if IPFS daemon is running (by checking its API port) -// and starts it via initializeIpfs if not. +// Auto-restart logic now calls the async initializeIpfs const startIpfsAutoRestart = async () => { let pendingStart = false; const start = async () => { - if (pendingStart) return; + if (pendingStart) { + return; + } pendingStart = true; try { - const apiPort = isDev ? 50029 : 50019; // Check the *actual* daemon port + const apiPort = isDev ? 50029 : 50019; const started = await tcpPortUsed.check(apiPort, '127.0.0.1'); if (!started) { console.log(`IPFS API port ${apiPort} not detected. Initializing IPFS...`); - await initializeIpfs(); + await initializeIpfs(); // Initialize IPFS daemon (will also try to start proxy via startDevProxyOnce) } else { console.log(`IPFS API port ${apiPort} already in use. Assuming IPFS is running.`); - // Ensure dev proxy is started if IPFS was already running. + // Ensure proxy is started even if IPFS was already running from a previous session startDevProxyOnce(); } } catch (e) { console.log('failed starting ipfs', e); - DefaultExport.onError?.(e); + try { + // try to run exported onError callback, can be undefined + DefaultExport.onError(e)?.catch?.((console.log)); + } catch (e) {} } pendingStart = false; }; - // Try starting dev proxy immediately in case IPFS is already running. + // Try starting the proxy once immediately at the beginning, + // in case IPFS is already running and the check above runs later. startDevProxyOnce(); - start(); // Initial check - setInterval(start, 5000); // Periodic check + // Start check/initialization loop + start(); + setInterval(start, 5000); // Check every 5 seconds }; startIpfsAutoRestart(); diff --git a/src/components/notification-handler/NotificationHandler.tsx b/src/components/notification-handler/NotificationHandler.tsx index 21c80b8a..82897d16 100644 --- a/src/components/notification-handler/NotificationHandler.tsx +++ b/src/components/notification-handler/NotificationHandler.tsx @@ -5,49 +5,53 @@ import useContentOptionsStore from '../../stores/use-content-options-store'; import { showLocalNotification } from '../../lib/push'; /** - * Listens for new notifications from useNotifications and triggers platform-specific - * local notification display if enabled in settings. - * Does not render any UI itself. + * This component handles listening for new notifications from the useNotifications hook + * and triggering the platform-specific local notification display. + * It doesn't render anything itself, but runs its logic via useEffect. */ export const NotificationHandler = () => { const { enableLocalNotifications } = useContentOptionsStore(); - const { notifications } = useNotifications(); + const { notifications } = useNotifications(); // Use real hook const location = useLocation(); - const previousNotificationsRef = useRef(notifications); + const previousNotificationsRef = useRef(notifications); // Use ref based on real hook useEffect(() => { + // Only proceed if notifications are enabled in settings if (!enableLocalNotifications) { return; } + // Check for new notifications compared to the previous state const previousCids = new Set(previousNotificationsRef.current?.map((n) => n.cid) || []); const newNotifications = notifications?.filter((n) => !previousCids.has(n.cid)) || []; newNotifications.forEach((notification) => { - // Don't notify if the user is already viewing the inbox + // Basic check: don't notify if the user is already on the inbox page if (location.pathname.startsWith('/inbox')) { console.log('[NotificationHandler] Skipping notification, user is in inbox.', notification.cid); return; } - // TODO: Enhance title/body/URL based on notification type (reply vs mention etc.) + // Construct the notification payload + // TODO: Enhance title/body/URL based on notification type (reply, mention, etc.) const payload = { title: 'New Notification', // Generic title for now - body: notification.text || 'You have a new notification.', // Fallback body - url: `/p/${notification.subplebbitAddress}/c/${notification.cid}`, // Link to comment + body: notification.text || 'You have a new notification.', // Use comment text or generic body + url: `/p/${notification.subplebbitAddress}/c/${notification.cid}`, // Link directly to the comment }; console.log('[NotificationHandler] Triggering notification:', payload); + // Show the notification showLocalNotification(payload); }); + // Update the reference for the next comparison previousNotificationsRef.current = notifications; - // Dependencies ensure this runs when relevant state changes, - // including location to re-evaluate the inbox check. + // Rerun when notifications, setting, or location changes }, [notifications, enableLocalNotifications, location.pathname]); - return null; + return null; // This component doesn't render anything }; // Optional: Default export if preferred diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index f86cae41..15f2dec1 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -31,7 +31,7 @@ import Version from '../version'; import { FAQ } from '../../views/about/about'; import { createCommunitySubtitles } from '../../constants/create-community-subtitles'; -const isElectron = window.isElectron === true; +const isElectron = window.electronApi?.isElectron === true; const RulesList = ({ rules }: { rules: string[] }) => { const { t } = useTranslation(); diff --git a/src/globals.d.ts b/src/globals.d.ts index 36622aa1..721d3c77 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -9,7 +9,8 @@ declare global { interface Window { electronApi?: { isElectron: boolean; - getNotificationStatus: () => Promise<'granted' | 'denied' | 'not-determined'>; + invoke: (channel: string, ...args: any[]) => Promise; + getNotificationStatus: () => Promise<'granted' | 'denied' | 'not-determined' | 'not-supported'>; getPlatform: () => Promise; testNotification: () => Promise<{ success: boolean; reason?: string }>; }; diff --git a/src/lib/push/electron.ts b/src/lib/push/electron.ts index ee9b24a7..47666605 100644 --- a/src/lib/push/electron.ts +++ b/src/lib/push/electron.ts @@ -1,44 +1,68 @@ import type { LocalNotification } from './common'; // Define the dedicated Seedit IPC API exposed via preload +// Keep the existing electron definition if other parts of the app use it declare global { interface Window { - seeditIpc?: { - send: (channel: string, ...args: any[]) => void; - // Add invoke/on if needed for seedit-specific IPC - }; - // Keep existing electron definition if other parts of the app use it electron?: { invoke: (channel: string, ...args: any[]) => Promise; + sendNotification?: (notification: Omit) => void; }; + // The new electronApi for cleaner separation + electronApi?: { + // Allow direct invoke as exposed in preload + invoke: (channel: string, ...args: any[]) => Promise; + isElectron: boolean; + getNotificationStatus: () => Promise<'granted' | 'denied' | 'not-determined' | 'not-supported'>; + getPlatform: () => Promise; + testNotification: () => Promise<{ success: boolean; reason?: string }>; + showNotification: (notification: Omit) => void; + }; + // Remove seeditIpc if no longer needed elsewhere + // seeditIpc?: { + // send: (channel: string, ...args: any[]) => void; + // }; } } /** * Requests permission to display notifications in Electron. - * On Electron, permission is implicitly granted, but we check if the API exists. + * Uses the new electronApi for clarity. */ export async function requestElectronNotificationPermission(): Promise { - // Check if the preload script successfully exposed the Seedit IPC API - const supported = typeof window.seeditIpc?.send === 'function'; + // Just check if the API exists via preload. + const supported = typeof window.electronApi?.getNotificationStatus === 'function'; if (!supported) { - console.warn('Seedit Electron IPC for notifications not available.'); + console.warn('Electron API for notifications not available via preload.'); } - // We don't need to ask the user for permission like in the browser + // No explicit permission needed from user in Electron, just API availability. return supported; } /** - * Shows a local notification via Electron IPC using the dedicated Seedit API. + * Shows a local notification via Electron IPC using the exposed 'electron.invoke'. */ export async function showElectronLocalNotification(notification: Omit): Promise { - if (window.seeditIpc?.send) { - try { - window.seeditIpc.send('show-notification', notification); - } catch (error) { - console.error('Error sending notification via Seedit IPC:', error); - } - } else { - console.warn('Seedit Electron IPC send function not available.'); + // Try the high-level API + if (typeof window.electronApi?.showNotification === 'function') { + // @ts-ignore + window.electronApi.showNotification(notification); + return; } + // Next, try the low-level send + if (typeof window.electron?.sendNotification === 'function') { + // @ts-ignore + window.electron.sendNotification(notification); + return; + } + // Finally, fallback to invoke + if (typeof window.electron?.invoke === 'function') { + try { + await window.electron.invoke('show-notification', notification); + } catch (err) { + console.error('Error sending notification via Electron invoke:', err); + } + return; + } + console.error('No available IPC method to send Electron notification.'); } diff --git a/src/lib/push/index.ts b/src/lib/push/index.ts index 4a34133d..9ecbc7d7 100644 --- a/src/lib/push/index.ts +++ b/src/lib/push/index.ts @@ -4,11 +4,15 @@ import { requestWebNotificationPermission, showWebLocalNotification } from './we import { requestNativeNotificationPermission, showNativeLocalNotification, initializeNativeNotificationListeners } from './native'; import { requestElectronNotificationPermission, showElectronLocalNotification } from './electron'; -// --- Platform Detection --- +// --- Platform Detection Functions --- +function checkIsElectron(): boolean { + // Check window property dynamically + return window.electronApi?.isElectron === true; +} -const isElectron = typeof window.seeditIpc?.send === 'function'; -const isNativePlatform = Capacitor.isNativePlatform(); -const isWebPlatform = !isNativePlatform && !isElectron; +function checkIsNativePlatform(): boolean { + return Capacitor.isNativePlatform(); +} let notificationIdCounter = Date.now(); // Simple counter for unique IDs @@ -19,10 +23,12 @@ let notificationIdCounter = Date.now(); // Simple counter for unique IDs * Should be called once when the app starts. */ export function initializeNotificationSystem(): void { - if (isNativePlatform) { + const isNative = checkIsNativePlatform(); + const isElectron = checkIsElectron(); + if (isNative) { initializeNativeNotificationListeners(); } - console.log('Notification system initialized for platform:', isNativePlatform ? 'Native' : isElectron ? 'Electron' : 'Web'); + console.log('Notification system initialized for platform:', isNative ? 'Native' : isElectron ? 'Electron' : 'Web'); } // --- Permissions --- @@ -32,14 +38,17 @@ export function initializeNotificationSystem(): void { * Must be called from a user interaction context (e.g., button click). */ export async function requestNotificationPermission(): Promise { - if (isNativePlatform) { + const isNative = checkIsNativePlatform(); + const isElectron = checkIsElectron(); + + if (isNative) { return requestNativeNotificationPermission(); } if (isElectron) { - // Permission is implicit, just check if IPC is working return requestElectronNotificationPermission(); } - if (isWebPlatform) { + // Fallback to Web Platform + if (!isNative && !isElectron) { return requestWebNotificationPermission(); } console.warn('Notification permission request: Unknown platform.'); @@ -52,21 +61,22 @@ export async function requestNotificationPermission(): Promise { * Shows a local notification using the appropriate platform API. */ export async function showLocalNotification(notificationData: Omit): Promise { - // Generate a unique ID required by Capacitor const id = notificationIdCounter++; const platformNotification = { ...notificationData, id }; console.log('Attempting to show notification:', platformNotification); - if (isNativePlatform) { + const isNative = checkIsNativePlatform(); + const isElectron = checkIsElectron(); + + if (isNative) { return showNativeLocalNotification(platformNotification); } if (isElectron) { - // Electron doesn't need the ID, but doesn't hurt to pass it return showElectronLocalNotification(platformNotification); } - if (isWebPlatform) { - // Service Worker doesn't need the ID, but doesn't hurt to pass it + // Fallback to Web Platform + if (!isNative && !isElectron) { return showWebLocalNotification(platformNotification); } diff --git a/src/plugins/file-uploader.ts b/src/plugins/file-uploader.ts index 18ce4f6d..95147bc8 100644 --- a/src/plugins/file-uploader.ts +++ b/src/plugins/file-uploader.ts @@ -41,12 +41,3 @@ const FileUploader = Capacitor.isNativePlatform() }; export default FileUploader; - -// Add TypeScript type declaration for Electron -declare global { - interface Window { - electron?: { - invoke(channel: string, ...args: any[]): Promise; - }; - } -} diff --git a/src/sw.ts b/src/sw.ts index 6c15d1af..65e27487 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -14,29 +14,32 @@ precacheAndRoute(self.__WB_MANIFEST); self.skipWaiting(); clientsClaim(); +// Interface for the notification data expected from the main app interface NotificationData { title: string; body: string; icon?: string; - url?: string; + url?: string; // URL to open on click } +// Listen for messages from the client (main app) self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SHOW_NOTIFICATION') { const data: NotificationData = event.data.payload; event.waitUntil( self.registration.showNotification(data.title, { body: data.body, - icon: data.icon || '/android-chrome-192x192.png', - data: { url: data.url }, - tag: data.body + '_' + Date.now(), + icon: data.icon || '/android-chrome-192x192.png', // Default icon + data: { url: data.url }, // Pass URL to click handler + tag: data.body + '_' + Date.now(), // Make the tag unique by adding a timestamp }), ); } }); +// Handle notification clicks self.addEventListener('notificationclick', (event) => { - event.notification.close(); + event.notification.close(); // Close the notification event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { diff --git a/src/views/about/about.tsx b/src/views/about/about.tsx index 2a2122a0..526c4776 100644 --- a/src/views/about/about.tsx +++ b/src/views/about/about.tsx @@ -50,10 +50,10 @@ export const FAQ = () => {

Welcome! Your account u/{account?.author?.shortAddress} was created automatically and it's stored locally ( - {window.isElectron ? 'on this desktop app' : isAndroid ? 'on this mobile app' : `on ${window.location.hostname}`}, not on a server). You can back up your - account in the preferences. There are no global rules or admins on Seedit, each community has its own rules and - moderators, so please be sure to read the rules of the community you are joining. You can connect peer-to-peer to any community by using the search bar, or you - can check out the default community list. + {window.electronApi?.isElectron ? 'on this desktop app' : isAndroid ? 'on this mobile app' : `on ${window.location.hostname}`}, not on a server). You can back + up your account in the preferences. There are no global rules or admins on Seedit, each community has its own rules + and moderators, so please be sure to read the rules of the community you are joining. You can connect peer-to-peer to any community by using the search bar, or + you can check out the default community list.


What is Seedit and how does it work?

diff --git a/src/views/settings/content-options/content-options.tsx b/src/views/settings/content-options/content-options.tsx index e63d8741..5ddb8ec1 100644 --- a/src/views/settings/content-options/content-options.tsx +++ b/src/views/settings/content-options/content-options.tsx @@ -8,10 +8,48 @@ const MediaOptions = () => { return (
+
thumbnails
+
+ +
+
+ +
+
+
media previews
+
+ +
+
+ +
+
+
Video File Player
+
+ +
+
{t('nsfw_content')}
- setBlurNsfwThumbnails(e.target.checked)} /> - +
); @@ -34,55 +72,80 @@ const CommunitiesOptions = () => { return (
+
{t('default_communities')}
{ if (el) { - const allChecked = !hideAdultCommunities && !hideGoreCommunities && !hideAntiCommunities && !hideVulgarCommunities; - const someChecked = !hideAdultCommunities || !hideGoreCommunities || !hideAntiCommunities || !hideVulgarCommunities; + const allHidden = hideAdultCommunities && hideGoreCommunities && hideAntiCommunities && hideVulgarCommunities; + const someHidden = hideAdultCommunities || hideGoreCommunities || hideAntiCommunities || hideVulgarCommunities; - el.checked = allChecked; - el.indeterminate = someChecked && !allChecked; + el.checked = allHidden; + el.indeterminate = someHidden && !allHidden; } }} onChange={(e) => { const newValue = e.target.checked; - setHideAdultCommunities(!newValue); - setHideGoreCommunities(!newValue); - setHideAntiCommunities(!newValue); - setHideVulgarCommunities(!newValue); + setHideAdultCommunities(newValue); + setHideGoreCommunities(newValue); + setHideAntiCommunities(newValue); + setHideVulgarCommunities(newValue); }} />

-
{t('communities')}
+
topbar