Compare commits

...
Author SHA1 Message Date
StandardNotes CI ba231c633f chore(release): publish
- @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-10-27 22:52:36 +00:00
StandardNotes CI 040bce7584 chore(release): components 2022-10-27 22:52:22 +00:00
Mo 69c3f2be83 feat: daily notes (dev only) (#1894) 2022-10-27 17:21:31 -05:00
StandardNotes CI 064e054587 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].0
2022-10-26 21:03:43 +00:00
Aman Harwara 3bd1ad38ad feat: auto-activate biometrics prompt when mobile regains focus (#1891) 2022-10-27 02:02:33 +05:30
StandardNotes CI d817a2115e chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-26 12:22:23 +00:00
Mo 55296dcc77 fix(web): issue with hide pinned and hide protected not having effect 2022-10-26 06:45:45 -05:00
StandardNotes CI 350fbbd74d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-25 19:36:58 +00:00
Aman Harwara f48c3f9582 feat: add "Upload and link file" button to linking panel 2022-10-26 00:35:50 +05:30
100 changed files with 1843 additions and 211 deletions
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/api
## [1.18.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.18.2",
"version": "1.18.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.7.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/components-meta
## [2.7.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
### Reverts
@@ -1,7 +1,7 @@
{
"files": {
"main.js": "./static/js/main.js",
"main.css": "./static/css/main.09cbcc53.css",
"main.css": "./static/css/main.245e3425.css",
"static/media/material-icons-two-tone.woff": "./static/media/material-icons-two-tone..woff",
"static/media/material-icons-two-tone.woff2": "./static/media/material-icons-two-tone..woff2",
"static/media/material-icons-round.woff": "./static/media/material-icons-round..woff",
@@ -74,10 +74,10 @@
"static/media/KaTeX_Size3-Regular.woff2": "./static/media/KaTeX_Size3-Regular..woff2",
"index.html": "./index.html",
"main.js.map": "./static/js/main.js.map",
"main.09cbcc53.css.map": "./static/css/main.09cbcc53.css.map"
"main.245e3425.css.map": "./static/css/main.245e3425.css.map"
},
"entrypoints": [
"static/js/main.js",
"static/css/main.09cbcc53.css"
"static/css/main.245e3425.css"
]
}
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A lightweight WYSIWYG markdown editor, derivated from Milkdown editor"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Markdown Visual</title><script defer="defer" src="./static/js/main.js"></script><link href="./static/css/main.09cbcc53.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A lightweight WYSIWYG markdown editor, derivated from Milkdown editor"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Markdown Visual</title><script defer="defer" src="./static/js/main.js"></script><link href="./static/css/main.245e3425.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -70,9 +70,9 @@
"binary": "b621bfd81829a205f2f02e85d446e277ad20e367b92676ec20b3ee59b1bfab3f"
},
"org.standardnotes.markdown-visual-editor": {
"version": "1.1.3",
"base64": "32f86de2c5419ff0da9df8047bc3aa6e5dd9bf14ef3b59551a16b24696ea7456",
"binary": "4e18ee37a534e2d6810a8b486f3f945507d1cf2590c2480e01f55fec9b85b718"
"version": "1.1.4",
"base64": "7d206437fca5c4f5b2a4a4ee5a9a29fdb3e2fbb1637ff1dbe962f00f48128993",
"binary": "6c396c45102a114b6d507ad24bdae38168baf780d77f66d0f7c12b25c89b7c57"
},
"org.standardnotes.simple-task-editor": {
"version": "1.4.4",
@@ -80,9 +80,9 @@
"binary": "9df983a144b9686f4f58b03bf78bf1f84f53aea60251621a75586722bf576475"
},
"org.standardnotes.token-vault": {
"version": "2.2.5",
"base64": "f85e355fb8152702a29e44c02a912e1ff5ce94a95c00935a1657e17ec489fb14",
"binary": "a9bcc6ac6c028e2a9b29fb5359414026402b8a6ba02df754d8946984b01722de"
"version": "2.2.6",
"base64": "52ab80192bdee708b4ded34590ed644d0bb41e6438d88e5a48dce0490b55dcac",
"binary": "27efe92ba9a345d94818fac8b46c3281ba0462fc5ed805df0e0052384e1e2826"
},
"org.standardnotes.standard-sheets": {
"version": "1.5.1",
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/components-meta",
"version": "2.7.21",
"version": "2.7.22",
"private": true,
"author": "Standard Notes.",
"main": "dist",
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/markdown-visual
## [1.1.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-05)
**Note:** Version bump only for package @standardnotes/markdown-visual
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/markdown-visual",
"version": "1.1.3",
"version": "1.1.4",
"author": "Johnny Almonte <[email protected]>",
"description": "A lightweight WYSIWYG markdown editor for Standard Notes, derived from Milkdown.",
"keywords": [
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.2.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/authenticator
## [2.2.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-05)
**Note:** Version bump only for package @standardnotes/authenticator
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/authenticator",
"version": "2.2.5",
"version": "2.2.6",
"main": "dist/dist.js",
"author": "Standard Notes",
"private": true,
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.23.254](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.253](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.252](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.251](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.250](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.23.250",
"version": "3.23.254",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/encryption
## [1.18.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.18.7",
"version": "1.18.8",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.24.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.24.9",
"version": "1.24.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/files
## [1.11.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.11.9",
"version": "1.11.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.45.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/mobile
# [3.45.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Features
* auto-activate biometrics prompt when mobile regains focus ([#1891](https://github.com/standardnotes/app/issues/1891)) ([3bd1ad3](https://github.com/standardnotes/app/commit/3bd1ad38ad05ba6ea0e99911693ed7b25e2f8380))
## [3.44.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.44.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.44.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.44.3",
"version": "3.45.1",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+5 -1
View File
@@ -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'
@@ -610,4 +610,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
}
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.32.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
### Features
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
## [1.31.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.31.1",
"version": "1.32.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -28,6 +28,10 @@ export class SNTag extends DecryptedItem<TagContent> implements TagContentSpecia
this.preferences = this.payload.content.preferences
}
get isDailyEntry(): boolean {
return this.preferences?.entryMode === 'daily'
}
get noteReferences(): ContentReference[] {
const references = this.payload.references
return references.filter((ref) => ref.content_type === ContentType.Note)
@@ -16,4 +16,5 @@ export interface TagPreferences {
newNoteTitleFormat?: NewNoteTitleFormat
customNoteTitleFormat?: string
editorIdentifier?: FeatureIdentifier | string
entryMode?: 'normal' | 'daily'
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.183](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.182](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.181](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.180](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.179](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.3.179",
"version": "1.3.183",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.39.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
### Features
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
# [1.38.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Features
* auto-activate biometrics prompt when mobile regains focus ([#1891](https://github.com/standardnotes/app/issues/1891)) ([3bd1ad3](https://github.com/standardnotes/app/commit/3bd1ad38ad05ba6ea0e99911693ed7b25e2f8380))
## [1.37.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.37.1",
"version": "1.39.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
/**
+10
View File
@@ -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(),
)
})
})
+18 -10
View File
@@ -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'
+1 -1
View File
@@ -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
+1
View File
@@ -3,3 +3,4 @@ export * from './NoteViewController'
export * from './FileViewController'
export * from './ItemGroupController'
export * from './ReactNativeToWebEvent'
export * from './TemplateNoteViewControllerOptions'
@@ -805,12 +805,13 @@ export class ItemManager
public createTemplateItem<
C extends Models.ItemContent = Models.ItemContent,
I extends Models.DecryptedItemInterface<C> = Models.DecryptedItemInterface<C>,
>(contentType: ContentType, content?: C): I {
>(contentType: ContentType, content?: C, override?: Partial<Models.DecryptedPayload<C>>): I {
const payload = new Models.DecryptedPayload<C>({
uuid: UuidGenerator.GenerateUuid(),
content_type: contentType,
content: Models.FillItemContent<C>(content || {}),
...Models.PayloadTimestampDefaults(),
...override,
})
const item = Models.CreateDecryptedItemFromPayload<C, I>(payload)
return item
@@ -29,6 +29,7 @@ import {
CreateEncryptedBackupFileContextPayload,
DecryptedItemInterface,
DecryptedItemMutator,
DecryptedPayload,
DecryptedPayloadInterface,
EncryptedItemInterface,
FileItem,
@@ -221,8 +222,8 @@ export class MutatorService extends AbstractService implements MutatorClientInte
public createTemplateItem<
C extends ItemContent = ItemContent,
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
>(contentType: ContentType, content?: C): I {
return this.itemManager.createTemplateItem(contentType, content)
>(contentType: ContentType, content?: C, override?: Partial<DecryptedPayload<C>>): I {
return this.itemManager.createTemplateItem(contentType, content, override)
}
public async setItemNeedsSync(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.144.1",
"version": "2.145.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.6.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
### Features
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
# [1.5.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-05)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/styles",
"version": "1.5.0",
"version": "1.6.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
+1
View File
@@ -15,6 +15,7 @@
--sn-stylekit-danger-color: #cc2128;
--sn-stylekit-danger-contrast-color: #ffffff;
--sn-stylekit-danger-light-color: #f9e4e5;
--sn-stylekit-shadow-color: #c8c8c8;
--sn-stylekit-background-color: #ffffff;
+8
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.10.3",
"version": "1.10.5",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+24
View File
@@ -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.90.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-27)
### Features
* daily notes (dev only) ([#1894](https://github.com/standardnotes/app/issues/1894)) ([69c3f2b](https://github.com/standardnotes/app/commit/69c3f2be837152a1f937559327021c2128234c69))
# [3.89.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Features
* auto-activate biometrics prompt when mobile regains focus ([#1891](https://github.com/standardnotes/app/issues/1891)) ([3bd1ad3](https://github.com/standardnotes/app/commit/3bd1ad38ad05ba6ea0e99911693ed7b25e2f8380))
## [3.88.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-26)
### Bug Fixes
* **web:** issue with hide pinned and hide protected not having effect ([55296dc](https://github.com/standardnotes/app/commit/55296dcc77aeabc53673407fb2fe9be76bc41e52))
# [3.88.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Features
* add "Upload and link file" button to linking panel ([f48c3f9](https://github.com/standardnotes/app/commit/f48c3f958252283d000300fc38932cc5efca243c))
## [3.87.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-25)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.87.2",
"version": "3.90.0",
"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'
@@ -330,4 +330,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,40 @@ 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
}
}
biometricsButtonRef.current?.click()
} else {
inputRef.current?.focus()
}
}, [application, prompt.validation])
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 +73,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,68 @@
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 { isDateInSameDay } 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 days = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
return (
<div className={`w-300 ${className} border-left border-right border border-neutral`}>
<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">
{Array(days[month] + (startDay - 1))
.fill(null)
.map((_, index) => {
const d = index - (startDay - 2)
const date = new Date(year, month, d)
const activities = activityMap[dateToDateOnlyString(date)] || []
return (
<CalendarDay
key={index}
day={d}
isToday={isDateInSameDay(date, today)}
activities={activities}
onClick={() => onDateSelect(date)}
hasPendingEntry={selectedDay && isDateInSameDay(selectedDay, date)}
/>
)
})}
</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,36 @@
import { FunctionComponent } from 'react'
import { CalendarActivity } from './CalendarActivity'
type Props = {
day: number
activities: CalendarActivity[]
isToday: boolean
onClick: () => void
hasPendingEntry?: boolean
}
const CalendarDay: FunctionComponent<Props> = ({ day, activities = [], hasPendingEntry, isToday, onClick }) => {
const hasActivity = day > 0 && activities.length > 0
const todayClassNames = 'bg-danger text-danger-contrast font-bold'
const hasActivityClassNames = 'bg-danger-light text-danger font-bold'
const defaultClassNames = 'bg-transparent hover:bg-contrast'
const hasPendingEntryNames = 'bg-contrast'
return (
<div className="h-7 w-[14.2%] p-0.5">
<div
className={`${
!hasActivity && !isToday ? defaultClassNames : ''
} flex h-full w-full cursor-pointer items-center justify-center rounded ${
isToday ? todayClassNames : hasActivity ? hasActivityClassNames : ''
} ${hasPendingEntry ? hasPendingEntryNames : ''}`}
key={day}
onClick={onClick}
>
{day > 0 ? day : ''}
</div>
</div>
)
}
export default CalendarDay
@@ -0,0 +1,4 @@
export type CalendarMonth = {
/** Any date in the month */
date: Date
}
@@ -0,0 +1,27 @@
import { addMonths, areDatesInSameMonth } from '@/Utils/DateUtils'
import { CalendarMonth } from './CalendarMonth'
import { insertMonthsWithTarget } from './CalendarUtilts'
describe('calendar utils', () => {
it('insertMonthsWithTarget in past', () => {
const today = new Date()
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
const targetMonth = addMonths(today, -12)
const result = insertMonthsWithTarget(months, targetMonth)
expect(result).toHaveLength(14)
expect(areDatesInSameMonth(result[0].date, targetMonth))
})
it('insertMonthsWithTarget in future', () => {
const today = new Date()
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
const targetMonth = addMonths(today, 12)
const result = insertMonthsWithTarget(months, targetMonth)
expect(result).toHaveLength(14)
expect(areDatesInSameMonth(result[result.length - 1].date, targetMonth))
})
})
@@ -0,0 +1,89 @@
import { addMonths, numberOfMonthsBetweenDates } from '@/Utils/DateUtils'
import { CalendarActivity } from './CalendarActivity'
import { CalendarMonth } from './CalendarMonth'
type DateOnlyString = string
export function dateToDateOnlyString(date: Date): DateOnlyString {
return date.toLocaleDateString()
}
type ActivityRecord = Record<DateOnlyString, CalendarActivity[]>
export function createActivityRecord(activities: CalendarActivity[]): ActivityRecord {
const map: Record<string, CalendarActivity[]> = {}
for (const activity of activities) {
const string = dateToDateOnlyString(activity.date)
if (!map[string]) {
map[string] = []
}
map[string].push(activity)
}
return map
}
export function isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}
export function getStartDayOfMonth(date: Date) {
const startDate = new Date(date.getFullYear(), date.getMonth(), 1).getDay()
return startDate === 0 ? 7 : startDate
}
/**
* Modifies months array in-place.
*/
export function insertMonths(months: CalendarMonth[], location: 'front' | 'end', number: number): CalendarMonth[] {
const earlierMonth = months[0].date
const laterMonth = months[months.length - 1].date
for (let i = 1; i <= number; i++) {
if (location === 'front') {
const minusNFromFirstMonth = addMonths(earlierMonth, -i)
months.unshift({
date: minusNFromFirstMonth,
})
} else {
const plusNFromLastMonth = addMonths(laterMonth, i)
months.push({
date: plusNFromLastMonth,
})
}
}
return months
}
/**
* Modifies months array in-place.
*/
export function insertMonthsWithTarget(months: CalendarMonth[], targetMonth: Date): CalendarMonth[] {
const firstMonth = months[0].date
const lastMonth = months[months.length - 1].date
const isBeforeFirstMonth = targetMonth.getTime() < firstMonth.getTime()
const numMonthsToAdd = Math.abs(
isBeforeFirstMonth
? numberOfMonthsBetweenDates(firstMonth, targetMonth)
: numberOfMonthsBetweenDates(lastMonth, targetMonth),
)
if (isBeforeFirstMonth) {
return insertMonths(months, 'front', numMonthsToAdd)
} else {
return insertMonths(months, 'end', numMonthsToAdd)
}
}
/**
* Modifies months array in-place.
*/
export function removeMonths(months: CalendarMonth[], location: 'front' | 'end', number: number): void {
if (location === 'front') {
months.splice(0, number)
} else {
months.splice(months.length - number - 1, number)
}
}
@@ -0,0 +1,17 @@
export const CalendarDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
export const CalendarDaysLeap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
export const CalendarDaysOfTheWeek = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
export const CalendarMonths = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
@@ -0,0 +1,305 @@
import { areDatesInSameMonth } from '@/Utils/DateUtils'
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
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'
type Props = {
activityType: CalendarActivityType
activities: CalendarActivity[]
onDateSelect: (date: Date) => void
selectedTemplateDay?: Date
selectedItemDay?: Date
}
export type InfiniteCalendarInterface = {
changeMonth: (month: Date) => void
}
const PageSize = 10
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
({ activities, onDateSelect, selectedTemplateDay, selectedItemDay }: Props, ref) => {
const [date, setDate] = useState(new Date())
const [month, setMonth] = useState(date.getMonth())
const [year, setYear] = useState(date.getFullYear())
const [expanded, setExpanded] = useState(true)
const [scrollWidth, setScrollWidth] = useState(0)
const today = new Date()
const [months, setMonths] = useState<CalendarMonth[]>(() => {
const base = [{ date: today }]
insertMonths(base, 'front', 2)
insertMonths(base, 'end', 2)
return base
})
useImperativeHandle(ref, () => ({
changeMonth(date: Date) {
setDate(date)
},
}))
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
const [didPaginateLeft, setDidPaginateLeft] = useState(false)
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
const scrollArea = useRef<HTMLDivElement>(null)
const hasMonthInList = useCallback(
(date: Date): boolean => {
for (const month of months) {
if (areDatesInSameMonth(month.date, date)) {
return true
}
}
return false
},
[months],
)
const insertMonthInList = useCallback(
(date: Date): void => {
setMonths(insertMonthsWithTarget(months, date))
},
[months, setMonths],
)
const resetNumberOfCalendarsToBase = useCallback(
(centerOnDate: Date) => {
const base = [{ date: centerOnDate }]
insertMonths(base, 'front', 1)
insertMonths(base, 'end', 1)
setMonths(base)
},
[setMonths],
)
useEffect(() => {
if (selectedTemplateDay) {
setDate(selectedTemplateDay)
}
}, [selectedTemplateDay])
useEffect(() => {
if (selectedItemDay) {
setDate(selectedItemDay)
}
}, [selectedItemDay])
const scrollToMonth = useCallback((date: Date) => {
const elementId = elementIdForMonth(date)
const element = document.getElementById(elementId)
if (!element) {
return
}
scrollArea.current!.scrollLeft = element.offsetLeft + -60
}, [])
useLayoutEffect(() => {
setMonth(date.getMonth())
setYear(date.getFullYear())
if (!hasMonthInList(date)) {
insertMonthInList(date)
}
scrollToMonth(date)
}, [date, hasMonthInList, insertMonthInList, scrollToMonth])
useEffect(() => {
if (!restoreScrollAfterExpand) {
return
}
if (scrollArea.current && expanded) {
scrollToMonth(date)
setRestoreScrollAfterExpand(false)
}
}, [expanded, scrollToMonth, date, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
useLayoutEffect(() => {
if (!scrollArea.current) {
return
}
if (didPaginateLeft) {
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollWidth
setDidPaginateLeft(false)
}
}, [months, didPaginateLeft, scrollWidth])
const paginateLeft = useCallback(() => {
if (scrollArea.current) {
setScrollWidth(scrollArea.current.scrollWidth)
}
const copy = months.slice()
insertMonths(copy, 'front', PageSize)
setDidPaginateLeft(true)
setMonths(copy)
}, [months, setMonths])
const paginateRight = useCallback(() => {
const copy = months.slice()
insertMonths(copy, 'end', PageSize)
setDidPaginateLeft(false)
setMonths(copy)
}, [months, setMonths])
const updateCurrentMonth = useCallback(
(index: number) => {
const newMonth = months[index]
setMonth(newMonth.date.getMonth())
setYear(newMonth.date.getFullYear())
},
[months, setMonth, setYear],
)
const visibilityObserver = useMemo(
() =>
new IntersectionObserver(
(entries) => {
const visibleEntry = entries.find((entry) => entry.isIntersecting)
if (visibleEntry) {
const id = visibleEntry.target.id
const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id)
updateCurrentMonth(index)
}
},
{ threshold: 0.9 },
),
[updateCurrentMonth, months],
)
const rightObserver = useMemo(
() =>
new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
paginateRight()
}
},
{ threshold: 0.5 },
),
[paginateRight],
)
const leftObserver = useMemo(
() =>
new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
paginateLeft()
}
},
{ threshold: 1.0 },
),
[paginateLeft],
)
useEffect(() => {
if (lastElement) {
rightObserver.observe(lastElement)
}
return () => {
if (lastElement) {
rightObserver.unobserve(lastElement)
}
}
}, [lastElement, rightObserver])
useEffect(() => {
if (firstElement) {
leftObserver.observe(firstElement)
}
return () => {
if (firstElement) {
leftObserver.unobserve(firstElement)
}
}
}, [firstElement, leftObserver])
const toggleVisibility = useCallback(() => {
setRestoreScrollAfterExpand(true)
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="w-full flex-shrink-0 border-b border-solid border-border">
<div
className="text-md flex cursor-pointer items-center justify-center py-2 px-4 text-center font-bold hover:bg-contrast"
onClick={toggleVisibility}
>
{CalendarMonths[month]} {year}
</div>
{expanded && (
<div
style={{ scrollBehavior: 'smooth' }}
ref={scrollArea}
id="calendar-scroller"
className="flex w-full overflow-x-scroll pb-2 md:max-w-full"
>
{months.map((month, index) => {
const isFirst = index === 0
const isLast = index === months.length - 1
const id = elementIdForMonth(month.date)
return (
<div
id={id}
key={id}
ref={(ref) => {
isFirst ? setFirstElement(ref) : isLast ? setLastElement(ref) : null
if (ref) {
visibilityObserver.observe(ref)
}
}}
>
<Calendar
key={id}
className="mx-2"
activities={activities}
onDateSelect={handleDaySelection}
startDate={month.date}
selectedDay={selectedTemplateDay}
/>
</div>
)
})}
</div>
)}
</div>
)
},
)
export default InfiniteCalendar
@@ -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,6 +230,19 @@ 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"
@@ -279,20 +295,35 @@ const ContentListView: FunctionComponent<Props> = ({
/>
</div>
</div>
{completedFullSync && !renderedItems.length ? <p className="empty-items-list opacity-50">No items.</p> : null}
{!completedFullSync && !renderedItems.length ? <p className="empty-items-list opacity-50">Loading...</p> : null}
{renderedItems.length ? (
<ContentList
items={renderedItems}
{selectedAsTag && dailyMode && (
<DailyContentList
items={items}
selectedTag={selectedAsTag}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}
itemListController={itemListController}
navigationController={navigationController}
notesController={notesController}
selectionController={selectionController}
onSelect={handleDailyListSelection}
/>
)}
{!dailyMode && renderedItems.length ? (
<>
{completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">No items.</p>
) : null}
{!completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">Loading...</p>
) : null}
<ContentList
items={renderedItems}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}
itemListController={itemListController}
navigationController={navigationController}
notesController={notesController}
selectionController={selectionController}
/>
</>
) : null}
<div className="absolute bottom-0 h-safe-bottom w-full" />
</ResponsivePaneContent>
@@ -0,0 +1,66 @@
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 today = templateEntryForDate(new Date())
return insertBlanks([today], 'end', count)
}
/**
* 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,309 @@
import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { ListableContentItem } from '../Types/ListableContentItem'
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
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'
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 [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
const [lastScrollHeight, setLastScrollHeight] = useState(0)
const [didPaginateTop, setDidPaginateTop] = useState(false)
const scrollArea = useRef<HTMLDivElement>(null)
const calendarRef = useRef<InfiniteCalendarInterface | null>(null)
const [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])
const calendarActivities: CalendarActivity[] = useMemo(() => {
return items.map((item) => {
return {
date: item.created_at,
item: item,
}
})
}, [items])
const paginateBottom = useCallback(() => {
const copy = dailyItems.slice()
insertBlanks(copy, 'end', PageSize)
setDailyItems(copy)
}, [dailyItems, setDailyItems])
const paginateTop = useCallback(() => {
if (scrollArea.current) {
setLastScrollHeight(scrollArea.current.scrollHeight)
}
const copy = dailyItems.slice()
insertBlanks(copy, 'front', PageSize)
setDidPaginateTop(true)
setDailyItems(copy)
}, [dailyItems, setDailyItems, setDidPaginateTop])
useLayoutEffect(() => {
if (!scrollArea.current) {
return
}
if (didPaginateTop) {
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - lastScrollHeight
setDidPaginateTop(false)
}
}, [didPaginateTop, lastScrollHeight])
const onListItemDidBecomeVisible = useCallback(
(elementId: string) => {
const dailyItem = dailyItems.find((candidate) => candidate.id === elementId)
if (dailyItem) {
calendarRef?.current?.changeMonth(dailyItem.date)
}
},
[dailyItems],
)
const visibilityObserver = useMemo(
() =>
new IntersectionObserver(
(entries) => {
const visibleEntry = entries.find((entry) => entry.isIntersecting)
if (visibleEntry) {
onListItemDidBecomeVisible(visibleEntry.target.id)
}
},
{ threshold: 0.9 },
),
[onListItemDidBecomeVisible],
)
const bottomObserver = useMemo(
() =>
new IntersectionObserver(
(entries) => {
const first = entries[0]
if (first.isIntersecting) {
paginateBottom()
}
},
{ threshold: 0.5 },
),
[paginateBottom],
)
const topObserver = useMemo(
() =>
new IntersectionObserver(
(entries) => {
const first = entries[0]
if (first.isIntersecting) {
paginateTop()
}
},
{ threshold: 0.5 },
),
[paginateTop],
)
useEffect(() => {
if (lastElement) {
bottomObserver.observe(lastElement)
}
return () => {
if (lastElement) {
bottomObserver.unobserve(lastElement)
}
}
}, [lastElement, bottomObserver])
useEffect(() => {
if (firstElement) {
topObserver.observe(firstElement)
}
return () => {
if (firstElement) {
topObserver.unobserve(firstElement)
}
}
}, [firstElement, topObserver])
const onClickItem = useCallback(
async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => {
await onSelect(item, userTriggered)
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],
)
return (
<>
<InfiniteCalendar
activities={calendarActivities}
activityType={'created'}
onDateSelect={onCalendarSelect}
selectedTemplateDay={selectedDay}
selectedItemDay={selectedDay}
ref={calendarRef}
/>
<div
className={classNames(
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
'md:hover:[overflow-y:_overlay]',
)}
ref={scrollArea}
id={ElementIds.ContentList}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{dailyItems.map((dailyItem, index) => {
const isFirst = index === 0
const isLast = index === dailyItems.length - 1
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)}
ref={(ref) => {
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
if (ref) {
visibilityObserver.observe(ref)
}
}}
/>
))
} else {
return (
<DailyItemCell
selected={selectedDay && dailyItem.id === dateToDailyDayIdentifier(selectedDay)}
section={dailyItem}
id={dailyItem.id}
key={dailyItem.dateKey}
onClick={() => onClickTemplate(dailyItem.date)}
ref={(ref) => {
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
if (ref) {
visibilityObserver.observe(ref)
}
}}
/>
)
}
})}
</div>
</>
)
}
export default DailyContentList
@@ -0,0 +1,85 @@
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { SNTag } from '@standardnotes/snjs'
import { ComponentPropsWithoutRef, forwardRef, FunctionComponent, Ref } from 'react'
import ListItemFlagIcons from '../ListItemFlagIcons'
import ListItemMetadata from '../ListItemMetadata'
import ListItemTags from '../ListItemTags'
import ListItemNotePreviewText from '../ListItemNotePreviewText'
import { ListableContentItem } from '../Types/ListableContentItem'
import { DailyItemsDay } from './DailyItemsDaySection'
import { ListItemTitle } from '../ListItemTitle'
import { EmptyPlaceholderBars } from './EmptyPlaceholderBars'
type DaySquareProps = {
day: number
hasActivity: boolean
weekday: string
}
const DaySquare: FunctionComponent<DaySquareProps> = ({ day, hasActivity, weekday }) => {
return (
<div className="mr-5">
<div
className={`${
hasActivity ? 'bg-danger text-danger-contrast' : 'bg-neutral text-neutral-contrast'
} h-15 w-18 rounded p-2 text-center`}
>
<div className="text-sm font-bold">{weekday}</div>
<div className="text-4xl font-bold">{day}</div>
</div>
</div>
)
}
interface Props extends ComponentPropsWithoutRef<'div'> {
item?: ListableContentItem
onClick: () => void
section: DailyItemsDay
selected?: boolean
tags?: SNTag[]
hideDate?: boolean
hideTags?: boolean
hidePreview?: boolean
}
export const DailyItemCell = forwardRef(
(
{ item, tags = [], section, onClick, selected, hideDate = false, hidePreview = false, hideTags = false }: Props,
ref: Ref<HTMLDivElement>,
) => {
return (
<div
ref={ref}
onClick={onClick}
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
selected && 'selected border-l-2 border-solid border-danger'
}`}
id={section.id}
>
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-4">
<div className="flex items-start overflow-hidden text-base">
<DaySquare weekday={section.weekday} hasActivity={item != undefined} day={section.day} />
<div className="w-full leading-[1.3]">
{item && (
<>
<ListItemTitle item={item} />
<ListItemNotePreviewText hidePreview={hidePreview} item={item} lineLimit={5} />
<ListItemMetadata item={item} hideDate={hideDate} sortBy={'created_at'} />
<ListItemTags hideTags={hideTags} tags={tags} />
</>
)}
{!item && (
<div className="w-full">
<div className="break-word mr-2 font-semibold">{formatDateAndTimeForNote(section.date, false)}</div>
<EmptyPlaceholderBars rows={4} />
</div>
)}
</div>
</div>
</div>
{item && <ListItemFlagIcons item={item} hasFiles={false} />}
</div>
)
},
)
@@ -0,0 +1,8 @@
export type DailyItemsDay = {
dateKey: string
day: number
weekday: string
date: Date
isToday: boolean
id: string
}
@@ -0,0 +1,61 @@
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
type Props = {
rows: number
}
function randomNumber(min: number, max: number) {
const r = Math.random() * (max - min) + min
return Math.floor(r)
}
export const EmptyPlaceholderBars: FunctionComponent<Props> = ({ rows }) => {
const [barWidths, setBarWidths] = useState<number[]>([])
const [animationInterval, setAnimationInterval] = useState<ReturnType<typeof setTimeout> | null>(null)
const reloadWidths = useCallback(() => {
const widths = []
for (let i = 0; i < rows; i++) {
const width = randomNumber(70, 100)
widths.push(width)
}
setBarWidths(widths)
}, [setBarWidths, rows])
useEffect(() => {
reloadWidths()
}, [rows, reloadWidths])
const onHoverEnter = useCallback(() => {
reloadWidths()
const interval = setInterval(() => {
reloadWidths()
}, 750)
setAnimationInterval(interval)
}, [setAnimationInterval, reloadWidths])
const onHoverExit = useCallback(() => {
if (animationInterval) {
clearInterval(animationInterval)
setAnimationInterval(null)
}
}, [animationInterval, setAnimationInterval])
return (
<div onMouseEnter={onHoverEnter} onMouseLeave={onHoverExit} className="w-full">
{barWidths.map((width, index) => {
return (
<div
style={{ width: `${width}%` }}
key={index}
className={
'transition-slowest ease my-4 h-7 bg-passive-4-opacity-variant pb-3 transition-width duration-1000'
}
></div>
)
})}
</div>
)
}
@@ -0,0 +1,3 @@
export function dateToDailyDayIdentifier(date: Date): string {
return date.toLocaleDateString()
}
@@ -14,11 +14,11 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
application,
filesController,
selectionController,
hideDate,
hideIcon,
hideTags,
item,
onSelect,
selected,
sortBy,
tags,
@@ -44,7 +44,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
let shouldOpenContextMenu = selected
if (!selected) {
const { didSelect } = await selectionController.selectItem(item.uuid)
const { didSelect } = await onSelect(item)
if (didSelect) {
shouldOpenContextMenu = true
}
@@ -54,15 +54,15 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
openFileContextMenu(posX, posY)
}
},
[selected, selectionController, item.uuid, openFileContextMenu],
[selected, onSelect, item, openFileContextMenu],
)
const onClick = useCallback(async () => {
const { didSelect } = await selectionController.selectItem(item.uuid, true)
const { didSelect } = await onSelect(item, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item.uuid, selectionController, toggleAppPane])
}, [item, onSelect, toggleAppPane])
const IconComponent = () =>
getFileIconComponent(
@@ -5,7 +5,7 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '@/Components/Popover/Popover'
import DisplayOptionsMenu from './DisplayOptionsMenu'
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
import { IconType } from '@standardnotes/snjs'
import { IconType, isTag } from '@standardnotes/snjs'
import RoundIconButton from '@/Components/Button/RoundIconButton'
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
@@ -32,6 +32,7 @@ const ContentListHeader = ({
}: Props) => {
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
const isDailyEntry = isTag(selectedTag) && selectedTag.isDailyEntry
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
@@ -83,7 +84,13 @@ const ContentListHeader = ({
</Popover>
</div>
<button
className="absolute bottom-6 right-6 z-editor-title-bar ml-3 flex h-13 w-13 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125 md:static md:h-8 md:w-8"
className={classNames(
'absolute bottom-6 right-6 z-editor-title-bar ml-3 flex h-13 w-13 cursor-pointer items-center',
`justify-center rounded-full border border-solid border-transparent ${
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
}`,
'hover:brightness-125 md:static md:h-8 md:w-8',
)}
title={addButtonLabel}
aria-label={addButtonLabel}
onClick={addNewItem}
@@ -23,6 +23,9 @@ import { PreferenceMode } from './PreferenceMode'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
import Button from '@/Components/Button/Button'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { isDev } from '@/Utils'
const DailyEntryModeEnabled = isDev
const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
closeDisplayOptionsMenu,
@@ -36,6 +39,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
const [preferences, setPreferences] = useState<TagPreferences>({})
const hasSubscription = application.hasValidSubscription()
const controlsDisabled = currentMode === 'tag' && !hasSubscription
const isDailyEntry = selectedTag.preferences?.entryMode === 'daily'
const reloadPreferences = useCallback(() => {
const globalValues: TagPreferences = {
@@ -162,6 +166,13 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
}, [preferences, changePreferences])
const setEntryMode = useCallback(
(mode: 'normal' | 'daily') => {
void changePreferences({ entryMode: mode })
},
[changePreferences],
)
const TabButton: FunctionComponent<{
label: string
mode: PreferenceMode
@@ -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,33 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
Show trashed
</MenuItem>
{currentMode === 'tag' && DailyEntryModeEnabled && (
<>
<MenuItemSeparator />
<div className="px-3 py-1 text-xs font-semibold uppercase text-text">Entry Mode</div>
<MenuItem
disabled={controlsDisabled}
className="py-2"
type={MenuItemType.RadioButton}
onClick={() => setEntryMode('normal')}
checked={!selectedTag.preferences?.entryMode || selectedTag.preferences?.entryMode === 'normal'}
>
<div className="ml-2 flex flex-grow items-center justify-between">Normal</div>
</MenuItem>
<MenuItem
disabled={controlsDisabled}
className="py-2"
type={MenuItemType.RadioButton}
onClick={() => setEntryMode('daily')}
checked={isDailyEntry}
>
<div className="ml-2 flex flex-grow items-center justify-between">Daily</div>
</MenuItem>
</>
)}
<MenuItemSeparator />
<NewNotePreferences
@@ -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} />
@@ -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) {
@@ -1,6 +1,10 @@
import { ReactNativeToWebEvent, WebApplicationInterface } from '@standardnotes/snjs'
export type NativeMobileEventListener = (event: ReactNativeToWebEvent) => void
export class MobileWebReceiver {
private listeners: Set<NativeMobileEventListener> = new Set()
constructor(private application: WebApplicationInterface) {
this.listenForNativeMobileEvents()
}
@@ -39,6 +43,14 @@ export class MobileWebReceiver {
}
}
addReactListener = (listener: NativeMobileEventListener) => {
this.listeners.add(listener)
return () => {
this.listeners.delete(listener)
}
}
handleNativeEvent(event: ReactNativeToWebEvent) {
switch (event) {
case ReactNativeToWebEvent.EnteringBackground:
@@ -60,5 +72,7 @@ export class MobileWebReceiver {
default:
break
}
this.listeners.forEach((listener) => listener(event))
}
}
@@ -0,0 +1,31 @@
import { addDays } from '@/Utils/DateUtils'
import { numDaysBetweenDates } from './DateUtils'
describe('date utils', () => {
describe('numDaysBetweenDates', () => {
it('should return full days diff accurately', () => {
const today = new Date()
expect(numDaysBetweenDates(today, addDays(today, 1))).toEqual(1)
expect(numDaysBetweenDates(today, addDays(today, 2))).toEqual(2)
expect(numDaysBetweenDates(today, addDays(today, 3))).toEqual(3)
})
it('should return absolute value of difference', () => {
const today = new Date()
expect(numDaysBetweenDates(today, addDays(today, 3))).toEqual(3)
expect(numDaysBetweenDates(addDays(today, 3), today)).toEqual(3)
})
it('should return 1 day difference between two dates on different days but 1 hour apart', () => {
const today = new Date()
const oneHourBeforeMidnight = new Date()
oneHourBeforeMidnight.setHours(0, 0, 0, 0)
oneHourBeforeMidnight.setHours(-1, 0, 0, 0)
expect(today.toDateString()).not.toEqual(oneHourBeforeMidnight.toDateString())
expect(numDaysBetweenDates(today, oneHourBeforeMidnight)).toEqual(1)
})
})
})
@@ -12,13 +12,84 @@ export const formatDateForContextMenu = (date: Date | undefined) => {
return `${date.toDateString()} ${date.toLocaleTimeString()}`
}
export const formatDateAndTimeForNote = (date: Date) => {
return `${date.toLocaleDateString(undefined, {
export const formatDateAndTimeForNote = (date: Date, includeTime = true) => {
const dateString = `${date.toLocaleDateString(undefined, {
weekday: 'long',
day: 'numeric',
month: 'short',
year: 'numeric',
})} at ${date.toLocaleTimeString(undefined, {
timeStyle: 'short',
})}`
if (includeTime) {
return `${dateString} at ${date.toLocaleTimeString(undefined, {
timeStyle: 'short',
})}`
} else {
return dateString
}
}
export function numHoursBetweenDates(date1: Date, date2: Date): number {
return Math.abs(date1.getTime() - date2.getTime()) / 3600000
}
export function isDateInSameDay(date1: Date, date2: Date): boolean {
return date1.toLocaleDateString() === date2.toLocaleDateString()
}
export function numDaysBetweenDates(date1: Date, date2: Date): number {
if (numHoursBetweenDates(date1, date2) < 24) {
const dayOfWeekDiffers = date1.toLocaleDateString() !== date2.toLocaleDateString()
if (dayOfWeekDiffers) {
return 1
}
}
const diffInMs = date1.getTime() - date2.getTime()
const diffInDays = Math.abs(diffInMs / (1000 * 60 * 60 * 24))
return Math.floor(diffInDays)
}
export function addDays(date: Date, days: number) {
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
export function addMonths(date: Date, months: number) {
const result = new Date(date)
result.setMonth(result.getMonth() + months)
return result
}
export function getWeekdayName(date: Date, format: 'long' | 'short'): string {
return date.toLocaleString('default', { weekday: format })
}
export function areDatesInSameMonth(date1: Date, date2: Date): boolean {
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth()
}
export function numberOfMonthsBetweenDates(date1: Date, date2: Date, roundUpFractionalMonths = true) {
let startDate = date1
let endDate = date2
let inverse = false
if (date1 > date2) {
startDate = date2
endDate = date1
inverse = true
}
const yearsDifference = endDate.getFullYear() - startDate.getFullYear()
const monthsDifference = endDate.getMonth() - startDate.getMonth()
const daysDifference = endDate.getDate() - startDate.getDate()
let monthCorrection = 0
if (roundUpFractionalMonths === true && daysDifference > 0) {
monthCorrection = 1
} else if (roundUpFractionalMonths !== true && daysDifference < 0) {
monthCorrection = -1
}
return (inverse ? -1 : 1) * (yearsDifference * 12 + monthsDifference + monthCorrection)
}
@@ -146,7 +146,6 @@
&.selected {
background-color: var(--item-cell-selected-background-color);
border-left: 2px solid var(--item-cell-selected-border-left-color);
progress {
background-color: var(--note-preview-selected-progress-background-color);
+1
View File
@@ -115,6 +115,7 @@ module.exports = {
'warning-contrast': 'var(--sn-stylekit-warning-contrast-color)',
danger: 'var(--sn-stylekit-danger-color)',
'danger-contrast': 'var(--sn-stylekit-danger-contrast-color)',
'danger-light': 'var(--sn-stylekit-danger-light-color, var(--sn-stylekit-danger-color))',
default: 'var(--sn-stylekit-background-color)',
foreground: 'var(--sn-stylekit-foreground-color)',
contrast: 'var(--sn-stylekit-contrast-background-color)',