fix: remove element protocol support and update related functionality

This commit is contained in:
2026-05-25 01:01:06 +10:00
parent 6803eac8fb
commit a3c91b306a
3 changed files with 109 additions and 122 deletions

2
cinny

Submodule cinny updated: a4a0fa6758...d1626e3585

View File

@@ -39,7 +39,7 @@
"protocols": [ "protocols": [
{ {
"name": "Paarrot URL", "name": "Paarrot URL",
"schemes": ["paarrot", "element"] "schemes": ["paarrot"]
} }
], ],
"linux": { "linux": {
@@ -57,7 +57,7 @@
"Name": "Paarrot", "Name": "Paarrot",
"GenericName": "Matrix Client", "GenericName": "Matrix Client",
"Comment": "A Matrix client built with Cinny", "Comment": "A Matrix client built with Cinny",
"MimeType": "x-scheme-handler/paarrot;x-scheme-handler/element;", "MimeType": "x-scheme-handler/paarrot;",
"Categories": "Network;InstantMessaging;", "Categories": "Network;InstantMessaging;",
"Keywords": "matrix;chat;messaging;", "Keywords": "matrix;chat;messaging;",
"StartupWMClass": "paarrot" "StartupWMClass": "paarrot"

View File

@@ -1,7 +1,7 @@
const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer } = require('electron'); const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { exec, execFile } = require('child_process'); const { exec, execFile, execFileSync } = require('child_process');
const { promisify } = require('util'); const { promisify } = require('util');
const Store = require('electron-store'); const Store = require('electron-store');
const open = require('open'); const open = require('open');
@@ -19,24 +19,19 @@ const PORT = 44548;
const execAsync = promisify(exec); const execAsync = promisify(exec);
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
const store = new Store(); const store = new Store();
const PAARROT_PROTOCOL = 'paarrot'; const PROTOCOL_SCHEME = 'paarrot';
const ELEMENT_PROTOCOL = 'element';
const PRIMARY_PROTOCOL = PAARROT_PROTOCOL;
const SUPPORTED_PROTOCOLS = [PAARROT_PROTOCOL, ELEMENT_PROTOCOL];
let mainWindow = null; let mainWindow = null;
let tray = null; let tray = null;
let isQuitting = false; let isQuitting = false;
let apiServer = null; let apiServer = null;
let pendingDeepLink = null; let pendingDeepLink = null;
let protocolRegistrationState = Object.fromEntries( let protocolRegistrationState = {
SUPPORTED_PROTOCOLS.map((scheme) => [scheme, {
before: false, before: false,
registerCall: false, registerCall: false,
after: false, after: false,
error: null, error: null,
}]) };
);
// Allow audio playback without requiring a prior user gesture (needed for notification sounds) // Allow audio playback without requiring a prior user gesture (needed for notification sounds)
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
@@ -151,7 +146,7 @@ if (!gotTheLock) {
pendingDeepLink = extractDeepLinkFromArgv(process.argv); pendingDeepLink = extractDeepLinkFromArgv(process.argv);
/** /**
* Extracts a supported deep link URL from argv. * Extracts a paarrot:// deep link URL from argv.
* @param {string[]} argv - Process argument vector. * @param {string[]} argv - Process argument vector.
* @returns {string|null} The deep link URL or null. * @returns {string|null} The deep link URL or null.
*/ */
@@ -160,20 +155,13 @@ function extractDeepLinkFromArgv(argv) {
return null; return null;
} }
const match = argv.find((arg) => { const match = argv.find((arg) => typeof arg === 'string' && arg.toLowerCase().startsWith(`${PROTOCOL_SCHEME}://`));
if (typeof arg !== 'string') {
return false;
}
const lowerArg = arg.toLowerCase();
return SUPPORTED_PROTOCOLS.some((scheme) => lowerArg.startsWith(`${scheme}://`));
});
return match || null; return match || null;
} }
/** /**
* Converts a supported deep link into an in-app hash route. * Converts a paarrot:// deep link into an in-app hash route.
* @param {string} deepLink - The incoming protocol URL. * @param {string} deepLink - The incoming protocol URL.
* @returns {string|null} Hash route value without leading #. * @returns {string|null} Hash route value without leading #.
*/ */
@@ -182,9 +170,7 @@ function deepLinkToHashRoute(deepLink) {
return null; return null;
} }
const lowerDeepLink = deepLink.toLowerCase(); if (!deepLink.toLowerCase().startsWith(`${PROTOCOL_SCHEME}://`)) {
const isSupported = SUPPORTED_PROTOCOLS.some((scheme) => lowerDeepLink.startsWith(`${scheme}://`));
if (!isSupported) {
return null; return null;
} }
@@ -237,26 +223,34 @@ function applyDeepLink(deepLink) {
} }
/** /**
* Registers the app as handler for a URL scheme. * Registers the app as handler for paarrot:// links.
* @param {string} scheme - URL protocol scheme name.
* @returns {{ before: boolean; registerCall: boolean; after: boolean; error: string | null }} * @returns {{ before: boolean; registerCall: boolean; after: boolean; error: string | null }}
*/ */
function registerProtocolScheme(scheme) { function registerAppProtocol() {
const before = app.isDefaultProtocolClient(scheme); const before = app.isDefaultProtocolClient(PROTOCOL_SCHEME);
let registered = false; let registered = false;
let error = null; let error = null;
try { try {
if (process.defaultApp) { if (process.defaultApp) {
registered = app.setAsDefaultProtocolClient(scheme, process.execPath, [path.resolve(process.argv[1])]); registered = app.setAsDefaultProtocolClient(PROTOCOL_SCHEME, process.execPath, [path.resolve(process.argv[1])]);
} else { } else {
registered = app.setAsDefaultProtocolClient(scheme); registered = app.setAsDefaultProtocolClient(PROTOCOL_SCHEME);
} }
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : String(err); error = err instanceof Error ? err.message : String(err);
} }
const after = app.isDefaultProtocolClient(scheme); let after = app.isDefaultProtocolClient(PROTOCOL_SCHEME);
// In Windows dev mode, default ownership may stay false even when registry command points to this app.
if (!after && process.platform === 'win32') {
const currentCommand = getExpectedWindowsProtocolCommand();
const hkcuCommand = queryWindowsRegistryDefaultSync(`HKCU\\Software\\Classes\\${PROTOCOL_SCHEME}\\shell\\open\\command`);
if (hkcuCommand && normalizeWindowsCommand(hkcuCommand) === normalizeWindowsCommand(currentCommand)) {
after = true;
}
}
return { return {
before, before,
registerCall: registered, registerCall: registered,
@@ -266,14 +260,48 @@ function registerProtocolScheme(scheme) {
} }
/** /**
* Registers all supported URL protocol schemes. * Gets the command that should be registered for paarrot:// on Windows.
* @returns {string}
*/ */
function registerAppProtocols() { function getExpectedWindowsProtocolCommand() {
SUPPORTED_PROTOCOLS.forEach((scheme) => { return process.defaultApp && process.argv[1]
const schemeState = registerProtocolScheme(scheme); ? `"${process.execPath}" "${path.resolve(process.argv[1])}" "%1"`
protocolRegistrationState[scheme] = schemeState; : `"${process.execPath}" "%1"`;
console.log(`[Protocol] ${scheme}:// before=${schemeState.before} registerCall=${schemeState.registerCall} after=${schemeState.after}${schemeState.error ? ` error=${schemeState.error}` : ''}`); }
});
/**
* Normalizes a Windows command string for reliable comparisons.
* @param {string} command - Command string to normalize.
* @returns {string}
*/
function normalizeWindowsCommand(command) {
return String(command).replace(/\\/g, '/').replace(/\s+/g, ' ').trim().toLowerCase();
}
/**
* Reads the default Windows registry value synchronously.
* @param {string} keyPath - Registry key path.
* @returns {string|null} Default value content.
*/
function queryWindowsRegistryDefaultSync(keyPath) {
try {
const stdout = execFileSync('reg.exe', ['query', keyPath, '/ve'], { encoding: 'utf8' });
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
const valueLine = lines.find((line) => line.startsWith('(Default)'));
if (!valueLine) {
return null;
}
const parsed = valueLine.split(/\s{2,}/).filter(Boolean);
return parsed.length >= 3 ? parsed.slice(2).join(' ') : null;
} catch {
return null;
}
}
function syncProtocolRegistrationState() {
protocolRegistrationState = registerAppProtocol();
console.log(`[Protocol] ${PROTOCOL_SCHEME}:// before=${protocolRegistrationState.before} registerCall=${protocolRegistrationState.registerCall} after=${protocolRegistrationState.after}${protocolRegistrationState.error ? ` error=${protocolRegistrationState.error}` : ''}`);
} }
/** /**
@@ -320,13 +348,16 @@ async function queryWindowsRegistryDefault(keyPath) {
} }
/** /**
* Provides Windows-specific protocol association diagnostics for one scheme. * Provides Windows-specific protocol association diagnostics for paarrot://.
* @param {string} scheme - URL protocol scheme.
* @returns {Promise<{userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null}>} * @returns {Promise<{userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null}>}
*/ */
async function getWindowsProtocolDiagnosticsForScheme(scheme) { async function getWindowsProtocolDiagnostics() {
if (process.platform !== 'win32') {
return null;
}
const userChoiceProgId = await queryWindowsRegistryValue( const userChoiceProgId = await queryWindowsRegistryValue(
`HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\${scheme}\\UserChoice`, `HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\${PROTOCOL_SCHEME}\\UserChoice`,
'ProgId' 'ProgId'
); );
@@ -337,8 +368,8 @@ async function getWindowsProtocolDiagnosticsForScheme(scheme) {
(await queryWindowsRegistryDefault(`HKLM\\Software\\Classes\\${userChoiceProgId}\\shell\\open\\command`)); (await queryWindowsRegistryDefault(`HKLM\\Software\\Classes\\${userChoiceProgId}\\shell\\open\\command`));
} }
const hkcuCommand = await queryWindowsRegistryDefault(`HKCU\\Software\\Classes\\${scheme}\\shell\\open\\command`); const hkcuCommand = await queryWindowsRegistryDefault(`HKCU\\Software\\Classes\\${PROTOCOL_SCHEME}\\shell\\open\\command`);
const hklmCommand = await queryWindowsRegistryDefault(`HKLM\\Software\\Classes\\${scheme}\\shell\\open\\command`); const hklmCommand = await queryWindowsRegistryDefault(`HKLM\\Software\\Classes\\${PROTOCOL_SCHEME}\\shell\\open\\command`);
return { return {
userChoiceProgId, userChoiceProgId,
@@ -349,25 +380,22 @@ async function getWindowsProtocolDiagnosticsForScheme(scheme) {
} }
/** /**
* Ensures Windows has user-level URL protocol class entries for a scheme * Ensures Windows has user-level URL protocol class entries for paarrot://
* so the scheme appears in Default Apps link associations. * so the scheme appears in Default Apps link associations.
* @param {string} scheme - URL protocol scheme.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function ensureWindowsRegistryEntriesForScheme(scheme) { async function ensureWindowsProtocolRegistryEntries() {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
return; return;
} }
const openCommand = process.defaultApp && process.argv[1] const openCommand = getExpectedWindowsProtocolCommand();
? `"${process.execPath}" "${path.resolve(process.argv[1])}" "%1"`
: `"${process.execPath}" "%1"`;
const iconValue = `"${process.execPath}",0`; const iconValue = `"${process.execPath}",0`;
const protocolDisplayName = `URL:${scheme} Protocol`; const protocolDisplayName = `URL:${PROTOCOL_SCHEME} Protocol`;
await execFileAsync('reg.exe', [ await execFileAsync('reg.exe', [
'add', 'add',
`HKCU\\Software\\Classes\\${scheme}`, `HKCU\\Software\\Classes\\${PROTOCOL_SCHEME}`,
'/ve', '/ve',
'/t', '/t',
'REG_SZ', 'REG_SZ',
@@ -378,7 +406,7 @@ async function ensureWindowsRegistryEntriesForScheme(scheme) {
await execFileAsync('reg.exe', [ await execFileAsync('reg.exe', [
'add', 'add',
`HKCU\\Software\\Classes\\${scheme}`, `HKCU\\Software\\Classes\\${PROTOCOL_SCHEME}`,
'/v', '/v',
'URL Protocol', 'URL Protocol',
'/t', '/t',
@@ -390,7 +418,7 @@ async function ensureWindowsRegistryEntriesForScheme(scheme) {
await execFileAsync('reg.exe', [ await execFileAsync('reg.exe', [
'add', 'add',
`HKCU\\Software\\Classes\\${scheme}\\DefaultIcon`, `HKCU\\Software\\Classes\\${PROTOCOL_SCHEME}\\DefaultIcon`,
'/ve', '/ve',
'/t', '/t',
'REG_SZ', 'REG_SZ',
@@ -401,7 +429,7 @@ async function ensureWindowsRegistryEntriesForScheme(scheme) {
await execFileAsync('reg.exe', [ await execFileAsync('reg.exe', [
'add', 'add',
`HKCU\\Software\\Classes\\${scheme}\\shell\\open\\command`, `HKCU\\Software\\Classes\\${PROTOCOL_SCHEME}\\shell\\open\\command`,
'/ve', '/ve',
'/t', '/t',
'REG_SZ', 'REG_SZ',
@@ -411,51 +439,20 @@ async function ensureWindowsRegistryEntriesForScheme(scheme) {
]); ]);
} }
/**
* Provides Windows diagnostics for all supported schemes.
* @returns {Promise<null|Record<string, {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null}>>}
*/
async function getWindowsProtocolDiagnostics() {
if (process.platform !== 'win32') {
return null;
}
const diagnostics = {};
for (const scheme of SUPPORTED_PROTOCOLS) {
diagnostics[scheme] = await getWindowsProtocolDiagnosticsForScheme(scheme);
}
return diagnostics;
}
/** /**
* Returns renderer-friendly protocol diagnostics payload. * Returns renderer-friendly protocol diagnostics payload.
* @returns {Promise<{ success: boolean; data: { platform: string; primaryScheme: string; schemes: Record<string, {isDefault: boolean; before: boolean; registerCall: boolean; error: string | null}>; windows: null | Record<string, {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null}> } }>} * @returns {Promise<{ success: boolean; data: { scheme: string; platform: string; isDefault: boolean; before: boolean; registerCall: boolean; error: string | null; windows: null | {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null} } }>}
*/ */
async function getProtocolStatusPayload() { async function getProtocolStatusPayload() {
const schemes = {};
for (const scheme of SUPPORTED_PROTOCOLS) {
const state = protocolRegistrationState[scheme] || {
before: false,
registerCall: false,
after: false,
error: null,
};
schemes[scheme] = {
isDefault: app.isDefaultProtocolClient(scheme),
before: state.before,
registerCall: state.registerCall,
error: state.error,
};
}
return { return {
success: true, success: true,
data: { data: {
scheme: PROTOCOL_SCHEME,
platform: process.platform, platform: process.platform,
primaryScheme: PRIMARY_PROTOCOL, isDefault: app.isDefaultProtocolClient(PROTOCOL_SCHEME),
schemes, before: protocolRegistrationState.before,
registerCall: protocolRegistrationState.registerCall,
error: protocolRegistrationState.error,
windows: await getWindowsProtocolDiagnostics(), windows: await getWindowsProtocolDiagnostics(),
}, },
}; };
@@ -826,7 +823,13 @@ function createTray() {
// App lifecycle // App lifecycle
app.whenReady().then(() => { app.whenReady().then(() => {
registerAppProtocols(); if (process.platform === 'win32') {
ensureWindowsProtocolRegistryEntries().catch((error) => {
console.warn('[Protocol] Startup registry ensure failed:', error);
});
}
syncProtocolRegistrationState();
createWindow(); createWindow();
createTray(); createTray();
@@ -940,47 +943,31 @@ ipcMain.handle('window:is-maximized', () => {
/** /**
* Returns protocol registration health for the renderer settings page. * Returns protocol registration health for the renderer settings page.
* @returns {Promise<{ success: boolean; data: { platform: string; primaryScheme: string; schemes: Record<string, {isDefault: boolean; before: boolean; registerCall: boolean; error: string | null}>; windows: null | Record<string, {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null}> } }>} * @returns {Promise<{ success: boolean; data: { scheme: string; platform: string; isDefault: boolean; before: boolean; registerCall: boolean; error: string | null; windows: null | {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null} } }>}
*/ */
ipcMain.handle('protocol:get-status', () => { ipcMain.handle('protocol:get-status', () => {
return getProtocolStatusPayload(); return getProtocolStatusPayload();
}); });
/** /**
* Re-runs protocol registration and opens OS default-apps page when manual selection is required. * Re-runs protocol registry ensure and protocol registration.
* @returns {Promise<{ success: boolean; data: { openedSettings: boolean; platform: string; primaryScheme: string; schemes: Record<string, {isDefault: boolean; before: boolean; registerCall: boolean; error: string | null}>; windows: null | Record<string, {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null}> } }>} * @returns {Promise<{ success: boolean; data: { scheme: string; platform: string; isDefault: boolean; before: boolean; registerCall: boolean; error: string | null; windows: null | {userChoiceProgId: string | null; userChoiceCommand: string | null; hkcuCommand: string | null; hklmCommand: string | null} } }>}
*/ */
ipcMain.handle('protocol:repair', async () => { ipcMain.handle('protocol:repair', async () => {
if (process.platform === 'win32') { if (process.platform === 'win32') {
for (const scheme of SUPPORTED_PROTOCOLS) {
try { try {
await ensureWindowsRegistryEntriesForScheme(scheme); await ensureWindowsProtocolRegistryEntries();
} catch (error) { } catch (error) {
console.warn(`[Protocol] Failed to write Windows URL protocol registry entries for ${scheme}:`, error); console.warn('[Protocol] Failed to write Windows URL protocol registry entries:', error);
}
} }
} }
registerAppProtocols(); syncProtocolRegistrationState();
let openedSettings = false;
const isDefault = app.isDefaultProtocolClient(PRIMARY_PROTOCOL);
if (process.platform === 'win32' && !isDefault) {
try {
await shell.openExternal('ms-settings:defaultapps');
openedSettings = true;
} catch (error) {
console.warn('[Protocol] Failed to open Windows default apps settings:', error);
}
}
const status = await getProtocolStatusPayload(); const status = await getProtocolStatusPayload();
return { return {
success: true, success: true,
data: { data: status.data,
openedSettings,
...status.data,
},
}; };
}); });