feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,15 @@
import shim from '../../shim';
// Returns the Document that contains [elementRef]. This is useful when a component
// can be rendered in secondary windows and needs to access the component's container
// document.
const useDocument = (elementRef: Element|null) => {
const { useMemo } = shim.react();
return useMemo(() => {
return (elementRef ?? null)?.getRootNode() as Document|null;
}, [elementRef]);
};
export default useDocument;

查看文件

@@ -0,0 +1,43 @@
import PluginService from '../../services/plugins/PluginService';
import Logger from '@joplin/utils/Logger';
import shim from '../../shim';
const logger = Logger.create('usePlugin');
const usePlugin = (pluginId: string) => {
const React = shim.react();
const [pluginReloadCounter, setPluginReloadCounter] = React.useState(0);
const plugin = React.useMemo(() => {
if (!PluginService.instance().pluginIds.includes(pluginId)) {
return null;
}
if (pluginReloadCounter > 0) {
logger.debug('Reloading plugin', pluginId, 'because the set of loaded plugins changed.');
}
return PluginService.instance().pluginById(pluginId);
// The dependency on pluginReloadCounter is important -- it ensures that the plugin
// matches the one loaded in the PluginService.
}, [pluginId, pluginReloadCounter]);
const reloadCounterRef = React.useRef(0);
reloadCounterRef.current = pluginReloadCounter;
// The plugin may need to be re-fetched from the PluginService. When a plugin is reloaded,
// its Plugin object is replaced with a new one.
React.useEffect(() => {
const { remove } = PluginService.instance().addLoadedPluginsChangeListener(() => {
setPluginReloadCounter(reloadCounterRef.current + 1);
});
return () => {
remove();
};
}, []);
return plugin;
};
export default usePlugin;

查看文件

@@ -0,0 +1,13 @@
import { PluginStates } from '../../services/plugins/reducer';
import getActivePluginEditorViews from '../../services/plugins/utils/getActivePluginEditorViews';
import shim from '../../shim';
const { useMemo } = shim.react();
const useVisiblePluginEditorViewIds = (plugins: PluginStates, windowId: string) => {
return useMemo(() => {
const visibleViews = getActivePluginEditorViews(plugins, windowId, { mustBeVisible: true });
return visibleViews.flatMap(({ editorView }) => editorView.id);
}, [plugins, windowId]);
};
export default useVisiblePluginEditorViewIds;

查看文件

@@ -0,0 +1,37 @@
import { type DependencyList } from 'react';
import shim from '../shim';
const { useEffect } = shim.react();
type CleanupCallback = ()=> void;
export interface AsyncEffectEvent {
cancelled: boolean;
onCleanup: (callback: CleanupCallback)=> void;
}
export type EffectFunction = (event: AsyncEffectEvent)=> Promise<void>;
export default function(effect: EffectFunction, dependencies: DependencyList) {
useEffect(() => {
const onCleanupCallbacks: CleanupCallback[] = [];
const event: AsyncEffectEvent = {
cancelled: false,
onCleanup: (callback) => {
if (event.cancelled) {
callback();
} else {
onCleanupCallbacks.push(callback);
}
},
};
void effect(event);
return () => {
event.cancelled = true;
for (const callback of onCleanupCallbacks) {
callback();
}
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, dependencies);
}

查看文件

@@ -0,0 +1,39 @@
import { type RefObject } from 'react';
import shim from '../shim';
const { useCallback, useEffect, useState } = shim.react();
import useEventListener from './useEventListener';
interface Size {
width: number;
height: number;
}
function useElementSize(elementRef: RefObject<HTMLElement>): Size {
const [size, setSize] = useState({
width: 0,
height: 0,
});
// Prevent too many rendering using useCallback
const updateSize = useCallback(() => {
const node = elementRef?.current;
if (node) {
setSize({
width: node.offsetWidth || 0,
height: node.offsetHeight || 0,
});
}
}, [elementRef]);
// Initial size on mount
useEffect(() => {
updateSize();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
useEventListener('resize', updateSize);
return size;
}
export default useElementSize;

查看文件

@@ -0,0 +1,44 @@
import { type RefObject } from 'react';
import shim from '../shim';
const { useEffect, useRef } = shim.react();
type EventHandler = (event: Event)=> void;
function useEventListener(
eventName: string,
handler: EventHandler,
element?: RefObject<EventTarget | null>,
) {
// Create a ref that stores handler
const savedHandler = useRef<EventHandler | undefined>(undefined);
useEffect(() => {
// Define the listening target
const targetElement = element?.current || window;
if (!(targetElement && targetElement.addEventListener)) {
return null;
}
// Update saved handler if necessary
if (savedHandler.current !== handler) {
savedHandler.current = handler;
}
// Create event listener that calls handler function stored in ref
const eventListener = (event: Event) => {
// eslint-disable-next-line no-extra-boolean-cast
if (!!savedHandler?.current) {
savedHandler.current(event);
}
};
targetElement.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, eventListener);
};
}, [eventName, element, handler]);
}
export default useEventListener;

查看文件

@@ -0,0 +1,30 @@
import '../testing/dom-test-environment';
import type * as React from 'react';
import { renderHook } from '@testing-library/react';
import useNowEffect from './useNowEffect';
describe('useNowEffect', () => {
test('should call the cleanup callback when the effect function is called and after unmount', async () => {
const cleanupFunction = jest.fn(() => {});
const effectFunction = jest.fn(() => cleanupFunction);
const useTestHook = (dependencies: React.DependencyList) => {
return useNowEffect(effectFunction, dependencies);
};
const hook = renderHook(useTestHook, { initialProps: [0] });
expect(cleanupFunction).not.toHaveBeenCalled();
expect(effectFunction).toHaveBeenCalledTimes(1);
hook.rerender([0]);
expect(cleanupFunction).not.toHaveBeenCalled();
expect(effectFunction).toHaveBeenCalledTimes(1);
hook.rerender([1]);
expect(cleanupFunction).toHaveBeenCalledTimes(1);
expect(effectFunction).toHaveBeenCalledTimes(2);
hook.unmount();
expect(cleanupFunction).toHaveBeenCalledTimes(2);
expect(effectFunction).toHaveBeenCalledTimes(2);
});
});

查看文件

@@ -0,0 +1,28 @@
import type * as React from 'react';
import shim from '../shim';
type CleanupCallback = (()=> void)|null;
export type EffectFunction = ()=> CleanupCallback;
const { useRef, useMemo, useEffect } = shim.react();
// Like useEffect, but runs as soon as possible.
const useNowEffect = (effect: EffectFunction, dependencies: React.DependencyList) => {
const lastCleanup = useRef<CleanupCallback>(null);
const cleanupCallback = useMemo(() => {
lastCleanup.current?.();
lastCleanup.current = null;
return effect() ?? null;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- This is a custom hook
}, dependencies);
lastCleanup.current = cleanupCallback;
useEffect(() => {
return () => {
lastCleanup.current?.();
lastCleanup.current = null;
};
}, []);
};
export default useNowEffect;

查看文件

@@ -0,0 +1,13 @@
import shim from '../shim';
const { useRef, useEffect } = shim.react();
const usePrevious = <T> (value: T, initialValue: T = null) => {
const ref = useRef<T>(initialValue);
useEffect(() => {
ref.current = value;
});
return ref.current;
};
export default usePrevious;

查看文件

@@ -0,0 +1,36 @@
import '../testing/dom-test-environment';
import type * as React from 'react';
import { renderHook } from '@testing-library/react';
import useQueuedAsyncEffect from './useQueuedAsyncEffect';
import { runWithFakeTimers } from '../testing/test-utils';
describe('useQueuedAsyncEffect', () => {
test('should debounce effect updates', async () => {
const effectFunction = jest.fn(async () => { });
const useTestHook = (dependencies: React.DependencyList) => {
return useQueuedAsyncEffect(effectFunction, dependencies);
};
await runWithFakeTimers(async () => {
const result = renderHook(useTestHook, { initialProps: ['test'] });
// Should pause to allow debouncing.
expect(effectFunction).not.toHaveBeenCalled();
await jest.advanceTimersByTimeAsync(12500);
expect(effectFunction).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(1000);
// Changing twice quickly: Should only update once
result.rerender(['changed']);
expect(effectFunction).toHaveBeenCalledTimes(1);
result.rerender(['changed again']);
await jest.advanceTimersByTimeAsync(500);
expect(effectFunction).toHaveBeenCalledTimes(2);
await jest.advanceTimersByTimeAsync(500);
expect(effectFunction).toHaveBeenCalledTimes(2);
});
});
});

查看文件

@@ -0,0 +1,36 @@
import type * as React from 'react';
import shim from '../shim';
import AsyncActionQueue from '../AsyncActionQueue';
const { useEffect, useState } = shim.react();
interface AsyncEffectEvent {
cancelled: boolean;
}
export type EffectFunction = (event: AsyncEffectEvent)=> void|Promise<void>;
export interface Options {
interval?: number;
}
export default (
effect: EffectFunction,
dependencies: React.DependencyList,
{ interval = undefined }: Options = {},
) => {
const [queue] = useState(() => new AsyncActionQueue(interval));
useEffect(() => {
const event: AsyncEffectEvent = { cancelled: false };
queue.push(() => effect(event));
return () => {
event.cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- This is a custom hook
}, dependencies);
useEffect(() => {
return () => {
void queue.reset();
};
}, [queue]);
};