Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Handle URL links #5302

Merged
merged 25 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b17d8bc
Register to handle joplin:// links
roman-r-m Aug 12, 2021
f909fe6
Trying to get it to work on Linux
roman-r-m Aug 13, 2021
0a6390e
Actually open the note
roman-r-m Aug 13, 2021
0050489
Cleanup
roman-r-m Aug 13, 2021
6116103
Open the note from URL even if Joplin isn't running
roman-r-m Aug 13, 2021
f118f52
Handle openFolder and openTag too; change the URL format; extract ULR…
roman-r-m Aug 14, 2021
047883b
Read initial url from a field
roman-r-m Aug 14, 2021
f454c4e
Add a function to check for valid callback url
roman-r-m Aug 14, 2021
305d0ff
Rename copy folder URL -> copy notebook URL
roman-r-m Aug 14, 2021
ecf7180
Code review comments
roman-r-m Aug 14, 2021
f0361bf
Review comments - escape vars in url
roman-r-m Aug 14, 2021
2b6b4dd
Rename initialUrl -> initialCallbackUrl
roman-r-m Aug 14, 2021
ee46978
Review comments - throw an error if callback url is not valid
roman-r-m Aug 14, 2021
b02baa6
Update ignore files
roman-r-m Aug 14, 2021
90621a8
Fix linter errors
roman-r-m Aug 14, 2021
2386abe
Rename parseUrl -> parseCallbackUrl
roman-r-m Aug 14, 2021
20d1f74
Use enum
roman-r-m Aug 14, 2021
6c18c6d
Use url-parse
roman-r-m Aug 15, 2021
e73a4b7
Merge branch 'dev' into desktop-protocol
roman-r-m Aug 20, 2021
62c5f43
Rename enum values
roman-r-m Aug 20, 2021
f42fd0e
Fix enum usage
roman-r-m Aug 20, 2021
b269c2f
Simplify protocol setup
roman-r-m Aug 28, 2021
1126899
Code review changes
roman-r-m Sep 1, 2021
1a703c4
Rename ProtocolUtils -> callbackUrlUtils
roman-r-m Sep 1, 2021
886b6d1
Add stuff to maybe get it to work on MacOS
roman-r-m Sep 1, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,9 @@ packages/lib/Logger.js.map
packages/lib/PoorManIntervals.d.ts
packages/lib/PoorManIntervals.js
packages/lib/PoorManIntervals.js.map
packages/lib/ProtocolUtils.d.ts
packages/lib/ProtocolUtils.js
packages/lib/ProtocolUtils.js.map
packages/lib/SyncTargetJoplinCloud.d.ts
packages/lib/SyncTargetJoplinCloud.js
packages/lib/SyncTargetJoplinCloud.js.map
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,9 @@ packages/lib/Logger.js.map
packages/lib/PoorManIntervals.d.ts
packages/lib/PoorManIntervals.js
packages/lib/PoorManIntervals.js.map
packages/lib/ProtocolUtils.d.ts
packages/lib/ProtocolUtils.js
packages/lib/ProtocolUtils.js.map
packages/lib/SyncTargetJoplinCloud.d.ts
packages/lib/SyncTargetJoplinCloud.js
packages/lib/SyncTargetJoplinCloud.js.map
Expand Down
27 changes: 25 additions & 2 deletions packages/app-desktop/ElectronAppWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Logger from '@joplin/lib/Logger';
import { PluginMessage } from './services/plugins/PluginRunner';
import shim from '@joplin/lib/shim';
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';

const { BrowserWindow, Tray, screen } = require('electron');
const url = require('url');
Expand Down Expand Up @@ -30,12 +31,14 @@ export default class ElectronAppWrapper {
private buildDir_: string = null;
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;

constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean) {
constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
this.electronApp_ = electronApp;
this.env_ = env;
this.isDebugMode_ = isDebugMode;
this.profilePath_ = profilePath;
this.initialCallbackUrl_ = initialCallbackUrl;
}

electronApp() {
Expand All @@ -58,6 +61,10 @@ export default class ElectronAppWrapper {
return this.env_;
}

initialCallbackUrl() {
return this.initialCallbackUrl_;
}

createWindow() {
// Set to true to view errors if the application does not start
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
Expand Down Expand Up @@ -320,12 +327,18 @@ export default class ElectronAppWrapper {
}

// Someone tried to open a second instance - focus our window instead
this.electronApp_.on('second-instance', () => {
this.electronApp_.on('second-instance', (_e: any, argv: string[]) => {
const win = this.window();
if (!win) return;
if (win.isMinimized()) win.restore();
win.show();
win.focus();
if (process.platform !== 'darwin') {
const url = argv.find((arg) => isCallbackUrl(arg));
if (url) {
void this.openCallbackUrl(url);
}
}
});

return false;
Expand All @@ -352,6 +365,16 @@ export default class ElectronAppWrapper {
this.electronApp_.on('activate', () => {
this.win_.show();
});

this.electronApp_.on('open-url', (_event: any, url: string) => {
void this.openCallbackUrl(url);
});
}

async openCallbackUrl(url: string) {
this.win_.webContents.send('asynchronous-message', 'openCallbackUrl', {
url: url,
});
roman-r-m marked this conversation as resolved.
Show resolved Hide resolved
}

}
19 changes: 19 additions & 0 deletions packages/app-desktop/gui/MainScreen/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import ShareService from '@joplin/lib/services/share/ShareService';
import { reg } from '@joplin/lib/registry';
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import ElectronAppWrapper from '../../ElectronAppWrapper';
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';

const { connect } = require('react-redux');
Expand Down Expand Up @@ -187,6 +189,23 @@ class MainScreenComponent extends React.Component<Props, State> {
this.layoutModeListenerKeyDown = this.layoutModeListenerKeyDown.bind(this);

window.addEventListener('resize', this.window_resize);

ipcRenderer.on('asynchronous-message', (_event: any, message: string, args: any) => {
if (message === 'openCallbackUrl') {
this.openCallbackUrl(args.url);
}
});

const initialCallbackUrl = (bridge().electronApp() as ElectronAppWrapper).initialCallbackUrl();
if (initialCallbackUrl) {
this.openCallbackUrl(initialCallbackUrl);
}
}

private openCallbackUrl(url: string) {
console.log(`openUrl ${url}`);
const { command, params } = parseCallbackUrl(url);
void CommandService.instance().execute(command.toString(), params.id);
}

private updateLayoutPluginViews(layout: LayoutItem, plugins: PluginStates) {
Expand Down
21 changes: 21 additions & 0 deletions packages/app-desktop/gui/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Logger from '@joplin/lib/Logger';
import { FolderEntity } from '@joplin/lib/services/database/types';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { store } from '@joplin/lib/reducer';
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
const { themeStyle } = require('@joplin/lib/theme');
Expand All @@ -28,6 +29,7 @@ const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
const { clipboard } = require('electron');

const logger = Logger.create('Sidebar');

Expand Down Expand Up @@ -326,10 +328,29 @@ class SidebarComponent extends React.Component<Props, State> {
);
}

if (itemType === BaseModel.TYPE_FOLDER) {
menu.append(
new MenuItem({
label: _('Copy external link'),
click: () => {
clipboard.writeText(getFolderCallbackUrl(itemId));
},
})
);
}

if (itemType === BaseModel.TYPE_TAG) {
menu.append(new MenuItem(
menuUtils.commandToStatefulMenuItem('renameTag', itemId)
));
menu.append(
new MenuItem({
label: _('Copy external link'),
click: () => {
clipboard.writeText(getTagCallbackUrl(itemId));
},
})
);
}

const pluginViews = pluginUtils.viewsByType(this.pluginsRef.current, 'menuItem');
Expand Down
14 changes: 13 additions & 1 deletion packages/app-desktop/gui/utils/NoteListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import InteropServiceHelper from '../../InteropServiceHelper';
import { _ } from '@joplin/lib/locale';
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';

import BaseModel from '@joplin/lib/BaseModel';
const bridge = require('electron').remote.require('./bridge').default;
Expand All @@ -14,6 +15,7 @@ const MenuItem = bridge().MenuItem;
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
const { clipboard } = require('electron');

interface ContextMenuProps {
notes: any[];
Expand Down Expand Up @@ -122,7 +124,6 @@ export default class NoteListUtils {
new MenuItem({
label: _('Copy Markdown link'),
click: async () => {
const { clipboard } = require('electron');
const links = [];
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
Expand All @@ -133,6 +134,17 @@ export default class NoteListUtils {
})
);

if (noteIds.length == 1) {
menu.append(
new MenuItem({
label: _('Copy external link'),
click: () => {
clipboard.writeText(getNoteCallbackUrl(noteIds[0]));
},
})
);
}

if ([9, 10].includes(Setting.value('sync.target'))) {
menu.append(
new MenuItem(
Expand Down
7 changes: 6 additions & 1 deletion packages/app-desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Logger = require('@joplin/lib/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const packageInfo = require('./packageInfo.js');
const { isCallbackUrl } = require('@joplin/lib/ProtocolUtils');

// Electron takes the application name from package.json `name` and
// displays this in the tray icon toolip and message box titles, however in
Expand Down Expand Up @@ -36,7 +37,11 @@ const env = envFromArgs(process.argv);
const profilePath = profileFromArgs(process.argv);
const isDebugMode = !!process.argv && process.argv.indexOf('--debug') >= 0;

const wrapper = new ElectronAppWrapper(electronApp, env, profilePath, isDebugMode);
electronApp.setAsDefaultProtocolClient('joplin');

const initialCallbackUrl = process.argv.find((arg) => isCallbackUrl(arg));

const wrapper = new ElectronAppWrapper(electronApp, env, profilePath, isDebugMode, initialCallbackUrl);

initBridge(wrapper);

Expand Down
13 changes: 11 additions & 2 deletions packages/app-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,22 @@
"icon": "../../Assets/macOs.icns",
"target": "dmg",
"hardenedRuntime": true,
"entitlements": "./build-mac/entitlements.mac.inherit.plist"
"entitlements": "./build-mac/entitlements.mac.inherit.plist",
"extendInfo": {
"CFBundleURLTypes": [
{
"CFBundleURLSchemes": ["joplin"],
"CFBundleTypeRole": "Editor"
}
]
}
},
"linux": {
"icon": "../../Assets/LinuxIcons",
"category": "Office",
"desktop": {
"Icon": "joplin"
"Icon": "joplin",
"MimeType": "x-scheme-handler/joplin;"
},
"target": "AppImage"
},
Expand Down
37 changes: 37 additions & 0 deletions packages/lib/callbackUrlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const URL = require('url-parse');

export function isCallbackUrl(s: string) {
return s.startsWith('joplin://x-callback-url/');
}

export function getNoteCallbackUrl(noteId: string) {
return `joplin://x-callback-url/openNote?id=${encodeURIComponent(noteId)}`;
}

export function getFolderCallbackUrl(folderId: string) {
return `joplin://x-callback-url/openFolder?id=${encodeURIComponent(folderId)}`;
}

export function getTagCallbackUrl(tagId: string) {
return `joplin://x-callback-url/openTag?id=${encodeURIComponent(tagId)}`;
}

export const enum CallbackUrlCommand {
OpenNote = 'openNote',
OpenFolder = 'openFolder',
OpenTag = 'openTag',
}

export interface CallbackUrlInfo {
command: CallbackUrlCommand;
params: Record<string, string>;
}

export function parseCallbackUrl(s: string): CallbackUrlInfo {
if (!isCallbackUrl(s)) throw new Error(`Invalid callback url ${s}`);
const url = new URL(s, true);
return {
command: url.pathname.substring(url.pathname.lastIndexOf('/') + 1) as CallbackUrlCommand,
params: url.query,
};
}