1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-02-13 19:42:36 +02:00

Desktop: Fixes #11445: Link watched files to the current window (#11590)

This commit is contained in:
Henry Heino 2025-01-06 09:33:02 -08:00 committed by GitHub
parent 6220267abb
commit ac154ee1e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 90 additions and 13 deletions

View File

@ -295,6 +295,7 @@ packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js

1
.gitignore vendored
View File

@ -270,6 +270,7 @@ packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js

View File

@ -40,6 +40,8 @@ export interface AppWindowState extends WindowState {
visibleDialogs: VisibleDialogs;
dialogs: AppStateDialog[];
devToolsVisible: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
watchedResources: any;
}
interface BackgroundWindowStates {
@ -62,8 +64,6 @@ export interface AppState extends State, AppWindowState {
modalOverlayMessage: string|null;
// Extra reducer keys go here
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
watchedResources: any;
mainLayout: LayoutItem;
isResettingLayout: boolean;
}
@ -76,6 +76,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
noteVisiblePanes: ['editor', 'viewer'],
editorCodeView: true,
devToolsVisible: false,
watchedResources: {},
};
};

View File

@ -653,6 +653,7 @@ class Application extends BaseApplication {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
(action: any) => { this.store().dispatch(action); },
(path: string) => bridge().openItem(path),
() => this.store().getState().windowId,
);
// Forwards the local event to the global event manager, so that it can

View File

@ -59,6 +59,7 @@ import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/
import PluginService from '@joplin/lib/services/plugins/PluginService';
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
import useResourceUnwatcher from './utils/useResourceUnwatcher';
const debounce = require('debounce');
@ -358,6 +359,8 @@ function NoteEditorContent(props: NoteEditorProps) {
const windowId = useContext(WindowIdContext);
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
useResourceUnwatcher({ noteId: formNote.id, windowId });
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const externalEditWatcher_noteChange = useCallback((event: any) => {
if (event.id === formNote.id) {
@ -729,7 +732,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
selectedSearchId: windowState.selectedSearchId,
customCss: state.customViewerCss,
noteVisiblePanes: windowState.noteVisiblePanes,
watchedResources: state.watchedResources,
watchedResources: windowState.watchedResources,
highlightedWords: state.highlightedWords,
plugins: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,

View File

@ -0,0 +1,21 @@
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
import { useEffect } from 'react';
interface Props {
noteId: string;
windowId: string;
}
const useResourceUnwatcher = ({ noteId, windowId }: Props) => {
useEffect(() => {
// All resources associated with the current window should no longer be watched after:
// 1. The editor unloads, or
// 2. The note shown in the editor changes.
// Unwatching in a cleanup callback handles both cases.
return () => {
void ResourceEditWatcher.instance().stopWatchingAll(windowId);
};
}, [noteId, windowId]);
};
export default useResourceUnwatcher;

View File

@ -1,7 +1,6 @@
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
import type { EditorScrollPercents } from '../../../app.reducer';
interface Props {
@ -30,8 +29,6 @@ const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollP
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
});
void ResourceEditWatcher.instance().stopWatchingAll();
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
const clearScrollWhenReady = useCallback(() => {

View File

@ -9,10 +9,14 @@ import { ResourceEntity } from '../database/types';
const EventEmitter = require('events');
const chokidar = require('chokidar');
type WindowId = string;
interface WatchedItem {
resourceId: string;
title: string;
lastFileUpdatedTime: number;
lastResourceUpdatedTime: number;
watchedByWindows: WindowId[];
path: string;
asyncSaveQueue: AsyncActionQueue;
size: number;
@ -23,6 +27,7 @@ interface WatchedItems {
}
type OpenItemFn = (path: string)=> void;
type GetWindowIdFn = ()=> string;
export default class ResourceEditWatcher {
@ -41,6 +46,7 @@ export default class ResourceEditWatcher {
private eventEmitter_: any;
private tempDir_ = '';
private openItem_: OpenItemFn;
private getActiveWindowId_: GetWindowIdFn;
public constructor() {
this.logger_ = new Logger();
@ -51,10 +57,11 @@ export default class ResourceEditWatcher {
}
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
public initialize(logger: any, dispatch: Function, openItem: OpenItemFn) {
public initialize(logger: any, dispatch: Function, openItem: OpenItemFn, getWindowId: GetWindowIdFn) {
this.logger_ = logger;
this.dispatch = dispatch;
this.openItem_ = openItem;
this.getActiveWindowId_ = getWindowId;
}
public static instance() {
@ -239,6 +246,7 @@ export default class ResourceEditWatcher {
}
private async watch(resourceId: string): Promise<WatchedItem> {
const sourceWindowId = this.getActiveWindowId_();
let watchedItem = this.watchedItemByResourceId(resourceId);
if (!watchedItem) {
@ -246,8 +254,10 @@ export default class ResourceEditWatcher {
watchedItem = {
resourceId: resourceId,
title: '',
lastFileUpdatedTime: 0,
lastResourceUpdatedTime: 0,
watchedByWindows: [sourceWindowId],
asyncSaveQueue: new AsyncActionQueue(1000),
path: '',
size: -1,
@ -261,6 +271,10 @@ export default class ResourceEditWatcher {
watchedItem.lastFileUpdatedTime = stat.mtime.getTime();
watchedItem.lastResourceUpdatedTime = resource.updated_time;
watchedItem.size = stat.size;
watchedItem.title = resource.title;
// Reset the watching window list to handle the case where the active window
// was changed while loading the resource.
watchedItem.watchedByWindows = [this.getActiveWindowId_()];
this.watchFile(editFilePath);
@ -269,6 +283,18 @@ export default class ResourceEditWatcher {
id: resource.id,
title: resource.title,
});
} else if (!watchedItem.watchedByWindows.includes(sourceWindowId)) {
watchedItem = {
...this.watchedItems_[resourceId],
watchedByWindows: [...watchedItem.watchedByWindows, sourceWindowId],
};
this.watchedItems_[resourceId] = watchedItem;
this.dispatch({
type: 'RESOURCE_EDIT_WATCHER_SET',
id: watchedItem.resourceId,
title: watchedItem.title,
});
}
this.logger().info(`ResourceEditWatcher: Started watching ${watchedItem.path}`);
@ -318,16 +344,26 @@ export default class ResourceEditWatcher {
this.logger().info(`ResourceEditWatcher: Stopped watching ${item.path}`);
}
public async stopWatchingAll() {
public async stopWatchingAll(sourceWindow: string) {
const promises = [];
for (const resourceId in this.watchedItems_) {
const item = this.watchedItems_[resourceId];
promises.push(this.stopWatching(item.resourceId));
let item = this.watchedItems_[resourceId];
if (item.watchedByWindows.includes(sourceWindow)) {
const otherWatchingWindows = item.watchedByWindows.filter(id => id !== sourceWindow);
item = { ...item, watchedByWindows: otherWatchingWindows };
this.watchedItems_[resourceId] = item;
}
if (item.watchedByWindows.length === 0) {
promises.push(this.stopWatching(item.resourceId));
}
}
await Promise.all(promises);
this.dispatch({
type: 'RESOURCE_EDIT_WATCHER_CLEAR',
windowId: sourceWindow,
});
}

View File

@ -1,4 +1,5 @@
import produce, { Draft } from 'immer';
import { defaultWindowId, stateUtils } from '../../reducer';
export const defaultState = {
watchedResources: {},
@ -20,14 +21,28 @@ const reducer = produce((draft: Draft<any>, action: any) => {
break;
case 'RESOURCE_EDIT_WATCHER_REMOVE':
// RESOURCE_EDIT_WATCHER_REMOVE signals that a resource is no longer being watched.
// As such, it should be removed from all windows' resource lists:
for (const windowId in draft.backgroundWindows) {
// watchedResources is per-window only on desktop:
if ('watchedResources' in draft.backgroundWindows[windowId]) {
delete draft.backgroundWindows[windowId].watchedResources[action.id];
}
}
delete draft.watchedResources[action.id];
break;
case 'RESOURCE_EDIT_WATCHER_CLEAR':
case 'RESOURCE_EDIT_WATCHER_CLEAR': {
const windowState = stateUtils.windowStateById(draft, action.windowId ?? defaultWindowId);
draft.watchedResources = {};
// The window may have already been closed.
const windowExists = !!windowState;
if (windowExists) {
windowState.watchedResources = {};
}
break;
}
}
} catch (error) {

View File

@ -155,4 +155,5 @@ tablist
Edubirdie
Useviral
ldaps
Bluesky
Bluesky
unwatcher