mirror of
https://github.com/standardnotes/app
synced 2026-09-16 09:45:56 -04:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d6b67a36 | ||
|
|
35e2fa9eb6 | ||
|
|
4b07201a61 | ||
|
|
89927a3790 | ||
|
|
32f03d9470 | ||
|
|
88017ce3d9 | ||
|
|
b9b4bdf257 | ||
|
|
6071ebffeb | ||
|
|
f8181d9a4f | ||
|
|
026fbc1524 | ||
|
|
5058aaee33 | ||
|
|
f67b486a0d | ||
|
|
a9efce21f0 | ||
|
|
5bb923cff3 | ||
|
|
5a6f4fb35c | ||
|
|
9ac5bd0c27 | ||
|
|
02ad46147d |
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.260",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -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.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.45.1",
|
||||
"version": "3.45.7",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -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,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.189",
|
||||
"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,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.6",
|
||||
"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>
|
||||
)
|
||||
|
||||
+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)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { TagListSectionType } from './TagListSection'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
|
||||
type Props = {
|
||||
tag: SNTag
|
||||
@@ -237,6 +238,8 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
}
|
||||
}, [addDragTarget, linkingController, removeDragTarget, tag])
|
||||
|
||||
log(LoggingDomain.NavigationList, 'Rendering TagsListItem')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
||||
@@ -150,6 +150,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
||||
this.toastService = new ToastService()
|
||||
|
||||
this.applicationEventObserver = new ApplicationEventObserver(
|
||||
application,
|
||||
application.routeService,
|
||||
this.purchaseFlowController,
|
||||
this.accountMenuController,
|
||||
|
||||
@@ -26,8 +26,10 @@ import { SyncStatusController } from '@/Controllers/SyncStatusController'
|
||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||
|
||||
import { ApplicationEventObserver } from './ApplicationEventObserver'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
describe('ApplicationEventObserver', () => {
|
||||
let application: WebApplication
|
||||
let routeService: RouteServiceInterface
|
||||
let purchaseFlowController: PurchaseFlowController
|
||||
let accountMenuController: AccountMenuController
|
||||
@@ -40,6 +42,7 @@ describe('ApplicationEventObserver', () => {
|
||||
|
||||
const createObserver = () =>
|
||||
new ApplicationEventObserver(
|
||||
application,
|
||||
routeService,
|
||||
purchaseFlowController,
|
||||
accountMenuController,
|
||||
@@ -52,6 +55,8 @@ describe('ApplicationEventObserver', () => {
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
application = {} as jest.Mocked<WebApplication>
|
||||
|
||||
routeService = {} as jest.Mocked<RouteServiceInterface>
|
||||
routeService.getRoute = jest.fn().mockReturnValue({
|
||||
type: RouteType.None,
|
||||
@@ -223,4 +228,15 @@ describe('ApplicationEventObserver', () => {
|
||||
expect(syncStatusController.update).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Upon Database Loaded', () => {
|
||||
it('should handle mobile screenshot privacy setting', async () => {
|
||||
application.isNativeMobileWeb = jest.fn().mockReturnValue(true)
|
||||
application.handleInitialMobileScreenshotPrivacy = jest.fn()
|
||||
|
||||
await createObserver().handle(ApplicationEvent.LocalDataLoaded)
|
||||
|
||||
expect(application.handleInitialMobileScreenshotPrivacy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,9 +20,11 @@ import { SyncStatusController } from '@/Controllers/SyncStatusController'
|
||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||
|
||||
import { EventObserverInterface } from './EventObserverInterface'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
export class ApplicationEventObserver implements EventObserverInterface {
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private routeService: RouteServiceInterface,
|
||||
private purchaseFlowController: PurchaseFlowController,
|
||||
private accountMenuController: AccountMenuController,
|
||||
@@ -89,6 +91,11 @@ export class ApplicationEventObserver implements EventObserverInterface {
|
||||
case ApplicationEvent.SyncStatusChanged:
|
||||
this.syncStatusController.update(this.syncClient.getSyncStatus())
|
||||
break
|
||||
case ApplicationEvent.LocalDataLoaded:
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
this.application.handleInitialMobileScreenshotPrivacy()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { log as utilsLog } from '@standardnotes/utils'
|
||||
import { isDev } from './Utils'
|
||||
|
||||
export enum LoggingDomain {
|
||||
DailyNotes,
|
||||
NoteView,
|
||||
ItemsList,
|
||||
NavigationList,
|
||||
}
|
||||
|
||||
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||
[LoggingDomain.DailyNotes]: false,
|
||||
[LoggingDomain.NoteView]: false,
|
||||
[LoggingDomain.ItemsList]: false,
|
||||
[LoggingDomain.NavigationList]: false,
|
||||
}
|
||||
|
||||
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||
if (!isDev || !LoggingStatus[domain]) {
|
||||
return
|
||||
}
|
||||
|
||||
utilsLog(LoggingDomain[domain], ...args)
|
||||
}
|
||||
@@ -68,6 +68,9 @@ export class MobileWebReceiver {
|
||||
case ReactNativeToWebEvent.AndroidBackButtonPressed:
|
||||
void this.application.handleAndroidBackButtonPressed()
|
||||
break
|
||||
case ReactNativeToWebEvent.ColorSchemeChanged:
|
||||
void this.application.handleMobileColorSchemeChangeEvent()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -25,7 +25,7 @@ type ProviderProps = {
|
||||
application: WebApplication
|
||||
} & ChildrenProps
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <div>{children}</div>)
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||
|
||||
const AndroidBackHandlerProvider = ({ application, children }: ProviderProps) => {
|
||||
const addAndroidBackHandler = useCallback(
|
||||
|
||||
@@ -33,7 +33,7 @@ export function numHoursBetweenDates(date1: Date, date2: Date): number {
|
||||
return Math.abs(date1.getTime() - date2.getTime()) / 3600000
|
||||
}
|
||||
|
||||
export function isDateInSameDay(date1: Date, date2: Date): boolean {
|
||||
export function areDatesInSameDay(date1: Date, date2: Date): boolean {
|
||||
return date1.toLocaleDateString() === date2.toLocaleDateString()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user