1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-23 23:33:01 +02:00

Compare commits

..

22 Commits

Author SHA1 Message Date
Hubert
3ab14c4ab0 Merge remote-tracking branch 'origin/issue-7808' into issue-7808
# Conflicts:
#	packages/server/src/views/index/user.mustache
2023-08-18 09:02:58 -03:00
Hubert
74d407de6f Removing the spaces. 2023-08-18 09:01:37 -03:00
Hubert
9740dd9bbe Merge remote-tracking branch 'origin/dev' into issue-7808 2023-08-18 08:49:04 -03:00
Hubert
98b3432105 Removing the spaces. 2023-08-18 08:48:35 -03:00
Hubert
cdba5cfa8c Removing the spaces. 2023-08-18 08:45:30 -03:00
Hubert
0cfe4f1e33 Removing the spaces. 2023-08-18 08:44:59 -03:00
Hubert
56fb48d78b Fixes from review. 2023-08-18 08:40:40 -03:00
Laurent Cozic
87e51aa8e6 Doc: Remove support email 2023-08-18 11:50:44 +01:00
Henry Heino
41fdc0d44d Mobile: Fixes #8687: Hide markdown toolbar completely when low on vertical space (#8688) 2023-08-18 09:45:04 +01:00
renovate[bot]
a754a8d772 Update dependency knex to v2.5.0 (#8686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-18 09:44:48 +01:00
Hubert
41d0363fd0 Android: Fixes #8510: The voice typing box covers the texts in the editor (#8685) 2023-08-18 09:42:03 +01:00
Hubert
2a4c7a334e Server: Fixes #8307: Searching for user should be case insensitive (#8682) 2023-08-18 09:39:57 +01:00
Hubert
df1b0a96f4 Server: Fixes #8308: Sorting users by "total size" leads to a crash (#8680) 2023-08-18 09:36:41 +01:00
Henry Heino
0030681cb4 Mobile: Fixes #8310: Preserve image rotation (and other metadata) when resizing (#8669) 2023-08-18 09:34:31 +01:00
Henry Heino
e7014492c5 Desktop: Fixes #8661: Fix note editor blank after syncing an encrypted note with remote changes (#8666) 2023-08-18 09:31:45 +01:00
Joplin Bot
4804c1c0c3 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-08-18 06:19:25 +00:00
Joplin Bot
270d96ad07 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-08-18 00:35:18 +00:00
Hubert
5ff963704b Merge remote-tracking branch 'origin/dev' into issue-7808 2023-08-15 16:15:19 -03:00
Hubert
bea40d0997 Fixes appointed in the review. 2023-08-15 16:14:10 -03:00
Hubert
3dea9ab6e7 A shortcut to user resent the account confirmation email. 2023-08-11 15:59:53 -03:00
Hubert
dd869c43bb Added a condition to only show send account confirmation email when the user didn't confirm the email yet. 2023-08-10 17:00:03 -03:00
Hubert
7f5acdea5e Added the possibility to user send account confirmation email from their profile. 2023-08-10 15:25:24 -03:00
26 changed files with 364 additions and 131 deletions

View File

@@ -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
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -27,6 +27,7 @@ export interface NoteEditorProps {
isProvisional: boolean;
editorNoteStatuses: any;
syncStarted: boolean;
decryptionStarted: boolean;
bodyEditor: string;
notesParentType: string;
selectedNoteTags: any[];

View File

@@ -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!',
});
});
});
});

View File

@@ -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) {

View File

@@ -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();
});

View File

@@ -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;

View File

@@ -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();
}
});
}
});
});

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
);
};

View File

@@ -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

View File

@@ -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.

View File

@@ -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",

View File

@@ -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.

View File

@@ -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",

View File

@@ -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);
// }

View File

@@ -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: '',
};

View File

@@ -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'),
},
],

View File

@@ -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);

View File

@@ -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>

View File

@@ -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?

View File

@@ -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"