Compare commits

...
Author SHA1 Message Date
StandardNotes CI 73d6b67a36 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].6
2022-10-30 23:38:51 +00:00
Mo 35e2fa9eb6 fix(mobile): increase save debounce timeout 2022-10-30 18:05:28 -05:00
StandardNotes CI 4b07201a61 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2022-10-30 16:19:14 +00:00
Mo 89927a3790 perf: avoid uneccessary notes list item rerenders (#1904) 2022-10-30 10:48:23 -05:00
StandardNotes CI 32f03d9470 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2022-10-29 20:23:16 +00:00
Mo 88017ce3d9 fix(mobile): note view hide bar options on editor focus (#1903) 2022-10-29 14:53:03 -05:00
StandardNotes CI b9b4bdf257 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].3
2022-10-28 19:15:38 +00:00
Aman Harwara 6071ebffeb fix: color scheme handling on mobile (#1902) 2022-10-29 00:12:03 +05:30
StandardNotes CI f8181d9a4f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-10-28 17:51:02 +00:00
Mo 026fbc1524 style: copy 2022-10-28 12:17:30 -05:00
Mo 5058aaee33 fix: boolean 2022-10-28 12:14:23 -05:00
StandardNotes CI f67b486a0d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-28 16:16:17 +00:00
Mo a9efce21f0 refactor: daily notes (#1901) 2022-10-28 10:45:03 -05:00
Aman Harwara 5bb923cff3 fix: privacy overlay on iOS 2022-10-28 20:19:57 +05:30
Aman Harwara 5a6f4fb35c fix: screenshot privacy setting not working on app relaunch (#1900) 2022-10-28 19:31:15 +05:30
Aman Harwara 9ac5bd0c27 refactor: extract biometrics conditional to constant 2022-10-28 17:11:37 +05:30
Aman Harwara 02ad46147d fix: only auto-toggle biometrics prompt if user hasn't interacted 2022-10-28 16:07:26 +05:30
StandardNotes CI ba231c633f chore(release): publish
- @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-10-27 22:52:36 +00:00
StandardNotes CI 040bce7584 chore(release): components 2022-10-27 22:52:22 +00:00
Mo 69c3f2be83 feat: daily notes (dev only) (#1894) 2022-10-27 17:21:31 -05:00
StandardNotes CI 064e054587 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].0
2022-10-26 21:03:43 +00:00
Aman Harwara 3bd1ad38ad feat: auto-activate biometrics prompt when mobile regains focus (#1891) 2022-10-27 02:02:33 +05:30
StandardNotes CI d817a2115e chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-26 12:22:23 +00:00
Mo 55296dcc77 fix(web): issue with hide pinned and hide protected not having effect 2022-10-26 06:45:45 -05:00
StandardNotes CI 350fbbd74d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-25 19:36:58 +00:00
Aman Harwara f48c3f9582 feat: add "Upload and link file" button to linking panel 2022-10-26 00:35:50 +05:30
123 changed files with 2454 additions and 358 deletions
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/api
## [1.18.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.18.2",
"version": "1.18.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.7.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/components-meta
## [2.7.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
### Reverts
@@ -1,7 +1,7 @@
{
"files": {
"main.js": "./static/js/main.js",
"main.css": "./static/css/main.09cbcc53.css",
"main.css": "./static/css/main.245e3425.css",
"static/media/material-icons-two-tone.woff": "./static/media/material-icons-two-tone..woff",
"static/media/material-icons-two-tone.woff2": "./static/media/material-icons-two-tone..woff2",
"static/media/material-icons-round.woff": "./static/media/material-icons-round..woff",
@@ -74,10 +74,10 @@
"static/media/KaTeX_Size3-Regular.woff2": "./static/media/KaTeX_Size3-Regular..woff2",
"index.html": "./index.html",
"main.js.map": "./static/js/main.js.map",
"main.09cbcc53.css.map": "./static/css/main.09cbcc53.css.map"
"main.245e3425.css.map": "./static/css/main.245e3425.css.map"
},
"entrypoints": [
"static/js/main.js",
"static/css/main.09cbcc53.css"
"static/css/main.245e3425.css"
]
}
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A lightweight WYSIWYG markdown editor, derivated from Milkdown editor"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Markdown Visual</title><script defer="defer" src="./static/js/main.js"></script><link href="./static/css/main.09cbcc53.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A lightweight WYSIWYG markdown editor, derivated from Milkdown editor"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Markdown Visual</title><script defer="defer" src="./static/js/main.js"></script><link href="./static/css/main.245e3425.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -70,9 +70,9 @@
"binary": "b621bfd81829a205f2f02e85d446e277ad20e367b92676ec20b3ee59b1bfab3f"
},
"org.standardnotes.markdown-visual-editor": {
"version": "1.1.3",
"base64": "32f86de2c5419ff0da9df8047bc3aa6e5dd9bf14ef3b59551a16b24696ea7456",
"binary": "4e18ee37a534e2d6810a8b486f3f945507d1cf2590c2480e01f55fec9b85b718"
"version": "1.1.4",
"base64": "7d206437fca5c4f5b2a4a4ee5a9a29fdb3e2fbb1637ff1dbe962f00f48128993",
"binary": "6c396c45102a114b6d507ad24bdae38168baf780d77f66d0f7c12b25c89b7c57"
},
"org.standardnotes.simple-task-editor": {
"version": "1.4.4",
@@ -80,9 +80,9 @@
"binary": "9df983a144b9686f4f58b03bf78bf1f84f53aea60251621a75586722bf576475"
},
"org.standardnotes.token-vault": {
"version": "2.2.5",
"base64": "f85e355fb8152702a29e44c02a912e1ff5ce94a95c00935a1657e17ec489fb14",
"binary": "a9bcc6ac6c028e2a9b29fb5359414026402b8a6ba02df754d8946984b01722de"
"version": "2.2.6",
"base64": "52ab80192bdee708b4ded34590ed644d0bb41e6438d88e5a48dce0490b55dcac",
"binary": "27efe92ba9a345d94818fac8b46c3281ba0462fc5ed805df0e0052384e1e2826"
},
"org.standardnotes.standard-sheets": {
"version": "1.5.1",
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/components-meta",
"version": "2.7.21",
"version": "2.7.22",
"private": true,
"author": "Standard Notes.",
"main": "dist",
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/markdown-visual
## [1.1.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-05)
**Note:** Version bump only for package @standardnotes/markdown-visual
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/markdown-visual",
"version": "1.1.3",
"version": "1.1.4",
"author": "Johnny Almonte <[email protected]>",
"description": "A lightweight WYSIWYG markdown editor for Standard Notes, derived from Milkdown.",
"keywords": [
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.2.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/authenticator
## [2.2.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-05)
**Note:** Version bump only for package @standardnotes/authenticator
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/authenticator",
"version": "2.2.5",
"version": "2.2.6",
"main": "dist/dist.js",
"author": "Standard Notes",
"private": true,
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.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
## [3.23.253](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.252](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.251](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.250](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.23.250",
"version": "3.23.260",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/encryption
## [1.18.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.18.7",
"version": "1.18.8",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.24.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.24.9",
"version": "1.24.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/files
## [1.11.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.11.9",
"version": "1.11.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+46
View File
@@ -3,6 +3,52 @@
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
# [3.45.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Features
* auto-activate biometrics prompt when mobile regains focus ([#1891](https://github.com/standardnotes/app/issues/1891)) ([3bd1ad3](https://github.com/standardnotes/app/commit/3bd1ad38ad05ba6ea0e99911693ed7b25e2f8380))
## [3.44.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.44.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.44.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.44.3",
"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()
}
}
+25 -1
View File
@@ -13,7 +13,18 @@ import {
TransferPayload,
UuidString,
} from '@standardnotes/snjs'
import { Alert, 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() {
@@ -610,4 +626,12 @@ export class MobileDevice implements MobileDeviceInterface {
isUrlComponentUrl(url: string): boolean {
return Array.from(this.componentUrls.values()).includes(url)
}
async getAppState(): Promise<AppStateStatus> {
return AppState.currentState
}
async getColorScheme(): Promise<ColorSchemeName> {
return Appearance.getColorScheme()
}
}
+10 -3
View File
@@ -1,4 +1,5 @@
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Keyboard, Platform } from 'react-native'
import VersionInfo from 'react-native-version-info'
@@ -28,9 +29,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
const stateService = useMemo(() => new AppStateObserverService(), [])
const androidBackHandlerService = useMemo(() => new AndroidBackHandlerService(), [])
const colorSchemeService = useMemo(() => new ColorSchemeObserverService(), [])
const device = useMemo(
() => new MobileDevice(stateService, androidBackHandlerService),
[androidBackHandlerService, stateService],
() => new MobileDevice(stateService, androidBackHandlerService, colorSchemeService),
[androidBackHandlerService, colorSchemeService, stateService],
)
useEffect(() => {
@@ -44,6 +46,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
},
)
const removeColorSchemeServiceListener = colorSchemeService.addEventObserver((event: ReactNativeToWebEvent) => {
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
})
const keyboardShowListener = Keyboard.addListener('keyboardWillShow', () => {
device.reloadStatusBarStyle(false)
})
@@ -55,10 +61,11 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
return () => {
removeStateServiceListener()
removeBackHandlerServiceListener()
removeColorSchemeServiceListener()
keyboardShowListener.remove()
keyboardHideListener.remove()
}
}, [webViewRef, stateService, device, androidBackHandlerService])
}, [webViewRef, stateService, device, androidBackHandlerService, colorSchemeService])
useEffect(() => {
const observer = device.addMobileWebEventReceiver((event) => {
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.32.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
### Features
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
## [1.31.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.31.1",
"version": "1.32.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -28,6 +28,10 @@ export class SNTag extends DecryptedItem<TagContent> implements TagContentSpecia
this.preferences = this.payload.content.preferences
}
get isDailyEntry(): boolean {
return this.preferences?.entryMode === 'daily'
}
get noteReferences(): ContentReference[] {
const references = this.payload.references
return references.filter((ref) => ref.content_type === ContentType.Note)
@@ -16,4 +16,5 @@ export interface TagPreferences {
newNoteTitleFormat?: NewNoteTitleFormat
customNoteTitleFormat?: string
editorIdentifier?: FeatureIdentifier | string
entryMode?: 'normal' | 'daily'
}
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.3.182](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.181](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.180](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.179](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.3.179",
"version": "1.3.189",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+18
View File
@@ -3,6 +3,24 @@
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
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
# [1.38.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Features
* auto-activate biometrics prompt when mobile regains focus ([#1891](https://github.com/standardnotes/app/issues/1891)) ([3bd1ad3](https://github.com/standardnotes/app/commit/3bd1ad38ad05ba6ea0e99911693ed7b25e2f8380))
## [1.37.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.37.1",
"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
@@ -20,4 +20,6 @@ export interface MobileDeviceInterface extends DeviceInterface {
addComponentUrl(componentUuid: string, componentUrl: string): void
removeComponentUrl(componentUuid: string): void
isUrlComponentUrl(url: string): boolean
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
}
@@ -11,6 +11,7 @@ import {
DeletedItemInterface,
ItemContent,
PredicateInterface,
DecryptedPayload,
} from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService'
@@ -93,6 +94,7 @@ export interface ItemManagerInterface extends AbstractService {
>(
contentType: ContentType,
content?: C,
override?: Partial<DecryptedPayload<C>>,
): I
/**
@@ -3,6 +3,7 @@ import {
BackupFile,
DecryptedItemInterface,
DecryptedItemMutator,
DecryptedPayload,
EncryptedItemInterface,
FileItem,
ItemContent,
@@ -117,6 +118,7 @@ export interface MutatorClientInterface {
>(
contentType: ContentType,
content?: C,
override?: Partial<DecryptedPayload<C>>,
): I
/**
+22
View File
@@ -3,6 +3,28 @@
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
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
## [2.144.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/snjs
## [2.144.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
@@ -28,6 +28,7 @@ describe('note view controller', () => {
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
ContentType.Note,
expect.objectContaining({ noteType: NoteType.Plain }),
expect.anything(),
)
})
})
+40 -24
View File
@@ -1,3 +1,4 @@
import { InfoStrings } from '../Strings/Info'
import { NoteType } from '@standardnotes/features'
import {
NoteMutator,
@@ -11,14 +12,6 @@ import { removeFromArray } from '@standardnotes/utils'
import { ContentType } from '@standardnotes/common'
import { UuidString } from '@Lib/Types/UuidString'
import { SNApplication } from '../Application/Application'
import {
STRING_SAVING_WHILE_DOCUMENT_HIDDEN,
STRING_INVALID_NOTE,
NOTE_PREVIEW_CHAR_LIMIT,
STRING_ELLIPSES,
SAVE_TIMEOUT_NO_DEBOUNCE,
SAVE_TIMEOUT_DEBOUNCE,
} from './Types'
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
@@ -27,6 +20,15 @@ export type EditorValues = {
text: string
}
const StringEllipses = '...'
const NotePreviewCharLimit = 160
const SaveTimeoutDebounc = {
Desktop: 350,
ImmediateChange: 100,
NativeMobileWeb: 700,
}
export class NoteViewController implements ItemViewControllerInterface {
public item!: SNNote
public dealloced = false
@@ -34,7 +36,6 @@ export class NoteViewController implements ItemViewControllerInterface {
private removeStreamObserver?: () => void
public isTemplateNote = false
private saveTimeout?: ReturnType<typeof setTimeout>
private defaultTitle: string | undefined
private defaultTagUuid: UuidString | undefined
private defaultTag?: SNTag
public runtimeId = `${Math.random()}`
@@ -42,14 +43,13 @@ export class NoteViewController implements ItemViewControllerInterface {
constructor(
private application: SNApplication,
item?: SNNote,
templateNoteOptions?: TemplateNoteViewControllerOptions,
public templateNoteOptions?: TemplateNoteViewControllerOptions,
) {
if (item) {
this.item = item
}
if (templateNoteOptions) {
this.defaultTitle = templateNoteOptions.title
this.defaultTagUuid = templateNoteOptions.tag
}
@@ -80,13 +80,19 @@ export class NoteViewController implements ItemViewControllerInterface {
? this.application.componentManager.componentWithIdentifier(editorIdentifier)
: undefined
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(ContentType.Note, {
text: '',
title: this.defaultTitle || '',
noteType: defaultEditor?.noteType || NoteType.Plain,
editorIdentifier: editorIdentifier,
references: [],
})
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(
ContentType.Note,
{
text: '',
title: this.templateNoteOptions?.title || '',
noteType: defaultEditor?.noteType || NoteType.Plain,
editorIdentifier: editorIdentifier,
references: [],
},
{
created_at: this.templateNoteOptions?.createdAt || new Date(),
},
)
this.isTemplateNote = true
this.item = note
@@ -109,6 +115,10 @@ export class NoteViewController implements ItemViewControllerInterface {
}
private streamItems() {
if (this.dealloced) {
return
}
this.removeStreamObserver = this.application.streamItems<SNNote>(
ContentType.Note,
({ changed, inserted, source }) => {
@@ -174,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
}
@@ -183,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
}
@@ -199,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
@@ -217,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',
}
@@ -3,4 +3,8 @@ import { UuidString } from '@Lib/Types/UuidString'
export type TemplateNoteViewControllerOptions = {
title?: string
tag?: UuidString
createdAt?: Date
autofocusBehavior?: TemplateNoteViewAutofocusBehavior
}
export type TemplateNoteViewAutofocusBehavior = 'title' | 'editor'
-8
View File
@@ -1,8 +0,0 @@
export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN =
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'
export const STRING_INVALID_NOTE =
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."
export const STRING_ELLIPSES = '...'
export const NOTE_PREVIEW_CHAR_LIMIT = 80
export const SAVE_TIMEOUT_DEBOUNCE = 350
export const SAVE_TIMEOUT_NO_DEBOUNCE = 100
+1
View File
@@ -3,3 +3,4 @@ export * from './NoteViewController'
export * from './FileViewController'
export * from './ItemGroupController'
export * from './ReactNativeToWebEvent'
export * from './TemplateNoteViewControllerOptions'
@@ -805,12 +805,13 @@ export class ItemManager
public createTemplateItem<
C extends Models.ItemContent = Models.ItemContent,
I extends Models.DecryptedItemInterface<C> = Models.DecryptedItemInterface<C>,
>(contentType: ContentType, content?: C): I {
>(contentType: ContentType, content?: C, override?: Partial<Models.DecryptedPayload<C>>): I {
const payload = new Models.DecryptedPayload<C>({
uuid: UuidGenerator.GenerateUuid(),
content_type: contentType,
content: Models.FillItemContent<C>(content || {}),
...Models.PayloadTimestampDefaults(),
...override,
})
const item = Models.CreateDecryptedItemFromPayload<C, I>(payload)
return item
@@ -29,6 +29,7 @@ import {
CreateEncryptedBackupFileContextPayload,
DecryptedItemInterface,
DecryptedItemMutator,
DecryptedPayload,
DecryptedPayloadInterface,
EncryptedItemInterface,
FileItem,
@@ -221,8 +222,8 @@ export class MutatorService extends AbstractService implements MutatorClientInte
public createTemplateItem<
C extends ItemContent = ItemContent,
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
>(contentType: ContentType, content?: C): I {
return this.itemManager.createTemplateItem(contentType, content)
>(contentType: ContentType, content?: C, override?: Partial<DecryptedPayload<C>>): I {
return this.itemManager.createTemplateItem(contentType, content, override)
}
public async setItemNeedsSync(
+4
View File
@@ -4,4 +4,8 @@ export const InfoStrings = {
'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.',
BackupFileMoreRecentThanAccount:
"This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.",
SavingWhileDocumentHidden:
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.',
InvalidNote:
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.",
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.144.1",
"version": "2.145.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.6.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
### Features
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
# [1.5.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-05)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/styles",
"version": "1.5.0",
"version": "1.6.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
+1
View File
@@ -15,6 +15,7 @@
--sn-stylekit-danger-color: #cc2128;
--sn-stylekit-danger-contrast-color: #ffffff;
--sn-stylekit-danger-light-color: #f9e4e5;
--sn-stylekit-shadow-color: #c8c8c8;
--sn-stylekit-background-color: #ffffff;
+14
View File
@@ -3,6 +3,20 @@
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.10.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.10.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.10.3",
"version": "1.10.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20 -5
View File
@@ -59,11 +59,13 @@ export class ThemeManager extends AbstractService {
break
}
case ApplicationEvent.Launched: {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.colorSchemeEventHandler)
if (!this.application.isNativeMobileWeb()) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.colorSchemeEventHandler)
}
break
}
case ApplicationEvent.PreferencesChanged: {
this.handlePreferencesChangeEvent()
void this.handlePreferencesChangeEvent()
break
}
}
@@ -88,7 +90,16 @@ export class ThemeManager extends AbstractService {
})
}
private handlePreferencesChangeEvent(): void {
async handleMobileColorSchemeChangeEvent() {
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
if (useDeviceThemeSettings) {
const prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark'
this.setThemeAsPerColorScheme(prefersDarkColorScheme)
}
}
private async handlePreferencesChangeEvent() {
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
@@ -98,9 +109,13 @@ export class ThemeManager extends AbstractService {
}
if (hasPreferenceChanged && useDeviceThemeSettings) {
const prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
this.setThemeAsPerColorScheme(prefersDarkColorScheme.matches)
if (this.application.isNativeMobileWeb()) {
prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark'
}
this.setThemeAsPerColorScheme(prefersDarkColorScheme)
}
}
+60
View File
@@ -3,6 +3,66 @@
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
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
# [3.89.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Features
* auto-activate biometrics prompt when mobile regains focus ([#1891](https://github.com/standardnotes/app/issues/1891)) ([3bd1ad3](https://github.com/standardnotes/app/commit/3bd1ad38ad05ba6ea0e99911693ed7b25e2f8380))
## [3.88.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Bug Fixes
* **web:** issue with hide pinned and hide protected not having effect ([55296dc](https://github.com/standardnotes/app/commit/55296dcc77aeabc53673407fb2fe9be76bc41e52))
# [3.88.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Features
* add "Upload and link file" button to linking panel ([f48c3f9](https://github.com/standardnotes/app/commit/f48c3f958252283d000300fc38932cc5efca243c))
## [3.87.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.87.2",
"version": "3.90.6",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -35,7 +35,7 @@ import {
ThemeManager,
WebAlertService,
} from '@standardnotes/ui-services'
import { MobileWebReceiver } from '../NativeMobileWeb/MobileWebReceiver'
import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver'
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback'
@@ -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) {
@@ -330,4 +346,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter
openPurchaseFlow(): void {
this.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
}
addNativeMobileEventListener = (listener: NativeMobileEventListener) => {
if (!this.mobileWebReceiver) {
return
}
return this.mobileWebReceiver.addReactListener(listener)
}
}
@@ -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 (
@@ -1,5 +1,10 @@
import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs'
import { FunctionComponent, useEffect, useRef } from 'react'
import {
ChallengePrompt,
ChallengeValidation,
ProtectionSessionDurations,
ReactNativeToWebEvent,
} from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useRef } from 'react'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import { ChallengeModalValues } from './ChallengeModalValues'
@@ -27,6 +32,46 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
const inputRef = useRef<HTMLInputElement>(null)
const biometricsButtonRef = useRef<HTMLButtonElement>(null)
const activatePrompt = useCallback(async () => {
if (prompt.validation === ChallengeValidation.Biometric) {
if (application.isNativeMobileWeb()) {
const appState = await application.mobileDevice().getAppState()
if (appState !== 'active') {
return
}
}
const hasUserAlreadyInteracted = typeof values[prompt.id].value === 'boolean'
if (hasUserAlreadyInteracted) {
return
}
biometricsButtonRef.current?.click()
} else {
inputRef.current?.focus()
}
}, [application, prompt.id, prompt.validation, values])
useEffect(() => {
if (!application.isNativeMobileWeb()) {
return
}
const disposeListener = application.addNativeMobileEventListener((event: ReactNativeToWebEvent) => {
if (event === ReactNativeToWebEvent.GainingFocus) {
void activatePrompt()
}
})
return () => {
if (disposeListener) {
disposeListener()
}
}
}, [activatePrompt, application])
useEffect(() => {
const isNotFirstPrompt = index !== 0
@@ -34,12 +79,8 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
return
}
if (prompt.validation === ChallengeValidation.Biometric) {
biometricsButtonRef.current?.click()
} else {
inputRef.current?.focus()
}
}, [index, prompt.validation])
void activatePrompt()
}, [activatePrompt, index])
useEffect(() => {
if (isInvalid) {
@@ -0,0 +1,70 @@
import { useState, useEffect, FunctionComponent, useMemo } from 'react'
import { CalendarActivity } from './CalendarActivity'
import CalendarDay from './CalendarDay'
import { CalendarDays, CalendarDaysLeap, CalendarDaysOfTheWeek } from './Constants'
import { createActivityRecord, dateToDateOnlyString, isLeapYear, getStartDayOfMonth } from './CalendarUtilts'
import { areDatesInSameDay } from '@/Utils/DateUtils'
type Props = {
activities: CalendarActivity[]
onDateSelect: (date: Date) => void
startDate: Date
className?: string
selectedDay?: Date
}
const Calendar: FunctionComponent<Props> = ({ activities, startDate, onDateSelect, selectedDay, className }) => {
const activityMap = useMemo(() => createActivityRecord(activities), [activities])
const [date, setDate] = useState(startDate || new Date())
const [month, setMonth] = useState(date.getMonth())
const [year, setYear] = useState(date.getFullYear())
const [startDay, setStartDay] = useState(getStartDayOfMonth(date))
useEffect(() => {
setDate(startDate)
setMonth(startDate.getMonth())
setYear(startDate.getFullYear())
setStartDay(getStartDayOfMonth(startDate))
}, [startDate])
const today = new Date()
const dayBundle = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
const days = Array(dayBundle[month] + (startDay - 1)).fill(null)
return (
<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) => (
<div className={'flex h-8 w-[14.2%] items-center justify-center'} key={d}>
{d}
</div>
))}
</div>
<div className="flex w-full flex-wrap">
{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>
)
}
export default Calendar
@@ -0,0 +1,7 @@
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
export type CalendarActivityType = 'created' | 'edited'
export type CalendarActivity = {
date: Date
item: ListableContentItem
}
@@ -0,0 +1,40 @@
import { FunctionComponent } from 'react'
type Props = {
day: number
isToday: boolean
onClick: () => void
type: 'empty' | 'item' | 'template'
isLastMonth: boolean
}
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={`${classNames} flex h-full w-full cursor-pointer items-center justify-center rounded`}
key={day}
onClick={onClick}
>
{day}
</div>
</div>
)
}
export default CalendarDay
@@ -0,0 +1,4 @@
export type CalendarMonth = {
/** Any date in the month */
date: Date
}
@@ -0,0 +1,27 @@
import { addMonths, areDatesInSameMonth } from '@/Utils/DateUtils'
import { CalendarMonth } from './CalendarMonth'
import { insertMonthsWithTarget } from './CalendarUtilts'
describe('calendar utils', () => {
it('insertMonthsWithTarget in past', () => {
const today = new Date()
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
const targetMonth = addMonths(today, -12)
const result = insertMonthsWithTarget(months, targetMonth)
expect(result).toHaveLength(14)
expect(areDatesInSameMonth(result[0].date, targetMonth))
})
it('insertMonthsWithTarget in future', () => {
const today = new Date()
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
const targetMonth = addMonths(today, 12)
const result = insertMonthsWithTarget(months, targetMonth)
expect(result).toHaveLength(14)
expect(areDatesInSameMonth(result[result.length - 1].date, targetMonth))
})
})
@@ -0,0 +1,89 @@
import { addMonths, numberOfMonthsBetweenDates } from '@/Utils/DateUtils'
import { CalendarActivity } from './CalendarActivity'
import { CalendarMonth } from './CalendarMonth'
type DateOnlyString = string
export function dateToDateOnlyString(date: Date): DateOnlyString {
return date.toLocaleDateString()
}
type ActivityRecord = Record<DateOnlyString, CalendarActivity[]>
export function createActivityRecord(activities: CalendarActivity[]): ActivityRecord {
const map: Record<string, CalendarActivity[]> = {}
for (const activity of activities) {
const string = dateToDateOnlyString(activity.date)
if (!map[string]) {
map[string] = []
}
map[string].push(activity)
}
return map
}
export function isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}
export function getStartDayOfMonth(date: Date) {
const startDate = new Date(date.getFullYear(), date.getMonth(), 1).getDay()
return startDate === 0 ? 7 : startDate
}
/**
* Modifies months array in-place.
*/
export function insertMonths(months: CalendarMonth[], location: 'front' | 'end', number: number): CalendarMonth[] {
const earlierMonth = months[0].date
const laterMonth = months[months.length - 1].date
for (let i = 1; i <= number; i++) {
if (location === 'front') {
const minusNFromFirstMonth = addMonths(earlierMonth, -i)
months.unshift({
date: minusNFromFirstMonth,
})
} else {
const plusNFromLastMonth = addMonths(laterMonth, i)
months.push({
date: plusNFromLastMonth,
})
}
}
return months
}
/**
* Modifies months array in-place.
*/
export function insertMonthsWithTarget(months: CalendarMonth[], targetMonth: Date): CalendarMonth[] {
const firstMonth = months[0].date
const lastMonth = months[months.length - 1].date
const isBeforeFirstMonth = targetMonth.getTime() < firstMonth.getTime()
const numMonthsToAdd = Math.abs(
isBeforeFirstMonth
? numberOfMonthsBetweenDates(firstMonth, targetMonth)
: numberOfMonthsBetweenDates(lastMonth, targetMonth),
)
if (isBeforeFirstMonth) {
return insertMonths(months, 'front', numMonthsToAdd)
} else {
return insertMonths(months, 'end', numMonthsToAdd)
}
}
/**
* Modifies months array in-place.
*/
export function removeMonths(months: CalendarMonth[], location: 'front' | 'end', number: number): void {
if (location === 'front') {
months.splice(0, number)
} else {
months.splice(months.length - number - 1, number)
}
}
@@ -0,0 +1,17 @@
export const CalendarDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
export const CalendarDaysLeap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
export const CalendarDaysOfTheWeek = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
export const CalendarMonths = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
@@ -0,0 +1,224 @@
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
selectedDay?: Date
selectedDayType?: 'item' | 'template'
className?: string
}
export type InfiniteCalendarInterface = {
goToMonth: (month: Date) => void
}
const PageSize = 2
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
({ activities, onDateSelect, selectedDay, className }: Props, ref) => {
const [expanded, setExpanded] = useState(true)
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 }]
insertMonths(base, 'front', 2)
insertMonths(base, 'end', 2)
return base
})
const hasMonthInList = useCallback(
(date: Date): boolean => {
for (const month of months) {
if (areDatesInSameMonth(month.date, date)) {
return true
}
}
return false
},
[months],
)
const insertMonthInList = useCallback(
(date: Date): void => {
setMonths(insertMonthsWithTarget(months, date))
},
[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(
(centerOnDate: Date) => {
const base = [{ date: centerOnDate }]
insertMonths(base, 'front', 1)
insertMonths(base, 'end', 1)
setMonths(base)
},
[setMonths],
)
useEffect(() => {
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)
}
}, [selectedDay, goToMonth, previousSelectedDay])
useEffect(() => {
if (!restoreScrollAfterExpand) {
return
}
if (expanded) {
log(
LoggingDomain.DailyNotes,
'[Calendar] Scrolling to month',
activeDate,
'from restoreScrollAfterExpand useEffect',
)
scrollToMonth(activeDate)
setRestoreScrollAfterExpand(false)
}
}, [expanded, scrollToMonth, activeDate, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
const paginateLeft = useCallback(() => {
log(LoggingDomain.DailyNotes, '[Calendar] paginateLeft')
setMonths((prevMonths) => {
const copy = prevMonths.slice()
insertMonths(copy, 'front', PageSize)
return copy
})
}, [setMonths])
const paginateRight = useCallback(() => {
log(LoggingDomain.DailyNotes, '[Calendar] paginateRight')
setMonths((prevMonths) => {
const copy = prevMonths.slice()
insertMonths(copy, 'end', PageSize)
return copy
})
}, [setMonths])
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],
)
const toggleVisibility = useCallback(() => {
setRestoreScrollAfterExpand(true)
setExpanded(!expanded)
}, [expanded, setExpanded, setRestoreScrollAfterExpand])
const elementIdForMonth = (date: Date): string => {
return `month-${date.getFullYear()}-${date.getMonth()}`
}
const handleDaySelection = useCallback(
(date: Date) => {
resetNumberOfCalendarsToBase(date)
onDateSelect(date)
},
[onDateSelect, resetNumberOfCalendarsToBase],
)
return (
<div className={'border-b border-solid border-border'}>
<div
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[activeDate.getMonth()]} {activeDate.getFullYear()}
</div>
{expanded && (
<InfinteScroller
paginateFront={paginateLeft}
paginateEnd={paginateRight}
direction={'horizontal'}
onElementVisibility={onElementVisibility}
ref={scrollerRef}
className={className}
isMobileScreen={isMobileScreen()}
>
{months.map((month) => {
const id = elementIdForMonth(month.date)
return (
<div id={id} key={id}>
<Calendar
key={id}
className="mx-2"
activities={activities}
onDateSelect={handleDaySelection}
startDate={month.date}
selectedDay={selectedDay}
/>
</div>
)
})}
</InfinteScroller>
)}
</div>
)
},
)
export default InfiniteCalendar
@@ -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
}
@@ -12,6 +12,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import { NotesController } from '@/Controllers/NotesController'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { SNTag } from '@standardnotes/snjs'
type Props = {
application: WebApplication
@@ -39,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) => {
@@ -64,6 +66,35 @@ const ContentList: FunctionComponent<Props> = ({
[selectNextItem, selectPreviousItem],
)
const selectItem = useCallback(
(item: ListableContentItem, userTriggered?: boolean) => {
return selectionController.selectItem(item.uuid, userTriggered)
},
[selectionController],
)
const getTagsForItem = useCallback(
(item: ListableContentItem) => {
if (hideTags) {
return []
}
if (!selectedTag) {
return []
}
const tags = application.getItemTags(item)
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
if (isNavigatingOnlyTag) {
return []
}
return tags
},
[hideTags, selectedTag, application],
)
return (
<div
className={classNames(
@@ -88,8 +119,8 @@ const ContentList: FunctionComponent<Props> = ({
hideIcon={hideEditorIcon}
sortBy={sortBy}
filesController={filesController}
selectionController={selectionController}
navigationController={navigationController}
onSelect={selectItem}
tags={getTagsForItem(item)}
notesController={notesController}
/>
))}
@@ -1,38 +1,18 @@
import { ContentType, SNTag } from '@standardnotes/snjs'
import { FunctionComponent } from 'react'
import { ContentType } from '@standardnotes/snjs'
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) => {
const getTags = () => {
if (props.hideTags) {
return []
}
const selectedTag = props.navigationController.selected
if (!selectedTag) {
return []
}
const tags = props.application.getItemTags(props.item)
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
if (isNavigatingOnlyTag) {
return []
}
return tags
}
switch (props.item.content_type) {
case ContentType.Note:
return <NoteListItem tags={getTags()} {...props} />
return <NoteListItem {...props} />
case ContentType.File:
return <FileListItem tags={getTags()} {...props} />
return <FileListItem {...props} />
default:
return null
}
}
export default ContentListItem
export default React.memo(ContentListItem, (a, b) => !doListItemPropsMeritRerender(a, b))
@@ -26,6 +26,8 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import { LinkingController } from '@/Controllers/LinkingController'
import DailyContentList from './Daily/DailyContentList'
import { ListableContentItem } from './Types/ListableContentItem'
type Props = {
accountMenuController: AccountMenuController
@@ -106,12 +108,13 @@ const ContentListView: FunctionComponent<Props> = ({
panelTitle,
panelWidth,
renderedItems,
items,
searchBarElement,
} = itemListController
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
const { selected: selectedTag } = navigationController
const { selected: selectedTag, selectedAsTag } = navigationController
const icon = selectedTag?.iconString
@@ -227,11 +230,24 @@ const ContentListView: FunctionComponent<Props> = ({
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
const dailyMode = selectedAsTag?.isDailyEntry
const handleDailyListSelection = useCallback(
async (item: ListableContentItem, userTriggered: boolean) => {
await selectionController.selectItemWithScrollHandling(item, {
userTriggered: true,
scrollIntoView: userTriggered === false,
animated: false,
})
},
[selectionController],
)
return (
<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'
@@ -240,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
@@ -279,20 +295,35 @@ const ContentListView: FunctionComponent<Props> = ({
/>
</div>
</div>
{completedFullSync && !renderedItems.length ? <p className="empty-items-list opacity-50">No items.</p> : null}
{!completedFullSync && !renderedItems.length ? <p className="empty-items-list opacity-50">Loading...</p> : null}
{renderedItems.length ? (
<ContentList
items={renderedItems}
{selectedAsTag && dailyMode && (
<DailyContentList
items={items}
selectedTag={selectedAsTag}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}
itemListController={itemListController}
navigationController={navigationController}
notesController={notesController}
selectionController={selectionController}
onSelect={handleDailyListSelection}
/>
)}
{!dailyMode && renderedItems.length ? (
<>
{completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">No items.</p>
) : null}
{!completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">Loading...</p>
) : null}
<ContentList
items={renderedItems}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}
itemListController={itemListController}
navigationController={navigationController}
notesController={notesController}
selectionController={selectionController}
/>
</>
) : null}
<div className="absolute bottom-0 h-safe-bottom w-full" />
</ResponsivePaneContent>
@@ -0,0 +1,9 @@
import { createDailyItemsWithToday } from './CreateDailySections'
describe('create daily sections', () => {
it('createDailyItemsWithToday', () => {
const result = createDailyItemsWithToday(10)
expect(result).toHaveLength(10)
})
})
@@ -0,0 +1,67 @@
import { ListableContentItem } from '../Types/ListableContentItem'
import { addDays, getWeekdayName } from '@/Utils/DateUtils'
import { DailyItemsDay } from './DailyItemsDaySection'
import { dateToDailyDayIdentifier } from './Utils'
export const createItemsByDateMapping = (items: ListableContentItem[]) => {
const mapping: Record<string, ListableContentItem[]> = {}
for (const item of items) {
const key = dateToDailyDayIdentifier(item.created_at)
if (!mapping[key]) {
mapping[key] = []
}
mapping[key].push(item)
}
return mapping
}
export const templateEntryForDate = (date: Date): DailyItemsDay => {
const entryDateString = dateToDailyDayIdentifier(date)
return {
dateKey: entryDateString,
date: date,
day: date.getDate(),
isToday: entryDateString === dateToDailyDayIdentifier(new Date()),
id: entryDateString,
weekday: getWeekdayName(date, 'short'),
}
}
export function createDailyItemsWithToday(count: number): DailyItemsDay[] {
const items = [templateEntryForDate(new Date())]
insertBlanks(items, 'front', count / 2 - 1)
return insertBlanks(items, 'end', count / 2)
}
/**
* Modifies entries array in-place.
*/
export function insertBlanks(entries: DailyItemsDay[], location: 'front' | 'end', number: number): DailyItemsDay[] {
let laterDay, earlierDay
if (entries.length > 0) {
laterDay = entries[0].date
earlierDay = entries[entries.length - 1].date
} else {
const today = new Date()
laterDay = today
earlierDay = today
}
for (let i = 1; i <= number; i++) {
if (location === 'front') {
const plusNFromFirstDay = addDays(laterDay, i)
const futureEntry = templateEntryForDate(plusNFromFirstDay)
entries.unshift(futureEntry)
} else {
const minusNFromLastDay = addDays(earlierDay, -i)
const pastEntry = templateEntryForDate(minusNFromLastDay)
entries.push(pastEntry)
}
}
return entries
}
@@ -0,0 +1,229 @@
import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { ListableContentItem } from '../Types/ListableContentItem'
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
import { DailyItemsDay } from './DailyItemsDaySection'
import { DailyItemCell } from './DailyItemCell'
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
items: ListableContentItem[]
onSelect: (item: ListableContentItem, userTriggered: boolean) => Promise<void>
selectedTag: SNTag
selectedUuids: SelectedItemsController['selectedUuids']
}
const PageSize = 10
const DailyContentList: FunctionComponent<Props> = ({
items,
itemListController,
onSelect,
selectedUuids,
selectedTag,
}) => {
const { toggleAppPane } = useResponsiveAppPane()
const [needsSelectionReload, setNeedsSelectionReload] = useState(false)
const [todayItem, setTodayItem] = useState<DailyItemsDay>()
const [selectedDay, setSelectedDay] = useState<Date>()
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)
})
const { hideTags, hideDate, hideNotePreview } = itemListController.webDisplayOptions
const itemsByDateMapping = useMemo(() => {
return createItemsByDateMapping(items)
}, [items])
useEffect(() => {
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 {
date: item.created_at,
item: item,
}
})
}, [items])
const paginateBottom = useCallback(() => {
log(LoggingDomain.DailyNotes, '[ContentList] paginateBottom')
setDailyItems((prev) => {
const copy = prev.slice()
insertBlanks(copy, 'end', PageSize)
return copy
})
}, [setDailyItems])
const paginateTop = useCallback(() => {
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 && 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, lastVisibleDay],
)
const onClickItem = useCallback(
async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => {
await onSelect(item, userTriggered)
toggleAppPane(AppPaneId.Editor)
setSelectedDay(day.date)
},
[onSelect, toggleAppPane],
)
const onClickTemplate = useCallback(
(date: Date) => {
setSelectedDay(date)
itemListController.createNewNote(undefined, date, 'editor')
toggleAppPane(AppPaneId.Editor)
},
[setSelectedDay, itemListController, toggleAppPane],
)
const dailyItemForDate = useCallback(
(date: Date): DailyItemsDay | undefined => {
return dailyItems.find((candidate) => dateToDailyDayIdentifier(date) === candidate.dateKey)
},
[dailyItems],
)
useEffect(() => {
if (needsSelectionReload) {
setNeedsSelectionReload(false)
if (!todayItem) {
return
}
const items = itemsByDateMapping[todayItem.id]
if (items?.length > 0) {
const item = items[0]
const dailyItem = dailyItemForDate(item.created_at)
if (dailyItem) {
void onClickItem(dailyItem, items[0], false)
}
} else {
onClickTemplate(todayItem.date)
const itemElement = document.getElementById(todayItem.id)
itemElement?.scrollIntoView({ behavior: 'auto' })
}
}
}, [needsSelectionReload, onClickItem, onClickTemplate, todayItem, dailyItemForDate, itemsByDateMapping])
useEffect(() => {
setNeedsSelectionReload(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTag.uuid])
const onCalendarSelect = useCallback(
(date: Date) => {
const dailyItem = dailyItemForDate(date)
if (dailyItem) {
const items = itemsByDateMapping[dailyItem.id]
if (items?.length > 0) {
void onClickItem(dailyItem, items[0], false)
} else if (dailyItem) {
void onClickTemplate(dailyItem.date)
}
} else {
void onClickTemplate(date)
}
},
[onClickItem, onClickTemplate, dailyItemForDate, itemsByDateMapping],
)
const hasItemsOnSelectedDay = selectedDay && itemsByDateMapping[dateToDailyDayIdentifier(selectedDay)]?.length > 0
return (
<>
<InfiniteCalendar
activities={calendarActivities}
activityType={'created'}
onDateSelect={onCalendarSelect}
selectedDay={selectedDay}
selectedDayType={!selectedDay ? undefined : hasItemsOnSelectedDay ? 'item' : 'template'}
ref={calendarRef}
className={'flex-column flex'}
/>
<InfinteScroller
paginateFront={paginateTop}
paginateEnd={paginateBottom}
direction="vertical"
onElementVisibility={onListItemDidBecomeVisible}
className={'flex-1'}
ref={scrollerRef}
isMobileScreen={isMobileScreen()}
>
{dailyItems.map((dailyItem) => {
const items = itemsByDateMapping[dailyItem.id]
if (items) {
return items.map((item) => (
<DailyItemCell
selected={selectedUuids.has(item.uuid)}
section={dailyItem}
key={item.uuid}
id={dailyItem.id}
item={item}
hideDate={hideDate}
hidePreview={hideNotePreview}
hideTags={hideTags}
onClick={() => onClickItem(dailyItem, item, true)}
/>
))
} else {
return (
<DailyItemCell
selected={selectedDay && dailyItem.id === dateToDailyDayIdentifier(selectedDay)}
section={dailyItem}
id={dailyItem.id}
key={dailyItem.dateKey}
onClick={() => onClickTemplate(dailyItem.date)}
/>
)
}
})}
</InfinteScroller>
</>
)
}
export default DailyContentList
@@ -0,0 +1,85 @@
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { SNTag } from '@standardnotes/snjs'
import { ComponentPropsWithoutRef, forwardRef, FunctionComponent, Ref } from 'react'
import ListItemFlagIcons from '../ListItemFlagIcons'
import ListItemMetadata from '../ListItemMetadata'
import ListItemTags from '../ListItemTags'
import ListItemNotePreviewText from '../ListItemNotePreviewText'
import { ListableContentItem } from '../Types/ListableContentItem'
import { DailyItemsDay } from './DailyItemsDaySection'
import { ListItemTitle } from '../ListItemTitle'
import { EmptyPlaceholderBars } from './EmptyPlaceholderBars'
type DaySquareProps = {
day: number
hasActivity: boolean
weekday: string
}
const DaySquare: FunctionComponent<DaySquareProps> = ({ day, hasActivity, weekday }) => {
return (
<div className="mr-5">
<div
className={`${
hasActivity ? 'bg-danger text-danger-contrast' : 'bg-neutral text-neutral-contrast'
} h-15 w-18 rounded p-2 text-center`}
>
<div className="text-sm font-bold">{weekday}</div>
<div className="text-4xl font-bold">{day}</div>
</div>
</div>
)
}
interface Props extends ComponentPropsWithoutRef<'div'> {
item?: ListableContentItem
onClick: () => void
section: DailyItemsDay
selected?: boolean
tags?: SNTag[]
hideDate?: boolean
hideTags?: boolean
hidePreview?: boolean
}
export const DailyItemCell = forwardRef(
(
{ item, tags = [], section, onClick, selected, hideDate = false, hidePreview = false, hideTags = false }: Props,
ref: Ref<HTMLDivElement>,
) => {
return (
<div
ref={ref}
onClick={onClick}
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
selected && 'selected border-l-2 border-solid border-danger'
}`}
id={section.id}
>
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-4">
<div className="flex items-start overflow-hidden text-base">
<DaySquare weekday={section.weekday} hasActivity={item != undefined} day={section.day} />
<div className="w-full leading-[1.3]">
{item && (
<>
<ListItemTitle item={item} />
<ListItemNotePreviewText hidePreview={hidePreview} item={item} lineLimit={5} />
<ListItemMetadata item={item} hideDate={hideDate} sortBy={'created_at'} />
<ListItemTags hideTags={hideTags} tags={tags} />
</>
)}
{!item && (
<div className="w-full">
<div className="break-word mr-2 font-semibold">{formatDateAndTimeForNote(section.date, false)}</div>
<EmptyPlaceholderBars rows={4} />
</div>
)}
</div>
</div>
</div>
{item && <ListItemFlagIcons item={item} hasFiles={false} />}
</div>
)
},
)
@@ -0,0 +1,8 @@
export type DailyItemsDay = {
dateKey: string
day: number
weekday: string
date: Date
isToday: boolean
id: string
}
@@ -0,0 +1,61 @@
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
type Props = {
rows: number
}
function randomNumber(min: number, max: number) {
const r = Math.random() * (max - min) + min
return Math.floor(r)
}
export const EmptyPlaceholderBars: FunctionComponent<Props> = ({ rows }) => {
const [barWidths, setBarWidths] = useState<number[]>([])
const [animationInterval, setAnimationInterval] = useState<ReturnType<typeof setTimeout> | null>(null)
const reloadWidths = useCallback(() => {
const widths = []
for (let i = 0; i < rows; i++) {
const width = randomNumber(70, 100)
widths.push(width)
}
setBarWidths(widths)
}, [setBarWidths, rows])
useEffect(() => {
reloadWidths()
}, [rows, reloadWidths])
const onHoverEnter = useCallback(() => {
reloadWidths()
const interval = setInterval(() => {
reloadWidths()
}, 750)
setAnimationInterval(interval)
}, [setAnimationInterval, reloadWidths])
const onHoverExit = useCallback(() => {
if (animationInterval) {
clearInterval(animationInterval)
setAnimationInterval(null)
}
}, [animationInterval, setAnimationInterval])
return (
<div onMouseEnter={onHoverEnter} onMouseLeave={onHoverExit} className="w-full">
{barWidths.map((width, index) => {
return (
<div
style={{ width: `${width}%` }}
key={index}
className={
'transition-slowest ease my-4 h-7 bg-passive-4-opacity-variant pb-3 transition-width duration-1000'
}
></div>
)
})}
</div>
)
}
@@ -0,0 +1,3 @@
export function dateToDailyDayIdentifier(date: Date): string {
return date.toLocaleDateString()
}
@@ -14,11 +14,11 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
application,
filesController,
selectionController,
hideDate,
hideIcon,
hideTags,
item,
onSelect,
selected,
sortBy,
tags,
@@ -44,7 +44,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
let shouldOpenContextMenu = selected
if (!selected) {
const { didSelect } = await selectionController.selectItem(item.uuid)
const { didSelect } = await onSelect(item)
if (didSelect) {
shouldOpenContextMenu = true
}
@@ -54,15 +54,15 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
openFileContextMenu(posX, posY)
}
},
[selected, selectionController, item.uuid, openFileContextMenu],
[selected, onSelect, item, openFileContextMenu],
)
const onClick = useCallback(async () => {
const { didSelect } = await selectionController.selectItem(item.uuid, true)
const { didSelect } = await onSelect(item, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item.uuid, selectionController, toggleAppPane])
}, [item, onSelect, toggleAppPane])
const IconComponent = () =>
getFileIconComponent(
@@ -5,7 +5,7 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '@/Components/Popover/Popover'
import DisplayOptionsMenu from './DisplayOptionsMenu'
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
import { IconType } from '@standardnotes/snjs'
import { IconType, isTag } from '@standardnotes/snjs'
import RoundIconButton from '@/Components/Button/RoundIconButton'
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
@@ -32,6 +32,7 @@ const ContentListHeader = ({
}: Props) => {
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
const isDailyEntry = isTag(selectedTag) && selectedTag.isDailyEntry
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
@@ -83,7 +84,13 @@ const ContentListHeader = ({
</Popover>
</div>
<button
className="absolute bottom-6 right-6 z-editor-title-bar ml-3 flex h-13 w-13 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125 md:static md:h-8 md:w-8"
className={classNames(
'absolute bottom-6 right-6 z-editor-title-bar ml-3 flex h-13 w-13 cursor-pointer items-center',
`justify-center rounded-full border border-solid border-transparent ${
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
}`,
'hover:brightness-125 md:static md:h-8 md:w-8',
)}
title={addButtonLabel}
aria-label={addButtonLabel}
onClick={addNewItem}
@@ -23,6 +23,9 @@ import { PreferenceMode } from './PreferenceMode'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
import Button from '@/Components/Button/Button'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { isDev } from '@/Utils'
const DailyEntryModeEnabled = isDev
const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
closeDisplayOptionsMenu,
@@ -36,6 +39,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
const [preferences, setPreferences] = useState<TagPreferences>({})
const hasSubscription = application.hasValidSubscription()
const controlsDisabled = currentMode === 'tag' && !hasSubscription
const isDailyEntry = selectedTag.preferences?.entryMode === 'daily'
const reloadPreferences = useCallback(() => {
const globalValues: TagPreferences = {
@@ -162,6 +166,10 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
}, [preferences, changePreferences])
const toggleEntryMode = useCallback(() => {
void changePreferences({ entryMode: isDailyEntry ? 'normal' : 'daily' })
}, [isDailyEntry, changePreferences])
const TabButton: FunctionComponent<{
label: string
mode: PreferenceMode
@@ -200,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
@@ -230,7 +241,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div>
<MenuItem
disabled={controlsDisabled}
disabled={controlsDisabled || isDailyEntry}
className="py-2"
type={MenuItemType.RadioButton}
onClick={toggleSortByDateModified}
@@ -248,7 +259,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
</div>
</MenuItem>
<MenuItem
disabled={controlsDisabled}
disabled={controlsDisabled || isDailyEntry}
className="py-2"
type={MenuItemType.RadioButton}
onClick={toggleSortByCreationDate}
@@ -266,7 +277,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
</div>
</MenuItem>
<MenuItem
disabled={controlsDisabled}
disabled={controlsDisabled || isDailyEntry}
className="py-2"
type={MenuItemType.RadioButton}
onClick={toggleSortByTitle}
@@ -362,6 +373,24 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
Show trashed
</MenuItem>
{currentMode === 'tag' && DailyEntryModeEnabled && (
<>
<MenuItemSeparator />
<MenuItem
disabled={controlsDisabled}
type={MenuItemType.SwitchButton}
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
checked={isDailyEntry}
onChange={toggleEntryMode}
>
<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>
</>
)}
<MenuItemSeparator />
<NewNotePreferences
@@ -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>
)
},
)
@@ -0,0 +1,36 @@
import { sanitizeHtmlString } from '@standardnotes/snjs'
import { FunctionComponent } from 'react'
import { ListableContentItem } from './Types/ListableContentItem'
type Props = {
item: ListableContentItem
hidePreview: boolean
lineLimit?: number
}
const ListItemNotePreviewText: FunctionComponent<Props> = ({ item, hidePreview, lineLimit = 1 }) => {
if (item.hidePreview || item.protected || hidePreview) {
return null
}
return (
<div className={`overflow-hidden overflow-ellipsis text-sm ${item.archived ? 'opacity-60' : ''}`}>
{item.preview_html && (
<div
className="my-1"
dangerouslySetInnerHTML={{
__html: sanitizeHtmlString(item.preview_html),
}}
></div>
)}
{!item.preview_html && item.preview_plain && (
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.preview_plain}</div>
)}
{!item.preview_html && !item.preview_plain && item.text && (
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.text}</div>
)}
</div>
)
}
export default ListItemNotePreviewText
@@ -0,0 +1,10 @@
import { FunctionComponent } from 'react'
import { ListableContentItem } from './Types/ListableContentItem'
export const ListItemTitle: FunctionComponent<{ item: ListableContentItem }> = ({ item }) => {
return (
<div className="flex items-start justify-between overflow-hidden text-base font-semibold leading-[1.3]">
<div className={`break-word mr-2 ${item.archived ? 'opacity-60' : ''}`}>{item.title}</div>
</div>
)
}
@@ -1,5 +1,5 @@
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import { isFile, sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
import { isFile, SNNote } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef } from 'react'
import Icon from '@/Components/Icon/Icon'
@@ -11,11 +11,14 @@ import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
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,
notesController,
selectionController,
onSelect,
hideDate,
hideIcon,
hideTags,
@@ -48,7 +51,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
let shouldOpenContextMenu = selected
if (!selected) {
const { didSelect } = await selectionController.selectItem(item.uuid)
const { didSelect } = await onSelect(item)
if (didSelect) {
shouldOpenContextMenu = true
}
@@ -60,19 +63,21 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
}
const onClick = useCallback(async () => {
const { didSelect } = await selectionController.selectItem(item.uuid, true)
const { didSelect } = await onSelect(item, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item.uuid, selectionController, toggleAppPane])
}, [item, onSelect, toggleAppPane])
useContextMenuEvent(listItemRef, openContextMenu)
log(LoggingDomain.ItemsList, 'Rendering note list item', item.title)
return (
<div
ref={listItemRef}
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
selected && 'selected border-l-2 border-solid border-info'
selected && `selected border-l-2 border-solid border-accessory-tint-${tint}`
}`}
id={item.uuid}
onClick={onClick}
@@ -85,27 +90,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
<div className="pr-4" />
)}
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-0">
<div className="flex items-start justify-between overflow-hidden text-base font-semibold leading-[1.3]">
<div className="break-word mr-2">{item.title}</div>
</div>
{!hidePreview && !item.hidePreview && !item.protected && (
<div className="overflow-hidden overflow-ellipsis text-sm">
{item.preview_html && (
<div
className="my-1"
dangerouslySetInnerHTML={{
__html: sanitizeHtmlString(item.preview_html),
}}
></div>
)}
{!item.preview_html && item.preview_plain && (
<div className="leading-1.3 line-clamp-1 mt-1 overflow-hidden">{item.preview_plain}</div>
)}
{!item.preview_html && !item.preview_plain && item.text && (
<div className="leading-1.3 line-clamp-1 mt-1 overflow-hidden">{item.text}</div>
)}
</div>
)}
<ListItemTitle item={item} />
<ListItemNotePreviewText item={item} hidePreview={hidePreview} />
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
<ListItemTags hideTags={hideTags} tags={tags} />
<ListItemConflictIndicator item={item} />
@@ -1,17 +1,14 @@
import { WebApplication } from '@/Application/Application'
import { FilesController } from '@/Controllers/FilesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { SortableItem } from '@standardnotes/snjs'
import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs'
import { ListableContentItem } from './ListableContentItem'
export type AbstractListItemProps = {
application: WebApplication
filesController: FilesController
selectionController: SelectedItemsController
navigationController: NavigationController
notesController: NotesController
onSelect: (item: ListableContentItem, userTriggered?: boolean) => Promise<{ didSelect: boolean }>
hideDate: boolean
hideIcon: boolean
hideTags: boolean
@@ -19,4 +16,91 @@ export type AbstractListItemProps = {
item: ListableContentItem
selected: boolean
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
}
@@ -89,8 +89,8 @@ const FileMenuOptions: FunctionComponent<Props> = ({
void filesController.setProtectionForFiles(hasProtectedFiles, selectionController.selectedFiles)
}}
>
<Icon type="password" className="mr-2 text-neutral" />
Password protection
<Icon type="lock" className="mr-2 text-neutral" />
Password protect
</MenuItem>
<HorizontalSeparator classes="my-1" />
<MenuItem
@@ -94,6 +94,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
<LinkedItemsButton
filesController={viewControllerManager.filesController}
linkingController={viewControllerManager.linkingController}
featuresController={viewControllerManager.featuresController}
/>
<RoundIconButton
label="File information panel"
@@ -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
@@ -49,8 +49,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
}}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 text-neutral" />
Password protection
<Icon type="lock" className="mr-2 text-neutral" />
Password protect
</span>
<Switch className="pointer-events-none px-0" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} checked={isFileProtected} />
</button>

Some files were not shown because too many files have changed in this diff Show More