mirror of
https://github.com/standardnotes/app
synced 2026-09-20 03:13:51 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8181d9a4f | ||
|
|
026fbc1524 | ||
|
|
5058aaee33 | ||
|
|
f67b486a0d | ||
|
|
a9efce21f0 | ||
|
|
5bb923cff3 | ||
|
|
5a6f4fb35c | ||
|
|
9ac5bd0c27 | ||
|
|
02ad46147d | ||
|
|
ba231c633f | ||
|
|
040bce7584 | ||
|
|
69c3f2be83 | ||
|
|
064e054587 | ||
|
|
3bd1ad38ad | ||
|
|
d817a2115e | ||
|
|
55296dcc77 | ||
|
|
350fbbd74d | ||
|
|
f48c3f9582 |
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+3
-3
@@ -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
@@ -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>
|
||||
-3
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+3
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
packages/components/dist/assets/org.standardnotes.markdown-visual-editor/build/static/js/main.js.map
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
-6
@@ -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.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/components-meta",
|
||||
"version": "2.7.21",
|
||||
"version": "2.7.22",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
"main": "dist",
|
||||
|
||||
+4
@@ -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
-1
@@ -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,
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.23.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,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.23.250",
|
||||
"version": "3.23.256",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.18.7",
|
||||
"version": "1.18.8",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.24.9",
|
||||
"version": "1.24.10",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.11.9",
|
||||
"version": "1.11.10",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.45.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.44.3",
|
||||
"version": "3.45.3",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
TransferPayload,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
||||
import { Alert, AppState, AppStateStatus, 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'
|
||||
@@ -220,10 +220,12 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
|
||||
hideMobileInterfaceFromScreenshots(): void {
|
||||
hide()
|
||||
this.setAndroidScreenshotPrivacy(true)
|
||||
}
|
||||
|
||||
stopHidingMobileInterfaceFromScreenshots(): void {
|
||||
show()
|
||||
this.setAndroidScreenshotPrivacy(false)
|
||||
}
|
||||
|
||||
async getAllRawStorageKeyValues() {
|
||||
@@ -610,4 +612,8 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
isUrlComponentUrl(url: string): boolean {
|
||||
return Array.from(this.componentUrls.values()).includes(url)
|
||||
}
|
||||
|
||||
async getAppState(): Promise<AppStateStatus> {
|
||||
return AppState.currentState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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'
|
||||
}
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.3.179",
|
||||
"version": "1.3.185",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.37.1",
|
||||
"version": "1.39.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -20,4 +20,5 @@ 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'>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,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 +41,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 +78,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 +113,10 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
}
|
||||
|
||||
private streamItems() {
|
||||
if (this.dealloced) {
|
||||
return
|
||||
}
|
||||
|
||||
this.removeStreamObserver = this.application.streamItems<SNNote>(
|
||||
ContentType.Note,
|
||||
({ changed, inserted, source }) => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -3,6 +3,6 @@ export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN =
|
||||
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 NOTE_PREVIEW_CHAR_LIMIT = 160
|
||||
export const SAVE_TIMEOUT_DEBOUNCE = 350
|
||||
export const SAVE_TIMEOUT_NO_DEBOUNCE = 100
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.144.1",
|
||||
"version": "2.145.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.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,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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.10.3",
|
||||
"version": "1.10.5",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.87.2",
|
||||
"version": "3.90.2",
|
||||
"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()
|
||||
@@ -330,4 +342,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+27
@@ -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',
|
||||
]
|
||||
+224
@@ -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
|
||||
@@ -64,6 +65,33 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
[selectNextItem, selectPreviousItem],
|
||||
)
|
||||
|
||||
const selectItem = useCallback(
|
||||
(item: ListableContentItem, userTriggered?: boolean) => {
|
||||
return selectionController.selectItem(item.uuid, userTriggered)
|
||||
},
|
||||
[selectionController],
|
||||
)
|
||||
|
||||
const getTagsForItem = (item: ListableContentItem) => {
|
||||
if (hideTags) {
|
||||
return []
|
||||
}
|
||||
|
||||
const selectedTag = navigationController.selected
|
||||
if (!selectedTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tags = application.getItemTags(item)
|
||||
|
||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||
if (isNavigatingOnlyTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -88,8 +116,8 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
hideIcon={hideEditorIcon}
|
||||
sortBy={sortBy}
|
||||
filesController={filesController}
|
||||
selectionController={selectionController}
|
||||
navigationController={navigationController}
|
||||
onSelect={selectItem}
|
||||
tags={getTagsForItem(item)}
|
||||
notesController={notesController}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,35 +1,15 @@
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
import { ContentType } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import FileListItem from './FileListItem'
|
||||
import NoteListItem from './NoteListItem'
|
||||
import { AbstractListItemProps } 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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+9
@@ -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
|
||||
}
|
||||
+61
@@ -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(
|
||||
|
||||
+9
-2
@@ -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}
|
||||
|
||||
+33
-4
@@ -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
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
paginateFront: () => void
|
||||
paginateEnd: () => void
|
||||
direction: 'horizontal' | 'vertical'
|
||||
onElementVisibility?: (elementId: string) => void
|
||||
className?: string
|
||||
isMobileScreen?: boolean
|
||||
}
|
||||
|
||||
export type InfiniteScrollerInterface = {
|
||||
scrollToElementId: (id: string) => void
|
||||
}
|
||||
|
||||
export const InfinteScroller = forwardRef<InfiniteScrollerInterface, Props>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
paginateFront,
|
||||
paginateEnd,
|
||||
direction = 'vertical',
|
||||
onElementVisibility,
|
||||
className,
|
||||
isMobileScreen,
|
||||
}: Props,
|
||||
ref,
|
||||
) => {
|
||||
const frontSentinel = useRef<HTMLDivElement | null>(null)
|
||||
const endSentinel = useRef<HTMLDivElement | null>(null)
|
||||
const [ignoreFirstFrontSentinelEvent, setIgnoreFirstFrontSentinelEvent] = useState(true)
|
||||
const [needsMobilePaginationFix, setNeedsMobilePaginationFix] = useState(false)
|
||||
|
||||
const scrollArea = useRef<HTMLDivElement | null>(null)
|
||||
const [scrollSize, setScrollSize] = useState(0)
|
||||
const [didPaginateFront, setDidPaginateFront] = useState(false)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToElementId(id: string) {
|
||||
scrollToElementId(id)
|
||||
},
|
||||
}))
|
||||
|
||||
const visibilityObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||
if (visibleEntry) {
|
||||
onElementVisibility?.(visibleEntry.target.id)
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
),
|
||||
[onElementVisibility],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const childElements = Array.from(scrollArea.current!.children)
|
||||
for (const child of childElements) {
|
||||
visibilityObserver.observe(child)
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const child of childElements) {
|
||||
visibilityObserver.unobserve(child)
|
||||
}
|
||||
}
|
||||
}, [visibilityObserver, children])
|
||||
|
||||
const scrollToElementId = useCallback((id: string) => {
|
||||
const element = document.getElementById(id)
|
||||
if (!element) {
|
||||
log(LoggingDomain.DailyNotes, 'Element not found', id)
|
||||
return
|
||||
}
|
||||
|
||||
log(LoggingDomain.DailyNotes, 'Scrolling to element', id)
|
||||
element.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
})
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!scrollArea.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (didPaginateFront) {
|
||||
if (direction === 'vertical') {
|
||||
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize
|
||||
if (isMobileScreen) {
|
||||
setNeedsMobilePaginationFix(true)
|
||||
}
|
||||
} else {
|
||||
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollSize
|
||||
}
|
||||
setDidPaginateFront(false)
|
||||
}
|
||||
}, [didPaginateFront, scrollSize, direction, isMobileScreen])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
/**
|
||||
* iOS Safari has an issue rendering paginated items from the top where the new
|
||||
* scrolled to area is white until the user interacts with scroll again. The fix
|
||||
* we apply is to re-set scrollTop to its same value to trigger a refresh.
|
||||
* https://stackoverflow.com/questions/9807620
|
||||
*/
|
||||
if (needsMobilePaginationFix) {
|
||||
setTimeout(() => {
|
||||
if (!scrollArea.current) {
|
||||
return
|
||||
}
|
||||
|
||||
log(LoggingDomain.DailyNotes, '[InfiniteScroller] Applying mobile pagination fix')
|
||||
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize
|
||||
setNeedsMobilePaginationFix(false)
|
||||
}, 50)
|
||||
}
|
||||
}, [needsMobilePaginationFix, scrollSize])
|
||||
|
||||
const _paginateFront = useCallback(() => {
|
||||
if (direction === 'vertical') {
|
||||
setScrollSize(scrollArea!.current!.scrollHeight)
|
||||
} else {
|
||||
setScrollSize(scrollArea!.current!.scrollWidth)
|
||||
}
|
||||
setDidPaginateFront(true)
|
||||
paginateFront()
|
||||
}, [paginateFront, direction])
|
||||
|
||||
const _paginateEnd = useCallback(() => {
|
||||
paginateEnd()
|
||||
}, [paginateEnd])
|
||||
|
||||
const frontObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
if (ignoreFirstFrontSentinelEvent) {
|
||||
log(LoggingDomain.DailyNotes, '[InfiniteScroller] Ignoring first front sentinel event')
|
||||
setIgnoreFirstFrontSentinelEvent(false)
|
||||
return
|
||||
}
|
||||
_paginateFront()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[_paginateFront, ignoreFirstFrontSentinelEvent],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (frontSentinel.current) {
|
||||
frontObserver.observe(frontSentinel.current)
|
||||
}
|
||||
}, [frontObserver, frontSentinel])
|
||||
|
||||
const endObserver = useMemo(
|
||||
() =>
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
_paginateEnd()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
),
|
||||
[_paginateEnd],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (endSentinel.current) {
|
||||
endObserver.observe(endSentinel.current)
|
||||
}
|
||||
}, [endObserver, endSentinel])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollArea}
|
||||
className={className}
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
flexDirection: direction === 'vertical' ? 'column' : 'row',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 1, height: 1, backgroundColor: 'transparent' }} ref={frontSentinel}></div>
|
||||
{children}
|
||||
<div style={{ width: 1, height: 1, backgroundColor: 'transparent' }} ref={endSentinel}></div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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,13 @@ 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'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
notesController,
|
||||
selectionController,
|
||||
onSelect,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
hideTags,
|
||||
@@ -48,7 +50,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,11 +62,11 @@ 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)
|
||||
|
||||
@@ -72,7 +74,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
<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 +87,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} />
|
||||
|
||||
+3
-5
@@ -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 } 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,5 @@ export type AbstractListItemProps = {
|
||||
item: ListableContentItem
|
||||
selected: boolean
|
||||
sortBy: keyof SortableItem | undefined
|
||||
tags: SNTag[]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -11,9 +12,10 @@ type Props = {
|
||||
linkingController: LinkingController
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
filesController: FilesController
|
||||
featuresController: FeaturesController
|
||||
}
|
||||
|
||||
const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing }: Props) => {
|
||||
const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing, featuresController }: Props) => {
|
||||
const { isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -35,6 +37,7 @@ const LinkedItemsButton = ({ linkingController, filesController, onClickPreproce
|
||||
isOpen={isLinkingPanelOpen}
|
||||
linkingController={linkingController}
|
||||
filesController={filesController}
|
||||
featuresController={featuresController}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
@@ -6,7 +7,7 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import ClearInputButton from '../ClearInputButton/ClearInputButton'
|
||||
import Icon from '../Icon/Icon'
|
||||
@@ -158,10 +159,12 @@ const LinkedItemsSectionItem = ({
|
||||
const LinkedItemsPanel = ({
|
||||
linkingController,
|
||||
filesController,
|
||||
featuresController,
|
||||
isOpen,
|
||||
}: {
|
||||
linkingController: LinkingController
|
||||
filesController: FilesController
|
||||
featuresController: FeaturesController
|
||||
isOpen: boolean
|
||||
}) => {
|
||||
const {
|
||||
@@ -181,6 +184,9 @@ const LinkedItemsPanel = ({
|
||||
isEntitledToNoteLinking,
|
||||
} = linkingController
|
||||
|
||||
const { hasFiles } = featuresController
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const isSearching = !!searchQuery.length
|
||||
@@ -192,6 +198,30 @@ const LinkedItemsPanel = ({
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleFileInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const files = event.currentTarget.files
|
||||
|
||||
if (!files) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
filesController.uploadNewFile(files[i]).then((uploadedFiles) => {
|
||||
if (uploadedFiles) {
|
||||
void linkItemToSelectedItem(uploadedFiles[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const selectAndUploadFiles = () => {
|
||||
if (!fileInputRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
fileInputRef.current.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
@@ -284,10 +314,24 @@ const LinkedItemsPanel = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!linkedFiles.length && (
|
||||
{(!!linkedFiles.length || hasFiles) && (
|
||||
<div>
|
||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Files</div>
|
||||
<div className="my-1">
|
||||
<input
|
||||
type="file"
|
||||
className="absolute top-0 left-0 -z-50 h-px w-px opacity-0"
|
||||
multiple
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center gap-3 bg-transparent px-3 py-2 text-left text-sm text-text hover:bg-info-backdrop hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={selectAndUploadFiles}
|
||||
>
|
||||
<Icon type="add" />
|
||||
Upload and link file(s)
|
||||
</button>
|
||||
{linkedFiles.map((link) => (
|
||||
<LinkedItemsSectionItem
|
||||
key={link.id}
|
||||
|
||||
@@ -122,6 +122,9 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
this.controller = props.controller
|
||||
|
||||
this.onEditorComponentLoad = () => {
|
||||
if (!this.controller || this.controller.dealloced) {
|
||||
return
|
||||
}
|
||||
this.application.getDesktopService()?.redoSearch()
|
||||
}
|
||||
|
||||
@@ -233,7 +236,11 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
if (this.controller.isTemplateNote) {
|
||||
setTimeout(() => {
|
||||
this.focusTitle()
|
||||
if (this.controller.templateNoteOptions?.autofocusBehavior === 'editor') {
|
||||
this.focusEditor()
|
||||
} else {
|
||||
this.focusTitle()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -920,6 +927,10 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.controller.dealloced) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.state.showProtectedWarning || !this.application.isAuthorizedToRenderItem(this.note)) {
|
||||
return (
|
||||
<ProtectedItemOverlay
|
||||
@@ -999,6 +1010,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
linkingController={this.viewControllerManager.linkingController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
/>
|
||||
<ChangeEditorButton
|
||||
application={this.application}
|
||||
|
||||
@@ -318,7 +318,7 @@ const NotesOptions = ({
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className={iconClass} />
|
||||
<Icon type="lock" className={iconClass} />
|
||||
Password protect
|
||||
</span>
|
||||
<Switch className="px-0" checked={protect} />
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
WebAppEvent,
|
||||
NewNoteTitleFormat,
|
||||
useBoolean,
|
||||
TemplateNoteViewAutofocusBehavior,
|
||||
isTag,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/Application'
|
||||
@@ -165,18 +167,20 @@ export class ItemListController
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => {
|
||||
if (
|
||||
this.notes.length === 0 &&
|
||||
this.navigationController.selected instanceof SmartView &&
|
||||
this.navigationController.selected.uuid === SystemViewId.AllNotes &&
|
||||
this.noteFilterText === '' &&
|
||||
!this.getActiveItemController()
|
||||
) {
|
||||
this.createPlaceholderNote()?.catch(console.error)
|
||||
}
|
||||
})
|
||||
this.setCompletedFullSync(true)
|
||||
if (!this.completedFullSync) {
|
||||
void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => {
|
||||
if (
|
||||
this.notes.length === 0 &&
|
||||
this.navigationController.selected instanceof SmartView &&
|
||||
this.navigationController.selected.uuid === SystemViewId.AllNotes &&
|
||||
this.noteFilterText === '' &&
|
||||
!this.getActiveItemController()
|
||||
) {
|
||||
this.createPlaceholderNote()?.catch(console.error)
|
||||
}
|
||||
})
|
||||
this.setCompletedFullSync(true)
|
||||
}
|
||||
}, ApplicationEvent.CompletedFullSync),
|
||||
)
|
||||
|
||||
@@ -211,6 +215,7 @@ export class ItemListController
|
||||
notesToDisplay: observable,
|
||||
panelTitle: observable,
|
||||
panelWidth: observable,
|
||||
items: observable,
|
||||
renderedItems: observable,
|
||||
showDisplayOptionsMenu: observable,
|
||||
|
||||
@@ -253,7 +258,7 @@ export class ItemListController
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === CrossControllerEvent.TagChanged) {
|
||||
const payload = event.payload as { userTriggered: boolean }
|
||||
this.handleTagChange(payload.userTriggered)
|
||||
await this.handleTagChange(payload.userTriggered)
|
||||
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
|
||||
this.handleEditorChange().catch(console.error)
|
||||
}
|
||||
@@ -375,12 +380,6 @@ export class ItemListController
|
||||
)
|
||||
}
|
||||
|
||||
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
||||
return (
|
||||
itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange || !this.selectionController.selectedUuids.size
|
||||
)
|
||||
}
|
||||
|
||||
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
||||
const isSearching = this.noteFilterText.length > 0
|
||||
const itemExistsInUpdatedResults = this.items.find((item) => item.uuid === activeItem?.uuid)
|
||||
@@ -404,6 +403,18 @@ export class ItemListController
|
||||
return activeItem && !this.selectionController.isItemSelected(activeItem)
|
||||
}
|
||||
|
||||
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
||||
const selectedTag = this.navigationController.selected
|
||||
const isDailyEntry = selectedTag && isTag(selectedTag) && selectedTag.isDailyEntry
|
||||
if (isDailyEntry) {
|
||||
return false
|
||||
}
|
||||
|
||||
const userChangedTag = itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange
|
||||
const hasNoSelectedItem = !this.selectionController.selectedUuids.size
|
||||
return userChangedTag || hasNoSelectedItem
|
||||
}
|
||||
|
||||
private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
|
||||
const activeController = this.getActiveItemController()
|
||||
|
||||
@@ -491,14 +502,14 @@ export class ItemListController
|
||||
this.application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
)
|
||||
|
||||
newDisplayOptions.includePinned = useBoolean(
|
||||
!selectedTag?.preferences?.hidePinned,
|
||||
!this.application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
newDisplayOptions.includePinned = !useBoolean(
|
||||
selectedTag?.preferences?.hidePinned,
|
||||
this.application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
)
|
||||
|
||||
newDisplayOptions.includeProtected = useBoolean(
|
||||
!selectedTag?.preferences?.hideProtected,
|
||||
!this.application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
newDisplayOptions.includeProtected = !useBoolean(
|
||||
selectedTag?.preferences?.hideProtected,
|
||||
this.application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideNotePreview = useBoolean(
|
||||
@@ -561,48 +572,65 @@ export class ItemListController
|
||||
return { didReloadItems: true }
|
||||
}
|
||||
|
||||
async createNewNoteController(title?: string) {
|
||||
async createNewNoteController(
|
||||
title?: string,
|
||||
createdAt?: Date,
|
||||
autofocusBehavior?: TemplateNoteViewAutofocusBehavior,
|
||||
) {
|
||||
const selectedTag = this.navigationController.selected
|
||||
|
||||
const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined
|
||||
|
||||
await this.application.itemControllerGroup.createItemController({
|
||||
return this.application.itemControllerGroup.createItemController({
|
||||
title,
|
||||
tag: activeRegularTagUuid,
|
||||
createdAt,
|
||||
autofocusBehavior,
|
||||
})
|
||||
}
|
||||
|
||||
createNewNote = async () => {
|
||||
this.notesController.unselectNotes()
|
||||
|
||||
if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) {
|
||||
await this.navigationController.selectHomeNavigationView()
|
||||
titleForNewNote = (createdAt?: Date) => {
|
||||
if (this.isFiltering) {
|
||||
return this.noteFilterText
|
||||
}
|
||||
|
||||
const titleFormat =
|
||||
this.navigationController.selected?.preferences?.newNoteTitleFormat ||
|
||||
this.application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat])
|
||||
|
||||
let title = formatDateAndTimeForNote(new Date())
|
||||
|
||||
if (titleFormat === NewNoteTitleFormat.CurrentNoteCount) {
|
||||
title = `Note ${this.notes.length + 1}`
|
||||
} else if (titleFormat === NewNoteTitleFormat.CustomFormat) {
|
||||
return `Note ${this.notes.length + 1}`
|
||||
}
|
||||
|
||||
if (titleFormat === NewNoteTitleFormat.CustomFormat) {
|
||||
const customFormat =
|
||||
this.navigationController.selected?.preferences?.customNoteTitleFormat ||
|
||||
this.application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat])
|
||||
title = dayjs().format(customFormat)
|
||||
} else if (titleFormat === NewNoteTitleFormat.Empty) {
|
||||
title = ''
|
||||
|
||||
return dayjs(createdAt).format(customFormat)
|
||||
}
|
||||
|
||||
if (this.isFiltering) {
|
||||
title = this.noteFilterText
|
||||
if (titleFormat === NewNoteTitleFormat.Empty) {
|
||||
return ''
|
||||
}
|
||||
|
||||
await this.createNewNoteController(title)
|
||||
return formatDateAndTimeForNote(createdAt || new Date())
|
||||
}
|
||||
|
||||
createNewNote = async (title?: string, createdAt?: Date, autofocusBehavior?: TemplateNoteViewAutofocusBehavior) => {
|
||||
this.notesController.unselectNotes()
|
||||
|
||||
if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) {
|
||||
await this.navigationController.selectHomeNavigationView()
|
||||
}
|
||||
|
||||
const useTitle = title || this.titleForNewNote(createdAt)
|
||||
|
||||
const controller = await this.createNewNoteController(useTitle, createdAt, autofocusBehavior)
|
||||
|
||||
this.linkingController.reloadAllLinks()
|
||||
|
||||
this.selectionController.scrollToItem(controller.item)
|
||||
}
|
||||
|
||||
createPlaceholderNote = () => {
|
||||
@@ -721,7 +749,7 @@ export class ItemListController
|
||||
this.application.itemControllerGroup.closeItemController(controller)
|
||||
}
|
||||
|
||||
handleTagChange = (userTriggered: boolean) => {
|
||||
handleTagChange = async (userTriggered: boolean) => {
|
||||
const activeNoteController = this.getActiveItemController()
|
||||
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
|
||||
this.closeItemController(activeNoteController)
|
||||
@@ -739,7 +767,7 @@ export class ItemListController
|
||||
|
||||
this.reloadNotesDisplayOptions()
|
||||
|
||||
void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
|
||||
await this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
|
||||
}
|
||||
|
||||
onFilterEnter = () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
InternalEventBus,
|
||||
InternalEventPublishStrategy,
|
||||
VectorIconNameOrEmoji,
|
||||
isTag,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/Application'
|
||||
@@ -268,6 +269,13 @@ export class NavigationController
|
||||
return this.selected instanceof SmartView && this.selected.uuid === id
|
||||
}
|
||||
|
||||
public get selectedAsTag(): SNTag | undefined {
|
||||
if (!this.selected || !isTag(this.selected)) {
|
||||
return undefined
|
||||
}
|
||||
return this.selected
|
||||
}
|
||||
|
||||
setAddingSubtagTo(tag: SNTag | undefined): void {
|
||||
this.addingSubtagTo = tag
|
||||
}
|
||||
@@ -440,19 +448,21 @@ export class NavigationController
|
||||
|
||||
this.previouslySelected_ = this.selected_
|
||||
|
||||
this.setSelectedTagInstance(tag)
|
||||
await runInAction(async () => {
|
||||
this.setSelectedTagInstance(tag)
|
||||
|
||||
if (tag && this.application.items.isTemplateItem(tag)) {
|
||||
return
|
||||
}
|
||||
if (tag && this.application.items.isTemplateItem(tag)) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.eventBus.publishSync(
|
||||
{
|
||||
type: CrossControllerEvent.TagChanged,
|
||||
payload: { tag, previousTag: this.previouslySelected_, userTriggered: userTriggered },
|
||||
},
|
||||
InternalEventPublishStrategy.SEQUENCE,
|
||||
)
|
||||
await this.eventBus.publishSync(
|
||||
{
|
||||
type: CrossControllerEvent.TagChanged,
|
||||
payload: { tag, previousTag: this.previouslySelected_, userTriggered: userTriggered },
|
||||
},
|
||||
InternalEventPublishStrategy.SEQUENCE,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async selectHomeNavigationView(): Promise<void> {
|
||||
|
||||
@@ -287,18 +287,22 @@ export class SelectedItemsController
|
||||
item: {
|
||||
uuid: ListableContentItem['uuid']
|
||||
},
|
||||
{ userTriggered = false, scrollIntoView = true },
|
||||
{ userTriggered = false, scrollIntoView = true, animated = true },
|
||||
): Promise<void> => {
|
||||
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
|
||||
|
||||
if (didSelect && scrollIntoView) {
|
||||
const itemElement = document.getElementById(item.uuid)
|
||||
itemElement?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
})
|
||||
this.scrollToItem(item, animated)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToItem = (item: { uuid: ListableContentItem['uuid'] }, animated = true): void => {
|
||||
const itemElement = document.getElementById(item.uuid)
|
||||
itemElement?.scrollIntoView({
|
||||
behavior: animated ? 'smooth' : 'auto',
|
||||
})
|
||||
}
|
||||
|
||||
selectUuids = async (uuids: UuidString[], userTriggered = false) => {
|
||||
const itemsForUuids = this.application.items.findItems(uuids)
|
||||
if (itemsForUuids.length < 1) {
|
||||
|
||||
@@ -150,6 +150,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
||||
this.toastService = new ToastService()
|
||||
|
||||
this.applicationEventObserver = new ApplicationEventObserver(
|
||||
application,
|
||||
application.routeService,
|
||||
this.purchaseFlowController,
|
||||
this.accountMenuController,
|
||||
|
||||
@@ -26,8 +26,10 @@ import { SyncStatusController } from '@/Controllers/SyncStatusController'
|
||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||
|
||||
import { ApplicationEventObserver } from './ApplicationEventObserver'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
describe('ApplicationEventObserver', () => {
|
||||
let application: WebApplication
|
||||
let routeService: RouteServiceInterface
|
||||
let purchaseFlowController: PurchaseFlowController
|
||||
let accountMenuController: AccountMenuController
|
||||
@@ -40,6 +42,7 @@ describe('ApplicationEventObserver', () => {
|
||||
|
||||
const createObserver = () =>
|
||||
new ApplicationEventObserver(
|
||||
application,
|
||||
routeService,
|
||||
purchaseFlowController,
|
||||
accountMenuController,
|
||||
@@ -52,6 +55,8 @@ describe('ApplicationEventObserver', () => {
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
application = {} as jest.Mocked<WebApplication>
|
||||
|
||||
routeService = {} as jest.Mocked<RouteServiceInterface>
|
||||
routeService.getRoute = jest.fn().mockReturnValue({
|
||||
type: RouteType.None,
|
||||
@@ -223,4 +228,15 @@ describe('ApplicationEventObserver', () => {
|
||||
expect(syncStatusController.update).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Upon Database Loaded', () => {
|
||||
it('should handle mobile screenshot privacy setting', async () => {
|
||||
application.isNativeMobileWeb = jest.fn().mockReturnValue(true)
|
||||
application.handleInitialMobileScreenshotPrivacy = jest.fn()
|
||||
|
||||
await createObserver().handle(ApplicationEvent.LocalDataLoaded)
|
||||
|
||||
expect(application.handleInitialMobileScreenshotPrivacy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user