feat: add support for element:// deep links and register protocol handler
This commit is contained in:
Binary file not shown.
@@ -36,6 +36,12 @@
|
|||||||
"filter": ["**/*"]
|
"filter": ["**/*"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"protocols": [
|
||||||
|
{
|
||||||
|
"name": "Element URL",
|
||||||
|
"schemes": ["element"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
@@ -51,6 +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/element;",
|
||||||
"Categories": "Network;InstantMessaging;",
|
"Categories": "Network;InstantMessaging;",
|
||||||
"Keywords": "matrix;chat;messaging;",
|
"Keywords": "matrix;chat;messaging;",
|
||||||
"StartupWMClass": "paarrot"
|
"StartupWMClass": "paarrot"
|
||||||
|
|||||||
115
electron/main.js
115
electron/main.js
@@ -18,11 +18,13 @@ const PORT = 44548;
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
const ELEMENT_PROTOCOL = 'element';
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
// 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');
|
||||||
@@ -115,16 +117,116 @@ const gotTheLock = app.requestSingleInstanceLock();
|
|||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit();
|
app.quit();
|
||||||
} else {
|
} else {
|
||||||
app.on('second-instance', () => {
|
app.on('second-instance', (event, argv) => {
|
||||||
// Someone tried to run a second instance, focus our window
|
// Someone tried to run a second instance, focus our window
|
||||||
|
const deepLink = extractDeepLinkFromArgv(argv);
|
||||||
|
if (deepLink) {
|
||||||
|
pendingDeepLink = deepLink;
|
||||||
|
}
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
|
|
||||||
|
if (pendingDeepLink) {
|
||||||
|
applyDeepLink(pendingDeepLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pendingDeepLink = extractDeepLinkFromArgv(process.argv);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts an element:// deep link URL from argv.
|
||||||
|
* @param {string[]} argv - Process argument vector.
|
||||||
|
* @returns {string|null} The deep link URL or null.
|
||||||
|
*/
|
||||||
|
function extractDeepLinkFromArgv(argv) {
|
||||||
|
if (!Array.isArray(argv)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = argv.find((arg) => typeof arg === 'string' && arg.startsWith(`${ELEMENT_PROTOCOL}://`));
|
||||||
|
return match || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an element:// deep link into an in-app hash route.
|
||||||
|
* @param {string} deepLink - The incoming protocol URL.
|
||||||
|
* @returns {string|null} Hash route value without leading #.
|
||||||
|
*/
|
||||||
|
function deepLinkToHashRoute(deepLink) {
|
||||||
|
if (!deepLink || !deepLink.startsWith(`${ELEMENT_PROTOCOL}://`)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashIndex = deepLink.indexOf('#');
|
||||||
|
if (hashIndex === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashValue = deepLink.slice(hashIndex + 1);
|
||||||
|
if (!hashValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashValue.startsWith('/') ? hashValue : `/${hashValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a deep link to the current window and brings the app to foreground.
|
||||||
|
* @param {string} deepLink - The incoming protocol URL.
|
||||||
|
*/
|
||||||
|
function applyDeepLink(deepLink) {
|
||||||
|
if (!mainWindow) {
|
||||||
|
pendingDeepLink = deepLink;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashRoute = deepLinkToHashRoute(deepLink);
|
||||||
|
if (!hashRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.loadURL(`${VITE_DEV_SERVER}/#${hashRoute}`).catch((error) => {
|
||||||
|
console.error('[Protocol] Failed to load deep link route in dev:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(path.join(__dirname, '../cinny/dist/index.html'), { hash: hashRoute }).catch((error) => {
|
||||||
|
console.error('[Protocol] Failed to load deep link route in production:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDeepLink = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the app as handler for element:// links.
|
||||||
|
*/
|
||||||
|
function registerElementProtocol() {
|
||||||
|
const before = app.isDefaultProtocolClient(ELEMENT_PROTOCOL);
|
||||||
|
|
||||||
|
let registered = false;
|
||||||
|
if (process.defaultApp) {
|
||||||
|
registered = app.setAsDefaultProtocolClient(ELEMENT_PROTOCOL, process.execPath, [path.resolve(process.argv[1])]);
|
||||||
|
} else {
|
||||||
|
registered = app.setAsDefaultProtocolClient(ELEMENT_PROTOCOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = app.isDefaultProtocolClient(ELEMENT_PROTOCOL);
|
||||||
|
console.log(`[Protocol] element:// before=${before} registerCall=${registered} after=${after}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to get correct icon path in dev vs packaged app
|
// Helper to get correct icon path in dev vs packaged app
|
||||||
function getIconPath(iconName) {
|
function getIconPath(iconName) {
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
@@ -490,9 +592,15 @@ function createTray() {
|
|||||||
|
|
||||||
// App lifecycle
|
// App lifecycle
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
registerElementProtocol();
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
|
|
||||||
|
if (pendingDeepLink) {
|
||||||
|
applyDeepLink(pendingDeepLink);
|
||||||
|
}
|
||||||
|
|
||||||
// Start API server
|
// Start API server
|
||||||
apiServer = new PaarrotAPIServer(mainWindow);
|
apiServer = new PaarrotAPIServer(mainWindow);
|
||||||
apiServer.start().catch(err => {
|
apiServer.start().catch(err => {
|
||||||
@@ -517,6 +625,11 @@ app.whenReady().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('open-url', (event, url) => {
|
||||||
|
event.preventDefault();
|
||||||
|
applyDeepLink(url);
|
||||||
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
// On macOS, apps stay active until Cmd+Q
|
// On macOS, apps stay active until Cmd+Q
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
|
|||||||
Reference in New Issue
Block a user