You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-23 23:33:01 +02:00
Compare commits
22 Commits
issue-8307
...
issue-7808
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ab14c4ab0 | ||
|
|
74d407de6f | ||
|
|
9740dd9bbe | ||
|
|
98b3432105 | ||
|
|
cdba5cfa8c | ||
|
|
0cfe4f1e33 | ||
|
|
56fb48d78b | ||
|
|
87e51aa8e6 | ||
|
|
41fdc0d44d | ||
|
|
a754a8d772 | ||
|
|
41d0363fd0 | ||
|
|
2a4c7a334e | ||
|
|
df1b0a96f4 | ||
|
|
0030681cb4 | ||
|
|
e7014492c5 | ||
|
|
4804c1c0c3 | ||
|
|
270d96ad07 | ||
|
|
5ff963704b | ||
|
|
bea40d0997 | ||
|
|
3dea9ab6e7 | ||
|
|
dd869c43bb | ||
|
|
7f5acdea5e |
@@ -260,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
@@ -410,6 +411,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -246,6 +246,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
@@ -396,6 +397,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
|
||||
BIN
Assets/Aide.png
Normal file
BIN
Assets/Aide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -112,7 +112,7 @@
|
||||
}).then(async function(result) {
|
||||
if (!result.ok) {
|
||||
console.error('Could not create Stripe checkout session', await result.text());
|
||||
alert('The checkout session could not be created. Please contact support@joplincloud.com for support.');
|
||||
alert('The checkout session could not be created. Please contact us on the forum for support.');
|
||||
} else {
|
||||
return result.json();
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
syncStarted: props.syncStarted,
|
||||
decryptionStarted: props.decryptionStarted,
|
||||
noteId: effectiveNoteId,
|
||||
isProvisional: props.isProvisional,
|
||||
titleInputRef: titleInputRef,
|
||||
@@ -633,6 +634,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
isProvisional: state.provisionalNoteIds.includes(noteId),
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
syncStarted: state.syncStarted,
|
||||
decryptionStarted: state.decryptionWorker?.state !== 'idle',
|
||||
themeId: state.settings.theme,
|
||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface NoteEditorProps {
|
||||
isProvisional: boolean;
|
||||
editorNoteStatuses: any;
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
bodyEditor: string;
|
||||
notesParentType: string;
|
||||
selectedNoteTags: any[];
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
|
||||
|
||||
describe('useFormNote', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
it('should update note when decryption completes', async () => {
|
||||
const testNote = await Note.save({ title: 'Test Note!' });
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
return {
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: testNote.id,
|
||||
isProvisional: false,
|
||||
titleInputRef: null,
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
};
|
||||
};
|
||||
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(true, false),
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: testNote.title,
|
||||
});
|
||||
});
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_cipher_text: 'cipher_text',
|
||||
encryption_applied: 1,
|
||||
});
|
||||
|
||||
// Sync starting should cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 1,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
formNote.rerender(makeFormNoteProps(false, true));
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
|
||||
// Ending decryption should also cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,9 @@ export interface OnLoadEvent {
|
||||
formNote: FormNote;
|
||||
}
|
||||
|
||||
interface HookDependencies {
|
||||
export interface HookDependencies {
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
isProvisional: boolean;
|
||||
titleInputRef: any;
|
||||
@@ -61,15 +62,21 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
}
|
||||
|
||||
export default function useFormNote(dependencies: HookDependencies) {
|
||||
const { syncStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
||||
const {
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||
} = dependencies;
|
||||
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<boolean>(false);
|
||||
const [isNewNote, setIsNewNote] = useState(false);
|
||||
const prevSyncStarted = usePrevious(syncStarted);
|
||||
const prevDecryptionStarted = usePrevious(decryptionStarted);
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
async function initNoteState(n: any) {
|
||||
let originalCss = '';
|
||||
|
||||
@@ -107,7 +114,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!formNoteRefeshScheduled) return () => {};
|
||||
if (formNoteRefeshScheduled <= 0) return () => {};
|
||||
|
||||
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
@@ -126,7 +133,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}
|
||||
|
||||
await initNoteState(n);
|
||||
setFormNoteRefreshScheduled(false);
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
void loadNote();
|
||||
@@ -136,21 +143,32 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
};
|
||||
}, [formNoteRefeshScheduled, noteId]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefeshScheduled + 1);
|
||||
}, [formNoteRefeshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!prevSyncStarted) return;
|
||||
if (syncStarted) return;
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNote.hasChanged) return;
|
||||
|
||||
// Refresh the form note.
|
||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||
// from true to false, it doesn't cancel the note from loading.
|
||||
setFormNoteRefreshScheduled(true);
|
||||
}, [prevSyncStarted, syncStarted, formNote.hasChanged]);
|
||||
refreshFormNote();
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
prevDecryptionStarted, decryptionStarted,
|
||||
formNote.hasChanged, refreshFormNote,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) {
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
const { default: Logger, TargetType } = require('@joplin/utils/Logger');
|
||||
const initLib = require('@joplin/lib/initLib').default;
|
||||
|
||||
// TODO: Some libraries required by test-utils.js seem to fail to import with the
|
||||
// jsdom environment.
|
||||
//
|
||||
// Thus, require('@joplin/lib/testing/test-utils.js') fails and some setup must be
|
||||
// copied.
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget(TargetType.Console);
|
||||
logger.setLevel(Logger.LEVEL_WARN);
|
||||
Logger.initializeGlobalLogger(logger);
|
||||
initLib(logger);
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||
const sqlite3 = require('sqlite3');
|
||||
const SyncTargetNone = require('@joplin/lib/SyncTargetNone').default;
|
||||
|
||||
// Mock the S3 sync target -- the @aws-s3 libraries depend on an old version
|
||||
// of uuid that doesn't work with jest without additional configuration.
|
||||
jest.doMock('@joplin/lib/SyncTargetAmazonS3', () => {
|
||||
return SyncTargetNone;
|
||||
});
|
||||
|
||||
// @electron/remote requires electron to be running. Mock it.
|
||||
jest.mock('@electron/remote', () => {
|
||||
@@ -25,3 +20,18 @@ jest.mock('@electron/remote', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocking problematic libraries
|
||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllCleanUp();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react');
|
||||
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
@@ -18,19 +18,22 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
||||
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
||||
|
||||
const allButtonSpecs = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
const allButtonSpecs = useMemo(() => {
|
||||
const buttons = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
|
||||
// Sort from highest priority to lowest
|
||||
allButtonSpecs.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
// Sort from highest priority to lowest
|
||||
buttons.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
return buttons;
|
||||
}, [props.buttons]);
|
||||
|
||||
const allButtonComponents: ReactElement[] = [];
|
||||
let key = 0;
|
||||
@@ -67,7 +70,9 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
);
|
||||
|
||||
const mainButtons: ReactElement[] = [];
|
||||
if (maxButtonsEachSide < allButtonComponents.length) {
|
||||
if (maxButtonsEachSide >= allButtonComponents.length) {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
} else if (maxButtonsEachSide > 0) {
|
||||
// We want the menu to look something like this:
|
||||
// B I (…) 🔍 ⌨
|
||||
// where (…) shows/hides overflow.
|
||||
@@ -77,7 +82,7 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
|
||||
} else {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
}
|
||||
|
||||
const styles = props.styleSheet.styles;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
|
||||
import '@testing-library/jest-native';
|
||||
|
||||
import NoteEditor from './NoteEditor';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
describe('NoteEditor', () => {
|
||||
beforeEach(async () => {
|
||||
// Required to use ExtendedWebView
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
});
|
||||
|
||||
it('should hide the markdown toolbar when the window is small', async () => {
|
||||
const wrappedNoteEditor = render(
|
||||
<MenuProvider>
|
||||
<NoteEditor
|
||||
themeId={Setting.THEME_ARITIM_DARK}
|
||||
initialText='Testing...'
|
||||
style={{}}
|
||||
toolbarEnabled={true}
|
||||
readOnly={false}
|
||||
onChange={()=>{}}
|
||||
onSelectionChange={()=>{}}
|
||||
onUndoRedoDepthChange={()=>{}}
|
||||
onAttach={()=>{}}
|
||||
/>
|
||||
</MenuProvider>
|
||||
);
|
||||
|
||||
// Maps from screen height to whether the markdown toolbar should be visible.
|
||||
const testCases: [number, boolean][] = [
|
||||
[10, false],
|
||||
[1000, true],
|
||||
[100, false],
|
||||
[80, false],
|
||||
[600, true],
|
||||
];
|
||||
|
||||
const noteEditorRoot = await wrappedNoteEditor.findByTestId('note-editor-root');
|
||||
|
||||
const setRootHeight = (height: number) => {
|
||||
act(() => {
|
||||
// See https://stackoverflow.com/a/61774123
|
||||
fireEvent(noteEditorRoot, 'layout', {
|
||||
nativeEvent: {
|
||||
layout: { height },
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
for (const [height, visible] of testCases) {
|
||||
setRootHeight(height);
|
||||
|
||||
await waitFor(async () => {
|
||||
const showMoreButton = await screen.queryByLabelText(_('Show more actions'));
|
||||
if (visible) {
|
||||
expect(showMoreButton).not.toBeNull();
|
||||
} else {
|
||||
expect(showMoreButton).toBeNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import ExtendedWebView from '../ExtendedWebView';
|
||||
const React = require('react');
|
||||
import { forwardRef, RefObject, useImperativeHandle } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
import SelectionFormatting from './SelectionFormatting';
|
||||
@@ -368,6 +368,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
console.error('NoteEditor: webview error');
|
||||
}, []);
|
||||
|
||||
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 toolbar = <MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
@@ -385,10 +398,14 @@ function NoteEditor(props: Props, ref: any) {
|
||||
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
|
||||
// when an editable region (e.g. a the full-screen NoteEditor) is focused.
|
||||
return (
|
||||
<View style={{
|
||||
...props.style,
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<View
|
||||
testID='note-editor-root'
|
||||
onLayout={onContainerLayout}
|
||||
style={{
|
||||
...props.style,
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<EditLinkDialog
|
||||
visible={linkDialogVisible}
|
||||
themeId={props.themeId}
|
||||
@@ -419,7 +436,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
searchState={searchState}
|
||||
/>
|
||||
|
||||
{props.toolbarEnabled ? toolbar : null}
|
||||
{toolbarEnabled ? toolbar : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { themeStyle, editorFont } = require('../global-style.js');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const ImageResizer = require('react-native-image-resizer').default;
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import { ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
@@ -586,7 +586,16 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
|
||||
reg.logger().info(`Resizing image ${localFilePath}`);
|
||||
const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); // , 0, targetPath);
|
||||
const resizedImage = await ImageResizer.createResizedImage(
|
||||
localFilePath,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
format,
|
||||
85, // quality
|
||||
undefined, // rotation
|
||||
undefined, // outputPath
|
||||
true // keep metadata
|
||||
);
|
||||
|
||||
const resizedImagePath = resizedImage.uri;
|
||||
reg.logger().info('Resized image ', resizedImagePath);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Banner, ActivityIndicator, Modal } from 'react-native-paper';
|
||||
import { Banner, ActivityIndicator } from 'react-native-paper';
|
||||
import { _, languageName } from '@joplin/lib/locale';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { getVosk, Recorder, startRecording, Vosk } from '../../services/voiceTyping/vosk';
|
||||
@@ -107,18 +107,16 @@ export default (props: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={true} style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
||||
<Banner
|
||||
visible={true}
|
||||
icon={renderIcon()}
|
||||
actions={[
|
||||
{
|
||||
label: _('Done'),
|
||||
onPress: onDismiss,
|
||||
},
|
||||
]}>
|
||||
{`${_('Voice typing...')}\n${renderContent()}`}
|
||||
</Banner>
|
||||
</Modal>
|
||||
<Banner
|
||||
visible={true}
|
||||
icon={renderIcon()}
|
||||
actions={[
|
||||
{
|
||||
label: _('Done'),
|
||||
onPress: onDismiss,
|
||||
},
|
||||
]}>
|
||||
{`${_('Voice typing...')}\n${renderContent()}`}
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -353,7 +353,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-image-picker (5.6.0):
|
||||
- React-Core
|
||||
- react-native-image-resizer (1.4.5):
|
||||
- react-native-image-resizer (3.0.5):
|
||||
- React-Core
|
||||
- react-native-netinfo (9.4.1):
|
||||
- React-Core
|
||||
@@ -584,7 +584,7 @@ DEPENDENCIES:
|
||||
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||
- react-native-image-resizer (from `../node_modules/react-native-image-resizer`)
|
||||
- "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)"
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-rsa-native (from `../node_modules/react-native-rsa-native`)
|
||||
- "react-native-saf-x (from `../node_modules/@joplin/react-native-saf-x`)"
|
||||
@@ -703,7 +703,7 @@ EXTERNAL SOURCES:
|
||||
react-native-image-picker:
|
||||
:path: "../node_modules/react-native-image-picker"
|
||||
react-native-image-resizer:
|
||||
:path: "../node_modules/react-native-image-resizer"
|
||||
:path: "../node_modules/@bam.tech/react-native-image-resizer"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-rsa-native:
|
||||
@@ -824,7 +824,7 @@ SPEC CHECKSUMS:
|
||||
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
|
||||
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
|
||||
react-native-image-picker: db60857e03d63721f19b6f4027de20429ddd9cba
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
|
||||
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: 129cd2ddf120a1f6164c724b2846d172666b33de
|
||||
|
||||
@@ -33,6 +33,21 @@ document.createRange = () => {
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
|
||||
// This library has the following error when running within Jest:
|
||||
// Invariant Violation: `new NativeEventEmitter()` requires a non-null argument.
|
||||
jest.mock('react-native-device-info', () => {
|
||||
return {
|
||||
hasNotch: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
// react-native-webview expects native iOS/Android code so needs to be mocked.
|
||||
jest.mock('react-native-webview', () => {
|
||||
const { View } = require('react-native');
|
||||
return {
|
||||
WebView: View,
|
||||
};
|
||||
});
|
||||
|
||||
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
|
||||
// Use a temporary folder instead.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"postinstall": "jetify && yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bam.tech/react-native-image-resizer": "3.0.5",
|
||||
"@joplin/lib": "~2.12",
|
||||
"@joplin/react-native-alarm-notification": "~2.12",
|
||||
"@joplin/react-native-saf-x": "~2.12",
|
||||
@@ -56,7 +57,6 @@
|
||||
"react-native-gesture-handler": "2.12.0",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-image-picker": "5.6.0",
|
||||
"react-native-image-resizer": "1.4.5",
|
||||
"react-native-localize": "3.0.2",
|
||||
"react-native-modal-datetime-picker": "15.0.1",
|
||||
"react-native-paper": "5.9.1",
|
||||
|
||||
@@ -34,7 +34,6 @@ const { FileApiDriverLocal } = require('../file-api-driver-local');
|
||||
const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js');
|
||||
const { FileApiDriverDropbox } = require('../file-api-driver-dropbox.js');
|
||||
const { FileApiDriverOneDrive } = require('../file-api-driver-onedrive.js');
|
||||
const { FileApiDriverAmazonS3 } = require('../file-api-driver-amazon-s3.js');
|
||||
import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||
const SyncTargetMemory = require('../SyncTargetMemory.js');
|
||||
const SyncTargetFilesystem = require('../SyncTargetFilesystem.js');
|
||||
@@ -60,7 +59,6 @@ import Synchronizer from '../Synchronizer';
|
||||
import SyncTargetNone from '../SyncTargetNone';
|
||||
import { setRSA } from '../services/e2ee/ppk';
|
||||
const md5 = require('md5');
|
||||
const { S3Client } = require('@aws-sdk/client-s3');
|
||||
const { Dirnames } = require('../services/synchronizer/utils/types');
|
||||
import RSA from '../services/e2ee/RSA.node';
|
||||
import { State as ShareState } from '../services/share/reducer';
|
||||
@@ -627,6 +625,13 @@ async function initFileApi() {
|
||||
const appDir = await api.appDirectory();
|
||||
fileApi = new FileApi(appDir, new FileApiDriverOneDrive(api));
|
||||
} else if (syncTargetId_ === SyncTargetRegistry.nameToId('amazon_s3')) {
|
||||
// (Most of?) the @aws-sdk libraries depend on an old version of uuid
|
||||
// that doesn't work with jest (without converting ES6 exports to CommonJS).
|
||||
//
|
||||
// Require it dynamically so that this doesn't break test environments that
|
||||
// aren't configured to do this conversion.
|
||||
const { FileApiDriverAmazonS3 } = require('../file-api-driver-amazon-s3.js');
|
||||
const { S3Client } = require('@aws-sdk/client-s3');
|
||||
|
||||
// We make sure for S3 tests run in band because tests
|
||||
// share the same directory which will cause locking errors.
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"fs-extra": "11.1.1",
|
||||
"html-entities": "1.4.0",
|
||||
"jquery": "3.7.0",
|
||||
"knex": "2.4.2",
|
||||
"knex": "2.5.0",
|
||||
"koa": "2.14.2",
|
||||
"markdown-it": "13.0.1",
|
||||
"mustache": "4.2.0",
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// function stripeConfig() {
|
||||
// if (!joplin || !joplin.stripeConfig) throw new Error('Stripe config is not set');
|
||||
// return joplin.stripeConfig;
|
||||
// }
|
||||
|
||||
// function newStripe() {
|
||||
// return Stripe(stripeConfig().publishableKey);
|
||||
// }
|
||||
|
||||
// async function createStripeCheckoutSession(priceId) {
|
||||
// const urlQuery = new URLSearchParams(location.search);
|
||||
// const coupon = urlQuery.get('coupon') || '';
|
||||
|
||||
// console.info('Creating Stripe session for price:', priceId, 'Coupon:', coupon);
|
||||
|
||||
// const result = await fetch(`${stripeConfig().webhookBaseUrl}/stripe/createCheckoutSession`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// priceId: priceId,
|
||||
// coupon: coupon,
|
||||
// }),
|
||||
// });
|
||||
|
||||
// if (!result.ok) {
|
||||
// console.error('Could not create Stripe checkout session', await result.text());
|
||||
// alert('The checkout session could not be created. Please contact support@joplincloud.com for support.');
|
||||
// } else {
|
||||
// return result.json();
|
||||
// }
|
||||
// }
|
||||
|
||||
// async function startStripeCheckout(priceId) {
|
||||
// const data = await createStripeCheckoutSession(stripeId);
|
||||
|
||||
// const result = await stripe.redirectToCheckout({
|
||||
// sessionId: data.sessionId,
|
||||
// });
|
||||
|
||||
// console.info('Redirected to checkout', result);
|
||||
// }
|
||||
@@ -63,7 +63,7 @@ async function handleConfirmEmailNotification(ctx: AppContext): Promise<Notifica
|
||||
if (!ctx.joplin.owner.email_confirmed) {
|
||||
return {
|
||||
id: 'confirmEmail',
|
||||
messageHtml: renderMarkdown('An email has been sent to you containing an activation link to complete your registration.\n\nMake sure you click it to secure your account and keep access to it.'),
|
||||
messageHtml: renderMarkdown(`An email has been sent to you containing an activation link to complete your registration. If you did not receive it, you may send it again from [your profile page](${profileUrl()}).\n\nMake sure you click it to secure your account and keep access to it.`),
|
||||
levelClassName: levelClassName(NotificationLevel.Important),
|
||||
closeUrl: '',
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ router.get('admin/users', async (_path: SubPath, ctx: AppContext) => {
|
||||
label: _('Email'),
|
||||
},
|
||||
{
|
||||
name: 'account',
|
||||
name: 'account_type',
|
||||
label: _('Account'),
|
||||
},
|
||||
{
|
||||
@@ -145,15 +145,15 @@ router.get('admin/users', async (_path: SubPath, ctx: AppContext) => {
|
||||
label: _('Max Item Size'),
|
||||
},
|
||||
{
|
||||
name: 'total_size',
|
||||
name: 'total_item_size',
|
||||
label: _('Total Size'),
|
||||
},
|
||||
{
|
||||
name: 'max_total_size',
|
||||
name: 'max_total_item_size',
|
||||
label: _('Max Total Size'),
|
||||
},
|
||||
{
|
||||
name: 'can_share',
|
||||
name: 'can_share_folder',
|
||||
label: _('Can Share'),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -90,6 +90,7 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, formUser: User =
|
||||
view.content.error = error;
|
||||
view.content.postUrl = postUrl;
|
||||
view.content.csrfTag = await createCsrfTag(ctx);
|
||||
view.content.showSendAccountConfirmationEmailButton = !user.email_confirmed;
|
||||
|
||||
if (subscription) {
|
||||
const lastPaymentAttempt = models.subscription().lastPaymentAttempt(subscription);
|
||||
@@ -213,6 +214,7 @@ interface FormFields {
|
||||
update_subscription_basic_button: string;
|
||||
update_subscription_pro_button: string;
|
||||
stop_impersonate_button: string;
|
||||
send_account_confirmation_email: string;
|
||||
}
|
||||
|
||||
router.post('users', async (path: SubPath, ctx: AppContext) => {
|
||||
@@ -246,6 +248,8 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
|
||||
// that user, except the current one (otherwise they would be
|
||||
// logged out).
|
||||
if (userToSave.password) await models.session().deleteByUserId(userToSave.id, contextSessionId(ctx));
|
||||
} else if (fields.send_account_confirmation_email) {
|
||||
await models.user().sendAccountConfirmationEmail(user);
|
||||
} else if (fields.stop_impersonate_button) {
|
||||
await stopImpersonating(ctx);
|
||||
return redirect(ctx, config().baseUrl);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<p id="password_strength" class="help"></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Repeat password</label>
|
||||
<div class="control">
|
||||
@@ -37,6 +37,9 @@
|
||||
|
||||
<div class="control block">
|
||||
<input type="submit" name="post_button" class="button is-primary" value="{{buttonTitle}}" />
|
||||
{{#showSendAccountConfirmationEmailButton}}
|
||||
<input type="submit" name="send_account_confirmation_email" class="button is-link" value="Send account confirmation email" />
|
||||
{{/showSendAccountConfirmationEmailButton}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +53,7 @@
|
||||
<p class="help">Click for more info about the Pro plan and to upgrade your account.</p>
|
||||
</div>
|
||||
{{/showUpdateSubscriptionPro}}
|
||||
|
||||
|
||||
<div class="control block">
|
||||
<p><a class="button is-link" target="_blank" href="{{stripePortalUrl}}">Manage subscription</a></p>
|
||||
<p class="help">Click to update your payment details, switch to a different billing cycle or plan, or to cancel your subscription.</p>
|
||||
|
||||
@@ -18,7 +18,7 @@ Moreover, by getting a subscription you are supporting the development of the pr
|
||||
|
||||
We offer a 50% Education Discount for students and teachers. To claim it, please contact us from your university or school email address. You will then receive a URL you can use to subscribe to Joplin Cloud while benefiting from the 50% discount. This is valid for a whole year and can be renewed for as long as you are in education by contacting us again.
|
||||
|
||||
We may also offer bulk discounts for companies, associations and nonprofit organisations. Please [contact us](mailto:support@joplincloud.com) for more details.
|
||||
We may also offer bulk discounts for companies, associations and nonprofit organisations. Please [contact us](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/Aide.png) for more details.
|
||||
|
||||
## Where is Joplin Cloud data located?
|
||||
|
||||
|
||||
66
yarn.lock
66
yarn.lock
@@ -2840,6 +2840,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bam.tech/react-native-image-resizer@npm:3.0.5":
|
||||
version: 3.0.5
|
||||
resolution: "@bam.tech/react-native-image-resizer@npm:3.0.5"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: f555bd10aafab1a797b6f5142e59b1bb11f229b61d35c3e6c9d734bbd4999f97bcc86853f69965d758ca872aad024add7a852041da498b177eee923ef82e506d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bcoe/v8-coverage@npm:^0.2.3":
|
||||
version: 0.2.3
|
||||
resolution: "@bcoe/v8-coverage@npm:0.2.3"
|
||||
@@ -4502,6 +4512,7 @@ __metadata:
|
||||
"@babel/core": 7.20.2
|
||||
"@babel/preset-env": 7.20.2
|
||||
"@babel/runtime": 7.20.0
|
||||
"@bam.tech/react-native-image-resizer": 3.0.5
|
||||
"@codemirror/commands": 6.2.2
|
||||
"@codemirror/lang-cpp": 6.0.2
|
||||
"@codemirror/lang-html": 6.4.3
|
||||
@@ -4576,7 +4587,6 @@ __metadata:
|
||||
react-native-gesture-handler: 2.12.0
|
||||
react-native-get-random-values: 1.9.0
|
||||
react-native-image-picker: 5.6.0
|
||||
react-native-image-resizer: 1.4.5
|
||||
react-native-localize: 3.0.2
|
||||
react-native-modal-datetime-picker: 15.0.1
|
||||
react-native-paper: 5.9.1
|
||||
@@ -4908,7 +4918,7 @@ __metadata:
|
||||
jest-expect-message: 1.1.3
|
||||
jquery: 3.7.0
|
||||
jsdom: 22.1.0
|
||||
knex: 2.4.2
|
||||
knex: 2.5.0
|
||||
koa: 2.14.2
|
||||
markdown-it: 13.0.1
|
||||
mustache: 4.2.0
|
||||
@@ -21663,7 +21673,46 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"knex@npm:2.4.2, knex@npm:^2.4.2":
|
||||
"knex@npm:2.5.0":
|
||||
version: 2.5.0
|
||||
resolution: "knex@npm:2.5.0"
|
||||
dependencies:
|
||||
colorette: 2.0.19
|
||||
commander: ^10.0.0
|
||||
debug: 4.3.4
|
||||
escalade: ^3.1.1
|
||||
esm: ^3.2.25
|
||||
get-package-type: ^0.1.0
|
||||
getopts: 2.3.0
|
||||
interpret: ^2.2.0
|
||||
lodash: ^4.17.21
|
||||
pg-connection-string: 2.6.1
|
||||
rechoir: ^0.8.0
|
||||
resolve-from: ^5.0.0
|
||||
tarn: ^3.0.2
|
||||
tildify: 2.0.0
|
||||
peerDependenciesMeta:
|
||||
better-sqlite3:
|
||||
optional: true
|
||||
mysql:
|
||||
optional: true
|
||||
mysql2:
|
||||
optional: true
|
||||
pg:
|
||||
optional: true
|
||||
pg-native:
|
||||
optional: true
|
||||
sqlite3:
|
||||
optional: true
|
||||
tedious:
|
||||
optional: true
|
||||
bin:
|
||||
knex: bin/cli.js
|
||||
checksum: 83c9fa20423546cbfcfedd28095645ad22c4a3529c02a94cbfe970800c0542c0dde502245237666f52da7edfe4b404b4acd3d5179fc37008ddaa6025b1fcfb9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"knex@npm:^2.4.2":
|
||||
version: 2.4.2
|
||||
resolution: "knex@npm:2.4.2"
|
||||
dependencies:
|
||||
@@ -26484,7 +26533,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-connection-string@npm:^2.6.1":
|
||||
"pg-connection-string@npm:2.6.1, pg-connection-string@npm:^2.6.1":
|
||||
version: 2.6.1
|
||||
resolution: "pg-connection-string@npm:2.6.1"
|
||||
checksum: 882344a47e1ecf3a91383e0809bf2ac48facea97fcec0358d6e060e1cbcb8737acde419b4c86f05da4ce4a16634ee50fff1d2bb787d73b52ccbfde697243ad8a
|
||||
@@ -27912,15 +27961,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-image-resizer@npm:1.4.5":
|
||||
version: 1.4.5
|
||||
resolution: "react-native-image-resizer@npm:1.4.5"
|
||||
peerDependencies:
|
||||
react-native: ">=v0.40.0"
|
||||
checksum: c530b119f25c27b669148461554440acd05fb37f0a24a1afc3d58c861e426551c70c679983c0399034fd54ca3575c89d0863778c49295721cb33b65bbf3f0f37
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-localize@npm:3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "react-native-localize@npm:3.0.2"
|
||||
|
||||
Reference in New Issue
Block a user