Make likable paths across desktop app work

This commit is contained in:
Marquis Kurt 2020-06-17 20:45:09 -04:00
parent 33b9f9d76d
commit 6656a3749e
No known key found for this signature in database
GPG Key ID: 725636D259F5402D
3 changed files with 251 additions and 214 deletions

View File

@ -2,10 +2,17 @@
// Electron script to run Hyperspace as an app // Electron script to run Hyperspace as an app
// © 2018 Hyperspace developers. Licensed under NPL v1. // © 2018 Hyperspace developers. Licensed under NPL v1.
const { app, Menu, protocol, BrowserWindow, shell, systemPreferences } = require('electron'); const {
const windowStateKeeper = require('electron-window-state'); app,
const { autoUpdater } = require('electron-updater'); Menu,
const path = require('path'); 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 // Check for any updates to the app
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
@ -18,7 +25,7 @@ let mainWindow;
// file:// protocol, which is necessary for Mastodon to redirect // file:// protocol, which is necessary for Mastodon to redirect
// to when authorizing Hyperspace. // to when authorizing Hyperspace.
protocol.registerSchemesAsPrivileged([ 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 * Register the protocol for Hyperspace
*/ */
function registerProtocol() { function registerProtocol() {
protocol.registerFileProtocol('hyperspace', (request, callback) => { protocol.registerFileProtocol(
"hyperspace",
// Check to make sure we're doing a GET request (request, callback) => {
if (request.method !== "GET") { // Check to make sure we're doing a GET request
callback({error: -322}); if (request.method !== "GET") {
return null; 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 * Create the window and all of its properties
*/ */
function createWindow() { function createWindow() {
// Create a window state manager that keeps track of the width // Create a window state manager that keeps track of the width
// and height of the main window. // and height of the main window.
let mainWindowState = windowStateKeeper({ let mainWindowState = windowStateKeeper({
@ -115,68 +123,72 @@ function createWindow() {
}); });
// Create a browser window with some settings // Create a browser window with some settings
mainWindow = new BrowserWindow( mainWindow = new BrowserWindow({
{ // Use the values from the window state keeper
// Use the values from the window state keeper // to draw the window exactly as it was left.
// to draw the window exactly as it was left. // If not possible, derive it from the default
// If not possible, derive it from the default // values defined earlier.
// values defined earlier. x: mainWindowState.x,
x: mainWindowState.x, y: mainWindowState.y,
y: mainWindowState.y, width: mainWindowState.width,
width: mainWindowState.width, height: mainWindowState.height,
height: mainWindowState.height,
// Set a minimum width to prevent element collisions.
minWidth: 300,
// Set important web preferences. // Set a minimum width to prevent element collisions.
webPreferences: {nodeIntegration: true}, minWidth: 300,
// Set some preferences that are specific to macOS. // Set important web preferences.
titleBarStyle: 'hiddenInset', webPreferences: { nodeIntegration: true },
vibrancy: "sidebar",
transparent: isDarwin(),
backgroundColor: isDarwin()? "#80000000": "#FFF",
// Hide the window until the contents load // Set some preferences that are specific to macOS.
show: false 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. // Set up event listeners to track changes in the window state.
mainWindowState.manage(mainWindow); mainWindowState.manage(mainWindow);
// Load the main app and open the index page. // Load the main app and open the index page.
mainWindow.loadURL("hyperspace://hyperspace/app/"); 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 // Watch for a change in macOS's dark mode and reload the window to apply changes, as well as accent color
if (isDarwin()) { if (isDarwin()) {
systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => { systemPreferences.subscribeNotification(
if (mainWindow != null) { "AppleInterfaceThemeChangedNotification",
mainWindow.webContents.reload(); () => {
if (mainWindow != null) {
mainWindow.webContents.reload();
}
} }
}); );
systemPreferences.subscribeNotification('AppleColorPreferencesChangedNotification', () => { systemPreferences.subscribeNotification(
if (mainWindow != null) { "AppleColorPreferencesChangedNotification",
mainWindow.webContents.reload(); () => {
if (mainWindow != null) {
mainWindow.webContents.reload();
}
} }
}); );
} }
// Only show the window when ready // Only show the window when ready
mainWindow.once('ready-to-show', () => { mainWindow.once("ready-to-show", () => {
mainWindow.show(); mainWindow.show();
}); });
// Delete the window when closed // Delete the window when closed
mainWindow.on('closed', () => { mainWindow.on("closed", () => {
mainWindow = null mainWindow = null;
}); });
// Hijack any links with a blank target and open them in the default // Hijack any links with a blank target and open them in the default
// browser instead of a new Electron window // browser instead of a new Electron window
mainWindow.webContents.on('new-window', (event, url) => { mainWindow.webContents.on("new-window", (event, url) => {
event.preventDefault(); event.preventDefault();
shell.openExternal(url); shell.openExternal(url);
}); });
@ -199,18 +211,17 @@ function safelyGoTo(url) {
* Create the menu bar and attach it to a window * Create the menu bar and attach it to a window
*/ */
function createMenubar() { function createMenubar() {
// Create an instance of the Menu class // Create an instance of the Menu class
let menu = Menu; let menu = Menu;
// Create a menu bar template // Create a menu bar template
const menuBar = [ const menuBar = [
{ {
label: 'File', label: "File",
submenu: [ submenu: [
{ {
label: 'New Window', label: "New Window",
accelerator: 'CmdOrCtrl+N', accelerator: "CmdOrCtrl+N",
click() { click() {
if (mainWindow == null) { if (mainWindow == null) {
registerProtocol(); registerProtocol();
@ -219,106 +230,110 @@ function createMenubar() {
} }
}, },
{ {
label: 'New Post', label: "New Post",
accelerator: 'Shift+CmdOrCtrl+N', accelerator: "Shift+CmdOrCtrl+N",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#compose") safelyGoTo("hyperspace://hyperspace/app/#compose");
} }
} }
] ]
}, },
{ {
label: 'Edit', label: "Edit",
submenu: [ submenu: [
{ role: 'undo' }, { role: "undo" },
{ role: 'redo' }, { role: "redo" },
{ type: 'separator' }, { type: "separator" },
{ role: 'cut' }, { role: "cut" },
{ role: 'copy' }, { role: "copy" },
{ role: 'paste' }, { role: "paste" },
{ role: 'pasteandmatchstyle' }, { role: "pasteandmatchstyle" },
{ role: 'delete' }, { role: "delete" },
{ role: 'selectall' } { role: "selectall" }
] ]
}, },
{ {
label: 'View', label: "View",
submenu: [ submenu: [
{ {
label: 'Back', label: "Back",
accelerator: 'CmdOrCtrl+[', accelerator: "CmdOrCtrl+[",
click() { click() {
if (mainWindow != null && mainWindow.webContents.canGoBack()) { if (
mainWindow.webContents.goBack() mainWindow != null &&
mainWindow.webContents.canGoBack()
) {
mainWindow.webContents.goBack();
} }
} }
}, },
{ {
label: 'Forward', label: "Forward",
accelerator: 'CmdOrCtrl+]', accelerator: "CmdOrCtrl+]",
click() { click() {
if (mainWindow != null && mainWindow.webContents.canGoForward()) { if (
mainWindow.webContents.goForward() mainWindow != null &&
mainWindow.webContents.canGoForward()
) {
mainWindow.webContents.goForward();
} }
} }
}, },
{ role: 'reload' }, { role: "reload" },
{ role: 'forcereload' }, { role: "forcereload" },
{ type: 'separator' }, { type: "separator" },
{ {
label: 'Open Dev Tools', label: "Open Dev Tools",
click () { click() {
try { try {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} catch (err) { } catch (err) {
console.error("Couldn't open dev tools: " + err); console.error("Couldn't open dev tools: " + err);
} }
}, },
accelerator: 'Shift+CmdOrCtrl+I' accelerator: "Shift+CmdOrCtrl+I"
}, },
{ type: 'separator' }, { type: "separator" },
{ role: 'togglefullscreen' } { role: "togglefullscreen" }
] ]
}, },
{ {
label: "Timelines", label: "Timelines",
submenu: [ submenu: [
{ {
label: 'Home', label: "Home",
accelerator: "CmdOrCtrl+0", accelerator: "CmdOrCtrl+0",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/home") safelyGoTo("hyperspace://hyperspace/app/#/home");
} }
}, },
{ {
label: 'Local', label: "Local",
accelerator: "CmdOrCtrl+1", accelerator: "CmdOrCtrl+1",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/local") safelyGoTo("hyperspace://hyperspace/app/#/local");
} }
}, },
{ {
label: 'Public', label: "Public",
accelerator: "CmdOrCtrl+2", accelerator: "CmdOrCtrl+2",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/public") safelyGoTo("hyperspace://hyperspace/app/#/public");
} }
}, },
{ {
label: 'Messages', label: "Messages",
accelerator: "CmdOrCtrl+3", accelerator: "CmdOrCtrl+3",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/messages") safelyGoTo("hyperspace://hyperspace/app/#/messages");
} }
}, },
{ type: 'separator' }, { type: "separator" },
{ {
label: 'Activity', label: "Activity",
accelerator: 'Alt+CmdOrCtrl+A', accelerator: "Alt+CmdOrCtrl+A",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/activity") safelyGoTo("hyperspace://hyperspace/app/#/activity");
} }
} }
] ]
@ -327,127 +342,138 @@ function createMenubar() {
label: "Account", label: "Account",
submenu: [ submenu: [
{ {
label: 'Notifications', label: "Notifications",
accelerator: "Alt+CmdOrCtrl+N", accelerator: "Alt+CmdOrCtrl+N",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/notifications") safelyGoTo(
"hyperspace://hyperspace/app/#/notifications"
);
} }
}, },
{ {
label: 'Recommendations', label: "Recommendations",
accelerator: "Alt+CmdOrCtrl+R", accelerator: "Alt+CmdOrCtrl+R",
click() { 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", accelerator: "Shift+CmdOrCtrl+P",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/you") safelyGoTo("hyperspace://hyperspace/app/#/you");
} }
}, },
{ {
label: 'Follow Requests', label: "Follow Requests",
accelerator: "Alt+CmdOrCtrl+E", accelerator: "Alt+CmdOrCtrl+E",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/requests") safelyGoTo("hyperspace://hyperspace/app/#/requests");
} }
}, },
{ {
label: 'Blocked Servers', label: "Blocked Servers",
accelerator: "Shift+CmdOrCtrl+B", accelerator: "Shift+CmdOrCtrl+B",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/blocked") safelyGoTo("hyperspace://hyperspace/app/#/blocked");
} }
}, },
{ type: 'separator'}, { type: "separator" },
{ {
label: 'Switch Accounts...', label: "Switch Accounts...",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/welcome") safelyGoTo("hyperspace://hyperspace/app/#/welcome");
} }
} }
] ]
}, },
{ {
role: 'window', role: "window",
submenu: [ submenu: [
{ role: 'minimize' }, { role: "minimize" },
{ role: 'close' }, { role: "close" },
{ type: 'separator' }, { type: "separator" }
] ]
}, },
{ {
role: 'help', role: "help",
submenu: [ submenu: [
{ {
label: 'Hyperspace Desktop Docs', label: "Hyperspace Desktop Docs",
click () { require('electron').shell.openExternal('https://hyperspace.marquiskurt.net/docs/') } click() {
require("electron").shell.openExternal(
"https://hyperspace.marquiskurt.net/docs/"
);
}
}, },
{ {
label: 'Report a Bug', label: "Report a Bug",
click () { require('electron').shell.openExternal('https://github.com/hyperspacedev/hyperspace/issues') } click() {
require("electron").shell.openExternal(
"https://github.com/hyperspacedev/hyperspace/issues"
);
}
}, },
{ type: 'separator' }, { type: "separator" },
{ {
label: 'Acknowledgements', label: "Acknowledgements",
click () { require('electron').shell.openExternal('https://github.com/hyperspacedev/hyperspace/blob/master/patreon.md') } click() {
require("electron").shell.openExternal(
"https://github.com/hyperspacedev/hyperspace/blob/master/patreon.md"
);
}
} }
] ]
} }
]; ];
if (process.platform === 'darwin') { if (process.platform === "darwin") {
menuBar.unshift({ menuBar.unshift({
label: app.getName(), label: app.getName(),
submenu: [ submenu: [
{ {
label: `About ${app.getName()}`, label: `About ${app.getName()}`,
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/about") safelyGoTo("hyperspace://hyperspace/app/#/about");
} }
}, },
{ type: 'separator' }, { type: "separator" },
{ {
label: "Preferences...", label: "Preferences...",
accelerator: 'Cmd+,', accelerator: "Cmd+,",
click() { click() {
safelyGoTo("hyperspace://hyperspace/app/#/settings"); safelyGoTo("hyperspace://hyperspace/app/#/settings");
} }
}, },
{ type: 'separator' }, { type: "separator" },
{ role: 'services' }, { role: "services" },
{ type: 'separator' }, { type: "separator" },
{ role: 'hide' }, { role: "hide" },
{ role: 'hideothers' }, { role: "hideothers" },
{ role: 'unhide' }, { role: "unhide" },
{ type: 'separator' }, { type: "separator" },
{ role: 'quit' } { role: "quit" }
] ]
}); });
// Edit menu // Edit menu
menuBar[2].submenu.push( menuBar[2].submenu.push(
{ type: 'separator' }, { type: "separator" },
{ {
label: 'Speech', label: "Speech",
submenu: [ submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }]
{ role: 'startspeaking' },
{ role: 'stopspeaking' }
]
} }
); );
// Window menu // Window menu
menuBar[6].submenu = [ menuBar[6].submenu = [
{ role: 'close' }, { role: "close" },
{ role: 'minimize' }, { role: "minimize" },
{ role: 'zoom' }, { role: "zoom" },
{ type: 'separator' }, { type: "separator" },
{ role: 'front' } { role: "front" }
] ];
} }
// Create the template for the menu and attach it to the application // 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 // When the app is ready, create the window and menu bar
app.on('ready', () => { app.on("ready", () => {
registerProtocol(); registerProtocol();
createWindow(); createWindow();
createMenubar(); createMenubar();
}); });
// Standard quit behavior changes for macOS // Standard quit behavior changes for macOS
app.on('window-all-closed', () => { app.on("window-all-closed", () => {
if (!isDarwin()) { if (!isDarwin()) {
app.quit() app.quit();
} }
}); });
// When the app is activated, create the window and menu bar // When the app is activated, create the window and menu bar
app.on('activate', () => { app.on("activate", () => {
if (mainWindow === null) { if (mainWindow === null) {
createWindow(); createWindow();
createMenubar(); createMenubar();

View File

@ -45,6 +45,7 @@ import { Account } from "../types/Account";
import { Relationship } from "../types/Relationship"; import { Relationship } from "../types/Relationship";
import { withSnackbar } from "notistack"; import { withSnackbar } from "notistack";
import { Dictionary } from "../interfaces/utils"; import { Dictionary } from "../interfaces/utils";
import { linkablePath } from "../utilities/desktop";
/** /**
* The state interface for the notifications page. * The state interface for the notifications page.
@ -616,7 +617,9 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
style={{ textAlign: "center" }} style={{ textAlign: "center" }}
> >
<Typography> <Typography>
<Link href="/#/settings#sp-notifications"> <Link
href={linkablePath("/#/settings#sp-notifications")}
>
Manage notification settings Manage notification settings
</Link> </Link>
</Typography> </Typography>

View File

@ -56,3 +56,11 @@ export function getElectronApp() {
const { remote } = eWin.require("electron"); const { remote } = eWin.require("electron");
return remote.app; 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;
}