Compare commits

...
Author SHA1 Message Date
StandardNotes CI 09e477213d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
2022-10-31 13:44:00 +00:00
Mo 05305b5298 test: fix calendar tests 2022-10-31 08:16:13 -05:00
Mo 4e72a796cb chore: remove unused mobile deps (#1905) 2022-10-30 19:00:04 -05:00
StandardNotes CI 73d6b67a36 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].6
2022-10-30 23:38:51 +00:00
Mo 35e2fa9eb6 fix(mobile): increase save debounce timeout 2022-10-30 18:05:28 -05:00
StandardNotes CI 4b07201a61 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2022-10-30 16:19:14 +00:00
Mo 89927a3790 perf: avoid uneccessary notes list item rerenders (#1904) 2022-10-30 10:48:23 -05:00
StandardNotes CI 32f03d9470 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2022-10-29 20:23:16 +00:00
Mo 88017ce3d9 fix(mobile): note view hide bar options on editor focus (#1903) 2022-10-29 14:53:03 -05:00
StandardNotes CI b9b4bdf257 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].3
2022-10-28 19:15:38 +00:00
Aman Harwara 6071ebffeb fix: color scheme handling on mobile (#1902) 2022-10-29 00:12:03 +05:30
StandardNotes CI f8181d9a4f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-10-28 17:51:02 +00:00
Mo 026fbc1524 style: copy 2022-10-28 12:17:30 -05:00
Mo 5058aaee33 fix: boolean 2022-10-28 12:14:23 -05:00
StandardNotes CI f67b486a0d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-28 16:16:17 +00:00
Mo a9efce21f0 refactor: daily notes (#1901) 2022-10-28 10:45:03 -05:00
Aman Harwara 5bb923cff3 fix: privacy overlay on iOS 2022-10-28 20:19:57 +05:30
Aman Harwara 5a6f4fb35c fix: screenshot privacy setting not working on app relaunch (#1900) 2022-10-28 19:31:15 +05:30
Aman Harwara 9ac5bd0c27 refactor: extract biometrics conditional to constant 2022-10-28 17:11:37 +05:30
Aman Harwara 02ad46147d fix: only auto-toggle biometrics prompt if user hasn't interacted 2022-10-28 16:07:26 +05:30
110 changed files with 1084 additions and 1091 deletions
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.
+28
View File
@@ -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 -1
View File
@@ -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,
+32
View File
@@ -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 */
-12
View File
@@ -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 */
-6
View File
@@ -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 -7
View File
@@ -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()
}
}
+21 -1
View File
@@ -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()
}
}
+10 -3
View File
@@ -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) => {
+28
View File
@@ -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 -1
View File
@@ -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",
+6
View File
@@ -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 -1
View File
@@ -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>
}
+12
View File
@@ -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
+22 -14
View File
@@ -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',
}
-8
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.145.0",
"version": "2.145.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.10.5",
"version": "1.10.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20 -5
View File
@@ -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)
}
}
+40
View File
@@ -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 -1
View File
@@ -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>
)
@@ -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,
})
@@ -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
@@ -0,0 +1,9 @@
import { createDailyItemsWithToday } from './CreateDailySections'
describe('create daily sections', () => {
it('createDailyItemsWithToday', () => {
const result = createDailyItemsWithToday(10)
expect(result).toHaveLength(10)
})
})
@@ -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)
}
/**
@@ -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>
</>
)
}
@@ -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>
</>
)}
@@ -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}
@@ -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