mirror of
https://github.com/standardnotes/app
synced 2026-09-19 00:13:54 -04:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e477213d | ||
|
|
05305b5298 | ||
|
|
4e72a796cb | ||
|
|
73d6b67a36 | ||
|
|
35e2fa9eb6 | ||
|
|
4b07201a61 | ||
|
|
89927a3790 | ||
|
|
32f03d9470 | ||
|
|
88017ce3d9 | ||
|
|
b9b4bdf257 | ||
|
|
6071ebffeb | ||
|
|
f8181d9a4f | ||
|
|
026fbc1524 | ||
|
|
5058aaee33 | ||
|
|
f67b486a0d | ||
|
|
a9efce21f0 | ||
|
|
5bb923cff3 | ||
|
|
5a6f4fb35c | ||
|
|
9ac5bd0c27 | ||
|
|
02ad46147d |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.23.261](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.260](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.259](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.258](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.257](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.256](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.255](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.254](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.23.254",
|
||||
"version": "3.23.261",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.45.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.45.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.45.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.45.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.45.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* color scheme handling on mobile ([#1902](https://github.com/standardnotes/app/issues/1902)) ([6071ebf](https://github.com/standardnotes/app/commit/6071ebffeb55c78eac79f39aecca07f6ebdbed93))
|
||||
|
||||
## [3.45.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.45.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* screenshot privacy setting not working on app relaunch ([#1900](https://github.com/standardnotes/app/issues/1900)) ([5a6f4fb](https://github.com/standardnotes/app/commit/5a6f4fb35c2e70a463e04ce5f71bde55bba4bb30))
|
||||
|
||||
## [3.45.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import React from 'react'
|
||||
import 'react-native'
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer'
|
||||
import Index from '../index.android'
|
||||
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<Index />)
|
||||
})
|
||||
/* eslint-enable */
|
||||
@@ -1,12 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import 'react-native';
|
||||
import React from 'react';
|
||||
import Index from '../index.ios.js';
|
||||
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<Index />);
|
||||
});
|
||||
/* eslint-enable */
|
||||
@@ -274,8 +274,6 @@ PODS:
|
||||
- glog
|
||||
- react-native-fingerprint-scanner (5.0.0):
|
||||
- React-Core
|
||||
- react-native-pager-view (5.4.25):
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (11.23.1):
|
||||
@@ -407,7 +405,6 @@ DEPENDENCIES:
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
|
||||
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
||||
- react-native-version-info (from `../node_modules/react-native-version-info`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
@@ -487,8 +484,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||
react-native-fingerprint-scanner:
|
||||
:path: "../node_modules/react-native-fingerprint-scanner"
|
||||
react-native-pager-view:
|
||||
:path: "../node_modules/react-native-pager-view"
|
||||
react-native-version-info:
|
||||
:path: "../node_modules/react-native-version-info"
|
||||
react-native-webview:
|
||||
@@ -566,7 +561,6 @@ SPEC CHECKSUMS:
|
||||
React-jsinspector: f4775ea9118cbe1f72b834f0f842baa7a99508d8
|
||||
React-logger: a1f028f6d8639a3f364ef80419e5e862e1115250
|
||||
react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e
|
||||
react-native-pager-view: da490aa1f902c9a5aeecf0909cc975ad0e92e53e
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581
|
||||
React-perflogger: 0afaf2f01a47fd0fc368a93bfbb5bd3b26db6e7f
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.45.1",
|
||||
"version": "3.45.8",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -64,12 +64,6 @@
|
||||
"pod-install": "^0.1.33",
|
||||
"prettier": "^2.6.0",
|
||||
"prettier-plugin-organize-imports": "^2.3.4",
|
||||
"react-devtools": "^4.24.6",
|
||||
"react-devtools-core": "^4.24.6",
|
||||
"react-native-monorepo-tools": "^1.1.4",
|
||||
"react-native-pager-view": "^5.4.15",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"replace-in-file": "^6.3.2",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AbstractService, InternalEventBus, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import { Appearance, NativeEventSubscription } from 'react-native'
|
||||
|
||||
export class ColorSchemeObserverService extends AbstractService<ReactNativeToWebEvent> {
|
||||
private removeListener: NativeEventSubscription
|
||||
|
||||
constructor() {
|
||||
const internalEventBus = new InternalEventBus()
|
||||
super(internalEventBus)
|
||||
|
||||
this.removeListener = Appearance.addChangeListener(() => {
|
||||
void this.notifyEvent(ReactNativeToWebEvent.ColorSchemeChanged)
|
||||
})
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeListener.remove()
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,18 @@ import {
|
||||
TransferPayload,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Alert, AppState, AppStateStatus, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
||||
import { ColorSchemeObserverService } from 'ColorSchemeObserverService'
|
||||
import {
|
||||
Alert,
|
||||
Appearance,
|
||||
AppState,
|
||||
AppStateStatus,
|
||||
ColorSchemeName,
|
||||
Linking,
|
||||
PermissionsAndroid,
|
||||
Platform,
|
||||
StatusBar,
|
||||
} from 'react-native'
|
||||
import FileViewer from 'react-native-file-viewer'
|
||||
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
||||
import FlagSecure from 'react-native-flag-secure-android'
|
||||
@@ -85,6 +96,7 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
constructor(
|
||||
private stateObserverService?: AppStateObserverService,
|
||||
private androidBackHandlerService?: AndroidBackHandlerService,
|
||||
private colorSchemeService?: ColorSchemeObserverService,
|
||||
) {}
|
||||
|
||||
deinit() {
|
||||
@@ -92,6 +104,8 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
;(this.stateObserverService as unknown) = undefined
|
||||
this.androidBackHandlerService?.deinit()
|
||||
;(this.androidBackHandlerService as unknown) = undefined
|
||||
this.colorSchemeService?.deinit()
|
||||
;(this.colorSchemeService as unknown) = undefined
|
||||
}
|
||||
|
||||
consoleLog(...args: any[]): void {
|
||||
@@ -220,10 +234,12 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
|
||||
hideMobileInterfaceFromScreenshots(): void {
|
||||
hide()
|
||||
this.setAndroidScreenshotPrivacy(true)
|
||||
}
|
||||
|
||||
stopHidingMobileInterfaceFromScreenshots(): void {
|
||||
show()
|
||||
this.setAndroidScreenshotPrivacy(false)
|
||||
}
|
||||
|
||||
async getAllRawStorageKeyValues() {
|
||||
@@ -614,4 +630,8 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
async getAppState(): Promise<AppStateStatus> {
|
||||
return AppState.currentState
|
||||
}
|
||||
|
||||
async getColorScheme(): Promise<ColorSchemeName> {
|
||||
return Appearance.getColorScheme()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Keyboard, Platform } from 'react-native'
|
||||
import VersionInfo from 'react-native-version-info'
|
||||
@@ -28,9 +29,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
|
||||
const stateService = useMemo(() => new AppStateObserverService(), [])
|
||||
const androidBackHandlerService = useMemo(() => new AndroidBackHandlerService(), [])
|
||||
const colorSchemeService = useMemo(() => new ColorSchemeObserverService(), [])
|
||||
const device = useMemo(
|
||||
() => new MobileDevice(stateService, androidBackHandlerService),
|
||||
[androidBackHandlerService, stateService],
|
||||
() => new MobileDevice(stateService, androidBackHandlerService, colorSchemeService),
|
||||
[androidBackHandlerService, colorSchemeService, stateService],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,6 +46,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
},
|
||||
)
|
||||
|
||||
const removeColorSchemeServiceListener = colorSchemeService.addEventObserver((event: ReactNativeToWebEvent) => {
|
||||
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
|
||||
})
|
||||
|
||||
const keyboardShowListener = Keyboard.addListener('keyboardWillShow', () => {
|
||||
device.reloadStatusBarStyle(false)
|
||||
})
|
||||
@@ -55,10 +61,11 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
return () => {
|
||||
removeStateServiceListener()
|
||||
removeBackHandlerServiceListener()
|
||||
removeColorSchemeServiceListener()
|
||||
keyboardShowListener.remove()
|
||||
keyboardHideListener.remove()
|
||||
}
|
||||
}, [webViewRef, stateService, device, androidBackHandlerService])
|
||||
}, [webViewRef, stateService, device, androidBackHandlerService, colorSchemeService])
|
||||
|
||||
useEffect(() => {
|
||||
const observer = device.addMobileWebEventReceiver((event) => {
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.190](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.189](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.188](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.187](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.186](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.185](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.184](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.183](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.3.183",
|
||||
"version": "1.3.190",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.39.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* color scheme handling on mobile ([#1902](https://github.com/standardnotes/app/issues/1902)) ([6071ebf](https://github.com/standardnotes/app/commit/6071ebffeb55c78eac79f39aecca07f6ebdbed93))
|
||||
|
||||
# [1.39.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.39.0",
|
||||
"version": "1.39.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface WebApplicationInterface extends ApplicationInterface {
|
||||
handleMobileGainingFocusEvent(): Promise<void>
|
||||
handleMobileLosingFocusEvent(): Promise<void>
|
||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||
handleMobileColorSchemeChangeEvent(): void
|
||||
isNativeMobileWeb(): boolean
|
||||
mobileDevice(): MobileDeviceInterface
|
||||
handleAndroidBackButtonPressed(): void
|
||||
|
||||
@@ -21,4 +21,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
||||
removeComponentUrl(componentUuid: string): void
|
||||
isUrlComponentUrl(url: string): boolean
|
||||
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
||||
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.145.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mobile:** increase save debounce timeout ([35e2fa9](https://github.com/standardnotes/app/commit/35e2fa9eb6df44beb7717ce38209ee4944286355))
|
||||
|
||||
## [2.145.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* color scheme handling on mobile ([#1902](https://github.com/standardnotes/app/issues/1902)) ([6071ebf](https://github.com/standardnotes/app/commit/6071ebffeb55c78eac79f39aecca07f6ebdbed93))
|
||||
|
||||
# [2.145.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InfoStrings } from '../Strings/Info'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import {
|
||||
NoteMutator,
|
||||
@@ -11,14 +12,6 @@ import { removeFromArray } from '@standardnotes/utils'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import { SNApplication } from '../Application/Application'
|
||||
import {
|
||||
STRING_SAVING_WHILE_DOCUMENT_HIDDEN,
|
||||
STRING_INVALID_NOTE,
|
||||
NOTE_PREVIEW_CHAR_LIMIT,
|
||||
STRING_ELLIPSES,
|
||||
SAVE_TIMEOUT_NO_DEBOUNCE,
|
||||
SAVE_TIMEOUT_DEBOUNCE,
|
||||
} from './Types'
|
||||
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
||||
|
||||
@@ -27,6 +20,15 @@ export type EditorValues = {
|
||||
text: string
|
||||
}
|
||||
|
||||
const StringEllipses = '...'
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
const SaveTimeoutDebounc = {
|
||||
Desktop: 350,
|
||||
ImmediateChange: 100,
|
||||
NativeMobileWeb: 700,
|
||||
}
|
||||
|
||||
export class NoteViewController implements ItemViewControllerInterface {
|
||||
public item!: SNNote
|
||||
public dealloced = false
|
||||
@@ -182,7 +184,7 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
const isTemplate = this.isTemplateNote
|
||||
|
||||
if (typeof document !== 'undefined' && document.hidden) {
|
||||
void this.application.alertService.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN)
|
||||
void this.application.alertService.alert(InfoStrings.SavingWhileDocumentHidden)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,7 +193,7 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
}
|
||||
|
||||
if (!this.application.items.findItem(this.item.uuid)) {
|
||||
void this.application.alertService.alert(STRING_INVALID_NOTE)
|
||||
void this.application.alertService.alert(InfoStrings.InvalidNote)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -207,9 +209,9 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
|
||||
if (!dto.dontUpdatePreviews) {
|
||||
const noteText = text || ''
|
||||
const truncate = noteText.length > NOTE_PREVIEW_CHAR_LIMIT
|
||||
const substring = noteText.substring(0, NOTE_PREVIEW_CHAR_LIMIT)
|
||||
const previewPlain = substring + (truncate ? STRING_ELLIPSES : '')
|
||||
const truncate = noteText.length > NotePreviewCharLimit
|
||||
const substring = noteText.substring(0, NotePreviewCharLimit)
|
||||
const previewPlain = substring + (truncate ? StringEllipses : '')
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
noteMutator.preview_plain = previewPlain
|
||||
@@ -225,7 +227,13 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
}
|
||||
|
||||
const noDebounce = dto.bypassDebouncer || this.application.noAccount()
|
||||
const syncDebouceMs = noDebounce ? SAVE_TIMEOUT_NO_DEBOUNCE : SAVE_TIMEOUT_DEBOUNCE
|
||||
|
||||
const syncDebouceMs = noDebounce
|
||||
? SaveTimeoutDebounc.ImmediateChange
|
||||
: this.application.isNativeMobileWeb()
|
||||
? SaveTimeoutDebounc.NativeMobileWeb
|
||||
: SaveTimeoutDebounc.Desktop
|
||||
|
||||
this.saveTimeout = setTimeout(() => {
|
||||
void this.application.sync.sync()
|
||||
}, syncDebouceMs)
|
||||
|
||||
@@ -4,4 +4,5 @@ export enum ReactNativeToWebEvent {
|
||||
GainingFocus = 'GainingFocus',
|
||||
LosingFocus = 'LosingFocus',
|
||||
AndroidBackButtonPressed = 'AndroidBackButtonPressed',
|
||||
ColorSchemeChanged = 'ColorSchemeChanged',
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN =
|
||||
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'
|
||||
export const STRING_INVALID_NOTE =
|
||||
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."
|
||||
export const STRING_ELLIPSES = '...'
|
||||
export const NOTE_PREVIEW_CHAR_LIMIT = 160
|
||||
export const SAVE_TIMEOUT_DEBOUNCE = 350
|
||||
export const SAVE_TIMEOUT_NO_DEBOUNCE = 100
|
||||
@@ -4,4 +4,8 @@ export const InfoStrings = {
|
||||
'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.',
|
||||
BackupFileMoreRecentThanAccount:
|
||||
"This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.",
|
||||
SavingWhileDocumentHidden:
|
||||
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.',
|
||||
InvalidNote:
|
||||
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.145.0",
|
||||
"version": "2.145.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* color scheme handling on mobile ([#1902](https://github.com/standardnotes/app/issues/1902)) ([6071ebf](https://github.com/standardnotes/app/commit/6071ebffeb55c78eac79f39aecca07f6ebdbed93))
|
||||
|
||||
## [1.10.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.10.5",
|
||||
"version": "1.10.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -59,11 +59,13 @@ export class ThemeManager extends AbstractService {
|
||||
break
|
||||
}
|
||||
case ApplicationEvent.Launched: {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.colorSchemeEventHandler)
|
||||
if (!this.application.isNativeMobileWeb()) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.colorSchemeEventHandler)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ApplicationEvent.PreferencesChanged: {
|
||||
this.handlePreferencesChangeEvent()
|
||||
void this.handlePreferencesChangeEvent()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -88,7 +90,16 @@ export class ThemeManager extends AbstractService {
|
||||
})
|
||||
}
|
||||
|
||||
private handlePreferencesChangeEvent(): void {
|
||||
async handleMobileColorSchemeChangeEvent() {
|
||||
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
|
||||
|
||||
if (useDeviceThemeSettings) {
|
||||
const prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark'
|
||||
this.setThemeAsPerColorScheme(prefersDarkColorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePreferencesChangeEvent() {
|
||||
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
|
||||
|
||||
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
|
||||
@@ -98,9 +109,13 @@ export class ThemeManager extends AbstractService {
|
||||
}
|
||||
|
||||
if (hasPreferenceChanged && useDeviceThemeSettings) {
|
||||
const prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
this.setThemeAsPerColorScheme(prefersDarkColorScheme.matches)
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark'
|
||||
}
|
||||
|
||||
this.setThemeAsPerColorScheme(prefersDarkColorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.90.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.90.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.90.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-30)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* avoid uneccessary notes list item rerenders ([#1904](https://github.com/standardnotes/app/issues/1904)) ([89927a3](https://github.com/standardnotes/app/commit/89927a37908fc9e84b981f1cba7c9abd37039c8b))
|
||||
|
||||
## [3.90.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mobile:** note view hide bar options on editor focus ([#1903](https://github.com/standardnotes/app/issues/1903)) ([88017ce](https://github.com/standardnotes/app/commit/88017ce3d9c84f0383e147179d88c0fcb6538331))
|
||||
|
||||
## [3.90.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* color scheme handling on mobile ([#1902](https://github.com/standardnotes/app/issues/1902)) ([6071ebf](https://github.com/standardnotes/app/commit/6071ebffeb55c78eac79f39aecca07f6ebdbed93))
|
||||
|
||||
## [3.90.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* boolean ([5058aae](https://github.com/standardnotes/app/commit/5058aaee3395ee2760ae4de72378db1de286a2bb))
|
||||
|
||||
## [3.90.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* only auto-toggle biometrics prompt if user hasn't interacted ([02ad461](https://github.com/standardnotes/app/commit/02ad46147dbb05543edb7bf9a4b95d7fa61ae713))
|
||||
* privacy overlay on iOS ([5bb923c](https://github.com/standardnotes/app/commit/5bb923cff3b595a0c78d16098e66006073ff2a59))
|
||||
* screenshot privacy setting not working on app relaunch ([#1900](https://github.com/standardnotes/app/issues/1900)) ([5a6f4fb](https://github.com/standardnotes/app/commit/5a6f4fb35c2e70a463e04ce5f71bde55bba4bb30))
|
||||
|
||||
# [3.90.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.90.0",
|
||||
"version": "3.90.7",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
|
||||
@@ -261,6 +261,18 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
setViewportHeightWithFallback()
|
||||
}
|
||||
|
||||
handleInitialMobileScreenshotPrivacy(): void {
|
||||
if (this.platform !== Platform.Android) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.protections.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice().setAndroidScreenshotPrivacy(true)
|
||||
} else {
|
||||
this.mobileDevice().setAndroidScreenshotPrivacy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async handleMobileLosingFocusEvent(): Promise<void> {
|
||||
if (this.protections.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice().stopHidingMobileInterfaceFromScreenshots()
|
||||
@@ -277,6 +289,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
setViewportHeightWithFallback()
|
||||
}
|
||||
|
||||
handleMobileColorSchemeChangeEvent() {
|
||||
void this.getThemeService().handleMobileColorSchemeChangeEvent()
|
||||
}
|
||||
|
||||
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
|
||||
const isLocked = await this.isLocked()
|
||||
if (isLocked) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Component } from 'react'
|
||||
export type PureComponentState = Partial<Record<string, any>>
|
||||
export type PureComponentProps = Partial<Record<string, any>>
|
||||
|
||||
export abstract class PureComponent<P = PureComponentProps, S = PureComponentState> extends Component<P, S> {
|
||||
export abstract class AbstractComponent<P = PureComponentProps, S = PureComponentState> extends Component<P, S> {
|
||||
private unsubApp!: () => void
|
||||
private reactionDisposers: IReactionDisposer[] = []
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@stan
|
||||
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||
import { alertDialog, RouteType } from '@standardnotes/ui-services'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import Navigation from '@/Components/Navigation/Navigation'
|
||||
import Navigation from '@/Components/Tags/Navigation'
|
||||
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
||||
import Footer from '@/Components/Footer/Footer'
|
||||
import SessionsModal from '@/Components/SessionsModal/SessionsModal'
|
||||
@@ -174,28 +174,22 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
}, [needsUnlock, launched])
|
||||
|
||||
const renderChallenges = useCallback(() => {
|
||||
return (
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
{challenges.map((challenge) => {
|
||||
return (
|
||||
<div className="sk-modal" key={`${challenge.id}${application.ephemeralIdentifier}`}>
|
||||
<ChallengeModal
|
||||
key={`${challenge.id}${application.ephemeralIdentifier}`}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
challenge={challenge}
|
||||
onDismiss={removeChallenge}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AndroidBackHandlerProvider>
|
||||
)
|
||||
return challenges.map((challenge) => (
|
||||
<div className="sk-modal" key={`${challenge.id}${application.ephemeralIdentifier}`}>
|
||||
<ChallengeModal
|
||||
key={`${challenge.id}${application.ephemeralIdentifier}`}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
challenge={challenge}
|
||||
onDismiss={removeChallenge}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}, [viewControllerManager, challenges, mainApplicationGroup, removeChallenge, application])
|
||||
|
||||
if (!renderAppContents) {
|
||||
return renderChallenges()
|
||||
return <AndroidBackHandlerProvider application={application}>{renderChallenges()}</AndroidBackHandlerProvider>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -42,11 +42,17 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const hasUserAlreadyInteracted = typeof values[prompt.id].value === 'boolean'
|
||||
|
||||
if (hasUserAlreadyInteracted) {
|
||||
return
|
||||
}
|
||||
|
||||
biometricsButtonRef.current?.click()
|
||||
} else {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [application, prompt.validation])
|
||||
}, [application, prompt.id, prompt.validation, values])
|
||||
|
||||
useEffect(() => {
|
||||
if (!application.isNativeMobileWeb()) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CalendarActivity } from './CalendarActivity'
|
||||
import CalendarDay from './CalendarDay'
|
||||
import { CalendarDays, CalendarDaysLeap, CalendarDaysOfTheWeek } from './Constants'
|
||||
import { createActivityRecord, dateToDateOnlyString, isLeapYear, getStartDayOfMonth } from './CalendarUtilts'
|
||||
import { isDateInSameDay } from '@/Utils/DateUtils'
|
||||
import { areDatesInSameDay } from '@/Utils/DateUtils'
|
||||
|
||||
type Props = {
|
||||
activities: CalendarActivity[]
|
||||
@@ -29,10 +29,11 @@ const Calendar: FunctionComponent<Props> = ({ activities, startDate, onDateSelec
|
||||
}, [startDate])
|
||||
|
||||
const today = new Date()
|
||||
const days = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
|
||||
const dayBundle = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
|
||||
const days = Array(dayBundle[month] + (startDay - 1)).fill(null)
|
||||
|
||||
return (
|
||||
<div className={`w-300 ${className} border-left border-right border border-neutral`}>
|
||||
<div className={`w-300 ${className} min-h-[210px]`}>
|
||||
<div className="mr-auto ml-auto w-70">
|
||||
<div className="flex w-full flex-wrap">
|
||||
{CalendarDaysOfTheWeek.map((d) => (
|
||||
@@ -42,23 +43,24 @@ const Calendar: FunctionComponent<Props> = ({ activities, startDate, onDateSelec
|
||||
))}
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap">
|
||||
{Array(days[month] + (startDay - 1))
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
const d = index - (startDay - 2)
|
||||
const date = new Date(year, month, d)
|
||||
const activities = activityMap[dateToDateOnlyString(date)] || []
|
||||
return (
|
||||
<CalendarDay
|
||||
key={index}
|
||||
day={d}
|
||||
isToday={isDateInSameDay(date, today)}
|
||||
activities={activities}
|
||||
onClick={() => onDateSelect(date)}
|
||||
hasPendingEntry={selectedDay && isDateInSameDay(selectedDay, date)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{days.map((_, index) => {
|
||||
const dayIndex = index - (startDay - 2)
|
||||
const date = new Date(year, month, dayIndex)
|
||||
const day = date.getDate()
|
||||
const activities = activityMap[dateToDateOnlyString(date)] || []
|
||||
const isTemplate = selectedDay && areDatesInSameDay(selectedDay, date)
|
||||
const type = activities.length > 0 ? 'item' : isTemplate ? 'template' : 'empty'
|
||||
return (
|
||||
<CalendarDay
|
||||
isLastMonth={dayIndex <= 0}
|
||||
key={index}
|
||||
day={day}
|
||||
isToday={areDatesInSameDay(date, today)}
|
||||
onClick={() => onDateSelect(date)}
|
||||
type={type}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { CalendarActivity } from './CalendarActivity'
|
||||
|
||||
type Props = {
|
||||
day: number
|
||||
activities: CalendarActivity[]
|
||||
isToday: boolean
|
||||
onClick: () => void
|
||||
hasPendingEntry?: boolean
|
||||
type: 'empty' | 'item' | 'template'
|
||||
isLastMonth: boolean
|
||||
}
|
||||
|
||||
const CalendarDay: FunctionComponent<Props> = ({ day, activities = [], hasPendingEntry, isToday, onClick }) => {
|
||||
const hasActivity = day > 0 && activities.length > 0
|
||||
const todayClassNames = 'bg-danger text-danger-contrast font-bold'
|
||||
const hasActivityClassNames = 'bg-danger-light text-danger font-bold'
|
||||
const defaultClassNames = 'bg-transparent hover:bg-contrast'
|
||||
const hasPendingEntryNames = 'bg-contrast'
|
||||
const CalendarDay: FunctionComponent<Props> = ({ day, type, isToday, onClick, isLastMonth }) => {
|
||||
let classNames = ''
|
||||
if (isToday) {
|
||||
classNames += 'bg-danger text-danger-contrast font-bold'
|
||||
} else if (isLastMonth) {
|
||||
classNames += 'text-passive-3'
|
||||
} else {
|
||||
if (type === 'empty') {
|
||||
classNames += 'bg-transparent hover:bg-contrast'
|
||||
} else if (type === 'item') {
|
||||
classNames += 'bg-danger-light text-danger font-bold'
|
||||
} else {
|
||||
classNames += 'bg-contrast'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-7 w-[14.2%] p-0.5">
|
||||
<div
|
||||
className={`${
|
||||
!hasActivity && !isToday ? defaultClassNames : ''
|
||||
} flex h-full w-full cursor-pointer items-center justify-center rounded ${
|
||||
isToday ? todayClassNames : hasActivity ? hasActivityClassNames : ''
|
||||
} ${hasPendingEntry ? hasPendingEntryNames : ''}`}
|
||||
className={`${classNames} flex h-full w-full cursor-pointer items-center justify-center rounded`}
|
||||
key={day}
|
||||
onClick={onClick}
|
||||
>
|
||||
{day > 0 ? day : ''}
|
||||
{day}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+27
-5
@@ -1,12 +1,30 @@
|
||||
import { addMonths, areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||
import { addCalendarMonths, areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||
import { CalendarMonth } from './CalendarMonth'
|
||||
import { insertMonthsWithTarget } from './CalendarUtilts'
|
||||
|
||||
describe('calendar utils', () => {
|
||||
it('handles flakey date', () => {
|
||||
const date = new Date('Mon Oct 31 2022 08:10:18 GMT-0500')
|
||||
const result = addCalendarMonths(date, -1)
|
||||
|
||||
expect(areDatesInSameMonth(date, result)).toEqual(false)
|
||||
})
|
||||
|
||||
it('addMonths', () => {
|
||||
const today = new Date()
|
||||
const result = addCalendarMonths(today, -1)
|
||||
|
||||
expect(areDatesInSameMonth(today, result)).toEqual(false)
|
||||
})
|
||||
|
||||
it('insertMonthsWithTarget in past', () => {
|
||||
const today = new Date()
|
||||
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
|
||||
const targetMonth = addMonths(today, -12)
|
||||
const months: CalendarMonth[] = [
|
||||
{ date: addCalendarMonths(today, -1) },
|
||||
{ date: today },
|
||||
{ date: addCalendarMonths(today, 1) },
|
||||
]
|
||||
const targetMonth = addCalendarMonths(today, -12)
|
||||
|
||||
const result = insertMonthsWithTarget(months, targetMonth)
|
||||
|
||||
@@ -16,8 +34,12 @@ describe('calendar utils', () => {
|
||||
|
||||
it('insertMonthsWithTarget in future', () => {
|
||||
const today = new Date()
|
||||
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
|
||||
const targetMonth = addMonths(today, 12)
|
||||
const months: CalendarMonth[] = [
|
||||
{ date: addCalendarMonths(today, -1) },
|
||||
{ date: today },
|
||||
{ date: addCalendarMonths(today, 1) },
|
||||
]
|
||||
const targetMonth = addCalendarMonths(today, 12)
|
||||
|
||||
const result = insertMonthsWithTarget(months, targetMonth)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addMonths, numberOfMonthsBetweenDates } from '@/Utils/DateUtils'
|
||||
import { addCalendarMonths, numberOfMonthsBetweenDates } from '@/Utils/DateUtils'
|
||||
import { CalendarActivity } from './CalendarActivity'
|
||||
import { CalendarMonth } from './CalendarMonth'
|
||||
|
||||
@@ -40,12 +40,12 @@ export function insertMonths(months: CalendarMonth[], location: 'front' | 'end',
|
||||
|
||||
for (let i = 1; i <= number; i++) {
|
||||
if (location === 'front') {
|
||||
const minusNFromFirstMonth = addMonths(earlierMonth, -i)
|
||||
const minusNFromFirstMonth = addCalendarMonths(earlierMonth, -i)
|
||||
months.unshift({
|
||||
date: minusNFromFirstMonth,
|
||||
})
|
||||
} else {
|
||||
const plusNFromLastMonth = addMonths(laterMonth, i)
|
||||
const plusNFromLastMonth = addCalendarMonths(laterMonth, i)
|
||||
months.push({
|
||||
date: plusNFromLastMonth,
|
||||
})
|
||||
|
||||
+107
-188
@@ -1,43 +1,39 @@
|
||||
import { areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { areDatesInSameDay, areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import Calendar from './Calendar'
|
||||
import { CalendarActivity, CalendarActivityType } from './CalendarActivity'
|
||||
import { CalendarMonth } from './CalendarMonth'
|
||||
import { CalendarMonths } from './Constants'
|
||||
import { insertMonths, insertMonthsWithTarget } from './CalendarUtilts'
|
||||
import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
import { usePrevious } from './usePrevious'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
|
||||
type Props = {
|
||||
activityType: CalendarActivityType
|
||||
activities: CalendarActivity[]
|
||||
onDateSelect: (date: Date) => void
|
||||
selectedTemplateDay?: Date
|
||||
selectedItemDay?: Date
|
||||
selectedDay?: Date
|
||||
selectedDayType?: 'item' | 'template'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type InfiniteCalendarInterface = {
|
||||
changeMonth: (month: Date) => void
|
||||
goToMonth: (month: Date) => void
|
||||
}
|
||||
|
||||
const PageSize = 10
|
||||
const PageSize = 2
|
||||
|
||||
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
({ activities, onDateSelect, selectedTemplateDay, selectedItemDay }: Props, ref) => {
|
||||
const [date, setDate] = useState(new Date())
|
||||
const [month, setMonth] = useState(date.getMonth())
|
||||
const [year, setYear] = useState(date.getFullYear())
|
||||
|
||||
({ activities, onDateSelect, selectedDay, className }: Props, ref) => {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const [scrollWidth, setScrollWidth] = useState(0)
|
||||
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
||||
const scrollerRef = useRef<InfiniteScrollerInterface | null>(null)
|
||||
const previousSelectedDay = usePrevious(selectedDay)
|
||||
|
||||
const [activeDate, setActiveDate] = useState(new Date())
|
||||
const today = new Date()
|
||||
const [months, setMonths] = useState<CalendarMonth[]>(() => {
|
||||
const base = [{ date: today }]
|
||||
@@ -46,18 +42,6 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
return base
|
||||
})
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
changeMonth(date: Date) {
|
||||
setDate(date)
|
||||
},
|
||||
}))
|
||||
|
||||
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
|
||||
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
|
||||
const [didPaginateLeft, setDidPaginateLeft] = useState(false)
|
||||
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
||||
const scrollArea = useRef<HTMLDivElement>(null)
|
||||
|
||||
const hasMonthInList = useCallback(
|
||||
(date: Date): boolean => {
|
||||
for (const month of months) {
|
||||
@@ -74,7 +58,38 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
(date: Date): void => {
|
||||
setMonths(insertMonthsWithTarget(months, date))
|
||||
},
|
||||
[months, setMonths],
|
||||
[months],
|
||||
)
|
||||
|
||||
const scrollToMonth = useCallback(
|
||||
(date: Date) => {
|
||||
const elementId = elementIdForMonth(date)
|
||||
scrollerRef.current?.scrollToElementId(elementId)
|
||||
},
|
||||
[scrollerRef],
|
||||
)
|
||||
|
||||
const goToMonth = useCallback(
|
||||
(month: Date) => {
|
||||
if (!hasMonthInList(month)) {
|
||||
insertMonthInList(month)
|
||||
}
|
||||
|
||||
log(LoggingDomain.DailyNotes, '[Calendar] Scrolling to month', month, 'from goToMonth')
|
||||
setActiveDate(month)
|
||||
scrollToMonth(month)
|
||||
},
|
||||
[hasMonthInList, insertMonthInList, scrollToMonth],
|
||||
)
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
goToMonth(date: Date) {
|
||||
goToMonth(date)
|
||||
},
|
||||
}),
|
||||
[goToMonth],
|
||||
)
|
||||
|
||||
const resetNumberOfCalendarsToBase = useCallback(
|
||||
@@ -88,153 +103,63 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTemplateDay) {
|
||||
setDate(selectedTemplateDay)
|
||||
if (selectedDay) {
|
||||
if (previousSelectedDay && areDatesInSameDay(previousSelectedDay, selectedDay)) {
|
||||
log(LoggingDomain.DailyNotes, '[Calendar] selectedDay has changed, but is same as previous', selectedDay)
|
||||
return
|
||||
}
|
||||
log(LoggingDomain.DailyNotes, '[Calendar] selectedDay has changed, going to month:', selectedDay)
|
||||
goToMonth(selectedDay)
|
||||
}
|
||||
}, [selectedTemplateDay])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItemDay) {
|
||||
setDate(selectedItemDay)
|
||||
}
|
||||
}, [selectedItemDay])
|
||||
|
||||
const scrollToMonth = useCallback((date: Date) => {
|
||||
const elementId = elementIdForMonth(date)
|
||||
const element = document.getElementById(elementId)
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollArea.current!.scrollLeft = element.offsetLeft + -60
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setMonth(date.getMonth())
|
||||
setYear(date.getFullYear())
|
||||
|
||||
if (!hasMonthInList(date)) {
|
||||
insertMonthInList(date)
|
||||
}
|
||||
|
||||
scrollToMonth(date)
|
||||
}, [date, hasMonthInList, insertMonthInList, scrollToMonth])
|
||||
}, [selectedDay, goToMonth, previousSelectedDay])
|
||||
|
||||
useEffect(() => {
|
||||
if (!restoreScrollAfterExpand) {
|
||||
return
|
||||
}
|
||||
|
||||
if (scrollArea.current && expanded) {
|
||||
scrollToMonth(date)
|
||||
if (expanded) {
|
||||
log(
|
||||
LoggingDomain.DailyNotes,
|
||||
'[Calendar] Scrolling to month',
|
||||
activeDate,
|
||||
'from restoreScrollAfterExpand useEffect',
|
||||
)
|
||||
scrollToMonth(activeDate)
|
||||
setRestoreScrollAfterExpand(false)
|
||||
}
|
||||
}, [expanded, scrollToMonth, date, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!scrollArea.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (didPaginateLeft) {
|
||||
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollWidth
|
||||
setDidPaginateLeft(false)
|
||||
}
|
||||
}, [months, didPaginateLeft, scrollWidth])
|
||||
}, [expanded, scrollToMonth, activeDate, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
|
||||
|
||||
const paginateLeft = useCallback(() => {
|
||||
if (scrollArea.current) {
|
||||
setScrollWidth(scrollArea.current.scrollWidth)
|
||||
}
|
||||
|
||||
const copy = months.slice()
|
||||
insertMonths(copy, 'front', PageSize)
|
||||
setDidPaginateLeft(true)
|
||||
setMonths(copy)
|
||||
}, [months, setMonths])
|
||||
log(LoggingDomain.DailyNotes, '[Calendar] paginateLeft')
|
||||
setMonths((prevMonths) => {
|
||||
const copy = prevMonths.slice()
|
||||
insertMonths(copy, 'front', PageSize)
|
||||
return copy
|
||||
})
|
||||
}, [setMonths])
|
||||
|
||||
const paginateRight = useCallback(() => {
|
||||
const copy = months.slice()
|
||||
insertMonths(copy, 'end', PageSize)
|
||||
setDidPaginateLeft(false)
|
||||
setMonths(copy)
|
||||
}, [months, setMonths])
|
||||
log(LoggingDomain.DailyNotes, '[Calendar] paginateRight')
|
||||
setMonths((prevMonths) => {
|
||||
const copy = prevMonths.slice()
|
||||
insertMonths(copy, 'end', PageSize)
|
||||
return copy
|
||||
})
|
||||
}, [setMonths])
|
||||
|
||||
const updateCurrentMonth = useCallback(
|
||||
(index: number) => {
|
||||
const newMonth = months[index]
|
||||
setMonth(newMonth.date.getMonth())
|
||||
setYear(newMonth.date.getFullYear())
|
||||
const onElementVisibility = useCallback(
|
||||
(id: string) => {
|
||||
const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id)
|
||||
if (index >= 0) {
|
||||
const newMonth = months[index]
|
||||
log(LoggingDomain.DailyNotes, '[Calendar] Month element did become visible, setting activeDate', newMonth)
|
||||
setActiveDate(newMonth.date)
|
||||
}
|
||||
},
|
||||
[months, setMonth, setYear],
|
||||
[months],
|
||||
)
|
||||
|
||||
const visibilityObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||
if (visibleEntry) {
|
||||
const id = visibleEntry.target.id
|
||||
const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id)
|
||||
updateCurrentMonth(index)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.9 },
|
||||
),
|
||||
[updateCurrentMonth, months],
|
||||
)
|
||||
|
||||
const rightObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
paginateRight()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[paginateRight],
|
||||
)
|
||||
|
||||
const leftObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
paginateLeft()
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
),
|
||||
[paginateLeft],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (lastElement) {
|
||||
rightObserver.observe(lastElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (lastElement) {
|
||||
rightObserver.unobserve(lastElement)
|
||||
}
|
||||
}
|
||||
}, [lastElement, rightObserver])
|
||||
|
||||
useEffect(() => {
|
||||
if (firstElement) {
|
||||
leftObserver.observe(firstElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (firstElement) {
|
||||
leftObserver.unobserve(firstElement)
|
||||
}
|
||||
}
|
||||
}, [firstElement, leftObserver])
|
||||
|
||||
const toggleVisibility = useCallback(() => {
|
||||
setRestoreScrollAfterExpand(true)
|
||||
|
||||
@@ -254,48 +179,42 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full flex-shrink-0 border-b border-solid border-border">
|
||||
<div className={'border-b border-solid border-border'}>
|
||||
<div
|
||||
className="text-md flex cursor-pointer items-center justify-center py-2 px-4 text-center font-bold hover:bg-contrast"
|
||||
onClick={toggleVisibility}
|
||||
className={classNames(
|
||||
'text-md flex cursor-pointer items-center justify-center py-2 px-4',
|
||||
'text-center font-bold hover:bg-contrast',
|
||||
)}
|
||||
>
|
||||
{CalendarMonths[month]} {year}
|
||||
{CalendarMonths[activeDate.getMonth()]} {activeDate.getFullYear()}
|
||||
</div>
|
||||
{expanded && (
|
||||
<div
|
||||
style={{ scrollBehavior: 'smooth' }}
|
||||
ref={scrollArea}
|
||||
id="calendar-scroller"
|
||||
className="flex w-full overflow-x-scroll pb-2 md:max-w-full"
|
||||
<InfinteScroller
|
||||
paginateFront={paginateLeft}
|
||||
paginateEnd={paginateRight}
|
||||
direction={'horizontal'}
|
||||
onElementVisibility={onElementVisibility}
|
||||
ref={scrollerRef}
|
||||
className={className}
|
||||
isMobileScreen={isMobileScreen()}
|
||||
>
|
||||
{months.map((month, index) => {
|
||||
const isFirst = index === 0
|
||||
const isLast = index === months.length - 1
|
||||
{months.map((month) => {
|
||||
const id = elementIdForMonth(month.date)
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
key={id}
|
||||
ref={(ref) => {
|
||||
isFirst ? setFirstElement(ref) : isLast ? setLastElement(ref) : null
|
||||
|
||||
if (ref) {
|
||||
visibilityObserver.observe(ref)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div id={id} key={id}>
|
||||
<Calendar
|
||||
key={id}
|
||||
className="mx-2"
|
||||
activities={activities}
|
||||
onDateSelect={handleDaySelection}
|
||||
startDate={month.date}
|
||||
selectedDay={selectedTemplateDay}
|
||||
selectedDay={selectedDay}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</InfinteScroller>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const usePrevious = <T>(value: T): T | undefined => {
|
||||
const ref = useRef<T>()
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
})
|
||||
return ref.current
|
||||
}
|
||||
@@ -40,6 +40,7 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
const { selectPreviousItem, selectNextItem } = selectionController
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = itemListController.webDisplayOptions
|
||||
const { sortBy } = itemListController.displayOptions
|
||||
const selectedTag = navigationController.selected
|
||||
|
||||
const onScroll: UIEventHandler = useCallback(
|
||||
(e) => {
|
||||
@@ -72,25 +73,27 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
[selectionController],
|
||||
)
|
||||
|
||||
const getTagsForItem = (item: ListableContentItem) => {
|
||||
if (hideTags) {
|
||||
return []
|
||||
}
|
||||
const getTagsForItem = useCallback(
|
||||
(item: ListableContentItem) => {
|
||||
if (hideTags) {
|
||||
return []
|
||||
}
|
||||
|
||||
const selectedTag = navigationController.selected
|
||||
if (!selectedTag) {
|
||||
return []
|
||||
}
|
||||
if (!selectedTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tags = application.getItemTags(item)
|
||||
const tags = application.getItemTags(item)
|
||||
|
||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||
if (isNavigatingOnlyTag) {
|
||||
return []
|
||||
}
|
||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||
if (isNavigatingOnlyTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
return tags
|
||||
},
|
||||
[hideTags, selectedTag, application],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ContentType } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import React, { FunctionComponent } from 'react'
|
||||
import FileListItem from './FileListItem'
|
||||
import NoteListItem from './NoteListItem'
|
||||
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
||||
import { AbstractListItemProps, doListItemPropsMeritRerender } from './Types/AbstractListItemProps'
|
||||
|
||||
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||
switch (props.item.content_type) {
|
||||
@@ -15,4 +15,4 @@ const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentListItem
|
||||
export default React.memo(ContentListItem, (a, b) => !doListItemPropsMeritRerender(a, b))
|
||||
|
||||
@@ -247,7 +247,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
<div
|
||||
id="items-column"
|
||||
className={classNames(
|
||||
'sn-component section app-column flex h-screen flex-col pt-safe-top md:h-full',
|
||||
'sn-component section app-column flex h-screen flex-col overflow-hidden pt-safe-top md:h-full',
|
||||
'xl:w-87.5 xsm-only:!w-full sm-only:!w-full',
|
||||
isTabletScreenSize && !isNotesListVisibleOnTablets
|
||||
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
|
||||
@@ -256,7 +256,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
aria-label={'Notes & Files'}
|
||||
ref={itemsViewPanelRef}
|
||||
>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Items}>
|
||||
<ResponsivePaneContent className="overflow-hidden" paneId={AppPaneId.Items}>
|
||||
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
|
||||
<div id="items-title-bar-container">
|
||||
<input
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { createDailyItemsWithToday } from './CreateDailySections'
|
||||
|
||||
describe('create daily sections', () => {
|
||||
it('createDailyItemsWithToday', () => {
|
||||
const result = createDailyItemsWithToday(10)
|
||||
|
||||
expect(result).toHaveLength(10)
|
||||
})
|
||||
})
|
||||
+3
-2
@@ -31,8 +31,9 @@ export const templateEntryForDate = (date: Date): DailyItemsDay => {
|
||||
}
|
||||
|
||||
export function createDailyItemsWithToday(count: number): DailyItemsDay[] {
|
||||
const today = templateEntryForDate(new Date())
|
||||
return insertBlanks([today], 'end', count)
|
||||
const items = [templateEntryForDate(new Date())]
|
||||
insertBlanks(items, 'front', count / 2 - 1)
|
||||
return insertBlanks(items, 'end', count / 2)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+47
-127
@@ -1,10 +1,7 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
|
||||
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
||||
@@ -14,6 +11,9 @@ import { SNTag } from '@standardnotes/snjs'
|
||||
import { CalendarActivity } from '../Calendar/CalendarActivity'
|
||||
import { dateToDailyDayIdentifier } from './Utils'
|
||||
import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar'
|
||||
import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller'
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
|
||||
type Props = {
|
||||
itemListController: ItemListController
|
||||
@@ -36,12 +36,9 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
const [needsSelectionReload, setNeedsSelectionReload] = useState(false)
|
||||
const [todayItem, setTodayItem] = useState<DailyItemsDay>()
|
||||
const [selectedDay, setSelectedDay] = useState<Date>()
|
||||
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
|
||||
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
|
||||
const [lastScrollHeight, setLastScrollHeight] = useState(0)
|
||||
const [didPaginateTop, setDidPaginateTop] = useState(false)
|
||||
const scrollArea = useRef<HTMLDivElement>(null)
|
||||
const calendarRef = useRef<InfiniteCalendarInterface | null>(null)
|
||||
const [lastVisibleDay, setLastVisibleDay] = useState<DailyItemsDay>()
|
||||
const scrollerRef = useRef<InfiniteScrollerInterface | null>(null)
|
||||
|
||||
const [dailyItems, setDailyItems] = useState<DailyItemsDay[]>(() => {
|
||||
return createDailyItemsWithToday(PageSize)
|
||||
@@ -57,6 +54,12 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay)
|
||||
}, [dailyItems])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (todayItem && scrollerRef.current) {
|
||||
scrollerRef.current?.scrollToElementId(todayItem.id)
|
||||
}
|
||||
}, [todayItem, scrollerRef])
|
||||
|
||||
const calendarActivities: CalendarActivity[] = useMemo(() => {
|
||||
return items.map((item) => {
|
||||
return {
|
||||
@@ -67,108 +70,37 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
}, [items])
|
||||
|
||||
const paginateBottom = useCallback(() => {
|
||||
const copy = dailyItems.slice()
|
||||
insertBlanks(copy, 'end', PageSize)
|
||||
setDailyItems(copy)
|
||||
}, [dailyItems, setDailyItems])
|
||||
log(LoggingDomain.DailyNotes, '[ContentList] paginateBottom')
|
||||
setDailyItems((prev) => {
|
||||
const copy = prev.slice()
|
||||
insertBlanks(copy, 'end', PageSize)
|
||||
return copy
|
||||
})
|
||||
}, [setDailyItems])
|
||||
|
||||
const paginateTop = useCallback(() => {
|
||||
if (scrollArea.current) {
|
||||
setLastScrollHeight(scrollArea.current.scrollHeight)
|
||||
}
|
||||
const copy = dailyItems.slice()
|
||||
insertBlanks(copy, 'front', PageSize)
|
||||
setDidPaginateTop(true)
|
||||
setDailyItems(copy)
|
||||
}, [dailyItems, setDailyItems, setDidPaginateTop])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!scrollArea.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (didPaginateTop) {
|
||||
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - lastScrollHeight
|
||||
setDidPaginateTop(false)
|
||||
}
|
||||
}, [didPaginateTop, lastScrollHeight])
|
||||
log(LoggingDomain.DailyNotes, '[ContentList] paginateTop')
|
||||
setDailyItems((prev) => {
|
||||
const copy = prev.slice()
|
||||
insertBlanks(copy, 'front', PageSize)
|
||||
return copy
|
||||
})
|
||||
}, [setDailyItems])
|
||||
|
||||
const onListItemDidBecomeVisible = useCallback(
|
||||
(elementId: string) => {
|
||||
const dailyItem = dailyItems.find((candidate) => candidate.id === elementId)
|
||||
if (dailyItem) {
|
||||
calendarRef?.current?.changeMonth(dailyItem.date)
|
||||
if (dailyItem && dailyItem !== lastVisibleDay) {
|
||||
setLastVisibleDay(dailyItem)
|
||||
log(LoggingDomain.DailyNotes, '[ContentList] Item did become visible for date', dailyItem.date)
|
||||
calendarRef?.current?.goToMonth(dailyItem.date)
|
||||
} else {
|
||||
log(LoggingDomain.DailyNotes, '[ContentList] Ignoring duplicate day visibility')
|
||||
}
|
||||
},
|
||||
[dailyItems],
|
||||
[dailyItems, lastVisibleDay],
|
||||
)
|
||||
|
||||
const visibilityObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||
if (visibleEntry) {
|
||||
onListItemDidBecomeVisible(visibleEntry.target.id)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.9 },
|
||||
),
|
||||
[onListItemDidBecomeVisible],
|
||||
)
|
||||
|
||||
const bottomObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const first = entries[0]
|
||||
if (first.isIntersecting) {
|
||||
paginateBottom()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[paginateBottom],
|
||||
)
|
||||
|
||||
const topObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const first = entries[0]
|
||||
if (first.isIntersecting) {
|
||||
paginateTop()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[paginateTop],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (lastElement) {
|
||||
bottomObserver.observe(lastElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (lastElement) {
|
||||
bottomObserver.unobserve(lastElement)
|
||||
}
|
||||
}
|
||||
}, [lastElement, bottomObserver])
|
||||
|
||||
useEffect(() => {
|
||||
if (firstElement) {
|
||||
topObserver.observe(firstElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (firstElement) {
|
||||
topObserver.unobserve(firstElement)
|
||||
}
|
||||
}
|
||||
}, [firstElement, topObserver])
|
||||
|
||||
const onClickItem = useCallback(
|
||||
async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => {
|
||||
await onSelect(item, userTriggered)
|
||||
@@ -239,29 +171,29 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
[onClickItem, onClickTemplate, dailyItemForDate, itemsByDateMapping],
|
||||
)
|
||||
|
||||
const hasItemsOnSelectedDay = selectedDay && itemsByDateMapping[dateToDailyDayIdentifier(selectedDay)]?.length > 0
|
||||
return (
|
||||
<>
|
||||
<InfiniteCalendar
|
||||
activities={calendarActivities}
|
||||
activityType={'created'}
|
||||
onDateSelect={onCalendarSelect}
|
||||
selectedTemplateDay={selectedDay}
|
||||
selectedItemDay={selectedDay}
|
||||
selectedDay={selectedDay}
|
||||
selectedDayType={!selectedDay ? undefined : hasItemsOnSelectedDay ? 'item' : 'template'}
|
||||
ref={calendarRef}
|
||||
className={'flex-column flex'}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
|
||||
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
)}
|
||||
ref={scrollArea}
|
||||
id={ElementIds.ContentList}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
|
||||
<InfinteScroller
|
||||
paginateFront={paginateTop}
|
||||
paginateEnd={paginateBottom}
|
||||
direction="vertical"
|
||||
onElementVisibility={onListItemDidBecomeVisible}
|
||||
className={'flex-1'}
|
||||
ref={scrollerRef}
|
||||
isMobileScreen={isMobileScreen()}
|
||||
>
|
||||
{dailyItems.map((dailyItem, index) => {
|
||||
const isFirst = index === 0
|
||||
const isLast = index === dailyItems.length - 1
|
||||
{dailyItems.map((dailyItem) => {
|
||||
const items = itemsByDateMapping[dailyItem.id]
|
||||
if (items) {
|
||||
return items.map((item) => (
|
||||
@@ -275,12 +207,6 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
hidePreview={hideNotePreview}
|
||||
hideTags={hideTags}
|
||||
onClick={() => onClickItem(dailyItem, item, true)}
|
||||
ref={(ref) => {
|
||||
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
|
||||
if (ref) {
|
||||
visibilityObserver.observe(ref)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))
|
||||
} else {
|
||||
@@ -291,17 +217,11 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
id={dailyItem.id}
|
||||
key={dailyItem.dateKey}
|
||||
onClick={() => onClickTemplate(dailyItem.date)}
|
||||
ref={(ref) => {
|
||||
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
|
||||
if (ref) {
|
||||
visibilityObserver.observe(ref)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</InfinteScroller>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+14
-23
@@ -166,12 +166,9 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const setEntryMode = useCallback(
|
||||
(mode: 'normal' | 'daily') => {
|
||||
void changePreferences({ entryMode: mode })
|
||||
},
|
||||
[changePreferences],
|
||||
)
|
||||
const toggleEntryMode = useCallback(() => {
|
||||
void changePreferences({ entryMode: isDailyEntry ? 'normal' : 'daily' })
|
||||
}, [isDailyEntry, changePreferences])
|
||||
|
||||
const TabButton: FunctionComponent<{
|
||||
label: string
|
||||
@@ -211,7 +208,10 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
<h1 className="sk-h3 m-0 text-sm font-semibold">Upgrade for per-tag preferences</h1>
|
||||
</div>
|
||||
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
|
||||
Create powerful workflows and organizational layouts with per-tag display preferences.
|
||||
{DailyEntryModeEnabled &&
|
||||
'Create powerful workflows and organizational layouts with per-tag display preferences and the all-new Daily Notebook feature.'}
|
||||
{!DailyEntryModeEnabled &&
|
||||
'Create powerful workflows and organizational layouts with per-tag display preferences.'}
|
||||
</p>
|
||||
<Button
|
||||
primary
|
||||
@@ -376,26 +376,17 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
{currentMode === 'tag' && DailyEntryModeEnabled && (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold uppercase text-text">Entry Mode</div>
|
||||
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={() => setEntryMode('normal')}
|
||||
checked={!selectedTag.preferences?.entryMode || selectedTag.preferences?.entryMode === 'normal'}
|
||||
>
|
||||
<div className="ml-2 flex flex-grow items-center justify-between">Normal</div>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={() => setEntryMode('daily')}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={isDailyEntry}
|
||||
onChange={toggleEntryMode}
|
||||
>
|
||||
<div className="ml-2 flex flex-grow items-center justify-between">Daily</div>
|
||||
<div className="flex flex-col pr-5">
|
||||
<div className="text-xs font-semibold uppercase text-text">Daily Notebook</div>
|
||||
<div className="mt-1">Capture new notes daily with a calendar-based layout</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
paginateFront: () => void
|
||||
paginateEnd: () => void
|
||||
direction: 'horizontal' | 'vertical'
|
||||
onElementVisibility?: (elementId: string) => void
|
||||
className?: string
|
||||
isMobileScreen?: boolean
|
||||
}
|
||||
|
||||
export type InfiniteScrollerInterface = {
|
||||
scrollToElementId: (id: string) => void
|
||||
}
|
||||
|
||||
export const InfinteScroller = forwardRef<InfiniteScrollerInterface, Props>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
paginateFront,
|
||||
paginateEnd,
|
||||
direction = 'vertical',
|
||||
onElementVisibility,
|
||||
className,
|
||||
isMobileScreen,
|
||||
}: Props,
|
||||
ref,
|
||||
) => {
|
||||
const frontSentinel = useRef<HTMLDivElement | null>(null)
|
||||
const endSentinel = useRef<HTMLDivElement | null>(null)
|
||||
const [ignoreFirstFrontSentinelEvent, setIgnoreFirstFrontSentinelEvent] = useState(true)
|
||||
const [needsMobilePaginationFix, setNeedsMobilePaginationFix] = useState(false)
|
||||
|
||||
const scrollArea = useRef<HTMLDivElement | null>(null)
|
||||
const [scrollSize, setScrollSize] = useState(0)
|
||||
const [didPaginateFront, setDidPaginateFront] = useState(false)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToElementId(id: string) {
|
||||
scrollToElementId(id)
|
||||
},
|
||||
}))
|
||||
|
||||
const visibilityObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||
if (visibleEntry) {
|
||||
onElementVisibility?.(visibleEntry.target.id)
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
),
|
||||
[onElementVisibility],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const childElements = Array.from(scrollArea.current!.children)
|
||||
for (const child of childElements) {
|
||||
visibilityObserver.observe(child)
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const child of childElements) {
|
||||
visibilityObserver.unobserve(child)
|
||||
}
|
||||
}
|
||||
}, [visibilityObserver, children])
|
||||
|
||||
const scrollToElementId = useCallback((id: string) => {
|
||||
const element = document.getElementById(id)
|
||||
if (!element) {
|
||||
log(LoggingDomain.DailyNotes, 'Element not found', id)
|
||||
return
|
||||
}
|
||||
|
||||
log(LoggingDomain.DailyNotes, 'Scrolling to element', id)
|
||||
element.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
})
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!scrollArea.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (didPaginateFront) {
|
||||
if (direction === 'vertical') {
|
||||
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize
|
||||
if (isMobileScreen) {
|
||||
setNeedsMobilePaginationFix(true)
|
||||
}
|
||||
} else {
|
||||
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollSize
|
||||
}
|
||||
setDidPaginateFront(false)
|
||||
}
|
||||
}, [didPaginateFront, scrollSize, direction, isMobileScreen])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
/**
|
||||
* iOS Safari has an issue rendering paginated items from the top where the new
|
||||
* scrolled to area is white until the user interacts with scroll again. The fix
|
||||
* we apply is to re-set scrollTop to its same value to trigger a refresh.
|
||||
* https://stackoverflow.com/questions/9807620
|
||||
*/
|
||||
if (needsMobilePaginationFix) {
|
||||
setTimeout(() => {
|
||||
if (!scrollArea.current) {
|
||||
return
|
||||
}
|
||||
|
||||
log(LoggingDomain.DailyNotes, '[InfiniteScroller] Applying mobile pagination fix')
|
||||
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize
|
||||
setNeedsMobilePaginationFix(false)
|
||||
}, 50)
|
||||
}
|
||||
}, [needsMobilePaginationFix, scrollSize])
|
||||
|
||||
const _paginateFront = useCallback(() => {
|
||||
if (direction === 'vertical') {
|
||||
setScrollSize(scrollArea!.current!.scrollHeight)
|
||||
} else {
|
||||
setScrollSize(scrollArea!.current!.scrollWidth)
|
||||
}
|
||||
setDidPaginateFront(true)
|
||||
paginateFront()
|
||||
}, [paginateFront, direction])
|
||||
|
||||
const _paginateEnd = useCallback(() => {
|
||||
paginateEnd()
|
||||
}, [paginateEnd])
|
||||
|
||||
const frontObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
if (ignoreFirstFrontSentinelEvent) {
|
||||
log(LoggingDomain.DailyNotes, '[InfiniteScroller] Ignoring first front sentinel event')
|
||||
setIgnoreFirstFrontSentinelEvent(false)
|
||||
return
|
||||
}
|
||||
_paginateFront()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[_paginateFront, ignoreFirstFrontSentinelEvent],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (frontSentinel.current) {
|
||||
frontObserver.observe(frontSentinel.current)
|
||||
}
|
||||
}, [frontObserver, frontSentinel])
|
||||
|
||||
const endObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
_paginateEnd()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[_paginateEnd],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (endSentinel.current) {
|
||||
endObserver.observe(endSentinel.current)
|
||||
}
|
||||
}, [endObserver, endSentinel])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollArea}
|
||||
className={className}
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
flexDirection: direction === 'vertical' ? 'column' : 'row',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 1, height: 1, backgroundColor: 'transparent' }} ref={frontSentinel}></div>
|
||||
{children}
|
||||
<div style={{ width: 1, height: 1, backgroundColor: 'transparent' }} ref={endSentinel}></div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -13,6 +13,7 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
import ListItemNotePreviewText from './ListItemNotePreviewText'
|
||||
import { ListItemTitle } from './ListItemTitle'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
@@ -70,6 +71,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
|
||||
useContextMenuEvent(listItemRef, openContextMenu)
|
||||
|
||||
log(LoggingDomain.ItemsList, 'Rendering note list item', item.title)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={listItemRef}
|
||||
|
||||
+87
-1
@@ -1,7 +1,7 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { SortableItem, SNTag } from '@standardnotes/snjs'
|
||||
import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs'
|
||||
import { ListableContentItem } from './ListableContentItem'
|
||||
|
||||
export type AbstractListItemProps = {
|
||||
@@ -18,3 +18,89 @@ export type AbstractListItemProps = {
|
||||
sortBy: keyof SortableItem | undefined
|
||||
tags: SNTag[]
|
||||
}
|
||||
|
||||
export function doListItemPropsMeritRerender(previous: AbstractListItemProps, next: AbstractListItemProps): boolean {
|
||||
const simpleComparison: (keyof AbstractListItemProps)[] = [
|
||||
'onSelect',
|
||||
'hideDate',
|
||||
'hideIcon',
|
||||
'hideTags',
|
||||
'hidePreview',
|
||||
'selected',
|
||||
'sortBy',
|
||||
]
|
||||
|
||||
for (const key of simpleComparison) {
|
||||
if (previous[key] !== next[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (previous['item'] !== next['item']) {
|
||||
if (doesItemChangeMeritRerender(previous['item'], next['item'])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return doesTagsChangeMeritRerender(previous['tags'], next['tags'])
|
||||
}
|
||||
|
||||
function doesTagsChangeMeritRerender(previous: SNTag[], next: SNTag[]): boolean {
|
||||
if (previous === next) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (previous.length !== next.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (previous.length === 0 && next.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Uuids(previous).sort().join() !== Uuids(next).sort().join()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
previous
|
||||
.map((t) => t.title)
|
||||
.sort()
|
||||
.join() !==
|
||||
next
|
||||
.map((t) => t.title)
|
||||
.sort()
|
||||
.join()
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function doesItemChangeMeritRerender(previous: ListableContentItem, next: ListableContentItem): boolean {
|
||||
if (previous.uuid !== next.uuid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const propertiesMeritingRerender: (keyof ListableContentItem)[] = [
|
||||
'title',
|
||||
'protected',
|
||||
'updatedAtString',
|
||||
'createdAtString',
|
||||
'hidePreview',
|
||||
'preview_html',
|
||||
'preview_plain',
|
||||
'archived',
|
||||
'starred',
|
||||
'pinned',
|
||||
]
|
||||
|
||||
for (const key of propertiesMeritingRerender) {
|
||||
if (previous[key] !== next[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { destroyAllObjectProperties, preventRefreshing } from '@/Utils'
|
||||
import { ApplicationEvent, ApplicationDescriptor, WebAppEvent } from '@standardnotes/snjs'
|
||||
import {
|
||||
@@ -41,7 +41,7 @@ type State = {
|
||||
arbitraryStatusMessage?: string
|
||||
}
|
||||
|
||||
class Footer extends PureComponent<Props, State> {
|
||||
class Footer extends AbstractComponent<Props, State> {
|
||||
public user?: unknown
|
||||
private didCheckForOffline = false
|
||||
private completedInitialSync = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileItem, FileViewController, NoteViewController } from '@standardnotes/snjs'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
|
||||
@@ -22,7 +22,7 @@ type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
class NoteGroupView extends PureComponent<Props, State> {
|
||||
class NoteGroupView extends AbstractComponent<Props, State> {
|
||||
private removeChangeObserver!: () => void
|
||||
|
||||
constructor(props: Props) {
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import PanelResizer, { PanelResizeType, PanelSide } from '@/Components/PanelResizer/PanelResizer'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
isPayloadSourceRetrieved,
|
||||
isPayloadSourceInternalChange,
|
||||
ContentType,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
ComponentArea,
|
||||
PrefKey,
|
||||
ComponentViewerInterface,
|
||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||
ContentType,
|
||||
EditorFontSize,
|
||||
EditorLineHeight,
|
||||
isPayloadSourceInternalChange,
|
||||
isPayloadSourceRetrieved,
|
||||
NoteType,
|
||||
NoteViewController,
|
||||
PayloadEmitSource,
|
||||
PrefKey,
|
||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
WebAppEvent,
|
||||
EditorLineHeight,
|
||||
EditorFontSize,
|
||||
NoteType,
|
||||
} from '@standardnotes/snjs'
|
||||
import { debounce, isDesktopApplication } from '@/Utils'
|
||||
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
|
||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||
import { EditorEventSource } from '../../Types/EditorEventSource'
|
||||
import { confirmDialog, KeyboardModifier, KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||
import EditingDisabledBanner from './EditingDisabledBanner'
|
||||
import { reloadFont } from './FontFunctions'
|
||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
||||
import { NoteViewProps } from './NoteViewProps'
|
||||
import {
|
||||
transactionForAssociateComponentWithCurrentNote,
|
||||
transactionForDisassociateComponentWithCurrentNote,
|
||||
} from './TransactionFunctions'
|
||||
import { reloadFont } from './FontFunctions'
|
||||
import { NoteViewProps } from './NoteViewProps'
|
||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
||||
|
||||
const MinimumStatusDuration = 400
|
||||
const TextareaDebounce = 100
|
||||
@@ -74,6 +75,7 @@ type State = {
|
||||
/** Setting to true then false will allow the main content textarea to be destroyed
|
||||
* then re-initialized. Used when reloading spellcheck status. */
|
||||
textareaUnloading: boolean
|
||||
plaintextEditorFocused?: boolean
|
||||
|
||||
leftResizerWidth: number
|
||||
leftResizerOffset: number
|
||||
@@ -97,14 +99,13 @@ const PlaintextFontSizeMapping: Record<EditorFontSize, string> = {
|
||||
Large: 'text-xl',
|
||||
}
|
||||
|
||||
class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
readonly controller!: NoteViewController
|
||||
|
||||
private statusTimeout?: NodeJS.Timeout
|
||||
private lastEditorFocusEventSource?: EditorEventSource
|
||||
onEditorComponentLoad?: () => void
|
||||
|
||||
private scrollPosition = 0
|
||||
private removeTrashKeyObserver?: () => void
|
||||
private removeTabObserver?: () => void
|
||||
private removeComponentStreamObserver?: () => void
|
||||
@@ -193,9 +194,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
;(this.onPanelResizeFinish as unknown) = undefined
|
||||
;(this.stackComponentExpanded as unknown) = undefined
|
||||
;(this.toggleStackComponent as unknown) = undefined
|
||||
;(this.setScrollPosition as unknown) = undefined
|
||||
;(this.resetScrollPosition as unknown) = undefined
|
||||
;(this.onSystemEditorLoad as unknown) = undefined
|
||||
;(this.onSystemEditorRef as unknown) = undefined
|
||||
;(this.debounceReloadEditorComponent as unknown) = undefined
|
||||
;(this.textAreaChangeDebounceSave as unknown) = undefined
|
||||
;(this.editorContentRef as unknown) = undefined
|
||||
@@ -209,6 +208,24 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
return this.controller.item
|
||||
}
|
||||
|
||||
override shouldComponentUpdate(_nextProps: Readonly<NoteViewProps>, nextState: Readonly<State>): boolean {
|
||||
const complexObjects: (keyof State)[] = ['availableStackComponents', 'stackComponentViewers']
|
||||
for (const key of Object.keys(nextState) as (keyof State)[]) {
|
||||
if (complexObjects.includes(key)) {
|
||||
continue
|
||||
}
|
||||
const prevValue = this.state[key]
|
||||
const nextValue = nextState[key]
|
||||
|
||||
if (prevValue !== nextValue) {
|
||||
log(LoggingDomain.NoteView, 'Rendering due to state change', key, prevValue, nextValue)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
super.componentDidMount()
|
||||
|
||||
@@ -255,6 +272,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
onNoteInnerChange(note: SNNote, source: PayloadEmitSource): void {
|
||||
log(LoggingDomain.NoteView, 'On inner note change', PayloadEmitSource[source])
|
||||
|
||||
if (note.uuid !== this.note.uuid) {
|
||||
throw Error('Editor received changes for non-current note')
|
||||
}
|
||||
@@ -433,12 +452,15 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
streamItems() {
|
||||
this.removeComponentStreamObserver = this.application.streamItems(ContentType.Component, async ({ source }) => {
|
||||
log(LoggingDomain.NoteView, 'On component stream observer', PayloadEmitSource[source])
|
||||
if (isPayloadSourceInternalChange(source) || source === PayloadEmitSource.InitialObserverRegistrationPush) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.reloadStackComponents()
|
||||
this.debounceReloadEditorComponent()
|
||||
})
|
||||
@@ -456,6 +478,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
if (this.state.editorComponentViewerDidAlreadyReload && !force) {
|
||||
return
|
||||
}
|
||||
|
||||
const component = viewer.component
|
||||
this.application.componentManager.destroyComponentViewer(viewer)
|
||||
this.setState(
|
||||
@@ -491,6 +514,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
async reloadEditorComponent() {
|
||||
log(LoggingDomain.NoteView, 'Reload editor component')
|
||||
if (this.state.showProtectedWarning) {
|
||||
this.destroyCurrentEditorComponent()
|
||||
return
|
||||
@@ -583,13 +607,16 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
onTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> = ({ currentTarget }) => {
|
||||
const text = currentTarget.value
|
||||
|
||||
this.setState({
|
||||
editorText: text,
|
||||
})
|
||||
|
||||
this.textAreaChangeDebounceSave()
|
||||
}
|
||||
|
||||
textAreaChangeDebounceSave = () => {
|
||||
log(LoggingDomain.NoteView, 'Performing save after debounce')
|
||||
this.controller
|
||||
.save({
|
||||
editorValues: {
|
||||
@@ -611,10 +638,14 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
onTitleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
log(LoggingDomain.NoteView, 'Performing save after title change')
|
||||
|
||||
const title = currentTarget.value
|
||||
|
||||
this.setState({
|
||||
editorTitle: title,
|
||||
})
|
||||
|
||||
this.controller
|
||||
.save({
|
||||
editorValues: {
|
||||
@@ -644,6 +675,15 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource })
|
||||
}
|
||||
this.lastEditorFocusEventSource = undefined
|
||||
this.setState({ plaintextEditorFocused: true })
|
||||
}
|
||||
|
||||
onContentBlur = () => {
|
||||
if (this.lastEditorFocusEventSource) {
|
||||
this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource })
|
||||
}
|
||||
this.lastEditorFocusEventSource = undefined
|
||||
this.setState({ plaintextEditorFocused: false })
|
||||
}
|
||||
|
||||
setShowProtectedOverlay(show: boolean) {
|
||||
@@ -655,10 +695,12 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
this.application.alertService.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.note.locked) {
|
||||
this.application.alertService.alert(STRING_DELETE_LOCKED_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
const title = this.note.title.length ? `'${this.note.title}'` : 'this note'
|
||||
const text = StringDeleteNote(title, permanently)
|
||||
if (
|
||||
@@ -720,6 +762,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
async reloadPreferences() {
|
||||
log(LoggingDomain.NoteView, 'Reload preferences')
|
||||
const monospaceFont = this.application.getPreference(
|
||||
PrefKey.EditorMonospaceEnabled,
|
||||
PrefDefaults[PrefKey.EditorMonospaceEnabled],
|
||||
@@ -769,9 +812,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
/** @components */
|
||||
|
||||
async reloadStackComponents() {
|
||||
log(LoggingDomain.NoteView, 'Reload stack components')
|
||||
const stackComponents = sortAlphabetically(
|
||||
this.application.componentManager
|
||||
.componentsForArea(ComponentArea.EditorStack)
|
||||
@@ -837,20 +879,13 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
})
|
||||
}
|
||||
|
||||
setScrollPosition = () => {
|
||||
const editor = document.getElementById(ElementIds.NoteTextEditor) as HTMLInputElement
|
||||
this.scrollPosition = editor.scrollTop
|
||||
}
|
||||
|
||||
resetScrollPosition = () => {
|
||||
const editor = document.getElementById(ElementIds.NoteTextEditor) as HTMLInputElement
|
||||
editor.scrollTop = this.scrollPosition
|
||||
}
|
||||
|
||||
onSystemEditorLoad = (ref: HTMLTextAreaElement | null) => {
|
||||
onSystemEditorRef = (ref: HTMLTextAreaElement | null) => {
|
||||
if (this.removeTabObserver || !ref) {
|
||||
return
|
||||
}
|
||||
|
||||
log(LoggingDomain.NoteView, 'On system editor ref')
|
||||
|
||||
/**
|
||||
* Insert 4 spaces when a tab key is pressed,
|
||||
* only used when inside of the text editor.
|
||||
@@ -900,18 +935,12 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
},
|
||||
})
|
||||
|
||||
editor.addEventListener('scroll', this.setScrollPosition)
|
||||
editor.addEventListener('input', this.resetScrollPosition)
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
for (const record of records) {
|
||||
record.removedNodes.forEach((node) => {
|
||||
if (node === editor) {
|
||||
this.removeTabObserver?.()
|
||||
this.removeTabObserver = undefined
|
||||
editor.removeEventListener('scroll', this.setScrollPosition)
|
||||
editor.removeEventListener('scroll', this.resetScrollPosition)
|
||||
this.scrollPosition = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -942,6 +971,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
)
|
||||
}
|
||||
|
||||
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label="Note"
|
||||
@@ -1005,31 +1036,33 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
updateSavingIndicator={this.state.updateSavingIndicator}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<LinkedItemsButton
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
linkingController={this.viewControllerManager.linkingController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
/>
|
||||
<ChangeEditorButton
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<PinNoteButton
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<NotesOptionsPanel
|
||||
application={this.application}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
linkingController={this.viewControllerManager.linkingController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
{renderHeaderOptions && (
|
||||
<div className="flex items-center gap-3">
|
||||
<LinkedItemsButton
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
linkingController={this.viewControllerManager.linkingController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
/>
|
||||
<ChangeEditorButton
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<PinNoteButton
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<NotesOptionsPanel
|
||||
application={this.application}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
linkingController={this.viewControllerManager.linkingController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
|
||||
</div>
|
||||
@@ -1073,8 +1106,9 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
id={ElementIds.NoteTextEditor}
|
||||
onChange={this.onTextAreaChange}
|
||||
onFocus={this.onContentFocus}
|
||||
onBlur={this.onContentBlur}
|
||||
readOnly={this.state.noteLocked}
|
||||
ref={(ref) => ref && this.onSystemEditorLoad(ref)}
|
||||
ref={(ref) => ref && this.onSystemEditorRef(ref)}
|
||||
spellCheck={this.state.spellcheck}
|
||||
value={this.state.editorText}
|
||||
className={classNames(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { createRef } from 'react'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
@@ -38,7 +38,7 @@ type FormData = {
|
||||
status?: string
|
||||
}
|
||||
|
||||
class PasswordWizard extends PureComponent<Props, State> {
|
||||
class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
private currentPasswordInput = createRef<HTMLInputElement>()
|
||||
|
||||
constructor(props: Props) {
|
||||
|
||||
@@ -54,7 +54,7 @@ function useStateRef<State>(state: State): MutableRefObject<State> {
|
||||
return ref
|
||||
}
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <div>{children}</div>)
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||
|
||||
const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) => {
|
||||
const currentSelectedPane = paneController.currentPane
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
close: () => void
|
||||
}
|
||||
|
||||
class SyncResolutionMenu extends PureComponent<Props> {
|
||||
class SyncResolutionMenu extends AbstractComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props, props.application)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user