-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
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
Changes from 22 commits
b17d8bc
f909fe6
0a6390e
0050489
6116103
f118f52
047883b
f454c4e
305d0ff
ecf7180
f0361bf
2b6b4dd
ee46978
b02baa6
90621a8
2386abe
20d1f74
6c18c6d
e73a4b7
62c5f43
f42fd0e
b269c2f
1126899
1a703c4
886b6d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/ProtocolUtils'; | ||
|
||
const { BrowserWindow, Tray, screen } = require('electron'); | ||
const url = require('url'); | ||
|
@@ -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() { | ||
|
@@ -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_; | ||
|
@@ -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.openUrl(url); | ||
} | ||
} | ||
}); | ||
|
||
return false; | ||
|
@@ -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.openUrl(url); | ||
}); | ||
} | ||
|
||
async openUrl(url: string) { | ||
this.win_.webContents.send('asynchronous-message', 'openUrl', { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "openCallbackUrl" |
||
url: url, | ||
}); | ||
roman-r-m marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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/ProtocolUtils'; | ||
import ElectronAppWrapper from '../../ElectronAppWrapper'; | ||
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils'; | ||
|
||
const { connect } = require('react-redux'); | ||
|
@@ -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 === 'openUrl') { | ||
roman-r-m marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "openCallbackUrl" |
||
this.openUrl(args.url); | ||
} | ||
}); | ||
|
||
const initialCallbackUrl = (bridge().electronApp() as ElectronAppWrapper).initialCallbackUrl(); | ||
if (initialCallbackUrl) { | ||
this.openUrl(initialCallbackUrl); | ||
} | ||
} | ||
|
||
private openUrl(url: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. openCallbackUrl |
||
console.log(`openUrl ${url}`); | ||
const { command, params } = parseCallbackUrl(url); | ||
void CommandService.instance().execute(command.toString(), params.id); | ||
} | ||
|
||
private updateLayoutPluginViews(layout: LayoutItem, plugins: PluginStates) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { getFolderUrl, getTagUrl } from '@joplin/lib/ProtocolUtils'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all these callback URL functions should either be in their own namespace so that you can have for example It's certainly a bit verbose but the nature of this feature means the code is a bit everywhere so it's good to be able to quickly find it and identify it if needed. |
||
const { connect } = require('react-redux'); | ||
const shared = require('@joplin/lib/components/shared/side-menu-shared.js'); | ||
const { themeStyle } = require('@joplin/lib/theme'); | ||
|
@@ -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'); | ||
|
||
|
@@ -326,10 +328,29 @@ class SidebarComponent extends React.Component<Props, State> { | |
); | ||
} | ||
|
||
if (itemType === BaseModel.TYPE_FOLDER) { | ||
menu.append( | ||
new MenuItem({ | ||
label: _('Copy notebook URL'), | ||
click: () => { | ||
clipboard.writeText(getFolderUrl(itemId)); | ||
}, | ||
}) | ||
); | ||
} | ||
|
||
if (itemType === BaseModel.TYPE_TAG) { | ||
menu.append(new MenuItem( | ||
menuUtils.commandToStatefulMenuItem('renameTag', itemId) | ||
)); | ||
menu.append( | ||
new MenuItem({ | ||
label: _('Copy tag URL'), | ||
click: () => { | ||
clipboard.writeText(getTagUrl(itemId)); | ||
}, | ||
}) | ||
); | ||
} | ||
|
||
const pluginViews = pluginUtils.viewsByType(this.pluginsRef.current, 'menuItem'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { getNoteUrl } from '@joplin/lib/ProtocolUtils'; | ||
|
||
import BaseModel from '@joplin/lib/BaseModel'; | ||
const bridge = require('electron').remote.require('./bridge').default; | ||
|
@@ -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[]; | ||
|
@@ -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]); | ||
|
@@ -133,6 +134,17 @@ export default class NoteListUtils { | |
}) | ||
); | ||
|
||
if (noteIds.length == 1) { | ||
menu.append( | ||
new MenuItem({ | ||
label: _('Copy note URL'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit confusing because we already have "Copy markdown link". I think we need to discuss all this in the spec and perhaps also see how it's done in other applications? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Markdown link is a different format though. I'm not sure how people would use this function but it seems logical to allow for easy creation of links to items that can be linked. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about calling this "Copy external link"? (and then same for tags and folders) |
||
click: () => { | ||
clipboard.writeText(getNoteUrl(noteIds[0])); | ||
}, | ||
}) | ||
); | ||
} | ||
|
||
if ([9, 10].includes(Setting.value('sync.target'))) { | ||
menu.append( | ||
new MenuItem( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
const URL = require('url-parse'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ProtocolUtils should be lowercase "protocolUtils". In general, util files should be lowercase and classes uppercase. But in fact could you call it "callbackUrlUtils"? |
||
|
||
export function isCallbackUrl(s: string) { | ||
return s.startsWith('joplin://x-callback-url/'); | ||
} | ||
|
||
export function getNoteUrl(noteId: string) { | ||
return `joplin://x-callback-url/openNote?id=${encodeURIComponent(noteId)}`; | ||
} | ||
|
||
export function getFolderUrl(folderId: string) { | ||
return `joplin://x-callback-url/openFolder?id=${encodeURIComponent(folderId)}`; | ||
} | ||
|
||
export function getTagUrl(tagId: string) { | ||
return `joplin://x-callback-url/openTag?id=${encodeURIComponent(tagId)}`; | ||
} | ||
|
||
export const enum Command { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CallbackUrlCommand |
||
OpenNote = 'openNote', | ||
OpenFolder = 'openFolder', | ||
OpenTag = 'openTag', | ||
} | ||
|
||
export interface CallbackUrlInfo { | ||
command: Command; | ||
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 Command, | ||
params: url.query, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be "openCallbackUrl" to keep the same naming convention.