Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/release-3.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
personalizedrefrigerator committed Dec 9, 2024
2 parents 95ca6c4 + 9c4142f commit ca97597
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 16 deletions.
11 changes: 7 additions & 4 deletions packages/app-mobile/components/ScreenHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const PADDING_V = 10;
type OnPressCallback=()=> void;

export interface FolderPickerOptions {
enabled: boolean;
visible: boolean;
disabled?: boolean;
selectedFolderId?: string;
onValueChange?: OnValueChangedListener;
mustSelect?: boolean;
Expand Down Expand Up @@ -515,10 +516,12 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
});
}

const createTitleComponent = (disabled: boolean, hideableAfterTitleComponents: ReactElement) => {
const createTitleComponent = (hideableAfterTitleComponents: ReactElement) => {
const folderPickerOptions = this.props.folderPickerOptions;

if (folderPickerOptions && folderPickerOptions.enabled) {
if (folderPickerOptions && folderPickerOptions.visible) {
const hasSelectedNotes = this.props.selectedNoteIds.length > 0;
const disabled = this.props.folderPickerOptions.disabled ?? !hasSelectedNotes;
return (
<FolderPicker
themeId={themeId}
Expand Down Expand Up @@ -602,7 +605,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
{betaIconComp}
</>;

const titleComp = createTitleComponent(headerItemDisabled, hideableRightComponents);
const titleComp = createTitleComponent(hideableRightComponents);

const contextMenuStyle: ViewStyle = {
paddingTop: PADDING_V,
Expand Down
19 changes: 15 additions & 4 deletions packages/app-mobile/components/screens/Note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { isSupportedLanguage } from '../../services/voiceTyping/vosk';
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { join } from 'path';
import { Dispatch } from 'redux';
import { RefObject, useContext } from 'react';
import { RefObject, useContext, useRef } from 'react';
import { SelectionRange } from '../NoteEditor/types';
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { AppState } from '../../utils/types';
Expand Down Expand Up @@ -1383,15 +1383,16 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp

public folderPickerOptions() {
const options = {
enabled: !this.state.readOnly,
visible: !this.state.readOnly,
disabled: false,
selectedFolderId: this.state.folder ? this.state.folder.id : null,
onValueChange: this.folderPickerOptions_valueChanged,
};

if (
this.folderPickerOptions_
&& options.selectedFolderId === this.folderPickerOptions_.selectedFolderId
&& options.enabled === this.folderPickerOptions_.enabled
&& options.visible === this.folderPickerOptions_.visible
) {
return this.folderPickerOptions_;
}
Expand Down Expand Up @@ -1649,9 +1650,19 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// which can cause some bugs where previously set state to another note would interfere
// how the new note should be rendered
const NoteScreenWrapper = (props: Props) => {
const lastNonNullNoteIdRef = useRef(props.noteId);
if (props.noteId) {
lastNonNullNoteIdRef.current = props.noteId;
}

// This keeps the current note open even if it's no longer present in selectedNoteIds.
// This might happen, for example, if the selected note is moved to an unselected
// folder.
const noteId = lastNonNullNoteIdRef.current;

const dialogs = useContext(DialogContext);
return (
<NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} />
<NoteScreenComponent key={noteId} dialogs={dialogs} {...props} />
);
};

Expand Down
4 changes: 2 additions & 2 deletions packages/app-mobile/components/screens/Notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,11 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {

public folderPickerOptions() {
const options = {
enabled: this.props.noteSelectionEnabled,
visible: this.props.noteSelectionEnabled,
mustSelect: true,
};

if (this.folderPickerOptions_ && options.enabled === this.folderPickerOptions_.enabled) return this.folderPickerOptions_;
if (this.folderPickerOptions_ && options.visible === this.folderPickerOptions_.visible) return this.folderPickerOptions_;

this.folderPickerOptions_ = options;
return this.folderPickerOptions_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
<ScreenHeader
title={_('Search')}
folderPickerOptions={{
enabled: props.noteSelectionEnabled,
visible: props.noteSelectionEnabled,
mustSelect: true,
}}
showSideMenuButton={false}
Expand Down
40 changes: 39 additions & 1 deletion packages/lib/services/trash/permanentlyDeleteOldItems.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Day, msleep } from '@joplin/utils/time';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import { setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, switchClient } from '../../testing/test-utils';
import permanentlyDeleteOldItems from './permanentlyDeleteOldItems';
import Setting from '../../models/Setting';

Expand Down Expand Up @@ -75,4 +75,42 @@ describe('permanentlyDeleteOldItems', () => {
expect(await Folder.count()).toBe(1);
});

it('should not auto-delete read-only items', async () => {
const shareId = 'testShare';

// Simulates a folder having been deleted a long time ago
const longTimeAgo = 1000;

const readOnlyFolder = await Folder.save({
title: 'Read-only folder',
share_id: shareId,
deleted_time: longTimeAgo,
});
const readOnlyNote1 = await Note.save({
title: 'Read-only note',
parent_id: readOnlyFolder.id,
share_id: shareId,
deleted_time: longTimeAgo,
});
const readOnlyNote2 = await Note.save({
title: 'Read-only note 2',
share_id: shareId,
deleted_time: longTimeAgo,
});
const writableNote = await Note.save({
title: 'Editable note',
deleted_time: longTimeAgo,
});

const cleanup = simulateReadOnlyShareEnv(shareId);
await permanentlyDeleteOldItems(Day);

// Should preserve only the read-only items.
expect(await Folder.load(readOnlyFolder.id)).toBeTruthy();
expect(await Note.load(readOnlyNote1.id)).toBeTruthy();
expect(await Note.load(readOnlyNote2.id)).toBeTruthy();
expect(await Note.load(writableNote.id)).toBeFalsy();

cleanup();
});
});
43 changes: 39 additions & 4 deletions packages/lib/services/trash/permanentlyDeleteOldItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,44 @@ import Setting from '../../models/Setting';
import Note from '../../models/Note';
import { Day, Hour } from '@joplin/utils/time';
import shim from '../../shim';
import { itemIsReadOnlySync } from '../../models/utils/readOnly';
import BaseItem from '../../models/BaseItem';
import { ModelType } from '../../BaseModel';
import ItemChange from '../../models/ItemChange';

const logger = Logger.create('permanentlyDeleteOldData');

const readOnlyItemsRemoved = async (itemIds: string[], itemType: ModelType) => {
const result = [];
for (const id of itemIds) {
const item = await BaseItem.loadItem(itemType, id);

// Only do the share-related read-only checks. If other checks are done,
// readOnly will always be true because the item is in the trash.
const shareChecksOnly = true;
const readOnly = itemIsReadOnlySync(
itemType,
ItemChange.SOURCE_UNSPECIFIED,
item,
Setting.value('sync.userId'),
BaseItem.syncShareCache,
shareChecksOnly,
);
if (!readOnly) {
result.push(id);
}
}
return result;
};

const itemsToDelete = async (ttl: number|null = null) => {
const result = await Folder.trashItemsOlderThan(ttl);
const folderIds = await readOnlyItemsRemoved(result.folderIds, ModelType.Folder);
const noteIds = await readOnlyItemsRemoved(result.noteIds, ModelType.Note);

return { folderIds, noteIds };
};

const permanentlyDeleteOldItems = async (ttl: number = null) => {
ttl = ttl === null ? Setting.value('trash.ttlDays') * Day : ttl;

Expand All @@ -17,13 +52,13 @@ const permanentlyDeleteOldItems = async (ttl: number = null) => {
return;
}

const result = await Folder.trashItemsOlderThan(ttl);
logger.info('Items to permanently delete:', result);
const toDelete = await itemsToDelete(ttl);
logger.info('Items to permanently delete:', toDelete);

await Note.batchDelete(result.noteIds, { sourceDescription: 'permanentlyDeleteOldItems' });
await Note.batchDelete(toDelete.noteIds, { sourceDescription: 'permanentlyDeleteOldItems' });

// We only auto-delete folders if they are empty.
for (const folderId of result.folderIds) {
for (const folderId of toDelete.folderIds) {
const noteIds = await Folder.noteIds(folderId, { includeDeleted: true });
if (!noteIds.length) {
logger.info(`Deleting empty folder: ${folderId}`);
Expand Down

0 comments on commit ca97597

Please sign in to comment.