From 6656a3749ef114f83af4bfb2f99a25b6aac424a8 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Wed, 17 Jun 2020 20:45:09 -0400 Subject: [PATCH] Make likable paths across desktop app work --- public/electron.js | 452 +++++++++++++++++++----------------- src/pages/Notifications.tsx | 5 +- src/utilities/desktop.tsx | 8 + 3 files changed, 251 insertions(+), 214 deletions(-) diff --git a/public/electron.js b/public/electron.js index 547afe6..0605057 100644 --- a/public/electron.js +++ b/public/electron.js @@ -2,10 +2,17 @@ // Electron script to run Hyperspace as an app // © 2018 Hyperspace developers. Licensed under NPL v1. -const { app, Menu, protocol, BrowserWindow, shell, systemPreferences } = require('electron'); -const windowStateKeeper = require('electron-window-state'); -const { autoUpdater } = require('electron-updater'); -const path = require('path'); +const { + app, + Menu, + protocol, + BrowserWindow, + shell, + systemPreferences +} = require("electron"); +const windowStateKeeper = require("electron-window-state"); +const { autoUpdater } = require("electron-updater"); +const path = require("path"); // Check for any updates to the app autoUpdater.checkForUpdatesAndNotify(); @@ -18,7 +25,7 @@ let mainWindow; // file:// protocol, which is necessary for Mastodon to redirect // to when authorizing Hyperspace. protocol.registerSchemesAsPrivileged([ - { scheme: 'hyperspace', privileges: { standard: true, secure: true } } + { scheme: "hyperspace", privileges: { standard: true, secure: true } } ]); /** @@ -33,80 +40,81 @@ function isDarwin() { * Register the protocol for Hyperspace */ function registerProtocol() { - protocol.registerFileProtocol('hyperspace', (request, callback) => { - - // Check to make sure we're doing a GET request - if (request.method !== "GET") { - callback({error: -322}); - return null; + protocol.registerFileProtocol( + "hyperspace", + (request, callback) => { + // Check to make sure we're doing a GET request + if (request.method !== "GET") { + callback({ error: -322 }); + return null; + } + + // Check to make sure we're actually working with a hyperspace + // protocol and that the host is 'hyperspace' + const parsedUrl = new URL(request.url); + if (parsedUrl.protocol !== "hyperspace:") { + callback({ error: -302 }); + return; + } + + if (parsedUrl.host !== "hyperspace") { + callback({ error: -105 }); + return; + } + + // Convert the parsed URL to a list of strings. + const target = parsedUrl.pathname.split("/"); + + // Check that the target isn't trying to go somewhere + // else. If it is, throw a "FILE_NOT_FOUND" error + if (target[0] !== "") { + callback({ error: -6 }); + return; + } + + // Check if the last target item in the list is empty. + // If so, replace it with "index.html" so that it can + // load a page. + if (target[target.length - 1] === "") { + target[target.length - 1] = "index.html"; + } + + // Check the middle target and redirect to the appropriate + // build files of the desktop app when running. + let baseDirectory; + if (target[1] === "app" || target[1] === "oauth") { + baseDirectory = __dirname + "/../build/"; + } else { + // If it doesn't match above, throw a "FILE_NOT_FOUND" error. + callback({ error: -6 }); + return; + } + + // Create a normalized version of the string. + baseDirectory = path.normalize(baseDirectory); + + // Check to make sure the target isn't trying to go out of bounds. + // If it is, throw a "FILE_NOT_FOUND" error. + const relTarget = path.normalize(path.join(...target.slice(2))); + if (relTarget.startsWith("..")) { + callback({ error: -6 }); + return; + } + + // Create the absolute target path and return it. + const absTarget = path.join(baseDirectory, relTarget); + callback({ path: absTarget }); + }, + error => { + if (error) console.error("Failed to register protocol"); } - - // Check to make sure we're actually working with a hyperspace - // protocol and that the host is 'hyperspace' - const parsedUrl = new URL(request.url); - if (parsedUrl.protocol !== "hyperspace:") { - callback({error: -302}); - return; - } - - if (parsedUrl.host !== "hyperspace") { - callback({error: -105}); - return; - } - - // Convert the parsed URL to a list of strings. - const target = parsedUrl.pathname.split("/"); - - // Check that the target isn't trying to go somewhere - // else. If it is, throw a "FILE_NOT_FOUND" error - if (target[0] !== "") { - callback({error: -6}); - return; - } - - // Check if the last target item in the list is empty. - // If so, replace it with "index.html" so that it can - // load a page. - if (target[target.length -1] === "") { - target[target.length -1] = "index.html"; - } - - // Check the middle target and redirect to the appropriate - // build files of the desktop app when running. - let baseDirectory; - if (target[1] === "app" || target[1] === "oauth") { - baseDirectory = __dirname + "/../build/"; - } else { - // If it doesn't match above, throw a "FILE_NOT_FOUND" error. - callback({error: -6}); - } - - // Create a normalized version of the string. - baseDirectory = path.normalize(baseDirectory); - - // Check to make sure the target isn't trying to go out of bounds. - // If it is, throw a "FILE_NOT_FOUND" error. - const relTarget = path.normalize(path.join(...target.slice(2))); - if (relTarget.startsWith('..')) { - callback({error: -6}); - return; - } - - // Create the absolute target path and return it. - const absTarget = path.join(baseDirectory, relTarget); - callback({ path: absTarget }); - - }, (error) => { - if (error) console.error('Failed to register protocol'); - }); - + ); } /** * Create the window and all of its properties */ function createWindow() { - // Create a window state manager that keeps track of the width // and height of the main window. let mainWindowState = windowStateKeeper({ @@ -115,68 +123,72 @@ function createWindow() { }); // Create a browser window with some settings - mainWindow = new BrowserWindow( - { - // Use the values from the window state keeper - // to draw the window exactly as it was left. - // If not possible, derive it from the default - // values defined earlier. - x: mainWindowState.x, - y: mainWindowState.y, - width: mainWindowState.width, - height: mainWindowState.height, - - // Set a minimum width to prevent element collisions. - minWidth: 300, + mainWindow = new BrowserWindow({ + // Use the values from the window state keeper + // to draw the window exactly as it was left. + // If not possible, derive it from the default + // values defined earlier. + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, - // Set important web preferences. - webPreferences: {nodeIntegration: true}, + // Set a minimum width to prevent element collisions. + minWidth: 300, - // Set some preferences that are specific to macOS. - titleBarStyle: 'hiddenInset', - vibrancy: "sidebar", - transparent: isDarwin(), - backgroundColor: isDarwin()? "#80000000": "#FFF", + // Set important web preferences. + webPreferences: { nodeIntegration: true }, - // Hide the window until the contents load - show: false - } - ); + // Set some preferences that are specific to macOS. + titleBarStyle: "hiddenInset", + vibrancy: "sidebar", + transparent: isDarwin(), + backgroundColor: isDarwin() ? "#80000000" : "#FFF", + + // Hide the window until the contents load + show: false + }); // Set up event listeners to track changes in the window state. mainWindowState.manage(mainWindow); - + // Load the main app and open the index page. mainWindow.loadURL("hyperspace://hyperspace/app/"); - + // Watch for a change in macOS's dark mode and reload the window to apply changes, as well as accent color if (isDarwin()) { - systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => { - if (mainWindow != null) { - mainWindow.webContents.reload(); + systemPreferences.subscribeNotification( + "AppleInterfaceThemeChangedNotification", + () => { + if (mainWindow != null) { + mainWindow.webContents.reload(); + } } - }); + ); - systemPreferences.subscribeNotification('AppleColorPreferencesChangedNotification', () => { - if (mainWindow != null) { - mainWindow.webContents.reload(); + systemPreferences.subscribeNotification( + "AppleColorPreferencesChangedNotification", + () => { + if (mainWindow != null) { + mainWindow.webContents.reload(); + } } - }); + ); } // Only show the window when ready - mainWindow.once('ready-to-show', () => { + mainWindow.once("ready-to-show", () => { mainWindow.show(); }); // Delete the window when closed - mainWindow.on('closed', () => { - mainWindow = null + mainWindow.on("closed", () => { + mainWindow = null; }); // Hijack any links with a blank target and open them in the default // browser instead of a new Electron window - mainWindow.webContents.on('new-window', (event, url) => { + mainWindow.webContents.on("new-window", (event, url) => { event.preventDefault(); shell.openExternal(url); }); @@ -199,18 +211,17 @@ function safelyGoTo(url) { * Create the menu bar and attach it to a window */ function createMenubar() { - // Create an instance of the Menu class let menu = Menu; - + // Create a menu bar template const menuBar = [ { - label: 'File', + label: "File", submenu: [ { - label: 'New Window', - accelerator: 'CmdOrCtrl+N', + label: "New Window", + accelerator: "CmdOrCtrl+N", click() { if (mainWindow == null) { registerProtocol(); @@ -219,106 +230,110 @@ function createMenubar() { } }, { - label: 'New Post', - accelerator: 'Shift+CmdOrCtrl+N', + label: "New Post", + accelerator: "Shift+CmdOrCtrl+N", click() { - safelyGoTo("hyperspace://hyperspace/app/#compose") + safelyGoTo("hyperspace://hyperspace/app/#compose"); } } - ] }, { - label: 'Edit', + label: "Edit", submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' } + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "pasteandmatchstyle" }, + { role: "delete" }, + { role: "selectall" } ] }, { - label: 'View', + label: "View", submenu: [ { - label: 'Back', - accelerator: 'CmdOrCtrl+[', + label: "Back", + accelerator: "CmdOrCtrl+[", click() { - if (mainWindow != null && mainWindow.webContents.canGoBack()) { - mainWindow.webContents.goBack() + if ( + mainWindow != null && + mainWindow.webContents.canGoBack() + ) { + mainWindow.webContents.goBack(); } } }, { - label: 'Forward', - accelerator: 'CmdOrCtrl+]', + label: "Forward", + accelerator: "CmdOrCtrl+]", click() { - if (mainWindow != null && mainWindow.webContents.canGoForward()) { - mainWindow.webContents.goForward() + if ( + mainWindow != null && + mainWindow.webContents.canGoForward() + ) { + mainWindow.webContents.goForward(); } } }, - { role: 'reload' }, - { role: 'forcereload' }, - { type: 'separator' }, + { role: "reload" }, + { role: "forcereload" }, + { type: "separator" }, { - label: 'Open Dev Tools', - click () { + label: "Open Dev Tools", + click() { try { mainWindow.webContents.openDevTools(); } catch (err) { console.error("Couldn't open dev tools: " + err); } - }, - accelerator: 'Shift+CmdOrCtrl+I' + accelerator: "Shift+CmdOrCtrl+I" }, - { type: 'separator' }, - { role: 'togglefullscreen' } + { type: "separator" }, + { role: "togglefullscreen" } ] }, { label: "Timelines", submenu: [ { - label: 'Home', + label: "Home", accelerator: "CmdOrCtrl+0", click() { - safelyGoTo("hyperspace://hyperspace/app/#/home") + safelyGoTo("hyperspace://hyperspace/app/#/home"); } }, { - label: 'Local', + label: "Local", accelerator: "CmdOrCtrl+1", click() { - safelyGoTo("hyperspace://hyperspace/app/#/local") + safelyGoTo("hyperspace://hyperspace/app/#/local"); } }, { - label: 'Public', + label: "Public", accelerator: "CmdOrCtrl+2", click() { - safelyGoTo("hyperspace://hyperspace/app/#/public") + safelyGoTo("hyperspace://hyperspace/app/#/public"); } }, { - label: 'Messages', + label: "Messages", accelerator: "CmdOrCtrl+3", click() { - safelyGoTo("hyperspace://hyperspace/app/#/messages") + safelyGoTo("hyperspace://hyperspace/app/#/messages"); } }, - { type: 'separator' }, + { type: "separator" }, { - label: 'Activity', - accelerator: 'Alt+CmdOrCtrl+A', + label: "Activity", + accelerator: "Alt+CmdOrCtrl+A", click() { - safelyGoTo("hyperspace://hyperspace/app/#/activity") + safelyGoTo("hyperspace://hyperspace/app/#/activity"); } } ] @@ -327,127 +342,138 @@ function createMenubar() { label: "Account", submenu: [ { - label: 'Notifications', + label: "Notifications", accelerator: "Alt+CmdOrCtrl+N", click() { - safelyGoTo("hyperspace://hyperspace/app/#/notifications") + safelyGoTo( + "hyperspace://hyperspace/app/#/notifications" + ); } }, { - label: 'Recommendations', + label: "Recommendations", accelerator: "Alt+CmdOrCtrl+R", click() { - safelyGoTo("hyperspace://hyperspace/app/#/recommended") + safelyGoTo("hyperspace://hyperspace/app/#/recommended"); } }, - { type: 'separator' }, + { type: "separator" }, { - label: 'Edit Profile', + label: "Edit Profile", accelerator: "Shift+CmdOrCtrl+P", click() { - safelyGoTo("hyperspace://hyperspace/app/#/you") + safelyGoTo("hyperspace://hyperspace/app/#/you"); } }, { - label: 'Follow Requests', + label: "Follow Requests", accelerator: "Alt+CmdOrCtrl+E", click() { - safelyGoTo("hyperspace://hyperspace/app/#/requests") + safelyGoTo("hyperspace://hyperspace/app/#/requests"); } }, { - label: 'Blocked Servers', + label: "Blocked Servers", accelerator: "Shift+CmdOrCtrl+B", click() { - safelyGoTo("hyperspace://hyperspace/app/#/blocked") + safelyGoTo("hyperspace://hyperspace/app/#/blocked"); } }, - { type: 'separator'}, + { type: "separator" }, { - label: 'Switch Accounts...', + label: "Switch Accounts...", click() { - safelyGoTo("hyperspace://hyperspace/app/#/welcome") + safelyGoTo("hyperspace://hyperspace/app/#/welcome"); } } ] }, { - role: 'window', + role: "window", submenu: [ - { role: 'minimize' }, - { role: 'close' }, - { type: 'separator' }, + { role: "minimize" }, + { role: "close" }, + { type: "separator" } ] }, { - role: 'help', + role: "help", submenu: [ { - label: 'Hyperspace Desktop Docs', - click () { require('electron').shell.openExternal('https://hyperspace.marquiskurt.net/docs/') } + label: "Hyperspace Desktop Docs", + click() { + require("electron").shell.openExternal( + "https://hyperspace.marquiskurt.net/docs/" + ); + } }, { - label: 'Report a Bug', - click () { require('electron').shell.openExternal('https://github.com/hyperspacedev/hyperspace/issues') } + label: "Report a Bug", + click() { + require("electron").shell.openExternal( + "https://github.com/hyperspacedev/hyperspace/issues" + ); + } }, - { type: 'separator' }, + { type: "separator" }, { - label: 'Acknowledgements', - click () { require('electron').shell.openExternal('https://github.com/hyperspacedev/hyperspace/blob/master/patreon.md') } + label: "Acknowledgements", + click() { + require("electron").shell.openExternal( + "https://github.com/hyperspacedev/hyperspace/blob/master/patreon.md" + ); + } } ] } ]; - if (process.platform === 'darwin') { + if (process.platform === "darwin") { menuBar.unshift({ label: app.getName(), submenu: [ { label: `About ${app.getName()}`, click() { - safelyGoTo("hyperspace://hyperspace/app/#/about") + safelyGoTo("hyperspace://hyperspace/app/#/about"); } }, - { type: 'separator' }, + { type: "separator" }, { label: "Preferences...", - accelerator: 'Cmd+,', + accelerator: "Cmd+,", click() { safelyGoTo("hyperspace://hyperspace/app/#/settings"); } }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideothers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" } ] }); // Edit menu menuBar[2].submenu.push( - { type: 'separator' }, + { type: "separator" }, { - label: 'Speech', - submenu: [ - { role: 'startspeaking' }, - { role: 'stopspeaking' } - ] + label: "Speech", + submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }] } ); // Window menu menuBar[6].submenu = [ - { role: 'close' }, - { role: 'minimize' }, - { role: 'zoom' }, - { type: 'separator' }, - { role: 'front' } - ] + { role: "close" }, + { role: "minimize" }, + { role: "zoom" }, + { type: "separator" }, + { role: "front" } + ]; } // Create the template for the menu and attach it to the application @@ -456,21 +482,21 @@ function createMenubar() { } // When the app is ready, create the window and menu bar -app.on('ready', () => { +app.on("ready", () => { registerProtocol(); createWindow(); createMenubar(); }); // Standard quit behavior changes for macOS -app.on('window-all-closed', () => { +app.on("window-all-closed", () => { if (!isDarwin()) { - app.quit() + app.quit(); } }); // When the app is activated, create the window and menu bar -app.on('activate', () => { +app.on("activate", () => { if (mainWindow === null) { createWindow(); createMenubar(); diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index 383deb2..b102aed 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -45,6 +45,7 @@ import { Account } from "../types/Account"; import { Relationship } from "../types/Relationship"; import { withSnackbar } from "notistack"; import { Dictionary } from "../interfaces/utils"; +import { linkablePath } from "../utilities/desktop"; /** * The state interface for the notifications page. @@ -616,7 +617,9 @@ class NotificationsPage extends Component { style={{ textAlign: "center" }} > - + Manage notification settings diff --git a/src/utilities/desktop.tsx b/src/utilities/desktop.tsx index 849a529..c1f3571 100644 --- a/src/utilities/desktop.tsx +++ b/src/utilities/desktop.tsx @@ -56,3 +56,11 @@ export function getElectronApp() { const { remote } = eWin.require("electron"); return remote.app; } + +/** + * Get the linkable version of a path for the web and desktop. + * @param path The path to make a linkable version of + */ +export function linkablePath(path: string): string { + return isDesktopApp() ? "/app" + path : path; +}