You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-29 23:48:19 +02:00
Compare commits
9 Commits
android-v3
...
android-v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6baf036dc | ||
|
|
610f00029f | ||
|
|
10be1a0240 | ||
|
|
99a9be535c | ||
|
|
614a95abb8 | ||
|
|
7cbaae3847 | ||
|
|
9e2a6d22ea | ||
|
|
f576e116a8 | ||
|
|
b0e912157b |
@@ -959,6 +959,7 @@ packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useBackHandler.js
|
||||
packages/app-mobile/utils/hooks/useDebounced.js
|
||||
packages/app-mobile/utils/hooks/useIsScreenReaderEnabled.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -931,6 +931,7 @@ packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useBackHandler.js
|
||||
packages/app-mobile/utils/hooks/useDebounced.js
|
||||
packages/app-mobile/utils/hooks/useIsScreenReaderEnabled.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
|
||||
@@ -89,8 +89,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097784
|
||||
versionName "3.5.4"
|
||||
versionCode 2097785
|
||||
versionName "3.5.5"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ const defaultEditorProps = {
|
||||
onUndoRedoDepthChange: ()=>{},
|
||||
onScroll: ()=>{},
|
||||
onAttach: async ()=>{},
|
||||
onSearchVisibleChange: ()=>{},
|
||||
noteResources: {},
|
||||
plugins: {},
|
||||
mode: EditorType.Markdown,
|
||||
|
||||
@@ -34,14 +34,17 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import WarningBanner from './WarningBanner';
|
||||
import useIsScreenReaderEnabled from '../../utils/hooks/useIsScreenReaderEnabled';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
import useDebounced from '../../utils/hooks/useDebounced';
|
||||
|
||||
const logger = Logger.create('NoteEditor');
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type ScrollEventHandler = (event: EditorScrolledEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionRangeChangeEvent)=> void;
|
||||
type OnAttachCallback = (filePath?: string)=> Promise<void>;
|
||||
type OnChange = (event: ChangeEvent)=> void;
|
||||
type OnSearchVisibleChange = (visible: boolean)=> void;
|
||||
type OnScroll = (event: EditorScrolledEvent)=> void;
|
||||
type OnUndoRedoDepthChange = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type OnSelectionChange = (event: SelectionRangeChangeEvent)=> void;
|
||||
type OnAttach = (filePath?: string)=> Promise<void>;
|
||||
|
||||
interface Props {
|
||||
ref: Ref<EditorControl>;
|
||||
@@ -60,11 +63,12 @@ interface Props {
|
||||
plugins: PluginStates;
|
||||
noteResources: ResourceInfos;
|
||||
|
||||
onScroll: ScrollEventHandler;
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
onAttach: OnAttachCallback;
|
||||
onScroll: OnScroll;
|
||||
onChange: OnChange;
|
||||
onSearchVisibleChange: OnSearchVisibleChange;
|
||||
onSelectionChange: OnSelectionChange;
|
||||
onUndoRedoDepthChange: OnUndoRedoDepthChange;
|
||||
onAttach: OnAttach;
|
||||
}
|
||||
|
||||
function fontFamilyFromSettings() {
|
||||
@@ -260,6 +264,19 @@ const useHighlightActiveLine = () => {
|
||||
return canHighlight && Setting.value('editor.highlightActiveLine');
|
||||
};
|
||||
|
||||
const useHasSpaceForToolbar = () => {
|
||||
const [hasSpaceForToolbar, setHasSpaceForToolbar] = useState(true);
|
||||
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const containerHeight = event.nativeEvent.layout.height;
|
||||
|
||||
setHasSpaceForToolbar(containerHeight >= 140);
|
||||
}, []);
|
||||
|
||||
const debouncedHasSpaceForToolbar = useDebounced(hasSpaceForToolbar, Second / 4);
|
||||
return { hasSpaceForToolbar: debouncedHasSpaceForToolbar, onContainerLayout };
|
||||
};
|
||||
|
||||
function NoteEditor(props: Props) {
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
@@ -295,6 +312,7 @@ function NoteEditor(props: Props) {
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
|
||||
const editorControlRef = useRef<EditorControl|null>(null);
|
||||
const lastSearchVisibleRef = useRef<boolean|undefined>(undefined);
|
||||
const onEditorEvent = (event: EditorEvent) => {
|
||||
let exhaustivenessCheck: never;
|
||||
switch (event.kind) {
|
||||
@@ -325,15 +343,21 @@ function NoteEditor(props: Props) {
|
||||
// If the change to the search was done by this editor, it was already applied to the
|
||||
// search state. Skipping the update in this case also helps avoid overwriting the
|
||||
// search state with an older value.
|
||||
const showSearch = event.searchState.dialogVisible ?? lastSearchVisibleRef.current;
|
||||
if (hasExternalChange) {
|
||||
setSearchState(event.searchState);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
if (showSearch) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
}
|
||||
}
|
||||
|
||||
if (showSearch !== lastSearchVisibleRef.current) {
|
||||
props.onSearchVisibleChange(showSearch);
|
||||
lastSearchVisibleRef.current = showSearch;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditorEventType.Remove:
|
||||
@@ -387,18 +411,6 @@ function NoteEditor(props: Props) {
|
||||
return editorControl;
|
||||
});
|
||||
|
||||
const [hasSpaceForToolbar, setHasSpaceForToolbar] = useState(true);
|
||||
const toolbarEnabled = props.toolbarEnabled && hasSpaceForToolbar;
|
||||
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const containerHeight = event.nativeEvent.layout.height;
|
||||
|
||||
if (containerHeight < 140) {
|
||||
setHasSpaceForToolbar(false);
|
||||
} else {
|
||||
setHasSpaceForToolbar(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onAttach = useCallback(async (type: string, base64: string) => {
|
||||
const tempFilePath = join(Setting.value('tempDir'), `paste.${uuid.createNano()}.${toFileExtension(type)}`);
|
||||
@@ -416,7 +428,11 @@ function NoteEditor(props: Props) {
|
||||
searchVisible: searchState.dialogVisible,
|
||||
}), [selectionState, searchState.dialogVisible]);
|
||||
|
||||
|
||||
const { hasSpaceForToolbar, onContainerLayout } = useHasSpaceForToolbar();
|
||||
const toolbarEnabled = props.toolbarEnabled && hasSpaceForToolbar;
|
||||
const toolbar = <EditorToolbar editorState={toolbarEditorState} />;
|
||||
|
||||
const EditorComponent = props.mode === EditorType.Markdown ? MarkdownEditor : RichTextEditor;
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import NoteBodyViewer from '../../NoteBodyViewer/NoteBodyViewer';
|
||||
import checkPermissions from '../../../utils/checkPermissions';
|
||||
import NoteEditor from '../../NoteEditor/NoteEditor';
|
||||
import * as React from 'react';
|
||||
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native';
|
||||
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent, useWindowDimensions } from 'react-native';
|
||||
import { Platform, PermissionsAndroid } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -77,7 +77,10 @@ import { IconButton } from 'react-native-paper';
|
||||
import { writeTextToCacheFile } from '../../../utils/ShareUtils';
|
||||
import shareFile from '../../../utils/shareFile';
|
||||
import NotePositionService from '@joplin/lib/services/NotePositionService';
|
||||
import useKeyboardState from '../../../utils/hooks/useKeyboardState';
|
||||
import VoiceTyping from '../../../services/voiceTyping/VoiceTyping';
|
||||
import useDebounced from '../../../utils/hooks/useDebounced';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@@ -122,12 +125,14 @@ interface Props extends BaseProps {
|
||||
interface ComponentProps extends Props {
|
||||
dialogs: DialogControl;
|
||||
visibleEditorPluginIds: string[];
|
||||
lowVerticalSpace: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
note: NoteEntity;
|
||||
mode: NoteViewerMode;
|
||||
readOnly: boolean;
|
||||
searchVisible: boolean;
|
||||
folder: FolderEntity|null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
lastSavedNote: any;
|
||||
@@ -215,6 +220,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
showCamera: false,
|
||||
showImageEditor: false,
|
||||
showAudioRecorder: false,
|
||||
searchVisible: false,
|
||||
imageEditorResource: null,
|
||||
noteResources: {},
|
||||
imageEditorResourceFilepath: null,
|
||||
@@ -390,6 +396,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return this.props.useEditorBeta;
|
||||
}
|
||||
|
||||
private onSearchVisibleChange_ = (visible: boolean) => {
|
||||
this.setState({ searchVisible: visible });
|
||||
};
|
||||
|
||||
private onUndoRedoDepthChange(event: UndoRedoDepthChangeEvent) {
|
||||
if (this.useEditorBeta()) {
|
||||
@@ -1600,6 +1609,14 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
const keywords = this.props.searchQuery && !!this.props.ftsEnabled ? this.props.highlightedWords : emptyArray;
|
||||
|
||||
const increaseSpaceForEditor = this.props.lowVerticalSpace
|
||||
&& this.state.mode === 'edit'
|
||||
// For now, only dismiss other UI when search is visible. This provides a way to re-show the hidden UI (by dismissing search).
|
||||
&& this.state.searchVisible
|
||||
// Tapping on the title input when search is visible should edit the title, even if showing the keyboard decreases the
|
||||
// available space.
|
||||
&& !this.titleTextFieldRef.current?.isFocused();
|
||||
|
||||
let bodyComponent = null;
|
||||
|
||||
if (editorView) {
|
||||
@@ -1669,7 +1686,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
|
||||
bodyComponent = <NoteEditor
|
||||
ref={this.editorRef}
|
||||
toolbarEnabled={this.props.toolbarEnabled}
|
||||
toolbarEnabled={this.props.toolbarEnabled && !increaseSpaceForEditor}
|
||||
themeId={this.props.themeId}
|
||||
noteId={this.props.noteId}
|
||||
noteHash={this.props.noteHash}
|
||||
@@ -1680,6 +1697,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
onChange={this.onMarkdownEditorTextChange}
|
||||
onSelectionChange={this.onEditorSelectionChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
onSearchVisibleChange={this.onSearchVisibleChange_}
|
||||
onAttach={this.onAttach}
|
||||
noteResources={this.state.noteResources}
|
||||
readOnly={this.state.readOnly}
|
||||
@@ -1790,25 +1808,27 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
|
||||
const { editorPlugin: activeEditorPlugin } = getActivePluginEditorView(this.props.plugins, this.props.windowId);
|
||||
|
||||
const header = <ScreenHeader
|
||||
folderPickerOptions={this.folderPickerOptions()}
|
||||
menuOptions={this.menuOptions()}
|
||||
showSaveButton={showSaveButton}
|
||||
saveButtonDisabled={saveButtonDisabled}
|
||||
onSaveButtonPress={this.saveNoteButton_press}
|
||||
showSideMenuButton={false}
|
||||
showSearchButton={false}
|
||||
showUndoButton={(this.state.undoRedoButtonState.canUndo || this.state.undoRedoButtonState.canRedo) && this.state.mode === 'edit'}
|
||||
showRedoButton={this.state.undoRedoButtonState.canRedo && this.state.mode === 'edit'}
|
||||
showPluginEditorButton={!!activeEditorPlugin}
|
||||
undoButtonDisabled={!this.state.undoRedoButtonState.canUndo && this.state.undoRedoButtonState.canRedo}
|
||||
onUndoButtonPress={this.screenHeader_undoButtonPress}
|
||||
onRedoButtonPress={this.screenHeader_redoButtonPress}
|
||||
title={getDisplayParentTitle(this.state.note, this.state.folder)}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader
|
||||
folderPickerOptions={this.folderPickerOptions()}
|
||||
menuOptions={this.menuOptions()}
|
||||
showSaveButton={showSaveButton}
|
||||
saveButtonDisabled={saveButtonDisabled}
|
||||
onSaveButtonPress={this.saveNoteButton_press}
|
||||
showSideMenuButton={false}
|
||||
showSearchButton={false}
|
||||
showUndoButton={(this.state.undoRedoButtonState.canUndo || this.state.undoRedoButtonState.canRedo) && this.state.mode === 'edit'}
|
||||
showRedoButton={this.state.undoRedoButtonState.canRedo && this.state.mode === 'edit'}
|
||||
showPluginEditorButton={!!activeEditorPlugin}
|
||||
undoButtonDisabled={!this.state.undoRedoButtonState.canUndo && this.state.undoRedoButtonState.canRedo}
|
||||
onUndoButtonPress={this.screenHeader_undoButtonPress}
|
||||
onRedoButtonPress={this.screenHeader_redoButtonPress}
|
||||
title={getDisplayParentTitle(this.state.note, this.state.folder)}
|
||||
/>
|
||||
{titleComp}
|
||||
{!increaseSpaceForEditor && header}
|
||||
{!increaseSpaceForEditor && titleComp}
|
||||
{bodyComponent}
|
||||
{renderVoiceTypingDialogs()}
|
||||
{renderActionButton()}
|
||||
@@ -1826,6 +1846,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
}
|
||||
|
||||
const useHasLowAvailableSpace = () => {
|
||||
const windowDimensions = useWindowDimensions();
|
||||
const keyboardState = useKeyboardState();
|
||||
const verticalSpaceAvailable = windowDimensions.height - keyboardState.dockedKeyboardHeight;
|
||||
|
||||
const lowVerticalScreenSpace = verticalSpaceAvailable < 270;
|
||||
// Debounce state updates to avoid multiple re-renders when the keyboard is hidden, then quickly
|
||||
// re-shown (e.g. when moving focus between text inputs).
|
||||
return useDebounced(lowVerticalScreenSpace, Second / 10);
|
||||
};
|
||||
|
||||
// We added this change to reset the component state when the props.noteId is changed.
|
||||
// NoteScreenComponent original implementation assumed that noteId would never change,
|
||||
// which can cause some bugs where previously set state to another note would interfere
|
||||
@@ -1833,9 +1864,16 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
const NoteScreenWrapper = (props: Props) => {
|
||||
const dialogs = useContext(DialogContext);
|
||||
const visibleEditorPluginIds = useVisiblePluginEditorViewIds(props.plugins, props.windowId);
|
||||
const lowVerticalSpace = useHasLowAvailableSpace();
|
||||
|
||||
return (
|
||||
<NoteScreenComponent key={props.noteId} dialogs={dialogs} visibleEditorPluginIds={visibleEditorPluginIds} {...props} />
|
||||
<NoteScreenComponent
|
||||
key={props.noteId}
|
||||
dialogs={dialogs}
|
||||
visibleEditorPluginIds={visibleEditorPluginIds}
|
||||
lowVerticalSpace={lowVerticalSpace}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -80,6 +80,21 @@ const useCss = (editorTheme: Theme) => {
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--exit .toolbar-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${Setting.value('buildFlag.ui.disableSmallScreenIncompatibleFeatures') ? `
|
||||
/* As of December 2025, the help overlay is difficult to use on small screens
|
||||
(slow to load, help text overlapping content in some cases). */
|
||||
.js-draw .toolbar-help-overlay-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* As of December 2025, the pipette button is difficult to use on small screens:
|
||||
It may not be clear that it's necessary to dismiss the tool menu in order to
|
||||
pick a color from the screen. */
|
||||
.js-draw .color-input-container > button.pipetteButton.pipetteButton {
|
||||
display: none;
|
||||
}
|
||||
` : ''}
|
||||
`;
|
||||
}, [editorTheme]);
|
||||
};
|
||||
|
||||
@@ -1550,7 +1550,7 @@ PODS:
|
||||
- react-native-vector-icons-material-icons (12.4.0)
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.15.0):
|
||||
- react-native-webview (13.16.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2373,7 +2373,7 @@ SPEC CHECKSUMS:
|
||||
react-native-vector-icons-material-design-icons: 76cd460b3540b80527b4a80fb7f867f7deedb498
|
||||
react-native-vector-icons-material-icons: d67e485a05560416ff6b5977d5fa7e0eb6af6870
|
||||
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
|
||||
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
|
||||
react-native-webview: 8ad427a520a3b94d2006a62bb7756be726116af5
|
||||
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
|
||||
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
|
||||
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@joplin/react-native-saf-x": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@js-draw/material-icons": "1.32.0",
|
||||
"@js-draw/material-icons": "1.33.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.5",
|
||||
"@react-native-community/geolocation": "3.4.0",
|
||||
@@ -51,7 +51,7 @@
|
||||
"expo-av": "15.1.7",
|
||||
"expo-camera": "16.1.11",
|
||||
"expo-local-authentication": "16.0.5",
|
||||
"js-draw": "1.32.0",
|
||||
"js-draw": "1.33.0",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
"path-browserify": "1.0.1",
|
||||
@@ -80,7 +80,7 @@
|
||||
"react-native-svg": "15.13.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-webview": "13.16.0",
|
||||
"react-native-zip-archive": "7.0.2",
|
||||
"react-redux": "8.1.3",
|
||||
"redux": "4.2.1",
|
||||
@@ -114,7 +114,7 @@
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.153",
|
||||
"@types/serviceworker": "0.0.154",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
|
||||
@@ -213,7 +213,7 @@ const modelLocalFilepath = () => {
|
||||
};
|
||||
|
||||
const whisper: VoiceTypingProvider = {
|
||||
supported: () => !!SpeechToTextModule && Setting.value('featureFlag.voiceTypingEnabled'),
|
||||
supported: () => !!SpeechToTextModule && Setting.value('buildFlag.voiceTypingEnabled'),
|
||||
modelLocalFilepath: modelLocalFilepath,
|
||||
getDownloadUrl: (locale) => {
|
||||
const lang = languageCodeOnly(locale).toLowerCase();
|
||||
|
||||
14
packages/app-mobile/utils/hooks/useDebounced.ts
Normal file
14
packages/app-mobile/utils/hooks/useDebounced.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
|
||||
import { useState } from 'react';
|
||||
|
||||
const useDebounced = <T> (value: T, interval: number) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useQueuedAsyncEffect(() => {
|
||||
setDebouncedValue(value);
|
||||
}, [value], { interval });
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
export default useDebounced;
|
||||
@@ -3,11 +3,22 @@ import { SearchState } from '../../types';
|
||||
import { Plugin, EditorState, Command, Transaction } from 'prosemirror-state';
|
||||
import { EditorEvent, EditorEventType } from '../../events';
|
||||
|
||||
type VisibleMetaState = {
|
||||
visible: boolean;
|
||||
changeSource: string;
|
||||
};
|
||||
const getVisibleMeta = (tr: Transaction): VisibleMetaState|undefined => (
|
||||
tr.getMeta(visiblePlugin)
|
||||
);
|
||||
const setVisibleMeta = (tr: Transaction, metaState: VisibleMetaState): Transaction => (
|
||||
tr.setMeta(visiblePlugin, metaState)
|
||||
);
|
||||
|
||||
const visiblePlugin = new Plugin({
|
||||
state: {
|
||||
init: () => false,
|
||||
apply: (tr, value) => {
|
||||
const visibleMeta = tr.getMeta(visiblePlugin);
|
||||
const visibleMeta = getVisibleMeta(tr);
|
||||
if (visibleMeta) {
|
||||
return visibleMeta.visible;
|
||||
}
|
||||
@@ -24,7 +35,7 @@ export const setSearchVisible = (visible: boolean): Command => (state, dispatch)
|
||||
return false;
|
||||
}
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.setMeta(visiblePlugin, { visible }));
|
||||
dispatch(setVisibleMeta(state.tr, { visible, changeSource: 'setSearchVisible' }));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -58,7 +69,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
onEditorEvent({
|
||||
kind: EditorEventType.UpdateSearchDialog,
|
||||
searchState: currentState,
|
||||
changeSources: [transaction.getMeta(visiblePlugin)?.changeSource ?? 'unknown'],
|
||||
changeSources: [getVisibleMeta(transaction)?.changeSource ?? 'unknown'],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -81,16 +92,13 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
],
|
||||
updateState: (editorState: EditorState, searchState: SearchState, changeSource: string) => {
|
||||
let transaction = editorState.tr;
|
||||
setSearchVisible(searchState.dialogVisible)(editorState, (newTransaction) => {
|
||||
transaction = newTransaction;
|
||||
});
|
||||
transaction = setSearchState(transaction, new SearchQuery({
|
||||
search: searchState.searchText,
|
||||
caseSensitive: searchState.caseSensitive,
|
||||
regexp: searchState.useRegex,
|
||||
replace: searchState.replaceText,
|
||||
}));
|
||||
transaction.setMeta(visiblePlugin, { changeSource });
|
||||
setVisibleMeta(transaction, { changeSource, visible: searchState.dialogVisible });
|
||||
lastState = { ...searchState };
|
||||
|
||||
return transaction;
|
||||
|
||||
@@ -16,9 +16,10 @@ const customCssFilePath = (Setting: typeof SettingType, filename: string): strin
|
||||
return `${Setting.value('rootProfileDir')}/${filename}`;
|
||||
};
|
||||
|
||||
const showVoiceTypingSettings = () => (
|
||||
type VoiceTypingSettingSlice = Record<'buildFlag.voiceTypingEnabled', boolean>;
|
||||
const showVoiceTypingSettings = (settings: VoiceTypingSettingSlice) => (
|
||||
// For now, iOS and web don't support voice typing.
|
||||
shim.mobilePlatform() === 'android'
|
||||
shim.mobilePlatform() === 'android' && !!settings['buildFlag.voiceTypingEnabled']
|
||||
);
|
||||
|
||||
export enum CameraDirection {
|
||||
@@ -1910,14 +1911,20 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
// As of December 2025, the voice typing feature doesn't work well on low-resource devices.
|
||||
// There have been requests to allow disabling the voice typing feature at build time. This
|
||||
// feature flag allows doing so, by changing the default `value` from `true` to `false`:
|
||||
'featureFlag.voiceTypingEnabled': {
|
||||
'buildFlag.voiceTypingEnabled': {
|
||||
value: true,
|
||||
type: SettingItemType.Bool,
|
||||
public: false,
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => 'Voice typing: Enable the voice typing feature',
|
||||
section: 'note',
|
||||
advanced: true,
|
||||
},
|
||||
|
||||
'buildFlag.ui.disableSmallScreenIncompatibleFeatures': {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
public: false,
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => 'UI: Disable features known to be incompatible with small screens',
|
||||
},
|
||||
|
||||
'survey.webClientEval2025.progress': {
|
||||
@@ -1984,7 +1991,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => _('Voice typing: Glossary'),
|
||||
description: () => _('A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.'),
|
||||
show: () => showVoiceTypingSettings(),
|
||||
show: showVoiceTypingSettings,
|
||||
section: 'note',
|
||||
},
|
||||
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"android-v3.5.1": true,
|
||||
"ios-v13.5.1": true,
|
||||
"v3.5.9": true,
|
||||
"android-v3.5.3": true
|
||||
"android-v3.5.3": true,
|
||||
"android-v3.5.4": true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# Joplin Android Changelog
|
||||
|
||||
## [android-v3.5.5](https://github.com/laurent22/joplin/releases/tag/android-v3.5.5) (Pre-release) - 2025-12-26T10:53:20Z
|
||||
|
||||
- Improved: Update js-draw to v1.33.0 (#13990 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Updated packages react-native-webview (v13.16.0)
|
||||
- Fixed: Editor: Fix search/replace UI is partially off-screen on small-screen devices (#13978 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Feature flags: Fix "voice typing" feature flag (#13981 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.5.4](https://github.com/laurent22/joplin/releases/tag/android-v3.5.4) (Pre-release) - 2025-12-23T20:00:18Z
|
||||
|
||||
- Improved: Accessibility: Dark mode: Improve contrast of conflicts notebook title, error messages in "Logs" (#13925 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"prosemirror-model",
|
||||
"prosemirror-state",
|
||||
"prosemirror-view",
|
||||
"prosemirror-tables",
|
||||
"@fortawesome/fontawesome-svg-core",
|
||||
"@fortawesome/free-solid-svg-icons",
|
||||
"@svgr/webpack",
|
||||
|
||||
50
yarn.lock
50
yarn.lock
@@ -10526,7 +10526,7 @@ __metadata:
|
||||
"@joplin/turndown": "npm:~4.0.80"
|
||||
"@joplin/turndown-plugin-gfm": "npm:~1.0.62"
|
||||
"@joplin/utils": "npm:~3.5"
|
||||
"@js-draw/material-icons": "npm:1.32.0"
|
||||
"@js-draw/material-icons": "npm:1.33.0"
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0"
|
||||
"@react-native-clipboard/clipboard": "npm:1.16.3"
|
||||
"@react-native-community/cli": "npm:16.0.3"
|
||||
@@ -10552,7 +10552,7 @@ __metadata:
|
||||
"@types/node": "npm:18.19.130"
|
||||
"@types/react": "npm:19.0.14"
|
||||
"@types/react-redux": "npm:7.1.33"
|
||||
"@types/serviceworker": "npm:0.0.153"
|
||||
"@types/serviceworker": "npm:0.0.154"
|
||||
"@types/tar-stream": "npm:3.1.4"
|
||||
assert-browserify: "npm:2.0.0"
|
||||
babel-jest: "npm:29.7.0"
|
||||
@@ -10576,7 +10576,7 @@ __metadata:
|
||||
jest: "npm:29.7.0"
|
||||
jest-environment-jsdom: "npm:29.7.0"
|
||||
jetifier: "npm:2.0.0"
|
||||
js-draw: "npm:1.32.0"
|
||||
js-draw: "npm:1.33.0"
|
||||
jsdom: "npm:26.1.0"
|
||||
lodash: "npm:4.17.21"
|
||||
md5: "npm:2.3.0"
|
||||
@@ -10609,7 +10609,7 @@ __metadata:
|
||||
react-native-url-polyfill: "npm:2.0.0"
|
||||
react-native-version-info: "npm:1.1.1"
|
||||
react-native-web: "npm:0.21.1"
|
||||
react-native-webview: "npm:13.15.0"
|
||||
react-native-webview: "npm:13.16.0"
|
||||
react-native-zip-archive: "npm:7.0.2"
|
||||
react-redux: "npm:8.1.3"
|
||||
react-refresh: "npm:0.17.0"
|
||||
@@ -11365,21 +11365,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@js-draw/material-icons@npm:1.32.0":
|
||||
version: 1.32.0
|
||||
resolution: "@js-draw/material-icons@npm:1.32.0"
|
||||
"@js-draw/material-icons@npm:1.33.0":
|
||||
version: 1.33.0
|
||||
resolution: "@js-draw/material-icons@npm:1.33.0"
|
||||
peerDependencies:
|
||||
js-draw: ^1.0.1
|
||||
checksum: 10/89f2b1cf13ccb2146743f13ea6c85e0f086a926eafeb22f5fef7c1622826181a0c0f59dd571e5975a67eb3407f2388b21e32218dad4eafef43cc2d1c4d059337
|
||||
checksum: 10/7f3ac9ee3b6a1d575abe45e93ff7a2b9f2f2e5fa6d73e67a5a1f4a0be7a03239543d007bf42db6de619fcef68e016ac47a00f053efee2dee0d005177f6cb0733
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@js-draw/math@npm:^1.32.0":
|
||||
version: 1.32.0
|
||||
resolution: "@js-draw/math@npm:1.32.0"
|
||||
"@js-draw/math@npm:^1.33.0":
|
||||
version: 1.33.0
|
||||
resolution: "@js-draw/math@npm:1.33.0"
|
||||
dependencies:
|
||||
bezier-js: "npm:6.1.3"
|
||||
checksum: 10/3fd9d9fb8fb7ae231851e43eb2503d03fd963f665ecdebd99bf74feef24086eee07680d431f413b9b1a72ecd6c3b46ab539d8116bc3a10f1f3cfaefb168a28e7
|
||||
checksum: 10/8772e7a2392d9e1e27276777ca47ef3e31a407aa2a23efc2b914eb8963467e340b2031998571095c9309c0724fe0858b775fe939727cae9d013abcd819f49a5d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17094,10 +17094,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/serviceworker@npm:0.0.153":
|
||||
version: 0.0.153
|
||||
resolution: "@types/serviceworker@npm:0.0.153"
|
||||
checksum: 10/d5a0f9d638ebc1a0cbc9292d9a6146b940ac6d32e32b88fcf5e5bd2cd02c343c78ec7c0f9fb4340e3b3762dd5c48a3b54ea546520d31dc7ca319e95c9540a029
|
||||
"@types/serviceworker@npm:0.0.154":
|
||||
version: 0.0.154
|
||||
resolution: "@types/serviceworker@npm:0.0.154"
|
||||
checksum: 10/462ee7a32c6496aa953434219f4680a12d8f777021fd10c4dbcfd456ea7f3ca0a8170361367f596ac717a83fe72f1ec232e7b1656365d32f3bfeb38f67e937cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -34994,13 +34994,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-draw@npm:1.32.0":
|
||||
version: 1.32.0
|
||||
resolution: "js-draw@npm:1.32.0"
|
||||
"js-draw@npm:1.33.0":
|
||||
version: 1.33.0
|
||||
resolution: "js-draw@npm:1.33.0"
|
||||
dependencies:
|
||||
"@js-draw/math": "npm:^1.32.0"
|
||||
"@js-draw/math": "npm:^1.33.0"
|
||||
"@melloware/coloris": "npm:0.22.0"
|
||||
checksum: 10/3da563efc659e5081fa102bb570791581edcddedc40b2ba60596b7edd3dfe5dcd96b39a3486f266f6489a0ace4a335f5ee54d4aa726cab13e90d3d38a415a23a
|
||||
checksum: 10/1ea87975079b6c5ba2cfaa366e4d5b6d921f118e162a51706053f481c2969eff0b896343782c2a05405fdfa874fc92543a10e4bcd2d224a81d753c59d0e3a43b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -44105,16 +44105,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-webview@npm:13.15.0":
|
||||
version: 13.15.0
|
||||
resolution: "react-native-webview@npm:13.15.0"
|
||||
"react-native-webview@npm:13.16.0":
|
||||
version: 13.16.0
|
||||
resolution: "react-native-webview@npm:13.16.0"
|
||||
dependencies:
|
||||
escape-string-regexp: "npm:^4.0.0"
|
||||
invariant: "npm:2.2.4"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/7d4b23cb0536199ab2339644d8b6ce720e794ac9a8200f23746032a768a61af8727d2401fc000be09b4fb478dce6f795d7d400945a17ab013a3bbc92a5077e4e
|
||||
checksum: 10/dc3d84c4785676bac3f316ec2119d3239f64fd8b18400dcc1cf8fa4f05b9bf677cf167aa4e7da72c9f60760eb6303c99da22324e15ff5fb380a3b1b31f531132
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user