Compare commits

..
Author SHA1 Message Date
palerdot c7299cf679 fix(desktop): linux folder display bug fix 2023-03-10 12:36:40 +05:30
20 changed files with 83 additions and 246 deletions

No files matched your search

+3 -5
View File
@@ -21,7 +21,7 @@ import checkForUpdates from '../checkForUpdates';
const { connect } = require('react-redux');
import { reg } from '@joplin/lib/registry';
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import PluginService from '@joplin/lib/services/plugins/PluginService';
const packageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
@@ -128,7 +128,6 @@ interface Props {
customCss: string;
locale: string;
profileConfig: ProfileConfig;
pluginSettings: PluginSettings;
}
const commandNames: string[] = menuCommandNames();
@@ -488,7 +487,8 @@ function useMenu(props: Props) {
}
function _showAbout() {
const v = versionInfo(packageInfo, PluginService.instance().enabledPlugins(props.pluginSettings));
const v = versionInfo(packageInfo, PluginService.instance().plugins);
const copyToClipboard = bridge().showMessageBox(v.message, {
icon: `${bridge().electronApp().buildDir()}/icons/128x128.png`,
@@ -931,7 +931,6 @@ function useMenu(props: Props) {
props['spellChecker.languages'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['spellChecker.enabled'],
props.pluginSettings,
props.customCss,
props.locale,
props.profileConfig,
@@ -987,7 +986,6 @@ const mapStateToProps = (state: AppState) => {
['folders.sortOrder.field']: state.settings['folders.sortOrder.field'],
['notes.sortOrder.reverse']: state.settings['notes.sortOrder.reverse'],
['folders.sortOrder.reverse']: state.settings['folders.sortOrder.reverse'],
pluginSettings: state.settings['plugins.states'],
showNoteCounts: state.settings.showNoteCounts,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
showCompletedTodos: state.settings.showCompletedTodos,
@@ -77,6 +77,16 @@ function stripMarkup(markupLanguage: number, markup: string, options: any = null
return markupToHtml_.stripMarkup(markupLanguage, markup, options);
}
function createSyntheticClipboardEventWithoutHTML(): ClipboardEvent {
const clipboardData = new DataTransfer();
for (const format of clipboard.availableFormats()) {
if (format !== 'text/html') {
clipboardData.setData(format, clipboard.read(format));
}
}
return new ClipboardEvent('paste', { clipboardData });
}
interface TinyMceCommand {
name: string;
value?: any;
@@ -1066,24 +1076,24 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
}
}
async function onKeyDown(event: any) {
function onKeyDown(event: any) {
// It seems "paste as text" is handled automatically on Windows and Linux,
// so we need to run the below code only on macOS. If we were to run this
// on Windows/Linux, we would have this double-paste issue:
// https://github.com/laurent22/joplin/issues/4243
// While "paste as text" functionality is handled by Windows and Linux, if we
// want to allow the user to customize the shortcut we need to prevent when it
// has the default value so it doesn't paste the content twice
// (one by the system and the other by our code)
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
event.preventDefault();
pasteAsPlainText(null);
// Handle "paste as text". Note that when pressing CtrlOrCmd+Shift+V it's going
// to trigger the "keydown" event but not the "paste" event, so it's ok to process
// it here and we don't need to do anything special in onPaste
if (!shim.isWindows() && !shim.isLinux()) {
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
pasteAsPlainText();
}
}
}
function onPasteAsText() {
pasteAsPlainText(null);
async function onPasteAsText() {
await onPaste(createSyntheticClipboardEventWithoutHTML());
}
editor.on(TinyMceEditorEvents.KeyUp, onKeyUp);
@@ -294,7 +294,7 @@ const NoteListComponent = (props: Props) => {
useEffect(() => {
if (previousSelectedNoteIds !== props.selectedNoteIds && props.selectedNoteIds.length === 1) {
const id = props.selectedNoteIds[0];
const doRefocus = props.notes.length < previousNotes.length && !props.focusedField;
const doRefocus = props.notes.length < previousNotes.length;
for (let i = 0; i < props.notes.length; i++) {
if (props.notes[i].id === id) {
@@ -311,7 +311,8 @@ const NoteListComponent = (props: Props) => {
if (previousVisible !== props.visible) {
updateSizeState();
}
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
@@ -558,7 +559,6 @@ const mapStateToProps = (state: AppState) => {
highlightedWords: state.highlightedWords,
plugins: state.pluginService.plugins,
customCss: state.customCss,
focusedField: state.focusedField,
};
};
@@ -23,5 +23,4 @@ export interface Props {
highlightedWords: string[];
provisionalNoteIds: string[];
visible: boolean;
focusedField: string;
}
@@ -1,6 +1,6 @@
import { AppState } from '../../app.reducer';
import * as React from 'react';
import { useEffect, useRef, useMemo, useState } from 'react';
import { useEffect, useRef } from 'react';
import SearchBar from '../SearchBar/SearchBar';
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
import CommandService from '@joplin/lib/services/CommandService';
@@ -11,13 +11,6 @@ import { _ } from '@joplin/lib/locale';
const { connect } = require('react-redux');
const styled = require('styled-components').default;
enum BaseBreakpoint {
Sm = 75,
Md = 80,
Lg = 120,
Xl = 474,
}
interface Props {
showNewNoteButtons: boolean;
sortOrderButtonsVisible: boolean;
@@ -25,15 +18,6 @@ interface Props {
sortOrderReverse: boolean;
notesParentType: string;
height: number;
width: number;
onContentHeightChange: (sameRow: boolean)=> void;
}
interface Breakpoints {
Sm: number;
Md: number;
Lg: number;
Xl: number;
}
const StyledRoot = styled.div`
@@ -50,9 +34,7 @@ const StyledButton = styled(Button)`
width: auto;
height: 26px;
min-height: 26px;
min-width: 37px;
max-width: none;
white-space: nowrap;
flex: 1 0 auto;
.fa, .fas {
font-size: 11px;
@@ -72,13 +54,7 @@ const StyledPairButtonR = styled(Button)`
width: auto;
`;
const TopRow = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
`;
const BottomRow = styled.div`
const RowContainer = styled.div`
display: flex;
flex-direction: row;
flex: 1 1 auto;
@@ -92,102 +68,7 @@ const SortOrderButtonsContainer = styled.div`
`;
function NoteListControls(props: Props) {
const [dynamicBreakpoints, setDynamicBreakpoints] = useState<Breakpoints>({ Sm: BaseBreakpoint.Sm, Md: BaseBreakpoint.Md, Lg: BaseBreakpoint.Lg, Xl: BaseBreakpoint.Xl });
const searchBarRef = useRef(null);
const newNoteRef = useRef(null);
const newTodoRef = useRef(null);
const noteControlsRef = useRef(null);
const searchAndSortRef = useRef(null);
const getTextWidth = (text: string): number => {
const canvas = document.createElement('canvas');
if (!canvas) throw new Error('Failed to create canvas element');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get context');
const fontWeight = getComputedStyle(newNoteRef.current).getPropertyValue('font-weight');
const fontSize = getComputedStyle(newNoteRef.current).getPropertyValue('font-size');
const fontFamily = getComputedStyle(newNoteRef.current).getPropertyValue('font-family');
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
return ctx.measureText(text).width;
};
// Initialize language-specific breakpoints
useEffect(() => {
// Use the longest string to calculate the amount of extra width needed
const smAdditional = getTextWidth(_('note')) > getTextWidth(_('to-do')) ? getTextWidth(_('note')) : getTextWidth(_('to-do'));
const mdAdditional = getTextWidth(_('New note')) > getTextWidth(_('New to-do')) ? getTextWidth(_('New note')) : getTextWidth(_('New to-do'));
const Sm = BaseBreakpoint.Sm + smAdditional * 2;
const Md = BaseBreakpoint.Md + mdAdditional * 2;
const Lg = BaseBreakpoint.Lg + Md;
const Xl = BaseBreakpoint.Xl;
setDynamicBreakpoints({ Sm, Md, Lg, Xl });
}, []);
const breakpoint = useMemo(() => {
// Find largest breakpoint that width is less than
const index = Object.values(dynamicBreakpoints).findIndex(x => props.width < x);
return index === -1 ? dynamicBreakpoints.Xl : Object.values(dynamicBreakpoints)[index];
}, [props.width, dynamicBreakpoints]);
const noteButtonText = useMemo(() => {
if (breakpoint === dynamicBreakpoints.Sm) {
return '';
} else if (breakpoint === dynamicBreakpoints.Md) {
return _('note');
} else {
return _('New note');
}
}, [breakpoint, dynamicBreakpoints]);
const todoButtonText = useMemo(() => {
if (breakpoint === dynamicBreakpoints.Sm) {
return '';
} else if (breakpoint === dynamicBreakpoints.Md) {
return _('to-do');
} else {
return _('New to-do');
}
}, [breakpoint, dynamicBreakpoints]);
const noteIcon = useMemo(() => {
if (breakpoint === dynamicBreakpoints.Sm) {
return 'icon-note';
} else {
return 'fas fa-plus';
}
}, [breakpoint, dynamicBreakpoints]);
const todoIcon = useMemo(() => {
if (breakpoint === dynamicBreakpoints.Sm) {
return 'far fa-check-square';
} else {
return 'fas fa-plus';
}
}, [breakpoint, dynamicBreakpoints]);
const showTooltip = useMemo(() => {
if (breakpoint === dynamicBreakpoints.Sm) {
return true;
} else {
return false;
}
}, [breakpoint, dynamicBreakpoints.Sm]);
useEffect(() => {
if (breakpoint === dynamicBreakpoints.Xl) {
noteControlsRef.current.style.flexDirection = 'row';
searchAndSortRef.current.style.flex = '2 1 auto';
props.onContentHeightChange(true);
} else {
noteControlsRef.current.style.flexDirection = 'column';
props.onContentHeightChange(false);
}
}, [breakpoint, dynamicBreakpoints, props.onContentHeightChange]);
useEffect(() => {
CommandService.instance().registerRuntime('focusSearch', focusSearchRuntime(searchBarRef));
@@ -246,63 +127,64 @@ function NoteListControls(props: Props) {
if (!props.showNewNoteButtons) return null;
return (
<TopRow className="new-note-todo-buttons">
<StyledButton ref={newNoteRef}
<RowContainer>
<StyledButton
className="new-note-button"
tooltip={ showTooltip ? CommandService.instance().label('newNote') : '' }
iconName={noteIcon}
title={_('%s', noteButtonText)}
tooltip={CommandService.instance().label('newNote')}
iconName="fas fa-plus"
title={_('%s', 'New note')}
level={ButtonLevel.Primary}
size={ButtonSize.Small}
onClick={onNewNoteButtonClick}
/>
<StyledButton ref={newTodoRef}
<StyledButton
className="new-todo-button"
tooltip={ showTooltip ? CommandService.instance().label('newTodo') : '' }
iconName={todoIcon}
title={_('%s', todoButtonText)}
tooltip={CommandService.instance().label('newTodo')}
iconName="fas fa-plus"
title={_('%s', 'New to-do')}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onNewTodoButtonClick}
/>
</TopRow>
</RowContainer>
);
}
return (
<StyledRoot ref={noteControlsRef}>
<StyledRoot>
{renderNewNoteButtons()}
<BottomRow ref={searchAndSortRef} className="search-and-sort">
<RowContainer>
<SearchBar inputRef={searchBarRef}/>
{showsSortOrderButtons() &&
<SortOrderButtonsContainer>
<StyledPairButtonL
className="sort-order-field-button"
tooltip={sortOrderFieldTooltip()}
iconName={sortOrderFieldIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderFieldButtonClick}
/>
<StyledPairButtonR
className="sort-order-reverse-button"
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
iconName={sortOrderReverseIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderReverseButtonClick}
/>
</SortOrderButtonsContainer>
}
</BottomRow>
<SortOrderButtonsContainer>
{showsSortOrderButtons() &&
<StyledPairButtonL
className="sort-order-field-button"
tooltip={sortOrderFieldTooltip()}
iconName={sortOrderFieldIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderFieldButtonClick}
/>
}
{showsSortOrderButtons() &&
<StyledPairButtonR
className="sort-order-reverse-button"
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
iconName={sortOrderReverseIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderReverseButtonClick}
/>
}
</SortOrderButtonsContainer>
</RowContainer>
</StyledRoot>
);
}
const mapStateToProps = (state: AppState) => {
return {
// TODO: showNewNoteButtons and the logic associated is not needed anymore.
showNewNoteButtons: true,
showNewNoteButtons: state.focusedField !== 'globalSearch',
sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'],
sortOrderField: state.settings['notes.sortOrder.field'],
sortOrderReverse: state.settings['notes.sortOrder.reverse'],
@@ -1,6 +1,6 @@
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import NoteList from '../NoteList/NoteList';
import NoteListControls from '../NoteListControls/NoteListControls';
import { Size } from '../ResizableLayout/utils/types';
@@ -22,15 +22,7 @@ const StyledRoot = styled.div`
export default function NoteListWrapper(props: Props) {
const theme = themeStyle(props.themeId);
const [controlHeight, setControlHeight] = useState(theme.topRowHeight);
const onContentHeightChange = (sameRow: boolean) => {
if (sameRow) {
setControlHeight(theme.topRowHeight);
} else {
setControlHeight(theme.topRowHeight * 2);
}
};
const controlHeight = theme.topRowHeight;
const noteListSize = useMemo(() => {
return {
@@ -41,7 +33,7 @@ export default function NoteListWrapper(props: Props) {
return (
<StyledRoot>
<NoteListControls height={controlHeight} width={noteListSize.width} onContentHeightChange={onContentHeightChange}/>
<NoteListControls height={controlHeight}/>
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
</StyledRoot>
);
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.10.14",
"version": "2.10.8",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -27,7 +27,6 @@
},
"build": {
"appId": "net.cozic.joplin-desktop",
"compression": "maximum",
"productName": "Joplin",
"npmRebuild": false,
"afterSign": "./tools/notarizeMacApp.js",
+2 -2
View File
@@ -150,8 +150,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097685
versionName "2.10.9"
versionCode 2097684
versionName "2.10.8"
// ndk {
// abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
// }
@@ -13,11 +13,7 @@ interface Props {
}
export default (props: Props) => {
// The initial prompt is there so that the user can choose to opt-in to
// biometrics auth the first time the app is launched. However since it
// doesn't work properly, we disable it. We only want the user to enable the
// feature after they've read the description in the config screen.
const [initialPromptDone, setInitialPromptDone] = useState(true); // useState(Setting.value('security.biometricsInitialPromptDone'));
const [initialPromptDone, setInitialPromptDone] = useState(Setting.value('security.biometricsInitialPromptDone'));
const [display, setDisplay] = useState(!!props.sensorInfo.supportedSensors && (props.sensorInfo.enabled || !initialPromptDone));
const [tryBiometricsCheck, setTryBiometricsCheck] = useState(initialPromptDone);
@@ -8,18 +8,6 @@ export interface SensorInfo {
}
export default async (): Promise<SensorInfo> => {
// Early exit if the feature is disabled, so that we don't make any
// FingerprintScanner scanner calls, since it seems they can fail and freeze
// the app.
if (!Setting.value('security.biometricsEnabled')) {
return {
enabled: false,
sensorsHaveChanged: false,
supportedSensors: '',
};
}
let hasChanged = false;
let supportedSensors = '';
+2 -2
View File
@@ -432,7 +432,7 @@ PODS:
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (8.2.0):
- RNShare (8.1.0):
- React-Core
- RNVectorIcons (9.2.0):
- React-Core
@@ -751,7 +751,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: b089c33619bbfb0a32bc4069c858b9274e694187
RNShare: 48b3113cd089a2be8ff0515c3ae7a46a4db8a76b
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
+1 -1
View File
@@ -501,7 +501,7 @@ async function initialize(dispatch: Function) {
if (Setting.value('env') === 'prod') {
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
} else {
await db.open({ name: getDatabaseName(currentProfile, isSubProfile, '-1') });
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
// await db.clearForTesting();
}
@@ -24,10 +24,9 @@ export const getResourceDir = (profile: Profile, isSubProfile: boolean) => {
return `${getProfilesRootDir()}/resources-${profile.id}`;
};
// The suffix is for debugging only
export const getDatabaseName = (profile: Profile, isSubProfile: boolean, suffix: string = '') => {
if (!isSubProfile) return `joplin${suffix}.sqlite`;
return `joplin-${profile.id}${suffix}.sqlite`;
export const getDatabaseName = (profile: Profile, isSubProfile: boolean) => {
if (!isSubProfile) return 'joplin.sqlite';
return `joplin-${profile.id}.sqlite`;
};
export const loadProfileConfig = async () => {
+3 -9
View File
@@ -96,20 +96,14 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
let context = Setting.value(`sync.${this.syncTargetId()}.context`);
context = context === '' ? null : JSON.parse(context);
let accountProperties = context ? context.accountProperties : null;
const api = this.api();
if (!accountProperties) {
accountProperties = await api.execAccountPropertiesRequest();
accountProperties = await this.api_.execAccountPropertiesRequest();
context ? context.accountProperties = accountProperties : context = { accountProperties: accountProperties };
Setting.setValue(`sync.${this.syncTargetId()}.context`, JSON.stringify(context));
}
api.setAccountProperties(accountProperties);
this.api_.setAccountProperties(accountProperties);
const appDir = await this.api().appDirectory();
// the appDir might contain non-ASCII characters
// /[^\u0021-\u00ff]/ is used in Node.js to detect the unescaped characters.
// See https://github.com/nodejs/node/blob/bbbf97b6dae63697371082475dc8651a6a220336/lib/_http_client.js#L176
const baseDir = RegExp(/[^\u0021-\u00ff]/).exec(appDir) !== null ? encodeURI(appDir) : appDir;
const fileApi = new FileApi(baseDir, new FileApiDriverOneDrive(this.api()));
const fileApi = new FileApi(appDir, new FileApiDriverOneDrive(this.api()));
fileApi.setSyncTargetId(this.syncTargetId());
fileApi.setLogger(this.logger());
return fileApi;
+1 -8
View File
@@ -1634,17 +1634,10 @@ class Setting extends BaseModel {
storage: SettingStorage.Database,
},
// The biometrics feature is disabled by default and marked as beta
// because it seems to cause a freeze or slow down startup on
// certain devices. May be the reason for:
//
// - https://discourse.joplinapp.org/t/on-android-when-joplin-gets-started-offline/29951/1
// - https://github.com/laurent22/joplin/issues/7956
'security.biometricsEnabled': {
value: false,
type: SettingItemType.Bool,
label: () => `${_('Use biometrics to secure access to the app')} (Beta)`,
description: () => 'Important: This is a beta feature and it is not compatible with certain devices. If the app no longer starts after enabling this or is very slow to start, please uninstall and reinstall the app.',
label: () => _('Use biometrics to secure access to the app'),
public: true,
appTypes: [AppType.Mobile],
},
@@ -100,11 +100,6 @@ export default class PluginService extends BaseService {
return this.plugins_;
}
public enabledPlugins(pluginSettings: PluginSettings): Plugins {
const enabledPlugins = Object.fromEntries(Object.entries(this.plugins_).filter((p) => this.pluginEnabled(pluginSettings, p[0])));
return enabledPlugins;
}
public get pluginIds(): string[] {
return Object.keys(this.plugins_);
}
+1 -1
View File
@@ -60,7 +60,7 @@ const globalStyle: any = {
toolbarPadding: 6,
appearance: 'light',
mainPadding: 12,
topRowHeight: 50,
topRowHeight: 81,
editorPaddingLeft: 8,
};
-2
View File
@@ -86,7 +86,6 @@ describe('getPluginLists', () => {
);
plugins[plugin.manifest.id] = plugin;
}
const v = versionInfo(packageInfo, plugins);
expect(v.body).toMatch(/\n\nPlugin1: 1\nPlugin2: 1\nPlugin3: 1/);
@@ -111,7 +110,6 @@ describe('getPluginLists', () => {
plugins[plugin.manifest.id] = plugin;
}
const v = versionInfo(packageInfo, plugins);
const body = '\n';
-2
View File
@@ -16,8 +16,6 @@ function getPluginLists(plugins: Plugins): PluginList {
}
}
pluginList.sort(Intl.Collator().compare);
let completeList = '';
let summary = '';
if (pluginList.length > 0) {
-4
View File
@@ -1,9 +1,5 @@
# Joplin Android app changelog
## [android-v2.10.9](https://github.com/laurent22/joplin/releases/tag/android-v2.10.9) (Pre-release) - 2023-03-22T18:40:57Z
- Improved: Mark biometrics feature as beta and ensure no call is made if it is not enabled (e44a934)
## [android-v2.10.8](https://github.com/laurent22/joplin/releases/tag/android-v2.10.8) (Pre-release) - 2023-02-28T18:09:21Z
- Improved: Stop synchronization with unsupported WebDAV providers (#7819) (#7661 by [@julien](https://github.com/julien))