Compare commits

...
Author SHA1 Message Date
StandardNotes CI 9ca0604352 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
2023-06-27 17:09:34 +00:00
Aman Harwara d3378a704a refactor: fix conflicts view when conflicted copies are already in trash (#2339) 2023-06-27 22:08:46 +05:30
StandardNotes CI f858f31492 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
2023-06-26 15:24:21 +00:00
Aman Harwara dffeb7e710 chore: fix signin menu closing on incorrect mfa 2023-06-26 19:56:00 +05:30
Aman Harwara c378288d6e chore: fix ios height issue 2023-06-26 19:37:49 +05:30
StandardNotes CI 65a09b77d3 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2023-06-26 11:19:38 +00:00
Aman Harwara 24d159f6d8 chore: remove unused code 2023-06-26 16:20:03 +05:30
Aman Harwara 59fbf6f4c3 refactor: readonly note content 2023-06-26 16:12:35 +05:30
StandardNotes CI 28a52cb998 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2023-06-26 10:12:21 +00:00
Mo 7f5041c4a8 chore: check for external request 2023-06-26 04:40:46 -05:00
StandardNotes CI 4b93dc35a8 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2023-06-25 20:42:27 +00:00
Mo bb804fff45 fix: fixes issue where a renewed session would in some cases would not save to disk, causing a reauthentication prompt to occur 2023-06-25 15:12:22 -05:00
StandardNotes CI 8362c4e92c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
2023-06-25 18:50:31 +00:00
Aman Harwara b8481dfbe3 Merge branch 'main' of github.com:standardnotes/app 2023-06-25 23:53:31 +05:30
Aman Harwara 046eaac548 refactor: packageprovider 2023-06-25 23:53:23 +05:30
StandardNotes CI 747ba1b342 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
2023-06-25 12:46:27 +00:00
Aman Harwara 7c3b8d4534 chore: fix test 2023-06-25 17:47:35 +05:30
Aman Harwara 0613436198 chore: select current result when closing super search dialog 2023-06-25 16:37:05 +05:30
Aman Harwara 63b95391a6 refactor: use actual maps for uuidmap 2023-06-25 16:14:52 +05:30
Aman Harwara 195420101a chore: fix conflict map addition/removal 2023-06-25 16:05:50 +05:30
StandardNotes CI 74ba220982 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2023-06-25 09:26:54 +00:00
Aman Harwara e0e9249334 feat: Added a conflict resolution dialog and a Conflicts view for easier management of conflicts (#2337) 2023-06-25 14:27:51 +05:30
StandardNotes CI 49d43fd14b chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2023-06-23 20:59:33 +00:00
Shane Duffy 1ff34cf1a2 refactor: fix accessibility issue with note list items (#2338) 2023-06-24 02:01:05 +05:30
99 changed files with 1897 additions and 295 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/api
## [1.26.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/api
## [1.26.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Bug Fixes
* fixes issue where a renewed session would in some cases would not save to disk, causing a reauthentication prompt to occur ([bb804ff](https://github.com/standardnotes/app/commit/bb804fff45328eef3c66ec3a48dc000c21e37020))
## [1.26.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/api
## [1.26.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/api
## [1.26.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/api
## [1.26.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.26.10",
"version": "1.26.16",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8 -7
View File
@@ -99,7 +99,12 @@ export class HttpService implements HttpServiceInterface {
}
async runHttp<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>> {
if (this.inProgressRefreshSessionPromise) {
if (this.__latencySimulatorMs) {
await sleep(this.__latencySimulatorMs, true)
}
const isRefreshRequest = httpRequest.url === joinPaths(this.host, Paths.v1.refreshSession)
if (this.inProgressRefreshSessionPromise && !isRefreshRequest) {
await this.inProgressRefreshSessionPromise
httpRequest.authentication = this.session?.accessToken.value
@@ -107,17 +112,13 @@ export class HttpService implements HttpServiceInterface {
const request = this.createXmlRequest(httpRequest)
if (this.__latencySimulatorMs) {
await sleep(this.__latencySimulatorMs, true)
}
const response = await this.runRequest<T>(request, this.createRequestBody(httpRequest))
if (response.meta) {
if (response.meta && !httpRequest.external) {
this.updateMetaCallback?.(response.meta)
}
if (response.status === HttpStatusCode.ExpiredAccessToken) {
if (response.status === HttpStatusCode.ExpiredAccessToken && !isRefreshRequest && !httpRequest.external) {
if (this.inProgressRefreshSessionPromise) {
await this.inProgressRefreshSessionPromise
} else {
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.53](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.52](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.51](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-23)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-16)
**Note:** Version bump only for package @standardnotes/clipper
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/clipper",
"description": "Web clipper browser extension for Standard Notes",
"version": "1.1.46",
"version": "1.1.55",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.106.64](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.63](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.62](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.61](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.60](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.59](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.58](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.57](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.56](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-23)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-16)
**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.106.55",
"version": "3.106.64",
"license": "AGPL-3.0-or-later",
"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.
## [1.21.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.21.38",
"version": "1.21.42",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+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.28.51](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.47",
"version": "1.28.51",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+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.15.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/files
## [1.15.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/files
## [1.15.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/files
## [1.15.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/files
## [1.15.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.15.9",
"version": "1.15.13",
"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.12.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
# [1.11.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-17)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/icons",
"version": "1.11.0",
"version": "1.12.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
+2
View File
@@ -186,6 +186,7 @@ import SubtractIcon from './ic-subtract.svg'
import SuperscriptIcon from './ic-superscript.svg'
import SyncIcon from './ic-sync.svg'
import TasksIcon from './ic-tasks.svg'
import TextIcon from './ic-text.svg'
import TextCircleIcon from './ic-text-circle.svg'
import TextParagraphLongIcon from './ic-text-paragraph-long.svg'
import ThemesFilledIcon from './ic-themes-filled.svg'
@@ -392,6 +393,7 @@ export {
SuperscriptIcon,
SyncIcon,
TasksIcon,
TextIcon,
TextCircleIcon,
TextParagraphLongIcon,
ThemesFilledIcon,
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.53.64](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.63](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.62](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.61](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.60](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.59](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.58](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.57](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.56](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-23)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-16)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.53.55",
"version": "3.53.64",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.46.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/models
## [1.46.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/models
## [1.46.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/models
# [1.46.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
## [1.45.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
**Note:** Version bump only for package @standardnotes/models
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.45.5",
"version": "1.46.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -17,6 +17,7 @@ export * from './Interfaces/DeletedItem'
export * from './Interfaces/EncryptedItem'
export * from './Interfaces/ItemInterface'
export * from './Interfaces/TypeCheck'
export * from './Interfaces/UnionTypes'
export * from './Mutator/DecryptedItemMutator'
export * from './Mutator/DeleteMutator'
export * from './Mutator/ItemMutator'
@@ -185,10 +185,24 @@ export abstract class Collection<
if (this.isDecryptedElement(element)) {
const conflictOf = element.content.conflict_of
if (conflictOf) {
if (conflictOf && !element.content.trashed) {
this.conflictMap.establishRelationship(conflictOf, element.uuid)
}
const isConflictOfButTrashed = conflictOf && element.content.trashed
const isInConflictMapButIsNotConflictOf =
!conflictOf && this.conflictMap.getInverseRelationships(element.uuid).length > 0
const isInConflictMapButDoesNotHaveConflicts =
this.conflictMap.existsInDirectMap(element.uuid) &&
this.conflictMap.getDirectRelationships(element.uuid).length === 0
if (isInConflictMapButIsNotConflictOf || isInConflictMapButDoesNotHaveConflicts || isConflictOfButTrashed) {
this.conflictMap.removeFromMap(element.uuid)
}
this.referenceMap.setAllRelationships(
element.uuid,
element.references.map((r) => r.uuid),
@@ -203,6 +217,9 @@ export abstract class Collection<
if (element.deleted) {
this.nondeletedIndex.delete(element.uuid)
if (this.conflictMap.existsInDirectMap(element.uuid) || this.conflictMap.existsInInverseMap(element.uuid)) {
this.conflictMap.removeFromMap(element.uuid)
}
} else {
this.nondeletedIndex.add(element.uuid)
}
@@ -260,4 +277,8 @@ export abstract class Collection<
remove(array, { uuid: element.uuid as never })
this.typedMap[element.content_type] = array
}
public numberOfItemsWithConflicts(): number {
return this.conflictMap.directMapSize
}
}
@@ -6,12 +6,13 @@ import { ItemCollection } from './ItemCollection'
import { FillItemContent, ItemContent } from '../../../Abstract/Content/ItemContent'
describe('item collection', () => {
const createDecryptedPayload = (uuid?: string): DecryptedPayload => {
const createDecryptedPayload = (uuid?: string, content?: Partial<NoteContent>): DecryptedPayload => {
return new DecryptedPayload({
uuid: uuid || String(Math.random()),
content_type: ContentType.Note,
content: FillItemContent<NoteContent>({
title: 'foo',
...content,
}),
...PayloadTimestampDefaults(),
})
@@ -33,4 +34,81 @@ describe('item collection', () => {
expect(collection.all()).toHaveLength(1)
})
it('should not add conflicted copy to conflictMap if trashed', () => {
const collection = new ItemCollection()
const mainItem = new DecryptedItem(createDecryptedPayload())
const nonTrashedConflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
}),
)
const trashedConflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
trashed: true,
}),
)
collection.set([mainItem, nonTrashedConflictedItem, trashedConflictedItem])
expect(collection.conflictMap.existsInDirectMap(mainItem.uuid)).toBe(true)
expect(collection.conflictMap.getDirectRelationships(mainItem.uuid).includes(nonTrashedConflictedItem.uuid)).toBe(
true,
)
expect(collection.conflictMap.getDirectRelationships(mainItem.uuid).includes(trashedConflictedItem.uuid)).toBe(
false,
)
})
it('should remove conflicted copy from conflictMap if not conflicted anymore', () => {
const collection = new ItemCollection()
const mainItem = new DecryptedItem(createDecryptedPayload())
const conflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
}),
)
collection.set([mainItem, conflictedItem])
expect(collection.conflictMap.existsInInverseMap(conflictedItem.uuid)).toBe(true)
const updatedConflictedItem = new DecryptedItem(
conflictedItem.payload.copy({
content: { conflict_of: undefined } as unknown as jest.Mocked<ItemContent>,
}),
)
collection.set(updatedConflictedItem)
expect(collection.conflictMap.existsInInverseMap(conflictedItem.uuid)).toBe(false)
})
it('should remove conflict parent if it has no conflicts anymore', () => {
const collection = new ItemCollection()
const mainItem = new DecryptedItem(createDecryptedPayload())
const conflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
}),
)
collection.set([mainItem, conflictedItem])
expect(collection.conflictMap.existsInDirectMap(mainItem.uuid)).toBe(true)
const updatedConflictedItem = new DecryptedItem(
conflictedItem.payload.copy({
content: { conflict_of: undefined } as unknown as jest.Mocked<ItemContent>,
}),
)
collection.set([updatedConflictedItem, mainItem])
expect(collection.conflictMap.existsInDirectMap(mainItem.uuid)).toBe(false)
})
})
@@ -18,7 +18,7 @@ export class TagItemsIndex implements SNIndex {
private isItemCountable = (item: ItemInterface) => {
if (isDecryptedItem(item)) {
return !item.archived && !item.trashed
return !item.archived && !item.trashed && !item.conflictOf
}
return false
}
@@ -6,12 +6,14 @@ import { ItemWithTags } from './Search/ItemWithTags'
import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities'
import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types'
import { FilterDisplayOptions } from './DisplayOptions'
import { SystemViewId } from '../../Syncable/SmartView'
export function computeUnifiedFilterForDisplayOptions(
options: FilterDisplayOptions,
collection: ReferenceLookupCollection,
additionalFilters: ItemFilter[] = [],
): ItemFilter {
const filters = computeFiltersForDisplayOptions(options, collection)
const filters = computeFiltersForDisplayOptions(options, collection).concat(additionalFilters)
return (item: SearchableDecryptedItem) => {
return itemPassesFilters(item, filters)
@@ -74,5 +76,12 @@ export function computeFiltersForDisplayOptions(
filters.push((item) => itemMatchesQuery(item, query, collection))
}
if (
!viewsPredicate?.keypathIncludesString('conflict_of') &&
!options.views?.some((v) => v.uuid === SystemViewId.TrashedNotes)
) {
filters.push((item) => !item.conflictOf)
}
return filters
}
@@ -85,7 +85,19 @@ export function BuildSmartViews(options: FilterDisplayOptions): SmartView[] {
}),
)
return [notes, files, starred, archived, trash, untagged]
const conflicts = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.Conflicts,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Conflicts',
predicate: conflictsPredicate(options).toJson(),
}),
}),
)
return [notes, files, starred, archived, trash, untagged, conflicts]
}
function allNotesPredicate(options: FilterDisplayOptions) {
@@ -203,3 +215,26 @@ function starredNotesPredicate(options: FilterDisplayOptions) {
return predicate
}
function conflictsPredicate(options: FilterDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [new Predicate('content_type', '=', ContentType.Note)]
if (options.includeTrashed === false) {
subPredicates.push(new Predicate('trashed', '=', false))
}
if (options.includeArchived === false) {
subPredicates.push(new Predicate('archived', '=', false))
}
if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}
if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}
const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}
@@ -8,6 +8,7 @@ export const SmartViewIcons: Record<SystemViewId, IconType> = {
[SystemViewId.TrashedNotes]: 'trash',
[SystemViewId.UntaggedNotes]: 'hashtag-off',
[SystemViewId.StarredNotes]: 'star-filled',
[SystemViewId.Conflicts]: 'merge',
}
export function systemViewIcon(id: SystemViewId): IconType {
@@ -5,4 +5,5 @@ export enum SystemViewId {
TrashedNotes = 'trashed-notes',
UntaggedNotes = 'untagged-notes',
StarredNotes = 'starred-notes',
Conflicts = 'conflicts',
}
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.332](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.331](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.330](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.329](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.328](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.327](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.326](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.325](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.324](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-23)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.323](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-16)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.323",
"version": "1.4.332",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.60.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/services
## [1.60.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/services
## [1.60.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/services
## [1.60.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/services
## [1.60.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/services
# [1.60.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
## [1.59.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
**Note:** Version bump only for package @standardnotes/services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.59.12",
"version": "1.60.5",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -23,7 +23,7 @@ export class ItemCounter implements ItemCounterInterface {
continue
}
if (item.content_type === ContentType.Note) {
if (item.content_type === ContentType.Note && !item.conflictOf) {
counts.notes++
continue
@@ -18,6 +18,7 @@ import {
ItemsKeyInterface,
ItemContent,
DecryptedPayload,
AnyItemInterface,
} from '@standardnotes/models'
export interface ItemsClientInterface {
@@ -167,4 +168,7 @@ export interface ItemsClientInterface {
predicate: PredicateInterface<T>,
iconString?: string,
): Promise<SmartView>
conflictsOf(uuid: string): AnyItemInterface[]
numberOfNotesWithConflicts(): number
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.169.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/snjs
## [2.169.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/snjs
## [2.169.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Bug Fixes
* fixes issue where a renewed session would in some cases would not save to disk, causing a reauthentication prompt to occur ([bb804ff](https://github.com/standardnotes/app/commit/bb804fff45328eef3c66ec3a48dc000c21e37020))
## [2.169.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/snjs
## [2.169.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/snjs
# [2.169.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
## [2.168.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
**Note:** Version bump only for package @standardnotes/snjs
@@ -123,6 +123,7 @@ export class ItemManager
public setPrimaryItemDisplayOptions(options: Models.DisplayOptions): void {
const override: Models.FilterDisplayOptions = {}
const additionalFilters: Models.ItemFilter[] = []
if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.AllNotes)) {
if (options.includeArchived === undefined) {
@@ -142,6 +143,9 @@ export class ItemManager
override.includeArchived = true
}
}
if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.Conflicts)) {
additionalFilters.push((item) => this.collection.conflictsOf(item.uuid).length > 0)
}
this.rebuildSystemSmartViews({ ...options, ...override })
@@ -174,7 +178,7 @@ export class ItemManager
}
this.navigationDisplayController.setDisplayOptions({
customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection),
customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection, additionalFilters),
...updatedOptions,
})
}
@@ -1410,4 +1414,12 @@ export class ItemManager
},
})
}
public conflictsOf(uuid: string) {
return this.collection.conflictsOf(uuid)
}
public numberOfNotesWithConflicts(): number {
return this.collection.numberOfItemsWithConflicts()
}
}
+7 -7
View File
@@ -82,14 +82,14 @@ describe('item manager', function () {
const note = await this.createNote()
const tag = await this.createTag([note])
expect(this.itemManager.collection.referenceMap.directMap[tag.uuid]).to.eql([note.uuid])
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid])
})
it('inverse reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid]).to.eql([tag.uuid])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
})
it('inverse reference map should not have duplicates', async function () {
@@ -97,7 +97,7 @@ describe('item manager', function () {
const tag = await this.createTag([note])
await this.itemManager.changeItem(tag)
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid]).to.eql([tag.uuid])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
})
it('deleting from reference map', async function () {
@@ -105,8 +105,8 @@ describe('item manager', function () {
const tag = await this.createTag([note])
await this.itemManager.setItemToBeDeleted(note)
expect(this.itemManager.collection.referenceMap.directMap[tag.uuid]).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid].length).to.equal(0)
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0)
})
it('deleting referenced item should update referencing item references', async function () {
@@ -125,8 +125,8 @@ describe('item manager', function () {
mutator.removeItemAsRelationship(note)
})
expect(this.itemManager.collection.referenceMap.directMap[tag.uuid]).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid]).to.eql([])
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([])
})
it('emitting discardable payload should remove it from our collection', async function () {
+22
View File
@@ -105,6 +105,28 @@ describe('server session', function () {
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now())
})
it('should not deadlock while renewing session', async function () {
this.timeout(Factory.TwentySecondTimeout)
await Factory.registerUserToApplication({
application: this.application,
email: this.email,
password: this.password,
})
await sleepUntilSessionExpires(this.application)
// Apply a latency simulation so that ` this.inProgressRefreshSessionPromise = this.refreshSession()` does
// not have the chance to complete before it is assigned to the variable. This test came along with a fix
// where runHttp does not await a pending refreshSession promise if the request being made is itself a refreshSession request.
this.application.httpService.__latencySimulatorMs = 1000
await this.application.sync.sync(syncOptions)
const sessionAfterSync = this.application.apiService.getSession()
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now())
})
it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () {
await Factory.registerUserToApplication({
application: this.application,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.168.20",
"version": "2.169.5",
"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.8.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
## [1.7.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-16)
**Note:** Version bump only for package @standardnotes/styles
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/styles",
"version": "1.7.8",
"version": "1.8.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"publishConfig": {
+2 -1
View File
@@ -1,7 +1,8 @@
.windows-web,
.windows-desktop,
.linux-web,
.linux-desktop {
.linux-desktop,
.force-custom-scrollbar {
$thumb-width: 4px;
::-webkit-scrollbar {
+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.3.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-17)
**Note:** Version bump only for package @standardnotes/toast
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/toast",
"version": "1.3.31",
"version": "1.3.32",
"private": true,
"main": "./src/index.ts",
"scripts": {
+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.
## [1.26.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.26.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.26.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.26.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.26.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.26.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.26.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.26.14",
"version": "1.26.20",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.17.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/utils
## [1.17.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/utils
# [1.17.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
## [1.16.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-03-01)
**Note:** Version bump only for package @standardnotes/utils
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/utils",
"version": "1.16.5",
"version": "1.17.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+38 -22
View File
@@ -6,23 +6,39 @@ import { addIfUnique, removeFromArray } from '../Utils/Utils'
*/
export class UuidMap {
/** uuid to uuids that we have a relationship with */
private directMap: Partial<Record<string, string[]>> = {}
private directMap: Map<string, string[]> = new Map()
/** uuid to uuids that have a relationship with us */
private inverseMap: Partial<Record<string, string[]>> = {}
private inverseMap: Map<string, string[]> = new Map()
public get directMapSize(): number {
return this.directMap.size
}
public get inverseMapSize(): number {
return this.inverseMap.size
}
public makeCopy(): UuidMap {
const copy = new UuidMap()
copy.directMap = Object.assign({}, this.directMap)
copy.inverseMap = Object.assign({}, this.inverseMap)
copy.directMap = new Map(this.directMap)
copy.inverseMap = new Map(this.inverseMap)
return copy
}
public existsInDirectMap(uuid: string): boolean {
return this.directMap.has(uuid)
}
public existsInInverseMap(uuid: string): boolean {
return this.inverseMap.has(uuid)
}
public getDirectRelationships(uuid: string): string[] {
return this.directMap[uuid] || []
return this.directMap.get(uuid) || []
}
public getInverseRelationships(uuid: string): string[] {
return this.inverseMap[uuid] || []
return this.inverseMap.get(uuid) || []
}
public establishRelationship(uuidA: string, uuidB: string): void {
@@ -36,8 +52,8 @@ export class UuidMap {
}
public setAllRelationships(uuid: string, relationships: string[]): void {
const previousDirect = this.directMap[uuid] || []
this.directMap[uuid] = relationships
const previousDirect = this.directMap.get(uuid) || []
this.directMap.set(uuid, relationships)
/** Remove all previous values in case relationships have changed
* The updated references will be added afterwards.
@@ -54,41 +70,41 @@ export class UuidMap {
public removeFromMap(uuid: string): void {
/** Items that we reference */
const directReferences = this.directMap[uuid] || []
const directReferences = this.directMap.get(uuid) || []
for (const directReference of directReferences) {
removeFromArray(this.inverseMap[directReference] || [], uuid)
removeFromArray(this.inverseMap.get(directReference) || [], uuid)
}
delete this.directMap[uuid]
this.directMap.delete(uuid)
/** Items that are referencing us */
const inverseReferences = this.inverseMap[uuid] || []
const inverseReferences = this.inverseMap.get(uuid) || []
for (const inverseReference of inverseReferences) {
removeFromArray(this.directMap[inverseReference] || [], uuid)
removeFromArray(this.directMap.get(inverseReference) || [], uuid)
}
delete this.inverseMap[uuid]
this.inverseMap.delete(uuid)
}
private establishDirectRelationship(uuidA: string, uuidB: string): void {
const index = this.directMap[uuidA] || []
const index = this.directMap.get(uuidA) || []
addIfUnique(index, uuidB)
this.directMap[uuidA] = index
this.directMap.set(uuidA, index)
}
private establishInverseRelationship(uuidA: string, uuidB: string): void {
const inverseIndex = this.inverseMap[uuidB] || []
const inverseIndex = this.inverseMap.get(uuidB) || []
addIfUnique(inverseIndex, uuidA)
this.inverseMap[uuidB] = inverseIndex
this.inverseMap.set(uuidB, inverseIndex)
}
private deestablishDirectRelationship(uuidA: string, uuidB: string): void {
const index = this.directMap[uuidA] || []
const index = this.directMap.get(uuidA) || []
removeFromArray(index, uuidB)
this.directMap[uuidA] = index
this.directMap.set(uuidA, index)
}
private deestablishInverseRelationship(uuidA: string, uuidB: string): void {
const inverseIndex = this.inverseMap[uuidB] || []
const inverseIndex = this.inverseMap.get(uuidB) || []
removeFromArray(inverseIndex, uuidA)
this.inverseMap[uuidB] = inverseIndex
this.inverseMap.set(uuidB, inverseIndex)
}
}
+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.162.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)
**Note:** Version bump only for package @standardnotes/web
## [3.162.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/web
## [3.162.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/web
## [3.162.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)
**Note:** Version bump only for package @standardnotes/web
## [3.162.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/web
## [3.162.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/web
## [3.162.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
**Note:** Version bump only for package @standardnotes/web
# [3.162.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)
### Features
* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))
## [3.161.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-23)
**Note:** Version bump only for package @standardnotes/web
## [3.161.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-16)
**Note:** Version bump only for package @standardnotes/web
+102
View File
@@ -1,5 +1,107 @@
{
"versions": [
{
"version": "3.162.7",
"title": "[3.162.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-27)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.6",
"title": "[3.162.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.5",
"title": "[3.162.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.4",
"title": "[3.162.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-26)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.3",
"title": "[3.162.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.2",
"title": "[3.162.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.1",
"title": "[3.162.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.162.0",
"title": "[3.162.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-25)",
"date": null,
"body": "### Features\n\n* Added a conflict resolution dialog and a Conflicts view for easier management of conflicts ([#2337](https://github.com/standardnotes/app/issues/2337)) ([e0e9249](https://github.com/standardnotes/app/commit/e0e92493342b18560e7e55ece22518e738824df0))",
"parsed": {
"_": [
"Added a conflict resolution dialog and a Conflicts view for easier management of conflicts (#2337) (e0e9249)"
],
"Features": [
"Added a conflict resolution dialog and a Conflicts view for easier management of conflicts (#2337) (e0e9249)"
]
}
},
{
"version": "3.161.3",
"title": "[3.161.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-23)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.161.2",
"title": "[3.161.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-16)",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.161.2",
"version": "3.162.7",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes",
@@ -107,8 +107,9 @@
"app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write"
},
"dependencies": {
"@ariakit/react": "^0.2.1",
"@ariakit/react": "^0.2.8",
"@lexical/headless": "0.11.0",
"@radix-ui/react-slot": "^1.0.1"
"@radix-ui/react-slot": "^1.0.1",
"fast-diff": "^1.3.0"
}
}
@@ -19,7 +19,7 @@
--sn-stylekit-neutral-contrast-color: #ffffff;
--sn-stylekit-success-color: #2b9612;
--sn-stylekit-success-contrast-color: #ffffff;
--sn-stylekit-warning-color: #f6a200;
--sn-stylekit-warning-color: #cc8800;
--sn-stylekit-warning-contrast-color: #ffffff;
--sn-stylekit-danger-color: #f80324;
--sn-stylekit-danger-contrast-color: #ffffff;
@@ -27,7 +27,7 @@
--sn-stylekit-editor-foreground-color: var(--sn-stylekit-foreground-color);
--sn-stylekit-background-color: var(--background-color);
--sn-stylekit-foreground-color: var(--foreground-color);
--sn-stylekit-border-color: #000000;
--sn-stylekit-border-color: #181a1b;
--sn-stylekit-contrast-background-color: #000000;
--sn-stylekit-contrast-foreground-color: #ffffff;
--sn-stylekit-contrast-border-color: #000000;
@@ -236,7 +236,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<RevisionHistoryModal
application={application}
historyModalController={viewControllerManager.historyModalController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
subscriptionController={viewControllerManager.subscriptionController}
/>
@@ -58,7 +58,7 @@ const getClassName = (
let colors = primary ? getColorsForPrimaryVariant(style) : getColorsForNormalVariant(style)
let focusHoverStates = primary
? 'hover:brightness-125 focus:outline-none focus:brightness-125'
? 'hover:brightness-125 focus:outline-none focus-visible:brightness-125'
: 'focus:bg-contrast focus:outline-none hover:border-info hover:text-info hover:bg-contrast'
if (disabled) {
@@ -68,7 +68,7 @@ const getClassName = (
: 'focus:bg-default focus:outline-none hover:bg-default'
}
return `${rounded} font-bold ${width} ${padding} ${textSize} ${colors} ${borders} ${focusHoverStates} ${cursor}`
return `${rounded} font-bold select-none ${width} ${padding} ${textSize} ${colors} ${borders} ${focusHoverStates} ${cursor}`
}
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
@@ -0,0 +1,25 @@
import { classNames } from '@standardnotes/utils'
import Icon from '../Icon/Icon'
import { ComponentPropsWithoutRef } from 'react'
const CheckIndicator = ({ checked, className, ...props }: { checked: boolean } & ComponentPropsWithoutRef<'div'>) => (
<div
className={classNames(
'relative h-5 w-5 rounded border-2 md:h-4 md:w-4',
checked ? 'border-info bg-info' : 'border-passive-1',
className,
)}
role="presentation"
{...props}
>
{checked && (
<Icon
type="check"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-info-contrast"
size="small"
/>
)}
</div>
)
export default CheckIndicator
@@ -81,6 +81,7 @@ const FileListItemCard: FunctionComponent<DisplayableListItemProps<FileItem>> =
return (
<div
ref={listItemRef}
role="button"
className={classNames(
'content-list-item flex w-full cursor-pointer items-stretch text-text',
selected && 'selected border-l-2px border-solid border-info',
@@ -103,6 +103,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
return (
<div
ref={listItemRef}
role="button"
className={classNames(
'content-list-item flex w-full cursor-pointer items-stretch text-text',
selected && `selected border-l-2 border-solid border-accessory-tint-${tint}`,
@@ -102,6 +102,7 @@ export const IconNameToSvgMapping = {
listed: icons.ListedIcon,
lock: icons.LockIcon,
markdown: icons.MarkdownIcon,
merge: icons.MergeIcon,
more: icons.MoreIcon,
notes: icons.NotesIcon,
paragraph: icons.TextParagraphLongIcon,
@@ -126,6 +127,7 @@ export const IconNameToSvgMapping = {
superscript: icons.SuperscriptIcon,
sync: icons.SyncIcon,
tasks: icons.TasksIcon,
text: icons.TextIcon,
themes: icons.ThemesIcon,
trash: icons.TrashIcon,
tune: icons.TuneIcon,
@@ -23,6 +23,7 @@ type Props = {
isBidirectional: boolean
inlineFlex?: boolean
className?: string
readonly?: boolean
}
const LinkedItemBubble = ({
@@ -36,6 +37,7 @@ const LinkedItemBubble = ({
isBidirectional,
inlineFlex,
className,
readonly,
}: Props) => {
const ref = useRef<HTMLButtonElement>(null)
const application = useApplication()
@@ -60,6 +62,9 @@ const LinkedItemBubble = ({
const onClick: MouseEventHandler = (event) => {
if (wasClicked && event.target !== unlinkButtonRef.current) {
setWasClicked(false)
if (readonly) {
return
}
void activateItem?.(link.item)
} else {
setWasClicked(true)
@@ -112,7 +117,7 @@ const LinkedItemBubble = ({
onKeyDown={onKeyDown}
>
<Icon type={icon} className={classNames('mr-1 flex-shrink-0', iconClassName)} size="small" />
<span className="max-w-290px flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
<span className="flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
{tagTitle && <span className="text-passive-1">{tagTitle.titlePrefix}</span>}
<span className="flex items-center gap-1">
{link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && (
@@ -121,7 +126,7 @@ const LinkedItemBubble = ({
{getItemTitleInContextOfLinkBubble(link.item)}
</span>
</span>
{showUnlinkButton && (
{showUnlinkButton && !readonly && (
<a
ref={unlinkButtonRef}
role="button"
@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
import { LinkingController } from '@/Controllers/LinkingController'
import LinkedItemBubble from './LinkedItemBubble'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@standardnotes/utils'
@@ -18,9 +18,22 @@ type Props = {
linkingController: LinkingController
item: DecryptedItemInterface
hideToggle?: boolean
readonly?: boolean
className?: {
base?: string
withToggle?: string
}
isCollapsedByDefault?: boolean
}
const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = false }: Props) => {
const LinkedItemBubblesContainer = ({
item,
linkingController,
hideToggle = false,
readonly = false,
className = {},
isCollapsedByDefault = false,
}: Props) => {
const { toggleAppPane } = useResponsiveAppPane()
const commandService = useCommandService()
@@ -106,55 +119,66 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals
)
}
const [isCollapsed, setIsCollapsed] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(() => isCollapsedByDefault)
const itemsToDisplay = allItemsLinkedToItem.concat(notesLinkingToItem).concat(filesLinkingToItem)
const visibleItems = isCollapsed ? itemsToDisplay.slice(0, 5) : itemsToDisplay
const nonVisibleItems = itemsToDisplay.length - visibleItems.length
const [canShowContainerToggle, setCanShowContainerToggle] = useState(false)
const linkInputRef = useRef<HTMLInputElement>(null)
const linkContainerRef = useRef<HTMLDivElement>(null)
const [canShowContainerToggle, setCanShowContainerToggle] = useState(true)
const [linkContainer, setLinkContainer] = useState<HTMLDivElement | null>(null)
useEffect(() => {
const container = linkContainerRef.current
const linkInput = linkInputRef.current
if (!container || !linkInput) {
const container = linkContainer
if (!container) {
return
}
const resizeObserver = new ResizeObserver(() => {
if (container.clientHeight > linkInput.clientHeight) {
const firstChild = container.firstElementChild
if (!firstChild) {
return
}
const threshold = firstChild.clientHeight + 4
const didWrap = container.clientHeight > threshold
if (didWrap) {
setCanShowContainerToggle(true)
} else {
setCanShowContainerToggle(false)
}
})
resizeObserver.observe(linkContainerRef.current)
resizeObserver.observe(container)
return () => {
resizeObserver.disconnect()
}
}, [])
}, [linkContainer])
const shouldHideToggle = hideToggle || (!canShowContainerToggle && !isCollapsed)
if (readonly && itemsToDisplay.length === 0) {
return null
}
return (
<div
className={classNames(
'flex w-full justify-between',
itemsToDisplay.length > 0 && !shouldHideToggle && 'pt-2',
'flex w-full flex-wrap justify-between md:flex-nowrap',
itemsToDisplay.length > 0 && !shouldHideToggle ? 'pt-2 ' + className.withToggle : undefined,
isCollapsed ? 'gap-4' : 'gap-1',
className.base,
)}
>
<div
className={classNames(
'note-view-linking-container flex min-w-80 max-w-full items-center gap-2 bg-transparent md:-mr-2',
'note-view-linking-container flex min-w-80 max-w-full items-center gap-2 bg-transparent',
allItemsLinkedToItem.length || notesLinkingToItem.length ? 'mt-1' : 'mt-0.5',
isCollapsed ? 'overflow-hidden' : 'flex-wrap',
!shouldHideToggle && 'mr-2',
)}
ref={linkContainerRef}
ref={setLinkContainer}
>
{visibleItems.map((link) => (
<LinkedItemBubble
@@ -167,18 +191,20 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals
focusedId={focusedId}
setFocusedId={setFocusedId}
isBidirectional={isItemBidirectionallyLinked(link)}
readonly={readonly}
/>
))}
{isCollapsed && nonVisibleItems > 0 && <span className="flex-shrink-0">and {nonVisibleItems} more...</span>}
<ItemLinkAutocompleteInput
focusedId={focusedId}
linkingController={linkingController}
focusPreviousItem={focusPreviousItem}
setFocusedId={setFocusedId}
hoverLabel={`Focus input to add a link (${shortcut})`}
item={item}
ref={linkInputRef}
/>
{!readonly && (
<ItemLinkAutocompleteInput
focusedId={focusedId}
linkingController={linkingController}
focusPreviousItem={focusPreviousItem}
setFocusedId={setFocusedId}
hoverLabel={`Focus input to add a link (${shortcut})`}
item={item}
/>
)}
</div>
{itemsToDisplay.length > 0 && !shouldHideToggle && (
<RoundIconButton
@@ -0,0 +1,96 @@
import { SNNote, classNames } from '@standardnotes/snjs'
import { MouseEventHandler } from 'react'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { useApplication } from '../../ApplicationProvider'
import { useNoteAttributes } from '../../NotesOptions/NoteAttributes'
import CheckIndicator from '../../Checkbox/CheckIndicator'
import { VisuallyHidden } from '@ariakit/react'
import Icon from '../../Icon/Icon'
import StyledTooltip from '../../StyledTooltip/StyledTooltip'
export const ConflictListItem = ({
isSelected,
onClick,
title,
note,
disabled,
}: {
isSelected: boolean
disabled: boolean
onClick: MouseEventHandler<HTMLButtonElement>
title: string
note: SNNote
}) => {
const application = useApplication()
const { words, characters, paragraphs, dateLastModified, dateCreated, format } = useNoteAttributes(application, note)
return (
<button
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
className={classNames(
'flex w-full select-none flex-col overflow-hidden border-l-2 bg-transparent px-3 py-2.5 pl-4 text-left text-sm text-text',
isSelected ? 'border-info bg-info-backdrop' : 'border-transparent',
disabled
? 'cursor-not-allowed opacity-75'
: 'cursor-pointer hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
)}
onClick={onClick}
data-selected={isSelected}
disabled={disabled}
>
<div className="mb-1.5 flex items-center gap-2">
<CheckIndicator checked={isSelected} />
<div className="font-semibold">{title}</div>
</div>
<div className="w-full text-sm text-neutral lg:text-xs">
<div className="mb-1.5 flex items-center gap-2">
<StyledTooltip gutter={8} label="Last modified" className="!z-modal">
<div className="flex-shrink-0">
<Icon type="restore" size="medium" />
</div>
</StyledTooltip>
<VisuallyHidden>Last modified</VisuallyHidden> {dateLastModified}
</div>
<div className="mb-1.5 flex items-center gap-2">
<StyledTooltip gutter={8} label="Created" className="!z-modal">
<div className="flex-shrink-0">
<Icon type="pencil-filled" size="medium" />
</div>
</StyledTooltip>
<VisuallyHidden>Created</VisuallyHidden> {dateCreated}
</div>
<div className="mb-1.5 flex items-center gap-2 overflow-hidden">
<StyledTooltip gutter={8} label="Note ID" className="!z-modal">
<div className="flex-shrink-0">
<Icon type="info" size="medium" />
</div>
</StyledTooltip>
<VisuallyHidden>Note ID</VisuallyHidden>
<div className="overflow-hidden text-ellipsis whitespace-nowrap">{note.uuid}</div>
</div>
{typeof words === 'number' && (format === 'txt' || format === 'md') ? (
<div className="flex items-center gap-2">
<StyledTooltip gutter={8} label={`${words} words`} className="!z-modal">
<div className="flex items-center gap-1">
<Icon type="line-width" size="medium" />
{words}
</div>
</StyledTooltip>
<StyledTooltip gutter={8} label={`${characters} characters`} className="!z-modal">
<div className="flex items-center gap-1">
<Icon type="bold" size="small" />
<span>{characters}</span>
</div>
</StyledTooltip>
<StyledTooltip gutter={8} label={`${paragraphs} paragraphs`} className="!z-modal">
<div className="flex items-center gap-1">
<Icon type="paragraph" size="medium" />
<span>{paragraphs}</span>
</div>
</StyledTooltip>
</div>
) : null}
</div>
</button>
)
}
@@ -0,0 +1,128 @@
import { NoteType, SNNote, classNames } from '@standardnotes/snjs'
import { useEffect, useState } from 'react'
import fastdiff from 'fast-diff'
import { HeadlessSuperConverter } from '../../SuperEditor/Tools/HeadlessSuperConverter'
const DiffSpan = ({ state, text }: { state: fastdiff.Diff[0]; text: fastdiff.Diff[1] }) => (
<span
data-diff={state !== fastdiff.EQUAL ? state : undefined}
className={classNames(
'whitespace-pre-wrap',
state === fastdiff.INSERT && 'bg-success text-success-contrast',
state === fastdiff.DELETE && 'bg-danger text-danger-contrast',
)}
>
{text}
</span>
)
export const DiffView = ({
selectedNotes,
convertSuperToMarkdown,
}: {
selectedNotes: SNNote[]
convertSuperToMarkdown: boolean
}) => {
const [titleDiff, setTitleDiff] = useState<fastdiff.Diff[]>([])
const [textDiff, setTextDiff] = useState<fastdiff.Diff[]>([])
useEffect(() => {
const firstNote = selectedNotes[0]
const firstTitle = firstNote.title
const firstText =
firstNote.noteType === NoteType.Super && convertSuperToMarkdown
? new HeadlessSuperConverter().convertString(firstNote.text, 'md')
: firstNote.text
const secondNote = selectedNotes[1]
const secondTitle = secondNote.title
const secondText =
secondNote.noteType === NoteType.Super && convertSuperToMarkdown
? new HeadlessSuperConverter().convertString(secondNote.text, 'md')
: secondNote.text
const titleDiff = fastdiff(firstTitle, secondTitle, undefined, true)
const textDiff = fastdiff(firstText, secondText, undefined, true)
setTitleDiff(titleDiff)
setTextDiff(textDiff)
}, [convertSuperToMarkdown, selectedNotes])
const [preElement, setPreElement] = useState<HTMLPreElement | null>(null)
const [diffVisualizer, setDiffVisualizer] = useState<HTMLDivElement | null>(null)
const [hasOverflow, setHasOverflow] = useState(false)
useEffect(() => {
if (!preElement) {
return
}
setHasOverflow(preElement.scrollHeight > preElement.clientHeight)
}, [preElement, textDiff])
useEffect(() => {
if (!preElement || !diffVisualizer) {
return
}
if (!hasOverflow) {
return
}
if (!textDiff.length) {
return
}
diffVisualizer.innerHTML = ''
const preElementRect = preElement.getBoundingClientRect()
const diffVisualizerRect = diffVisualizer.getBoundingClientRect()
const diffs = preElement.querySelectorAll('[data-diff]')
diffs.forEach((diff) => {
const state = diff.getAttribute('data-diff')
if (!state) {
return
}
const parsedState = parseInt(state)
const rect = diff.getBoundingClientRect()
const topAsPercent = (rect.top - preElementRect.top) / preElement.scrollHeight
const topAdjustedForDiffVisualizer = diffVisualizerRect.height * topAsPercent
const heightAsPercent = rect.height / preElement.scrollHeight
const heightAdjustedForDiffVisualizer = diffVisualizerRect.height * heightAsPercent
const div = document.createElement('div')
div.className = `absolute top-0 left-0 w-full bg-${
parsedState === fastdiff.INSERT ? 'success' : 'danger'
} opacity-50`
div.style.height = `${heightAdjustedForDiffVisualizer}px`
div.style.top = `${topAdjustedForDiffVisualizer}px`
diffVisualizer.appendChild(div)
})
}, [preElement, hasOverflow, textDiff, diffVisualizer])
return (
<div className="force-custom-scrollbar relative flex flex-grow flex-col overflow-hidden">
<div className="w-full px-4 py-4 text-base font-bold">
{titleDiff.map(([state, text], index) => (
<DiffSpan state={state} text={text} key={index} />
))}
</div>
<pre
className="font-editor min-h-0 w-full flex-grow overflow-y-auto whitespace-pre-wrap p-4 pt-0 text-editor [&::-webkit-scrollbar]:bg-transparent"
ref={setPreElement}
>
{textDiff.map(([state, text], index) => (
<DiffSpan state={state} text={text} key={index} />
))}
</pre>
{hasOverflow && (
<div className="absolute top-0 right-0 z-[-1] h-full w-[19px] border-l border-border" ref={setDiffVisualizer} />
)}
</div>
)
}
@@ -0,0 +1,398 @@
import { NoteType, SNNote, classNames } from '@standardnotes/snjs'
import Modal, { ModalAction } from '../../Modal/Modal'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useApplication } from '../../ApplicationProvider'
import { confirmDialog } from '@standardnotes/ui-services'
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
import ModalDialogButtons from '../../Modal/ModalDialogButtons'
import {
Checkbox,
Select,
SelectArrow,
SelectItem,
SelectList,
Toolbar,
ToolbarItem,
useSelectStore,
useToolbarStore,
} from '@ariakit/react'
import Popover from '../../Popover/Popover'
import Icon from '../../Icon/Icon'
import Button from '../../Button/Button'
import Spinner from '../../Spinner/Spinner'
import Switch from '../../Switch/Switch'
import StyledTooltip from '../../StyledTooltip/StyledTooltip'
import { DiffView } from './DiffView'
import { ReadonlyNoteContent } from '../ReadonlyNoteContent'
import { ConflictListItem } from './ConflictListItem'
type ConflictAction = 'move-to-trash' | 'delete-permanently'
type MultipleSelectionMode = 'preview' | 'diff'
const NoteConflictResolutionModal = ({
currentNote,
conflictedNotes,
close,
}: {
currentNote: SNNote
conflictedNotes: SNNote[]
close: () => void
}) => {
const allVersions = useMemo(() => [currentNote].concat(conflictedNotes), [conflictedNotes, currentNote])
const application = useApplication()
const [selectedVersions, setSelectedVersions] = useState([currentNote.uuid])
const selectedNotes = useMemo(() => {
return allVersions.filter((note) => selectedVersions.includes(note.uuid))
}, [allVersions, selectedVersions])
const trashNote = useCallback(
async (note: SNNote) => {
await application.mutator
.changeItem(note, (mutator) => {
mutator.trashed = true
mutator.conflictOf = undefined
})
.catch(console.error)
setSelectedVersions([allVersions[0].uuid])
},
[allVersions, application.mutator],
)
const deleteNotePermanently = useCallback(
async (note: SNNote) => {
await application.mutator
.deleteItem(note)
.catch(console.error)
.then(() => {
setSelectedVersions([allVersions[0].uuid])
})
},
[allVersions, application.mutator],
)
const [selectedAction, setSelectionAction] = useState<ConflictAction>('move-to-trash')
const selectStore = useSelectStore({
value: selectedAction,
setValue: (value) => setSelectionAction(value as ConflictAction),
})
const [isPerformingAction, setIsPerformingAction] = useState(false)
const keepOnlySelected = useCallback(async () => {
const shouldDeletePermanently = selectedAction === 'delete-permanently'
const confirmDialogText = `This will keep only the selected versions and ${
shouldDeletePermanently ? 'delete the other versions permanently.' : 'move the other versions to the trash.'
} Are you sure?`
if (
await confirmDialog({
title: 'Keep only selected versions?',
text: confirmDialogText,
confirmButtonStyle: 'danger',
})
) {
const nonSelectedNotes = allVersions.filter((note) => !selectedVersions.includes(note.uuid))
selectStore.hide()
setIsPerformingAction(true)
await Promise.all(
nonSelectedNotes.map((note) => (shouldDeletePermanently ? deleteNotePermanently(note) : trashNote(note))),
)
await application.mutator.changeItems(selectedNotes, (mutator) => {
mutator.conflictOf = undefined
})
setIsPerformingAction(false)
void application.getViewControllerManager().selectionController.selectItem(selectedNotes[0].uuid, true)
void application.sync.sync()
close()
}
}, [
allVersions,
application,
close,
deleteNotePermanently,
selectStore,
selectedAction,
selectedNotes,
selectedVersions,
trashNote,
])
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const actions = useMemo(
(): ModalAction[] => [
{
label: 'Cancel',
onClick: close,
type: 'cancel',
mobileSlot: 'left',
},
],
[close],
)
const listRef = useRef<HTMLDivElement>(null)
useListKeyboardNavigation(listRef)
const [selectedMobileTab, setSelectedMobileTab] = useState<'list' | 'preview'>('list')
const toolbarStore = useToolbarStore()
const isSelectOpen = selectStore.useState('open')
const [selectAnchor, setSelectAnchor] = useState<HTMLButtonElement | null>(null)
const [multipleSelectionMode, setMultipleSelectionMode] = useState<MultipleSelectionMode>(
isMobileScreen ? 'diff' : 'preview',
)
const isPreviewMode = multipleSelectionMode === 'preview'
useEffect(() => {
if (selectedNotes.length !== 2) {
setMultipleSelectionMode('preview')
}
if (isMobileScreen && selectedNotes.length === 2) {
setMultipleSelectionMode('diff')
}
}, [isMobileScreen, selectedNotes.length])
const showSuperConversionInfo = selectedNotes.some((note) => note.noteType === NoteType.Super) && !isPreviewMode
const [compareSuperMarkdown, setCompareSuperMarkdown] = useState(true)
const [comparisonScrollPos, setComparisonScrollPos] = useState(0)
const [shouldSyncComparisonScroll, setShouldSyncComparisonScroll] = useState(true)
return (
<Modal
title="Resolve conflicts"
className={{
content: 'md:h-full md:w-[70vw]',
description: 'flex flex-col overflow-x-hidden md:flex-row',
}}
actions={actions}
close={close}
customFooter={
<ModalDialogButtons className={selectedNotes.length > 1 ? 'hidden md:flex' : ''}>
<Button className="mr-auto hidden md:inline-block" onClick={close} disabled={isPerformingAction}>
Cancel
</Button>
<Toolbar className="flex w-full items-stretch text-info-contrast md:w-auto" store={toolbarStore}>
<ToolbarItem
onClick={keepOnlySelected}
className="flex-grow rounded rounded-r-none bg-info px-3 py-1.5 text-base font-bold ring-info ring-offset-2 ring-offset-default hover:brightness-110 focus:ring-0 focus-visible:ring-2 focus-visible:brightness-110 lg:text-sm"
disabled={isPerformingAction}
>
{isPerformingAction ? (
<>
<Spinner className="h-4 w-4 border-info-contrast" />
</>
) : (
<>Keep selected, {selectedAction === 'move-to-trash' ? 'trash others' : 'delete others'}</>
)}
</ToolbarItem>
<Select
ref={setSelectAnchor}
render={
<ToolbarItem
className="relative rounded rounded-l-none bg-info py-1.5 px-3 ring-info hover:brightness-110 focus:ring-0 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-default focus-visible:brightness-110"
disabled={isPerformingAction}
>
<SelectArrow className="block rotate-180" />
<div className="absolute top-0 left-0 h-full w-[2px] bg-info brightness-[.85]" />
</ToolbarItem>
}
store={selectStore}
/>
<Popover
title="Conflict options"
open={isSelectOpen}
togglePopover={selectStore.toggle}
anchorElement={selectAnchor}
className="!fixed z-modal border border-border py-1"
side="top"
align="end"
offset={4}
hideOnClickInModal
>
<SelectList
className="cursor-pointer divide-y divide-border [&>[data-active-item]]:bg-passive-5"
store={selectStore}
>
<SelectItem className="px-2.5 py-2 hover:bg-passive-5" value="move-to-trash">
<div className="flex items-center gap-1 text-sm font-bold text-text">
{selectedAction === 'move-to-trash' ? (
<Icon type="check-bold" size="small" />
) : (
<div className="h-3.5 w-3.5" />
)}
Move others to trash
</div>
<div className="ml-4.5 text-neutral">
Only the selected version will be kept; others will be moved to trash.
</div>
</SelectItem>
<SelectItem className="px-2.5 py-2 hover:bg-passive-5" value="delete-permanently">
<div className="flex items-center gap-1 text-sm font-bold text-text">
{selectedAction === 'delete-permanently' ? (
<Icon type="check-bold" size="small" />
) : (
<div className="h-3.5 w-3.5" />
)}
Delete others permanently
</div>
<div className="ml-4.5 text-neutral">
Only the selected version will be kept; others will be deleted permanently.
</div>
</SelectItem>
</SelectList>
</Popover>
</Toolbar>
</ModalDialogButtons>
}
>
<div className="flex border-b border-border md:hidden">
<button
className={classNames(
'relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:shadow-inner',
selectedMobileTab === 'list' ? 'font-medium text-info shadow-bottom' : 'text-text',
)}
onClick={() => {
setSelectedMobileTab('list')
}}
>
List
</button>
<button
className={classNames(
'relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:shadow-inner',
selectedMobileTab === 'preview' ? 'font-medium text-info shadow-bottom' : 'text-text',
)}
onClick={() => {
setSelectedMobileTab('preview')
}}
>
Preview
</button>
</div>
<div
className={classNames(
'w-full overflow-y-auto border-r border-border py-1.5 md:flex md:w-auto md:min-w-60 md:flex-col',
selectedMobileTab !== 'list' && 'hidden md:flex',
)}
ref={listRef}
>
{allVersions.map((note, index) => (
<ConflictListItem
disabled={isPerformingAction}
isSelected={selectedVersions.includes(note.uuid)}
onClick={() => {
setSelectedVersions((versions) => {
if (!versions.includes(note.uuid)) {
return versions.length > 1 ? versions.slice(1).concat(note.uuid) : versions.concat(note.uuid)
}
return versions.length > 1 ? versions.filter((version) => version !== note.uuid) : versions
})
setSelectedMobileTab('preview')
}}
key={note.uuid}
title={index === 0 ? 'Current version' : `Version ${index + 1}`}
note={note}
/>
))}
</div>
<div
className={classNames(
'flex w-full flex-grow flex-col overflow-hidden',
selectedMobileTab !== 'preview' && 'hidden md:flex',
)}
>
{isPreviewMode && (
<div
className={classNames(
'min-h-0 w-full flex-grow divide-x divide-border pb-0.5',
isMobileScreen ? 'flex' : 'grid grid-rows-1',
)}
style={!isMobileScreen ? { gridTemplateColumns: `repeat(${selectedNotes.length}, 1fr)` } : undefined}
>
{selectedNotes.map((note) => (
<ReadonlyNoteContent
note={note}
content={note.content}
key={note.uuid}
scrollPos={comparisonScrollPos}
shouldSyncScroll={shouldSyncComparisonScroll}
onScroll={(event) => setComparisonScrollPos((event.target as HTMLElement).scrollTop)}
/>
))}
</div>
)}
{!isPreviewMode && selectedNotes.length === 2 && (
<DiffView selectedNotes={selectedNotes} convertSuperToMarkdown={compareSuperMarkdown} />
)}
{selectedNotes.length === 2 && (
<div className="flex min-h-11 items-center justify-center gap-2 border-t border-border px-4 py-1.5">
{isPreviewMode && (
<StyledTooltip
className="!z-modal !max-w-[50ch]"
label={shouldSyncComparisonScroll ? 'Scrolling is synced' : 'Scrolling is not synced. Click to sync.'}
showOnMobile
portal={false}
>
<div className="relative rounded-full p-1 hover:bg-contrast">
<Icon type={shouldSyncComparisonScroll ? 'link' : 'link-off'} className="text-neutral" />
<Checkbox
className="absolute top-0 left-0 right-0 bottom-0 cursor-pointer opacity-0"
checked={shouldSyncComparisonScroll}
onChange={() => setShouldSyncComparisonScroll((shouldSync) => !shouldSync)}
/>
</div>
</StyledTooltip>
)}
{!isMobileScreen && (
<>
<div className={showSuperConversionInfo ? 'ml-9' : ''}>Preview Mode</div>
<Switch
checked={!isPreviewMode}
onChange={function (checked: boolean): void {
setMultipleSelectionMode(checked ? 'diff' : 'preview')
}}
/>
</>
)}
<div className={isPreviewMode ? 'mr-9' : ''}>Diff Mode</div>
{showSuperConversionInfo && (
<StyledTooltip
className="!z-modal !max-w-[50ch]"
label={
<>
<div className="mb-2">
Super notes use JSON under the hood to create rich and flexible documents. While neatly organized,
it's not ideal to read or compare manually. Instead, this diff compares a Markdown rendition of
the notes.
</div>
<label className="mb-1 flex select-none items-center gap-2">
<Switch
checked={!compareSuperMarkdown}
onChange={(checked) => setCompareSuperMarkdown(!checked)}
/>
Compare JSON instead
</label>
</>
}
showOnMobile
showOnHover={false}
portal={false}
>
<button className="rounded-full p-1 hover:bg-contrast">
<Icon type="info" className="text-neutral" />
</button>
</StyledTooltip>
)}
</div>
)}
</div>
</Modal>
)
}
export default NoteConflictResolutionModal
@@ -9,7 +9,7 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
import { classNames } from '@standardnotes/utils'
import { classNames, pluralize } from '@standardnotes/utils'
import {
ApplicationEvent,
ComponentArea,
@@ -45,6 +45,10 @@ import { SuperEditorContentId } from '../SuperEditor/Constants'
import { NoteViewController } from './Controller/NoteViewController'
import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor'
import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths'
import Button from '../Button/Button'
import ModalOverlay from '../Modal/ModalOverlay'
import NoteConflictResolutionModal from './NoteConflictResolutionModal/NoteConflictResolutionModal'
import Icon from '../Icon/Icon'
const MinimumStatusDuration = 400
@@ -74,6 +78,9 @@ type State = {
updateSavingIndicator?: boolean
editorFeatureIdentifier?: string
noteType?: NoteType
conflictedNotes: SNNote[]
showConflictResolutionModal: boolean
}
class NoteView extends AbstractComponent<NoteViewProps, State> {
@@ -84,6 +91,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
private removeTrashKeyObserver?: () => void
private removeComponentStreamObserver?: () => void
private removeNoteStreamObserver?: () => void
private removeComponentManagerObserver?: () => void
private removeInnerNoteObserver?: () => void
@@ -120,6 +128,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
syncTakingTooLong: false,
editorFeatureIdentifier: this.controller.item.editorIdentifier,
noteType: this.controller.item.noteType,
conflictedNotes: [],
showConflictResolutionModal: false,
}
this.noteViewElementRef = createRef<HTMLDivElement>()
@@ -133,6 +143,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.removeComponentStreamObserver?.()
;(this.removeComponentStreamObserver as unknown) = undefined
this.removeNoteStreamObserver?.()
;(this.removeNoteStreamObserver as unknown) = undefined
this.removeInnerNoteObserver?.()
;(this.removeInnerNoteObserver as unknown) = undefined
@@ -418,6 +431,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
await this.reloadStackComponents()
this.debounceReloadEditorComponent()
})
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.Note, async () => {
this.setState({
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
})
})
}
private createComponentViewer(component: SNComponent) {
@@ -776,6 +795,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.setState({ plainEditorFocused: false })
}
toggleConflictResolutionModal = () => {
this.setState((state) => ({
showConflictResolutionModal: !state.showConflictResolutionModal,
}))
}
override render() {
if (this.controller.dealloced) {
return null
@@ -803,6 +828,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
? 'component'
: 'plain'
const shouldShowConflictsButton = this.state.conflictedNotes.length > 0
return (
<div aria-label="Note" className="section editor sn-component h-full md:max-h-full" ref={this.noteViewElementRef}>
{this.note && (
@@ -826,7 +853,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
id="editor-title-bar"
className="content-title-bar section-title-bar z-editor-title-bar w-full bg-default pt-4"
>
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-4 xl:items-center">
<div
className={classNames(
'mb-2 flex flex-wrap justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-4 xl:items-center',
shouldShowConflictsButton ? 'items-center' : 'items-start',
)}
>
<div className={classNames(this.state.noteLocked && 'locked', 'flex flex-grow items-center')}>
<MobileItemsListButton />
<div className="title flex-grow overflow-auto">
@@ -850,6 +882,19 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
updateSavingIndicator={this.state.updateSavingIndicator}
/>
</div>
{shouldShowConflictsButton && (
<Button
className="flex items-center"
primary
colorStyle="warning"
small
onClick={this.toggleConflictResolutionModal}
>
<Icon type="merge" size="small" className="mr-2" />
{this.state.conflictedNotes.length}{' '}
{pluralize(this.state.conflictedNotes.length, 'conflict', 'conflicts')}
</Button>
)}
{renderHeaderOptions && (
<div className="note-view-options-buttons flex items-center gap-3">
<LinkedItemsButton
@@ -979,6 +1024,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
})}
</div>
</div>
<ModalOverlay isOpen={this.state.showConflictResolutionModal} close={this.toggleConflictResolutionModal}>
<NoteConflictResolutionModal
currentNote={this.note}
conflictedNotes={this.state.conflictedNotes}
close={this.toggleConflictResolutionModal}
/>
</ModalOverlay>
</div>
)
}
@@ -0,0 +1,127 @@
import { ContentType, NoteContent, NoteType, SNNote, classNames } from '@standardnotes/snjs'
import { UIEventHandler, useEffect, useMemo, useRef } from 'react'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useApplication } from '../ApplicationProvider'
import ComponentView from '../ComponentView/ComponentView'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { BlocksEditor } from '../SuperEditor/BlocksEditor'
import { BlocksEditorComposer } from '../SuperEditor/BlocksEditorComposer'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
export const ReadonlyNoteContent = ({
note,
content,
showLinkedItems = true,
scrollPos,
shouldSyncScroll,
onScroll,
}: {
note: SNNote
content: NoteContent
showLinkedItems?: boolean
scrollPos?: number
shouldSyncScroll?: boolean
onScroll?: UIEventHandler
}) => {
const application = useApplication()
const linkingController = useLinkingController()
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const componentViewer = useMemo(() => {
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
if (!editorForCurrentNote) {
return undefined
}
const templateNoteForRevision = application.mutator.createTemplateItem(ContentType.Note, note.content) as SNNote
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
componentViewer.setReadonly(true)
componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision
return componentViewer
}, [application.componentManager, application.mutator, note])
useEffect(() => {
return () => {
if (componentViewer) {
application.componentManager.destroyComponentViewer(componentViewer)
}
}
}, [application, componentViewer])
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!shouldSyncScroll) {
return
}
if (!containerRef.current) {
return
}
const scroller = containerRef.current.querySelector('textarea, .ContentEditable__root')
if (!scroller) {
return
}
scroller.scrollTo({
top: scrollPos,
})
}, [scrollPos, shouldSyncScroll])
return (
<div className="flex h-full flex-grow flex-col overflow-hidden" ref={containerRef}>
<div className={classNames('w-full px-4 pt-4 text-base font-bold', componentViewer && 'pb-4')}>
<div className="title">{content.title}</div>
</div>
{showLinkedItems && (
<LinkedItemBubblesContainer
item={note}
linkingController={linkingController}
readonly
className={{ base: 'mt-2 px-4', withToggle: '!mt-1 !pt-0' }}
isCollapsedByDefault={isMobileScreen}
/>
)}
{componentViewer ? (
<div className="component-view">
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
</div>
) : content.noteType === NoteType.Super ? (
<ErrorBoundary>
<div className="w-full flex-grow overflow-hidden overflow-y-auto">
<BlocksEditorComposer readonly initialValue={content.text}>
<BlocksEditor
readonly
className="blocks-editor relative h-full resize-none p-4 text-base focus:shadow-none focus:outline-none"
spellcheck={content.spellcheck}
onScroll={onScroll}
></BlocksEditor>
</BlocksEditorComposer>
</div>
</ErrorBoundary>
) : (
<div className="relative mt-3 min-h-0 flex-grow overflow-hidden">
{content.text.length ? (
<textarea
readOnly={true}
className="font-editor h-full w-full resize-none border-0 bg-default p-4 pt-0 text-editor text-text"
value={content.text}
onScroll={onScroll}
/>
) : (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-passive-0">
Empty note.
</div>
)}
</div>
)}
</div>
)
}
@@ -1,13 +1,10 @@
import { useMemo, FunctionComponent } from 'react'
import { SNApplication, SNNote } from '@standardnotes/snjs'
import { SNApplication, SNNote, classNames } from '@standardnotes/snjs'
import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { calculateReadTime } from './Utils/calculateReadTime'
import { countNoteAttributes } from './Utils/countNoteAttributes'
export const NoteAttributes: FunctionComponent<{
application: SNApplication
note: SNNote
}> = ({ application, note }) => {
export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text])
const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
@@ -19,8 +16,29 @@ export const NoteAttributes: FunctionComponent<{
const editor = application.componentManager.editorForNote(note)
const format = editor?.package_info?.file_type || 'txt'
return {
words,
characters,
paragraphs,
readTime,
dateLastModified,
dateCreated,
format,
}
}
export const NoteAttributes: FunctionComponent<{
application: SNApplication
note: SNNote
className?: string
}> = ({ application, note, className }) => {
const { words, characters, paragraphs, readTime, dateLastModified, dateCreated, format } = useNoteAttributes(
application,
note,
)
return (
<div className="select-text px-3 py-1.5 text-sm font-medium text-neutral lg:text-xs">
<div className={classNames('select-text px-3 py-1.5 text-sm font-medium text-neutral lg:text-xs', className)}>
{typeof words === 'number' && (format === 'txt' || format === 'md') ? (
<>
<div className="mb-1">
@@ -7,13 +7,18 @@ import { getAppRect, getPopoverMaxHeight, getPositionedPopoverRect } from './Uti
const percentOf = (percent: number, value: number) => (percent / 100) * value
export type PopoverCSSProperties = CSSProperties & {
'--translate-x': string
'--translate-y': string
}
const getStylesFromRect = (
rect: DOMRect,
options: {
disableMobileFullscreenTakeover?: boolean
maxHeight?: number | 'none'
},
): CSSProperties => {
): PopoverCSSProperties => {
const { disableMobileFullscreenTakeover = false, maxHeight = 'none' } = options
const canApplyMaxHeight = maxHeight !== 'none' && (!isMobileScreen() || disableMobileFullscreenTakeover)
@@ -22,7 +27,9 @@ const getStylesFromRect = (
return {
willChange: 'transform',
transform: `translate(${shouldApplyMobileWidth ? marginForMobile / 2 : rect.x}px, ${rect.y}px)`,
'--translate-x': `${shouldApplyMobileWidth ? marginForMobile / 2 : rect.x}px`,
'--translate-y': `${rect.y}px`,
transform: 'translate(var(--translate-x), var(--translate-y))',
visibility: 'visible',
...(canApplyMaxHeight && {
maxHeight: `${maxHeight}px`,
@@ -53,7 +60,7 @@ export const getPositionedPopoverStyles = ({
disableMobileFullscreenTakeover,
maxHeightFunction,
offset,
}: Options): CSSProperties | null => {
}: Options): PopoverCSSProperties | null => {
if (!popoverRect || !anchorRect) {
return null
}
@@ -46,6 +46,8 @@ const Popover = ({
disableMobileFullscreenTakeover,
maxHeight,
portal,
offset,
hideOnClickInModal,
}: Props) => {
const popoverId = useRef(UuidGenerator.GenerateUuid())
@@ -126,6 +128,8 @@ const Popover = ({
title={title}
togglePopover={togglePopover}
portal={portal}
offset={offset}
hideOnClickInModal={hideOnClickInModal}
>
{children}
</PositionedPopoverContent>
@@ -1,14 +1,15 @@
import { useDocumentRect } from '@/Hooks/useDocumentRect'
import { useAutoElementRect } from '@/Hooks/useElementRect'
import { classNames } from '@standardnotes/utils'
import { useCallback, useLayoutEffect, useState } from 'react'
import { CSSProperties, useCallback, useLayoutEffect, useState } from 'react'
import Portal from '../Portal/Portal'
import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
import { PopoverCSSProperties, getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
import { PopoverContentProps } from './Types'
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { KeyboardKey } from '@standardnotes/ui-services'
import { getAdjustedStylesForNonPortalPopover } from './Utils/getAdjustedStylesForNonPortal'
const PositionedPopoverContent = ({
align = 'end',
@@ -25,6 +26,8 @@ const PositionedPopoverContent = ({
disableMobileFullscreenTakeover,
maxHeight,
portal = true,
offset,
hideOnClickInModal = false,
}: PopoverContentProps) => {
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const popoverRect = useAutoElementRect(popoverElement)
@@ -47,13 +50,21 @@ const PositionedPopoverContent = ({
side,
disableMobileFullscreenTakeover: disableMobileFullscreenTakeover,
maxHeightFunction: maxHeight,
offset,
})
let adjustedStyles: PopoverCSSProperties | undefined = undefined
if (!portal && popoverElement && styles) {
adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles)
}
usePopoverCloseOnClickOutside({
popoverElement,
anchorElement,
togglePopover,
childPopovers,
hideOnClickInModal,
disabled: disableClickOutside,
})
@@ -83,9 +94,12 @@ const PositionedPopoverContent = ({
isDesktopScreen || disableMobileFullscreenTakeover ? 'invisible' : '',
className,
)}
style={{
...styles,
}}
style={
{
...styles,
...adjustedStyles,
} as CSSProperties
}
ref={setPopoverElement}
data-popover={id}
onKeyDown={(event) => {
@@ -42,6 +42,8 @@ type CommonPopoverProps = {
disableMobileFullscreenTakeover?: boolean
title: string
portal?: boolean
offset?: number
hideOnClickInModal?: boolean
}
export type PopoverContentProps = CommonPopoverProps & {
@@ -95,25 +95,25 @@ export const getPositionedPopoverRect = (
if (side === 'top' || side === 'bottom') {
switch (align) {
case 'start':
positionPopoverRect.x = buttonRect.left - finalOffset
positionPopoverRect.x = buttonRect.left
break
case 'center':
positionPopoverRect.x = buttonRect.left - width / 2 + buttonRect.width / 2 - finalOffset
positionPopoverRect.x = buttonRect.left - width / 2 + buttonRect.width / 2
break
case 'end':
positionPopoverRect.x = buttonRect.right - width + finalOffset
positionPopoverRect.x = buttonRect.right - width
break
}
} else {
switch (align) {
case 'start':
positionPopoverRect.y = buttonRect.top - finalOffset
positionPopoverRect.y = buttonRect.top
break
case 'center':
positionPopoverRect.y = buttonRect.top - height / 2 + buttonRect.height / 2 - finalOffset
positionPopoverRect.y = buttonRect.top - height / 2 + buttonRect.height / 2
break
case 'end':
positionPopoverRect.y = buttonRect.bottom - height + finalOffset
positionPopoverRect.y = buttonRect.bottom - height
break
}
}
@@ -0,0 +1,19 @@
export function getAbsolutePositionedParent(element: HTMLElement | null): HTMLElement | null {
if (!element) {
return null
}
const parent = element.parentElement
if (!parent) {
return null
}
const position = window.getComputedStyle(parent).getPropertyValue('position')
if (position === 'absolute') {
return parent
}
return getAbsolutePositionedParent(parent)
}
@@ -0,0 +1,26 @@
import { PopoverCSSProperties } from '../GetPositionedPopoverStyles'
import { getAbsolutePositionedParent } from './getAbsolutePositionedParent'
export const getAdjustedStylesForNonPortalPopover = (popoverElement: HTMLElement, styles: PopoverCSSProperties) => {
const absoluteParent = getAbsolutePositionedParent(popoverElement)
const translateXProperty = styles?.['--translate-x']
const translateYProperty = styles?.['--translate-y']
const parsedTranslateX = translateXProperty ? parseInt(translateXProperty) : 0
const parsedTranslateY = translateYProperty ? parseInt(translateYProperty) : 0
if (!absoluteParent) {
return styles
}
const parentRect = absoluteParent.getBoundingClientRect()
const adjustedTranslateX = parsedTranslateX - parentRect.left
const adjustedTranslateY = parsedTranslateY - parentRect.top
return {
...styles,
'--translate-x': `${adjustedTranslateX}px`,
'--translate-y': `${adjustedTranslateY}px`,
} as PopoverCSSProperties
}
@@ -6,6 +6,7 @@ type Options = {
togglePopover?: () => void
childPopovers: Set<string>
disabled?: boolean
hideOnClickInModal?: boolean
}
export const usePopoverCloseOnClickOutside = ({
@@ -14,6 +15,7 @@ export const usePopoverCloseOnClickOutside = ({
togglePopover,
childPopovers,
disabled,
hideOnClickInModal = false,
}: Options) => {
useEffect(() => {
const closeIfClickedOutside = (event: MouseEvent) => {
@@ -23,10 +25,15 @@ export const usePopoverCloseOnClickOutside = ({
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
const closestPopoverId = target.closest('[data-popover]')?.getAttribute('data-popover')
const isDescendantOfChildPopover = closestPopoverId && childPopovers.has(closestPopoverId)
const isPopoverInModal = popoverElement?.closest('[data-dialog]')
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[data-dialog]')
const isPopoverInModal = popoverElement?.closest('[data-dialog], .sk-modal')
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[data-dialog], .sk-modal')
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfChildPopover && !isDescendantOfModal) {
if (
!isDescendantOfMenu &&
!isAnchorElement &&
!isDescendantOfChildPopover &&
(!isDescendantOfModal || (isDescendantOfModal && hideOnClickInModal))
) {
if (!disabled) {
togglePopover?.()
}
@@ -39,5 +46,5 @@ export const usePopoverCloseOnClickOutside = ({
document.removeEventListener('click', closeIfClickedOutside, { capture: true })
document.removeEventListener('contextmenu', closeIfClickedOutside, { capture: true })
}
}, [anchorElement, childPopovers, popoverElement, togglePopover, disabled])
}, [anchorElement, childPopovers, popoverElement, togglePopover, disabled, hideOnClickInModal])
}
@@ -1,19 +1,14 @@
import { WebApplication } from '@/Application/WebApplication'
import { ClientDisplayableError, FeatureDescription } from '@standardnotes/snjs'
import { GetFeatures } from '@standardnotes/snjs'
import { makeAutoObservable, observable } from 'mobx'
import { AnyPackageType } from '../Types/AnyPackageType'
export class PackageProvider {
static async load(application: WebApplication): Promise<PackageProvider | undefined> {
const response = await application.getAvailableSubscriptions()
if (!response || response instanceof ClientDisplayableError) {
return undefined
}
static async load(): Promise<PackageProvider | undefined> {
const versionMap: Map<string, string> = new Map()
collectFeatures(response.PLUS_PLAN?.features as FeatureDescription[], versionMap)
collectFeatures(response.PRO_PLAN?.features as FeatureDescription[], versionMap)
GetFeatures().forEach((feature) => {
versionMap.set(feature.identifier, 'Latest')
})
return new PackageProvider(versionMap)
}
@@ -28,13 +23,3 @@ export class PackageProvider {
return this.latestVersionsMap.get(extension.package_info.identifier)
}
}
function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map<string, string>) {
if (features == undefined) {
return
}
for (const feature of features) {
versionMap.set(feature.identifier, 'Latest')
}
}
@@ -67,7 +67,7 @@ export class PreferencesMenu {
}
private loadLatestVersions(): void {
PackageProvider.load(this.application)
PackageProvider.load()
.then((versions) => {
if (versions) {
this._extensionLatestVersions = versions
@@ -1,26 +1,19 @@
import RevisionContentLocked from './RevisionContentLocked'
import SelectedRevisionContent from './SelectedRevisionContent'
import { observer } from 'mobx-react-lite'
import { WebApplication } from '@/Application/WebApplication'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { NoteHistoryController, RevisionContentState } from '@/Controllers/NoteHistory/NoteHistoryController'
import Spinner from '@/Components/Spinner/Spinner'
import { ReadonlyNoteContent } from '../NoteView/ReadonlyNoteContent'
import { SNNote } from '@standardnotes/snjs'
type Props = {
application: WebApplication
noteHistoryController: NoteHistoryController
notesController: NotesController
note: SNNote
subscriptionController: SubscriptionController
}
const HistoryModalContentPane = ({
application,
noteHistoryController,
notesController,
subscriptionController,
}: Props) => {
const { contentState } = noteHistoryController
const HistoryModalContentPane = ({ noteHistoryController, subscriptionController, note }: Props) => {
const { selectedRevision, contentState } = noteHistoryController
switch (contentState) {
case RevisionContentState.Idle:
@@ -32,13 +25,10 @@ const HistoryModalContentPane = ({
case RevisionContentState.Loading:
return <Spinner className="absolute top-1/2 left-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2" />
case RevisionContentState.Loaded:
return (
<SelectedRevisionContent
application={application}
notesController={notesController}
noteHistoryController={noteHistoryController}
/>
)
if (!selectedRevision) {
return null
}
return <ReadonlyNoteContent note={note} content={selectedRevision.payload.content} showLinkedItems={false} />
case RevisionContentState.NotEntitled:
return <RevisionContentLocked subscriptionController={subscriptionController} />
default:
@@ -15,7 +15,6 @@ import MobileModalHeader from '../Modal/MobileModalHeader'
const HistoryModalDialogContent = ({
application,
dismissModal,
notesController,
subscriptionController,
note,
selectionController,
@@ -89,9 +88,8 @@ const HistoryModalDialogContent = ({
)}
>
<HistoryModalContentPane
application={application}
noteHistoryController={noteHistoryController}
notesController={notesController}
note={note}
subscriptionController={subscriptionController}
/>
</div>
@@ -28,7 +28,7 @@ const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionControlle
return (
<div className="flex h-full w-full items-center justify-center">
<div className="max-w-40% flex flex-col items-center text-center">
<div className="max-w-40% flex flex-col items-center px-8 text-center">
<HistoryLockedIllustration />
<div className="mt-2 mb-1 text-lg font-bold">Can't access this version</div>
<div className="leading-140% mb-4 text-passive-0">
@@ -9,7 +9,6 @@ import { useModalAnimation } from '../Modal/useModalAnimation'
const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
application,
historyModalController,
notesController,
selectionController,
subscriptionController,
}) => {
@@ -49,7 +48,6 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
application={application}
dismissModal={historyModalController.dismissModal}
note={historyModalController.note}
notesController={notesController}
selectionController={selectionController}
subscriptionController={subscriptionController}
/>
@@ -1,13 +1,11 @@
import { WebApplication } from '@/Application/WebApplication'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { SNNote } from '@standardnotes/snjs'
type CommonProps = {
application: WebApplication
notesController: NotesController
selectionController: SelectedItemsController
subscriptionController: SubscriptionController
}
@@ -1,93 +0,0 @@
import { WebApplication } from '@/Application/WebApplication'
import { ContentType, NoteType, SNNote } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useEffect, useMemo } from 'react'
import ComponentView from '@/Components/ComponentView/ComponentView'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { BlocksEditor } from '../SuperEditor/BlocksEditor'
import { BlocksEditorComposer } from '../SuperEditor/BlocksEditorComposer'
const ABSOLUTE_CENTER_CLASSNAME = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'
type SelectedRevisionContentProps = {
application: WebApplication
noteHistoryController: NoteHistoryController
notesController: NotesController
}
const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> = ({
application,
noteHistoryController,
notesController,
}) => {
const note = notesController.firstSelectedNote
const { selectedRevision } = noteHistoryController
const componentViewer = useMemo(() => {
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
if (!editorForCurrentNote) {
return undefined
}
const templateNoteForRevision = application.mutator.createTemplateItem(
ContentType.Note,
selectedRevision?.payload.content,
) as SNNote
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
componentViewer.setReadonly(true)
componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision
return componentViewer
}, [application.componentManager, application.mutator, note, selectedRevision?.payload.content])
useEffect(() => {
return () => {
if (componentViewer) {
application.componentManager.destroyComponentViewer(componentViewer)
}
}
}, [application, componentViewer])
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="w-full p-4 text-base font-bold">
<div className="title">{selectedRevision?.payload.content.title}</div>
</div>
{componentViewer ? (
<div className="component-view">
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
</div>
) : note?.noteType === NoteType.Super ? (
<ErrorBoundary>
<div className="w-full flex-grow overflow-hidden overflow-y-auto">
<BlocksEditorComposer readonly initialValue={selectedRevision?.payload.content.text}>
<BlocksEditor
readonly
className="blocks-editor relative h-full resize-none p-4 text-base focus:shadow-none focus:outline-none"
spellcheck={note.spellcheck}
></BlocksEditor>
</BlocksEditorComposer>
</div>
</ErrorBoundary>
) : (
<div className="relative min-h-0 flex-grow overflow-hidden">
{selectedRevision?.payload.content.text.length ? (
<textarea
readOnly={true}
className="font-editor h-full w-full resize-none border-0 bg-default p-4 pt-0 text-editor text-text"
value={selectedRevision?.payload.content.text}
/>
) : (
<div className={`text-passive-0 ${ABSOLUTE_CENTER_CLASSNAME}`}>Empty note.</div>
)}
</div>
)}
</div>
)
}
export default observer(SelectedRevisionContent)
@@ -1,46 +1,61 @@
import { classNames } from '@standardnotes/snjs'
import { ReactNode } from 'react'
import { ReactNode, useState } from 'react'
import { Tooltip, TooltipAnchor, TooltipOptions, useTooltipStore } from '@ariakit/react'
import { Slot } from '@radix-ui/react-slot'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { getPositionedPopoverStyles } from '../Popover/GetPositionedPopoverStyles'
import { getAdjustedStylesForNonPortalPopover } from '../Popover/Utils/getAdjustedStylesForNonPortal'
const StyledTooltip = ({
children,
className,
label,
showOnMobile = false,
showOnHover = true,
...props
}: {
children: ReactNode
label: NonNullable<ReactNode>
className?: string
label: string
showOnMobile?: boolean
showOnHover?: boolean
} & Partial<TooltipOptions>) => {
const [forceOpen, setForceOpen] = useState<boolean | undefined>()
const tooltip = useTooltipStore({
timeout: 350,
open: forceOpen,
})
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
if (isMobile) {
if (isMobile && !showOnMobile) {
return <>{children}</>
}
return (
<>
<TooltipAnchor store={tooltip} as={Slot}>
<TooltipAnchor
onFocus={() => setForceOpen(true)}
onBlur={() => setForceOpen(undefined)}
store={tooltip}
as={Slot}
showOnHover={showOnHover}
>
{children}
</TooltipAnchor>
<Tooltip
autoFocusOnShow={!showOnHover}
store={tooltip}
className={classNames(
'z-tooltip max-w-max rounded border border-border bg-contrast py-1.5 px-3 text-sm text-foreground shadow',
className,
)}
updatePosition={() => {
const { popoverElement, anchorElement } = tooltip.getState()
const { popoverElement, anchorElement, open } = tooltip.getState()
const documentElement = document.querySelector('.main-ui-view')
if (!popoverElement || !anchorElement || !documentElement) {
if (!popoverElement || !anchorElement || !documentElement || !open) {
return
}
@@ -55,10 +70,20 @@ const StyledTooltip = ({
popoverRect,
documentRect,
disableMobileFullscreenTakeover: true,
offset: 6,
offset: props.gutter ? props.gutter : 6,
})
if (!styles) {
return
}
Object.assign(popoverElement.style, styles)
if (!props.portal) {
const adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles)
popoverElement.style.setProperty('--translate-x', adjustedStyles['--translate-x'])
popoverElement.style.setProperty('--translate-y', adjustedStyles['--translate-y'])
}
}}
{...props}
>
@@ -1,4 +1,4 @@
import { FunctionComponent, useCallback, useState } from 'react'
import { FunctionComponent, UIEventHandler, useCallback, useState } from 'react'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
@@ -35,6 +35,7 @@ type BlocksEditorProps = {
spellcheck?: boolean
ignoreFirstChange?: boolean
readonly?: boolean
onScroll?: UIEventHandler
}
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
@@ -45,6 +46,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
spellcheck,
ignoreFirstChange = false,
readonly,
onScroll,
}) => {
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false)
const handleChange = useCallback(
@@ -79,6 +81,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
id={SuperEditorContentId}
className={classNames('ContentEditable__root overflow-y-auto', className)}
spellCheck={spellcheck}
onScroll={onScroll}
/>
<div className="search-highlight-container pointer-events-none absolute top-0 left-0 h-full w-full" />
</div>
@@ -1,5 +1,5 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $getNearestNodeFromDOMNode, TextNode } from 'lexical'
import { $getNearestNodeFromDOMNode, TextNode, $createRangeSelection, $setSelection, $isTextNode } from 'lexical'
import { useCallback, useEffect, useLayoutEffect, useMemo } from 'react'
import { createSearchHighlightElement } from './createSearchHighlightElement'
import { useSuperSearchContext } from './Context'
@@ -285,11 +285,31 @@ export const SearchPlugin = () => {
}
}, [editor])
const selectCurrentResult = useCallback(() => {
if (results.length === 0) {
return
}
const result = results[currentResultIndex]
if (!result) {
return
}
editor.update(() => {
const rangeSelection = $createRangeSelection()
$setSelection(rangeSelection)
const lexicalNode = $getNearestNodeFromDOMNode(result.node)
if ($isTextNode(lexicalNode)) {
lexicalNode.select(result.startIndex, result.endIndex)
}
})
}, [currentResultIndex, editor, results])
return (
<>
<SearchDialog
open={isSearchActive}
closeDialog={() => {
selectCurrentResult()
dispatch({ type: 'toggle-search' })
dispatch({ type: 'reset-search' })
editor.focus()
@@ -1,7 +1,7 @@
import Icon from '@/Components/Icon/Icon'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { SmartView, SystemViewId, isSystemView } from '@standardnotes/snjs'
import { ContentType, SmartView, SystemViewId, isSystemView } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import {
FormEventHandler,
@@ -14,6 +14,7 @@ import {
} from 'react'
import { classNames } from '@standardnotes/utils'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { useApplication } from '../ApplicationProvider'
type Props = {
view: SmartView
@@ -34,6 +35,8 @@ const getIconClass = (view: SmartView, isSelected: boolean): string => {
}
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEditingSmartView }) => {
const application = useApplication()
const [title, setTitle] = useState(view.title || '')
const inputRef = useRef<HTMLInputElement>(null)
@@ -91,6 +94,22 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEdit
const isFaded = false
const iconClass = getIconClass(view, isSelected)
const [conflictsCount, setConflictsCount] = useState(0)
useEffect(() => {
if (view.uuid !== SystemViewId.Conflicts) {
return
}
return application.streamItems(ContentType.Note, () => {
setConflictsCount(application.items.numberOfNotesWithConflicts())
})
}, [application, view])
if (view.uuid === SystemViewId.Conflicts && !conflictsCount) {
return null
}
return (
<>
<div
@@ -138,6 +157,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEdit
<div className={'count text-base lg:text-sm'}>
{view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount}
{view.uuid === SystemViewId.Files && tagsState.allFilesCount}
{view.uuid === SystemViewId.Conflicts && conflictsCount}
</div>
</div>
@@ -109,6 +109,12 @@ export const useListKeyboardNavigation = (
}
}, [setInitialFocus, shouldAutoFocus])
useEffect(() => {
if (listItems.current.length > 0) {
listItems.current[0].tabIndex = 0
}
}, [])
useEffect(() => {
const containerElement = container.current
containerElement?.addEventListener('keydown', keyDownHandler)
@@ -8,8 +8,8 @@ export const setCustomViewportHeight = (height: number, suffix: 'px' | 'vh', for
log(LoggingDomain.Viewport, `setCustomViewportHeight: ${value}`)
document.body.style.height = value
document.documentElement.style.setProperty('--ios-viewport-height', value)
document.body.style.height = 'var(--ios-viewport-height)'
if (forceTriggerResizeEvent) {
window.dispatchEvent(new Event('resize'))
@@ -85,12 +85,8 @@ input:focus {
box-shadow: 0 0 0 1px var(--sn-stylekit-info-color);
}
.sk-button:focus,
button:focus {
box-shadow: 0 0 0 2px var(--sn-stylekit-background-color), 0 0 0 4px var(--sn-stylekit-info-color);
}
.sk-button:focus-visible,
button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--sn-stylekit-background-color), 0 0 0 4px var(--sn-stylekit-info-color);
}
+1
View File
@@ -59,6 +59,7 @@ module.exports = {
minHeight: {
1: '0.25rem',
2: '0.5rem',
11: '2.75rem',
},
maxHeight: {
110: '27.5rem',
+23 -15
View File
@@ -22,36 +22,36 @@ __metadata:
languageName: node
linkType: hard
"@ariakit/core@npm:0.2.0":
version: 0.2.0
resolution: "@ariakit/core@npm:0.2.0"
checksum: f95e3db9ebb3a1b34ed8127f279c8d1a820ad04d42e59cb74b210df1bfb82bc7b5a3a2e965aeb089413015f2c7c573729230eb2e30494669c0a478a721aada3b
"@ariakit/core@npm:0.2.4":
version: 0.2.4
resolution: "@ariakit/core@npm:0.2.4"
checksum: be795380e7c6379af66f4700fc8333c0253a24b4009f714070f20e49d355ea7ee9a7caa84bdfca65ffd4796e950607a2d57c9d75e4dfa69e8076ad7791917684
languageName: node
linkType: hard
"@ariakit/react-core@npm:0.2.1":
version: 0.2.1
resolution: "@ariakit/react-core@npm:0.2.1"
"@ariakit/react-core@npm:0.2.8":
version: 0.2.8
resolution: "@ariakit/react-core@npm:0.2.8"
dependencies:
"@ariakit/core": 0.2.0
"@ariakit/core": 0.2.4
"@floating-ui/dom": ^1.0.0
use-sync-external-store: ^1.2.0
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
checksum: c10ba749afd48f16372bbed161587043dd1f97641b782f159031d4dafce71119b555403f99d71584e121b77b83d1f323578867119b08d779c49c34a22bab92f6
checksum: aeb94f4cb837fa534366f9a36b4fb430b4c46a54e256dbc11f25371ab618131d38877e16886094ecf91d734564cde53f69deb0649d1f413d16e639b55ca4cb8d
languageName: node
linkType: hard
"@ariakit/react@npm:^0.2.1":
version: 0.2.1
resolution: "@ariakit/react@npm:0.2.1"
"@ariakit/react@npm:^0.2.8":
version: 0.2.8
resolution: "@ariakit/react@npm:0.2.8"
dependencies:
"@ariakit/react-core": 0.2.1
"@ariakit/react-core": 0.2.8
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
checksum: 55de2d395fa6a2ae3ef3b383f7559af155e177b9065fe07b8baf8984689bc2ad0bb8531aac1c97ad2ca388c5cf3199b3407ffe3f292533d076bfd0613405d3d3
checksum: 8d974628e8a7ef34bb079d3fb94a5ece3bcf8d2e3bbcbd1f7f597f2599fa35a9a22b8a5acb3dd2863bd5ab590577f58a8be619b14889a5a28bd0bf0999c73dad
languageName: node
linkType: hard
@@ -4760,7 +4760,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@standardnotes/web@workspace:packages/web"
dependencies:
"@ariakit/react": ^0.2.1
"@ariakit/react": ^0.2.8
"@babel/core": "*"
"@babel/plugin-proposal-class-properties": ^7.18.6
"@babel/plugin-transform-react-jsx": ^7.19.0
@@ -4812,6 +4812,7 @@ __metadata:
eslint-config-prettier: ^8.5.0
eslint-plugin-react: ^7.31.11
eslint-plugin-react-hooks: ^4.6.0
fast-diff: ^1.3.0
identity-obj-proxy: ^3.0.0
jest: ^29.3.1
jest-environment-jsdom: ^29.3.1
@@ -11034,6 +11035,13 @@ __metadata:
languageName: node
linkType: hard
"fast-diff@npm:^1.3.0":
version: 1.3.0
resolution: "fast-diff@npm:1.3.0"
checksum: d22d371b994fdc8cce9ff510d7b8dc4da70ac327bcba20df607dd5b9cae9f908f4d1028f5fe467650f058d1e7270235ae0b8230809a262b4df587a3b3aa216c3
languageName: node
linkType: hard
"fast-glob@npm:^2.2.6":
version: 2.2.7
resolution: "fast-glob@npm:2.2.7"