mirror of
https://github.com/plebbit/seedit.git
synced 2026-06-11 17:46:27 -04:00
fix context flags, ipc methods, push notification logic
This commit is contained in:
@@ -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.
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
@@ -9,7 +9,8 @@ declare global {
|
||||
interface Window {
|
||||
electronApi?: {
|
||||
isElectron: boolean;
|
||||
getNotificationStatus: () => Promise<'granted' | 'denied' | 'not-determined'>;
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
getNotificationStatus: () => Promise<'granted' | 'denied' | 'not-determined' | 'not-supported'>;
|
||||
getPlatform: () => Promise<NodeJS.Platform>;
|
||||
testNotification: () => Promise<{ success: boolean; reason?: string }>;
|
||||
};
|
||||
|
||||
@@ -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<any>;
|
||||
sendNotification?: (notification: Omit<LocalNotification, 'id'>) => void;
|
||||
};
|
||||
// The new electronApi for cleaner separation
|
||||
electronApi?: {
|
||||
// Allow direct invoke as exposed in preload
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
isElectron: boolean;
|
||||
getNotificationStatus: () => Promise<'granted' | 'denied' | 'not-determined' | 'not-supported'>;
|
||||
getPlatform: () => Promise<NodeJS.Platform>;
|
||||
testNotification: () => Promise<{ success: boolean; reason?: string }>;
|
||||
showNotification: (notification: Omit<LocalNotification, 'id'>) => 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<boolean> {
|
||||
// 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<LocalNotification, 'id'>): Promise<void> {
|
||||
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.');
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
* Shows a local notification using the appropriate platform API.
|
||||
*/
|
||||
export async function showLocalNotification(notificationData: Omit<LocalNotification, 'id'>): Promise<void> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<any>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
13
src/sw.ts
13
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) => {
|
||||
|
||||
@@ -50,10 +50,10 @@ export const FAQ = () => {
|
||||
</h3>
|
||||
<p>
|
||||
Welcome! Your account <Link to='/profile'>u/{account?.author?.shortAddress}</Link> 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 <Link to='/settings#exportAccount'>preferences</Link>. 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 <Link to='/communities/vote'>default community list</Link>.
|
||||
{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 <Link to='/settings#exportAccount'>preferences</Link>. 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 <Link to='/communities/vote'>default community list</Link>.
|
||||
</p>
|
||||
<hr />
|
||||
<h3 id='whatIsSeedit'>What is Seedit and how does it work?</h3>
|
||||
|
||||
@@ -8,10 +8,48 @@ const MediaOptions = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.contentOptions}>
|
||||
<div className={styles.contentOptionTitle}>thumbnails</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type='radio' />
|
||||
Show thumbnails next to links
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type='radio' />
|
||||
Don't show thumbnails next to links
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<div className={styles.contentOptionTitle}>media previews</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type='radio' />
|
||||
Auto-expand media previews
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type='radio' />
|
||||
Don't auto-expand media previews on comments pages
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<div className={styles.contentOptionTitle}>Video File Player</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type='checkbox' />
|
||||
Autoplay video files on the comments page
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<div className={styles.contentOptionTitle}>{t('nsfw_content')}</div>
|
||||
<div>
|
||||
<input type='checkbox' id='blurNsfwThumbnails' checked={blurNsfwThumbnails} onChange={(e) => setBlurNsfwThumbnails(e.target.checked)} />
|
||||
<label htmlFor='blurNsfwThumbnails'>{t('blur_media')}</label>
|
||||
<label>
|
||||
<input type='checkbox' checked={blurNsfwThumbnails} onChange={(e) => setBlurNsfwThumbnails(e.target.checked)} />
|
||||
{t('blur_media')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -34,55 +72,80 @@ const CommunitiesOptions = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.contentOptions}>
|
||||
<div className={styles.contentOptionTitle}>{t('default_communities')}</div>
|
||||
<div>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='hideAdultCommunities'
|
||||
ref={(el) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor='hideAdultCommunities'>{t('hide_communities_tagged_as_nsfw')}</label>
|
||||
</div>
|
||||
<div className={styles.nsfwTag}>
|
||||
<label>
|
||||
<input type='checkbox' checked={!hideAdultCommunities} onChange={(e) => setHideAdultCommunities(!e.target.checked)} />
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={hideAdultCommunities}
|
||||
onChange={(e) => {
|
||||
setHideAdultCommunities(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{t('tagged_as_adult')}
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.nsfwTag}>
|
||||
<label>
|
||||
<input type='checkbox' checked={!hideGoreCommunities} onChange={(e) => setHideGoreCommunities(!e.target.checked)} />
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={hideGoreCommunities}
|
||||
onChange={(e) => {
|
||||
setHideGoreCommunities(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{t('tagged_as_gore')}
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.nsfwTag}>
|
||||
<label>
|
||||
<input type='checkbox' checked={!hideAntiCommunities} onChange={(e) => setHideAntiCommunities(!e.target.checked)} />
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={hideAntiCommunities}
|
||||
onChange={(e) => {
|
||||
setHideAntiCommunities(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{t('tagged_as_anti')}
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.nsfwTag}>
|
||||
<label>
|
||||
<input type='checkbox' checked={!hideVulgarCommunities} onChange={(e) => setHideVulgarCommunities(!e.target.checked)} />
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={hideVulgarCommunities}
|
||||
onChange={(e) => {
|
||||
setHideVulgarCommunities(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{t('tagged_as_vulgar')}
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<div className={styles.contentOptionTitle}>{t('communities')}</div>
|
||||
<div className={styles.contentOptionTitle}>topbar</div>
|
||||
<label>
|
||||
<input type='checkbox' checked={hideDefaultCommunities} onChange={(e) => setHideDefaultCommunities(e.target.checked)} />
|
||||
{t('hide_default_communities_from_topbar')}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import useContentOptionsStore from '../../../stores/use-content-options-store';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { requestNotificationPermission } from '../../../lib/push';
|
||||
import styles from './notifications-settings.module.css';
|
||||
import useContentOptionsStore from '../../../stores/use-content-options-store';
|
||||
|
||||
const NotificationsSettings = () => {
|
||||
const { enableLocalNotifications, setEnableLocalNotifications } = useContentOptionsStore();
|
||||
@@ -10,88 +9,75 @@ const NotificationsSettings = () => {
|
||||
const [platform, setPlatform] = useState<NodeJS.Platform | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showDeniedMessage, setShowDeniedMessage] = useState(false);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||
|
||||
// Function to check permission via API, memoized with useCallback
|
||||
const checkPermissionStatus = useCallback(async () => {
|
||||
// if (!window.electronApi?.getNotificationStatus) return; // Commented out
|
||||
console.warn('[NotificationsSettings] checkPermissionStatus called, but electronApi.getNotificationStatus is disabled.');
|
||||
if (!window.electronApi?.getNotificationStatus) return;
|
||||
|
||||
console.log('[Electron Native] Checking OS notification permission status...');
|
||||
try {
|
||||
// This call now correctly handles 'not-supported' internally in main.cjs
|
||||
// const nativeStatus = await window.electronApi.getNotificationStatus(); // Commented out
|
||||
const nativeStatus = 'unknown' as any; // Mock status, cast to any to bypass linter
|
||||
const nativeStatus = await window.electronApi.getNotificationStatus();
|
||||
console.log('[Electron Native] OS permission status from native API:', nativeStatus);
|
||||
|
||||
setPermissionStatus(nativeStatus); // Directly set the status received
|
||||
|
||||
if (nativeStatus === 'granted') {
|
||||
// On macOS, even if the API returns 'granted', we should do a real test
|
||||
// to confirm notifications are actually working, unless it's already tested ok
|
||||
// if (platform === 'darwin' && !testResult?.success) { // Logic using testNotificationPermission commented out
|
||||
// testNotificationPermission(); // Test to ensure it *really* works
|
||||
// } else
|
||||
if (!enableLocalNotifications) {
|
||||
// Update store only if needed
|
||||
setEnableLocalNotifications(true);
|
||||
}
|
||||
} else if (nativeStatus === 'denied') {
|
||||
if (nativeStatus === 'denied') {
|
||||
if (enableLocalNotifications) {
|
||||
setEnableLocalNotifications(false);
|
||||
}
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
} else if (nativeStatus === 'not-determined') {
|
||||
// If undetermined, try a direct test which might trigger the prompt
|
||||
// testNotificationPermission(); // Logic using testNotificationPermission commented out
|
||||
console.warn('[NotificationsSettings] Permission status is not-determined, cannot test.');
|
||||
} else if (nativeStatus === 'not-supported') {
|
||||
// If not supported, ensure checkbox is off
|
||||
if (enableLocalNotifications) {
|
||||
setEnableLocalNotifications(false);
|
||||
}
|
||||
} else if (nativeStatus === 'unknown') {
|
||||
// Handle the mocked 'unknown' state
|
||||
console.warn('[NotificationsSettings] Permission status is unknown (Electron API disabled).');
|
||||
// Optionally disable the checkbox or show a specific message
|
||||
if (enableLocalNotifications) {
|
||||
// Maybe keep it enabled but show a warning?
|
||||
// Or disable it:
|
||||
// setEnableLocalNotifications(false);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Electron Native] Error checking notification permissions:', err);
|
||||
// On error, fall back to the test notification approach as a last resort
|
||||
// testNotificationPermission(); // Logic using testNotificationPermission commented out
|
||||
setPermissionStatus('unknown'); // Set status to unknown on error too
|
||||
setPermissionStatus('unknown');
|
||||
}
|
||||
}, [enableLocalNotifications, setEnableLocalNotifications /*, testNotificationPermission */]); // Dependencies for checkPermissionStatus, commented out testNotificationPermission
|
||||
}, [setEnableLocalNotifications]);
|
||||
|
||||
// Run the direct test on mount
|
||||
// Run the check on mount
|
||||
useEffect(() => {
|
||||
if (window.electronApi) {
|
||||
// Get platform first
|
||||
if (window.electronApi.getPlatform) {
|
||||
window.electronApi.getPlatform().then(setPlatform).catch(console.error);
|
||||
}
|
||||
|
||||
// Then check notification permission status
|
||||
checkPermissionStatus();
|
||||
}
|
||||
}, [checkPermissionStatus]); // Now depends on the memoized function
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Keep dependencies empty - runs only once
|
||||
|
||||
const handleCheckboxChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isEnabled = event.target.checked;
|
||||
setIsLoading(true);
|
||||
setShowSuccessMessage(false);
|
||||
|
||||
try {
|
||||
if (isEnabled) {
|
||||
// If in Electron, do a direct test
|
||||
if (window.electronApi) {
|
||||
// await testNotificationPermission(); // Commented out
|
||||
console.warn('[NotificationsSettings] testNotificationPermission call skipped in handleCheckboxChange.');
|
||||
setPermissionStatus('unknown'); // Set to unknown as we can't test
|
||||
// If in Electron, check status first
|
||||
if (window.electronApi?.getNotificationStatus) {
|
||||
const currentStatus = await window.electronApi.getNotificationStatus();
|
||||
setPermissionStatus(currentStatus);
|
||||
if (currentStatus === 'granted') {
|
||||
setEnableLocalNotifications(true);
|
||||
setShowSuccessMessage(true);
|
||||
setTimeout(() => setShowSuccessMessage(false), 5000);
|
||||
} else if (currentStatus === 'denied') {
|
||||
setEnableLocalNotifications(false); // Ensure it's off if denied
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
} else if (currentStatus === 'not-determined') {
|
||||
setEnableLocalNotifications(false); // Keep it off until granted
|
||||
console.warn('[NotificationsSettings] Permission not determined. User must grant via OS prompt.');
|
||||
alert('Notification permission needed. The app will ask when it first tries to notify you, or check System Settings.');
|
||||
} else if (currentStatus === 'not-supported') {
|
||||
setEnableLocalNotifications(false); // Keep it off
|
||||
}
|
||||
} else {
|
||||
// Use the web browser API for non-Electron
|
||||
setPermissionStatus('requesting...');
|
||||
@@ -99,6 +85,8 @@ const NotificationsSettings = () => {
|
||||
if (granted) {
|
||||
setEnableLocalNotifications(true);
|
||||
setPermissionStatus('granted');
|
||||
setShowSuccessMessage(true);
|
||||
setTimeout(() => setShowSuccessMessage(false), 5000);
|
||||
} else {
|
||||
setEnableLocalNotifications(false);
|
||||
setPermissionStatus('denied');
|
||||
@@ -108,7 +96,6 @@ const NotificationsSettings = () => {
|
||||
}
|
||||
} else {
|
||||
setEnableLocalNotifications(false);
|
||||
// Don't change permissionStatus when disabling
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -117,11 +104,33 @@ const NotificationsSettings = () => {
|
||||
|
||||
// Function to manually test a notification
|
||||
const showTestNotification = () => {
|
||||
// If we're on Electron, we should verify permission status first
|
||||
if (window.electronApi) {
|
||||
// testNotificationPermission(); // Commented out
|
||||
console.warn('[NotificationsSettings] testNotificationPermission call skipped in showTestNotification.');
|
||||
alert('Cannot test Electron notifications currently.');
|
||||
// If we're on Electron, check status before attempting to show
|
||||
if (window.electronApi?.getNotificationStatus) {
|
||||
window.electronApi
|
||||
.getNotificationStatus()
|
||||
.then((status) => {
|
||||
if (status === 'granted') {
|
||||
// Use the showElectronLocalNotification via the main push index
|
||||
import('../../../lib/push').then(({ showLocalNotification }) => {
|
||||
showLocalNotification({
|
||||
title: 'Electron Test Notification!',
|
||||
body: 'If you see this, permissions are working!',
|
||||
});
|
||||
});
|
||||
} else if (status === 'denied') {
|
||||
alert('Notifications are denied in System Settings.');
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
} else if (status === 'not-determined') {
|
||||
alert('Permission not yet granted. The app will ask when it first tries to notify you (or test again).');
|
||||
} else {
|
||||
alert('Notifications may not be supported on this system.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error checking status before test:', err);
|
||||
alert('Could not check notification status before testing.');
|
||||
});
|
||||
} else {
|
||||
import('../../../lib/push').then(({ showLocalNotification }) => {
|
||||
showLocalNotification({
|
||||
@@ -168,7 +177,7 @@ const NotificationsSettings = () => {
|
||||
<span className={styles.permissionStatusRequesting}>Click "Allow" to enable notifications</span>
|
||||
</span>
|
||||
)}
|
||||
{permissionStatus === 'granted' && (
|
||||
{showSuccessMessage && permissionStatus === 'granted' && enableLocalNotifications && (
|
||||
<span className={styles.permissionStatus} data-status={permissionStatus}>
|
||||
<span className={styles.permissionStatusSuccess}>
|
||||
Success! You're done.
|
||||
|
||||
@@ -179,7 +179,7 @@ const PlebbitDataPathSettings = ({ plebbitDataPathRef }: SettingsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isElectron = window.isElectron === true;
|
||||
const isElectron = window.electronApi?.isElectron === true;
|
||||
|
||||
const PlebbitOptions = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FormattingHelpTable } from '../../components/reply-form/reply-form';
|
||||
import styles from './submit-page.module.css';
|
||||
|
||||
const isAndroid = Capacitor.getPlatform() === 'android';
|
||||
const isElectron = window.isElectron === true;
|
||||
const isElectron = window.electronApi?.isElectron === true;
|
||||
|
||||
const UrlField = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -347,7 +347,7 @@ const JSONSettings = ({ isReadOnly = false }: { isReadOnly?: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isElectron = window.isElectron === true;
|
||||
const isElectron = window.electronApi?.isElectron === true;
|
||||
|
||||
const SubplebbitSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -37,6 +37,9 @@ export default defineConfig({
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
strategies: 'injectManifest',
|
||||
injectManifest: {
|
||||
maximumFileSizeToCacheInBytes: 6000000,
|
||||
},
|
||||
srcDir: 'src',
|
||||
filename: 'sw.ts',
|
||||
devOptions: {
|
||||
@@ -74,7 +77,6 @@ export default defineConfig({
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
maximumFileSizeToCacheInBytes: 6000000,
|
||||
navigateFallback: 'index.html',
|
||||
navigateFallbackDenylist: [/^\/api/, /^\/_(.*)/],
|
||||
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -3718,22 +3718,6 @@
|
||||
uuid "8.3.2"
|
||||
zustand "4.0.0"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#19f82d4a5763c53fadf40c87bc1d830c111788f2":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#19f82d4a5763c53fadf40c87bc1d830c111788f2"
|
||||
dependencies:
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#26c097afa62a87c9650dcdfad52babbc77744cbc"
|
||||
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
|
||||
assert "2.0.0"
|
||||
ethers "5.6.9"
|
||||
localforage "1.10.0"
|
||||
lodash.isequal "4.5.0"
|
||||
memoizee "0.4.15"
|
||||
quick-lru "5.1.1"
|
||||
uint8arrays "3.1.1"
|
||||
uuid "8.3.2"
|
||||
zustand "4.0.0"
|
||||
|
||||
"@plebbit/proper-lockfile@github:plebbit/node-proper-lockfile#7fd6332117340c1d3d98dd0afee2d31cc06f72b8":
|
||||
version "4.1.2"
|
||||
resolved "https://codeload.github.com/plebbit/node-proper-lockfile/tar.gz/7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
|
||||
|
||||
Reference in New Issue
Block a user