mirror of
https://github.com/standardnotes/app
synced 2026-09-20 21:13:51 -04:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8362c4e92c | ||
|
|
b8481dfbe3 | ||
|
|
046eaac548 | ||
|
|
747ba1b342 | ||
|
|
7c3b8d4534 | ||
|
|
0613436198 | ||
|
|
63b95391a6 | ||
|
|
195420101a | ||
|
|
74ba220982 | ||
|
|
e0e9249334 | ||
|
|
49d43fd14b | ||
|
|
1ff34cf1a2 | ||
|
|
c6d30976e0 | ||
|
|
26a9f8c497 | ||
|
|
d129d51bfe | ||
|
|
6e6402da81 | ||
|
|
0e938c5596 | ||
|
|
db997955af | ||
|
|
9cae641693 | ||
|
|
08c38fc7cf | ||
|
|
153ec272d2 | ||
|
|
7d691aa409 | ||
|
|
9141da0832 | ||
|
|
39414e6cab | ||
|
|
1738485777 | ||
|
|
0cb73b5d18 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.26.10",
|
||||
"version": "1.26.13",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/clipper",
|
||||
"description": "Web clipper browser extension for Standard Notes",
|
||||
"version": "1.1.40",
|
||||
"version": "1.1.50",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.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
|
||||
|
||||
## [3.106.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.53](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.52](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.51](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.106.49",
|
||||
"version": "3.106.59",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.21.38",
|
||||
"version": "1.21.41",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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.28.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.28.46",
|
||||
"version": "1.28.50",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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.15.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.15.8",
|
||||
"version": "1.15.12",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface BackupServiceInterface {
|
||||
getFilesBackupsLocation(): string | undefined
|
||||
openFilesBackupsLocation(): Promise<void>
|
||||
openFileBackup(record: FileBackupRecord): Promise<void>
|
||||
getFileBackupAbsolutePath(record: FileBackupRecord): string
|
||||
getFileBackupAbsolutePath(record: FileBackupRecord): Promise<string>
|
||||
|
||||
isTextBackupsEnabled(): boolean
|
||||
enableTextBackups(): Promise<void>
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [3.53.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.53.53](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.53.52](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.53.51](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.53.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue on Android where pressing backspace in Super notes would delete characters on both side of the cursor ([#2336](https://github.com/standardnotes/app/issues/2336)) ([0cb73b5](https://github.com/standardnotes/app/commit/0cb73b5d18ece035e2fce904c859d548b8657053))
|
||||
|
||||
## [3.53.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.standardnotes;
|
||||
|
||||
import com.reactnativecommunity.webview.RNCWebViewManager;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import android.view.inputmethod.InputConnectionWrapper;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.webkit.WebView;
|
||||
|
||||
@ReactModule(name = CustomWebViewManager.REACT_CLASS)
|
||||
public class CustomWebViewManager extends RNCWebViewManager {
|
||||
/* This name must match what we’re referring to in JS */
|
||||
protected static final String REACT_CLASS = "CustomWebView";
|
||||
|
||||
protected static class CustomWebViewClient extends RNCWebViewClient {}
|
||||
|
||||
protected static class CustomWebView extends RNCWebView {
|
||||
public CustomWebView(ThemedReactContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
InputConnection con = super.onCreateInputConnection(outAttrs);
|
||||
if (con == null) {
|
||||
return null;
|
||||
}
|
||||
return new CustomInputConnection(con, true);
|
||||
}
|
||||
|
||||
private class CustomInputConnection extends InputConnectionWrapper {
|
||||
public CustomInputConnection(InputConnection target, boolean mutable) {
|
||||
super(target, mutable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
||||
// Un-comment if you wish to cancel the backspace:
|
||||
// return false;
|
||||
}
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
// magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace
|
||||
if (beforeLength == 1 && afterLength == 0) {
|
||||
// backspace
|
||||
return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||
&& sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
|
||||
}
|
||||
return super.deleteSurroundingText(beforeLength, afterLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
|
||||
return new CustomWebView(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
|
||||
view.setWebViewClient(new CustomWebViewClient());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.standardnotes;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
public class CustomWebViewPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList(new CustomWebViewManager());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
|
||||
packages.add(new Fido2ApiPackage());
|
||||
packages.add(new CustomWebViewPackage());
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.53.49",
|
||||
"version": "3.53.59",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Button, Keyboard, Platform, Text, View } from 'react-native'
|
||||
import { Button, Keyboard, Platform, requireNativeComponent, Text, View } from 'react-native'
|
||||
import VersionInfo from 'react-native-version-info'
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview'
|
||||
import { OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
|
||||
import { NativeWebViewAndroid, OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
|
||||
import { AndroidBackHandlerService } from './AndroidBackHandlerService'
|
||||
import { AppStateObserverService } from './AppStateObserverService'
|
||||
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
|
||||
import { MobileDevice, MobileDeviceEvent } from './Lib/MobileDevice'
|
||||
import { IsDev } from './Lib/Utils'
|
||||
|
||||
const CustomWebView: NativeWebViewAndroid = requireNativeComponent('CustomWebView')
|
||||
|
||||
const LoggingEnabled = IsDev
|
||||
|
||||
export const MobileWebAppContainer = () => {
|
||||
@@ -354,6 +356,12 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
* This is needed to prevent the keyboard from pushing the webview up and down when it appears and disappears.
|
||||
*/
|
||||
scrollEnabled={false}
|
||||
overScrollMode="never"
|
||||
nativeConfig={Platform.select({
|
||||
android: {
|
||||
component: CustomWebView,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.45.5",
|
||||
"version": "1.46.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -189,6 +189,17 @@ export abstract class Collection<
|
||||
this.conflictMap.establishRelationship(conflictOf, element.uuid)
|
||||
}
|
||||
|
||||
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) {
|
||||
this.conflictMap.removeFromMap(element.uuid)
|
||||
}
|
||||
|
||||
this.referenceMap.setAllRelationships(
|
||||
element.uuid,
|
||||
element.references.map((r) => r.uuid),
|
||||
@@ -203,6 +214,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 +274,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import { FilterDisplayOptions } from './DisplayOptions'
|
||||
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 +75,9 @@ export function computeFiltersForDisplayOptions(
|
||||
filters.push((item) => itemMatchesQuery(item, query, collection))
|
||||
}
|
||||
|
||||
if (!viewsPredicate?.keypathIncludesString('conflict_of')) {
|
||||
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',
|
||||
}
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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.4.322](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.321](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.320](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.319](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.318](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.317](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.317",
|
||||
"version": "1.4.327",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -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.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.59.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **desktop:** Fixed issue where local backup location would default to "[object Promise]" ([7d691aa](https://github.com/standardnotes/app/commit/7d691aa409af3636ad795491b8b817d9eb7a980d))
|
||||
|
||||
## [1.59.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.59.10",
|
||||
"version": "1.60.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('backup service', () => {
|
||||
device = {} as jest.Mocked<FileBackupsDevice>
|
||||
device.getFileBackupReadToken = jest.fn()
|
||||
device.readNextChunk = jest.fn()
|
||||
device.joinPaths = jest.fn()
|
||||
|
||||
session = {} as jest.Mocked<SessionsClientInterface>
|
||||
|
||||
@@ -84,6 +85,7 @@ describe('backup service', () => {
|
||||
history,
|
||||
internalEventBus,
|
||||
)
|
||||
backupService.getFilesBackupsLocation = jest.fn().mockReturnValue('/')
|
||||
|
||||
crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
|
||||
state: {},
|
||||
|
||||
@@ -147,9 +147,10 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
private async automaticallyEnableTextBackupsIfPreferenceNotSet(): Promise<void> {
|
||||
if (this.storage.getValue(StorageKey.TextBackupsEnabled) == undefined) {
|
||||
this.storage.setValue(StorageKey.TextBackupsEnabled, true)
|
||||
const location = `${await this.device.getUserDocumentsDirectory()}/${this.prependWorkspacePathForPath(
|
||||
TextBackupsDirectoryName,
|
||||
)}`
|
||||
const location = await this.device.joinPaths(
|
||||
await this.device.getUserDocumentsDirectory(),
|
||||
await this.prependWorkspacePathForPath(TextBackupsDirectoryName),
|
||||
)
|
||||
this.storage.setValue(StorageKey.TextBackupsLocation, location)
|
||||
}
|
||||
}
|
||||
@@ -378,13 +379,16 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
return record
|
||||
}
|
||||
|
||||
public getFileBackupAbsolutePath(record: FileBackupRecord): string {
|
||||
public getFileBackupAbsolutePath(record: FileBackupRecord): Promise<string> {
|
||||
const location = this.getFilesBackupsLocation()
|
||||
return `${location}/${record.relativePath}`
|
||||
if (!location) {
|
||||
throw new ClientDisplayableError('No files backups location set')
|
||||
}
|
||||
return this.device.joinPaths(location, record.relativePath)
|
||||
}
|
||||
|
||||
public async openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||
const location = this.getFileBackupAbsolutePath(record)
|
||||
const location = await this.getFileBackupAbsolutePath(record)
|
||||
await this.device.openLocation(location)
|
||||
}
|
||||
|
||||
@@ -459,7 +463,13 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
const path = `${this.getFilesBackupsLocation()}/${fileBackup.relativePath}/${fileBackup.binaryFileName}`
|
||||
const fileBackupsLocation = this.getFilesBackupsLocation()
|
||||
|
||||
if (!fileBackupsLocation) {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
const path = await this.device.joinPaths(fileBackupsLocation, fileBackup.relativePath, fileBackup.binaryFileName)
|
||||
const token = await this.device.getFileBackupReadToken(path)
|
||||
|
||||
let readMore = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -167,4 +167,6 @@ export interface ItemsClientInterface {
|
||||
predicate: PredicateInterface<T>,
|
||||
iconString?: string,
|
||||
): Promise<SmartView>
|
||||
|
||||
numberOfNotesWithConflicts(): number
|
||||
}
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.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
|
||||
|
||||
## [2.168.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.168.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)
|
||||
|
||||
**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,8 @@ export class ItemManager
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
numberOfNotesWithConflicts(): number {
|
||||
return this.collection.numberOfItemsWithConflicts()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.168.18",
|
||||
"version": "2.169.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/styles",
|
||||
"version": "1.7.8",
|
||||
"version": "1.8.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.windows-web,
|
||||
.windows-desktop,
|
||||
.linux-web,
|
||||
.linux-desktop {
|
||||
.linux-desktop,
|
||||
.force-custom-scrollbar {
|
||||
$thumb-width: 4px;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/toast",
|
||||
"version": "1.3.31",
|
||||
"version": "1.3.32",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.26.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.26.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.26.12",
|
||||
"version": "1.26.17",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/utils",
|
||||
"version": "1.16.5",
|
||||
"version": "1.17.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,56 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [3.161.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.161.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-02)
|
||||
|
||||
### Features
|
||||
|
||||
* You can now drag-n-drop notes onto tags to move them from one tag to another ([#2334](https://github.com/standardnotes/app/issues/2334)) ([08c38fc](https://github.com/standardnotes/app/commit/08c38fc7cfbce96c5de269073f5f0117000066a8))
|
||||
|
||||
## [3.160.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **desktop:** Fixed issue where local backup location would default to "[object Promise]" ([7d691aa](https://github.com/standardnotes/app/commit/7d691aa409af3636ad795491b8b817d9eb7a980d))
|
||||
|
||||
## [3.160.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where pane gestures on Android would break scroll in a daily journal tag ([39414e6](https://github.com/standardnotes/app/commit/39414e6caba4830192a54ea9553bcda36959c9bf))
|
||||
|
||||
## [3.160.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue on Android where pressing backspace in Super notes would delete characters on both side of the cursor ([#2336](https://github.com/standardnotes/app/issues/2336)) ([0cb73b5](https://github.com/standardnotes/app/commit/0cb73b5d18ece035e2fce904c859d548b8657053))
|
||||
|
||||
## [3.160.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
@@ -1,5 +1,130 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"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)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.161.1",
|
||||
"title": "[3.161.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-06)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.161.0",
|
||||
"title": "[3.161.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-02)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* You can now drag-n-drop notes onto tags to move them from one tag to another ([#2334](https://github.com/standardnotes/app/issues/2334)) ([08c38fc](https://github.com/standardnotes/app/commit/08c38fc7cfbce96c5de269073f5f0117000066a8))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"You can now drag-n-drop notes onto tags to move them from one tag to another (#2334) (08c38fc)"
|
||||
],
|
||||
"Features": [
|
||||
"You can now drag-n-drop notes onto tags to move them from one tag to another (#2334) (08c38fc)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.160.19",
|
||||
"title": "[3.160.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-31)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* **desktop:** Fixed issue where local backup location would default to \"[object Promise]\" ([7d691aa](https://github.com/standardnotes/app/commit/7d691aa409af3636ad795491b8b817d9eb7a980d))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"desktop: Fixed issue where local backup location would default to \"object Promise]\" ([7d691aa)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"desktop: Fixed issue where local backup location would default to \"object Promise]\" ([7d691aa)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.160.18",
|
||||
"title": "[3.160.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-30)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where pane gestures on Android would break scroll in a daily journal tag ([39414e6](https://github.com/standardnotes/app/commit/39414e6caba4830192a54ea9553bcda36959c9bf))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue where pane gestures on Android would break scroll in a daily journal tag (39414e6)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue where pane gestures on Android would break scroll in a daily journal tag (39414e6)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.160.17",
|
||||
"title": "[3.160.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-29)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue on Android where pressing backspace in Super notes would delete characters on both side of the cursor ([#2336](https://github.com/standardnotes/app/issues/2336)) ([0cb73b5](https://github.com/standardnotes/app/commit/0cb73b5d18ece035e2fce904c859d548b8657053))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue on Android where pressing backspace in Super notes would delete characters on both side of the cursor (#2336) (0cb73b5)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue on Android where pressing backspace in Super notes would delete characters on both side of the cursor (#2336) (0cb73b5)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.160.16",
|
||||
"title": "[3.160.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-26)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.160.16",
|
||||
"version": "3.162.2",
|
||||
"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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -18,7 +18,11 @@ import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
import { PreferenceMode } from './PreferenceMode'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat'
|
||||
import dayjsUTC from 'dayjs/plugin/utc'
|
||||
import dayjsTimezone from 'dayjs/plugin/timezone'
|
||||
dayjs.extend(dayjsAdvancedFormat)
|
||||
dayjs.extend(dayjsUTC)
|
||||
dayjs.extend(dayjsTimezone)
|
||||
import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { NoteTitleFormatOptions } from './NoteTitleFormatOptions'
|
||||
|
||||
@@ -13,6 +13,8 @@ import { ListItemTitle } from './ListItemTitle'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
|
||||
import { NoteDragDataFormat } from '../Tags/DragNDrop'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
application,
|
||||
@@ -70,9 +72,38 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
|
||||
const hasOffsetBorder = !isNextItemTiled
|
||||
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
const dragPreview = useRef<HTMLDivElement>()
|
||||
|
||||
const createDragPreview = () => {
|
||||
if (!listItemRef.current) {
|
||||
throw new Error('List item ref is not set')
|
||||
}
|
||||
|
||||
const element = listItemRef.current.cloneNode(true)
|
||||
// Only keep icon & title in drag preview
|
||||
Array.from(element.childNodes[1].childNodes).forEach((node, key) => {
|
||||
if (key !== 0) {
|
||||
node.remove()
|
||||
}
|
||||
})
|
||||
element.childNodes[2].remove()
|
||||
if (element instanceof HTMLDivElement) {
|
||||
element.style.width = `${listItemRef.current.clientWidth}px`
|
||||
element.style.position = 'absolute'
|
||||
element.style.top = '0'
|
||||
element.style.left = '0'
|
||||
element.style.zIndex = '-100000'
|
||||
document.body.appendChild(element)
|
||||
dragPreview.current = element
|
||||
}
|
||||
return element as HTMLDivElement
|
||||
}
|
||||
|
||||
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}`,
|
||||
@@ -81,6 +112,23 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
)}
|
||||
id={item.uuid}
|
||||
onClick={onClick}
|
||||
draggable={!isMobileScreen}
|
||||
onDragStart={(event) => {
|
||||
if (!listItemRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const { dataTransfer } = event
|
||||
|
||||
const element = createDragPreview()
|
||||
dataTransfer.setDragImage(element, 0, 0)
|
||||
dataTransfer.setData(NoteDragDataFormat, item.uuid)
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
if (dragPreview.current) {
|
||||
dragPreview.current.remove()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="mr-0 flex flex-col items-center justify-between gap-2 p-4 pr-4">
|
||||
|
||||
+10
-1
@@ -9,11 +9,20 @@ export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }>
|
||||
const application = useApplication()
|
||||
|
||||
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
|
||||
const [backupAbsolutePath, setBackupAbsolutePath] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
|
||||
}, [application, file])
|
||||
|
||||
useEffect(() => {
|
||||
if (!backupInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
void application.fileBackups?.getFileBackupAbsolutePath(backupInfo).then(setBackupAbsolutePath)
|
||||
}, [backupInfo, application])
|
||||
|
||||
const openFileBackup = useCallback(() => {
|
||||
if (backupInfo) {
|
||||
void application.fileBackups?.openFileBackup(backupInfo)
|
||||
@@ -35,7 +44,7 @@ export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }>
|
||||
>
|
||||
<div className="ml-2">
|
||||
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
|
||||
<div className="text-xs text-neutral">{application.fileBackups?.getFileBackupAbsolutePath(backupInfo)}</div>
|
||||
<div className="text-xs text-neutral">{backupAbsolutePath}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
+52
-26
@@ -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
|
||||
|
||||
+96
@@ -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>
|
||||
)
|
||||
}
|
||||
+128
@@ -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>
|
||||
)
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
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 { NoteContent } from './NoteContent'
|
||||
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) => (
|
||||
<NoteContent
|
||||
note={note}
|
||||
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
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { ContentType, NoteType, SNNote } 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 NoteContent = ({
|
||||
note,
|
||||
scrollPos,
|
||||
shouldSyncScroll,
|
||||
onScroll,
|
||||
}: {
|
||||
note: SNNote
|
||||
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="w-full px-4 pt-4 text-base font-bold">
|
||||
<div className="title">{note.title}</div>
|
||||
</div>
|
||||
<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>
|
||||
) : note?.noteType === NoteType.Super ? (
|
||||
<ErrorBoundary>
|
||||
<div className="w-full flex-grow overflow-hidden overflow-y-auto">
|
||||
<BlocksEditorComposer readonly initialValue={note.text}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
className="blocks-editor relative h-full resize-none p-4 text-base focus:shadow-none focus:outline-none"
|
||||
spellcheck={note.spellcheck}
|
||||
onScroll={onScroll}
|
||||
></BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<div className="relative mt-3 min-h-0 flex-grow overflow-hidden">
|
||||
{note.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={note.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>
|
||||
)
|
||||
}
|
||||
@@ -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,37 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
await this.reloadStackComponents()
|
||||
this.debounceReloadEditorComponent()
|
||||
})
|
||||
|
||||
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(
|
||||
ContentType.Note,
|
||||
async ({ inserted, changed, removed }) => {
|
||||
const insertedOrChanged = inserted.concat(changed)
|
||||
|
||||
for (const note of insertedOrChanged) {
|
||||
if (note.conflictOf === this.note.uuid && !note.trashed) {
|
||||
this.setState((state) => ({
|
||||
conflictedNotes: state.conflictedNotes
|
||||
.filter((conflictedNote) => conflictedNote.uuid !== note.uuid)
|
||||
.concat([note]),
|
||||
}))
|
||||
} else {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
conflictedNotes: state.conflictedNotes.filter((conflictedNote) => conflictedNote.uuid !== note.uuid),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const note of removed) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
conflictedNotes: state.conflictedNotes.filter((conflictedNote) => conflictedNote.uuid !== note.uuid),
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private createComponentViewer(component: SNComponent) {
|
||||
@@ -776,6 +820,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 +853,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 +878,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 +907,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 +1049,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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -83,6 +83,7 @@ export const usePaneSwipeGesture = (
|
||||
let moveTimestamp = startTimestamp
|
||||
let closestScrollContainer: HTMLElement | null
|
||||
let scrollContainerAxis: 'x' | 'y' | null = null
|
||||
let scrollContainerInitialOverflowY: string | null = null
|
||||
let canceled = false
|
||||
|
||||
const TouchMoveThreshold = requiresStartFromEdge ? 25 : 45
|
||||
@@ -105,6 +106,7 @@ export const usePaneSwipeGesture = (
|
||||
startTimestamp = Date.now()
|
||||
moveTimestamp = startTimestamp
|
||||
scrollContainerAxis = null
|
||||
scrollContainerInitialOverflowY = null
|
||||
canceled = false
|
||||
|
||||
const touch = event.touches[0]
|
||||
@@ -121,6 +123,7 @@ export const usePaneSwipeGesture = (
|
||||
|
||||
closestScrollContainer = getScrollParent(event.target as HTMLElement)
|
||||
if (closestScrollContainer) {
|
||||
scrollContainerInitialOverflowY = closestScrollContainer.style.overflowY
|
||||
closestScrollContainer.addEventListener('scroll', scrollListener, supportsPassive ? { passive: true } : false)
|
||||
|
||||
if (closestScrollContainer.scrollWidth > closestScrollContainer.clientWidth) {
|
||||
@@ -249,7 +252,9 @@ export const usePaneSwipeGesture = (
|
||||
const touchEndListener = () => {
|
||||
if (closestScrollContainer) {
|
||||
closestScrollContainer.removeEventListener('scroll', scrollListener)
|
||||
closestScrollContainer.style.overflowY = ''
|
||||
if (closestScrollContainer.style.overflowY === 'hidden') {
|
||||
closestScrollContainer.style.overflowY = scrollContainerInitialOverflowY || ''
|
||||
}
|
||||
}
|
||||
|
||||
if (canceled) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
+9
-2
@@ -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) => {
|
||||
@@ -26,7 +28,12 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
const isPopoverInModal = popoverElement?.closest('[data-dialog]')
|
||||
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[data-dialog]')
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
+6
-21
@@ -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
-1
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-52
@@ -4,7 +4,7 @@ import useModal from '../../Lexical/Hooks/useModal'
|
||||
import { InsertTableDialog } from '../../Plugins/TablePlugin'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { $getSelection, $isRangeSelection, DELETE_CHARACTER_COMMAND, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { GetAlignmentBlocks } from '../Blocks/Alignment'
|
||||
@@ -23,14 +23,11 @@ import { GetPasswordBlock } from '../Blocks/Password'
|
||||
import { GetQuoteBlock } from '../Blocks/Quote'
|
||||
import { GetTableBlock } from '../Blocks/Table'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { classNames, Platform } from '@standardnotes/snjs'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
||||
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||
import { SKAlert } from '@standardnotes/styles'
|
||||
|
||||
const DontShowSuperAndroidBackspaceAlertKey = 'dontShowSuperAndroidBackspaceAlert'
|
||||
|
||||
const MobileToolbarPlugin = () => {
|
||||
const application = useApplication()
|
||||
@@ -40,7 +37,6 @@ const MobileToolbarPlugin = () => {
|
||||
const [isInEditor, setIsInEditor] = useState(false)
|
||||
const [isInToolbar, setIsInToolbar] = useState(false)
|
||||
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
const isAndroid = application.platform === Platform.Android
|
||||
|
||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -204,38 +200,6 @@ const MobileToolbarPlugin = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAndroid) {
|
||||
return
|
||||
}
|
||||
|
||||
const dontShowAgain = application.getValue(DontShowSuperAndroidBackspaceAlertKey)
|
||||
|
||||
if (dontShowAgain) {
|
||||
return
|
||||
}
|
||||
|
||||
const alert = new SKAlert({
|
||||
title: 'Android backspace issue',
|
||||
text: 'There is a known issue with Super on Android where pressing the backspace will also delete the character after the cursor. We are working on a fix for this. Please use the backspace button in the toolbar as a workaround.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'OK',
|
||||
style: 'default',
|
||||
},
|
||||
{
|
||||
text: "Don't show again",
|
||||
style: 'default',
|
||||
action: () => {
|
||||
application.setValue(DontShowSuperAndroidBackspaceAlertKey, true)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
alert.present()
|
||||
}, [application, isAndroid])
|
||||
|
||||
const isFocusInEditorOrToolbar = isInEditor || isInToolbar
|
||||
if (!isMobile || !isFocusInEditorOrToolbar) {
|
||||
return null
|
||||
@@ -269,20 +233,6 @@ const MobileToolbarPlugin = () => {
|
||||
>
|
||||
<Icon type="keyboard-close" size="medium" />
|
||||
</button>
|
||||
{isAndroid && (
|
||||
<button
|
||||
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border py-3 px-3"
|
||||
aria-label="Backspace"
|
||||
ref={backspaceButtonRef}
|
||||
onClick={() => {
|
||||
editor.update(() => {
|
||||
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Icon type="backspace" size="medium" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
+21
-1
@@ -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 +1,2 @@
|
||||
export const TagDragDataFormat = 'application/x-sn-drag-tag'
|
||||
export const NoteDragDataFormat = 'application/x-sn-drag-note'
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FOCUSABLE_BUT_NOT_TABBABLE, TAG_FOLDERS_FEATURE_NAME } from '@/Constant
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { IconType, SNTag } from '@standardnotes/snjs'
|
||||
import { IconType, SNNote, SNTag } from '@standardnotes/snjs'
|
||||
import { computed } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
@@ -23,8 +23,9 @@ import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { TagListSectionType } from './TagListSection'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { TagDragDataFormat } from './DragNDrop'
|
||||
import { NoteDragDataFormat, TagDragDataFormat } from './DragNDrop'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
tag: SNTag
|
||||
@@ -40,7 +41,9 @@ const PADDING_BASE_PX = 14
|
||||
const PADDING_PER_LEVEL_PX = 21
|
||||
|
||||
export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
({ tag, type, features, navigationController: navigationController, level, onContextMenu, linkingController }) => {
|
||||
({ tag, type, features, navigationController, level, onContextMenu, linkingController }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [title, setTitle] = useState(tag.title || '')
|
||||
const [subtagTitle, setSubtagTitle] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -200,7 +203,10 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
)
|
||||
|
||||
const onDragEnter: DragEventHandler<HTMLDivElement> = useCallback((event): void => {
|
||||
if (event.dataTransfer.types.includes(TagDragDataFormat)) {
|
||||
if (
|
||||
event.dataTransfer.types.includes(TagDragDataFormat) ||
|
||||
event.dataTransfer.types.includes(NoteDragDataFormat)
|
||||
) {
|
||||
event.preventDefault()
|
||||
setIsBeingDraggedOver(true)
|
||||
}
|
||||
@@ -211,30 +217,42 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
}, [])
|
||||
|
||||
const onDragOver: DragEventHandler<HTMLDivElement> = useCallback((event): void => {
|
||||
if (event.dataTransfer.types.includes(TagDragDataFormat)) {
|
||||
if (
|
||||
event.dataTransfer.types.includes(TagDragDataFormat) ||
|
||||
event.dataTransfer.types.includes(NoteDragDataFormat)
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onDrop: DragEventHandler<HTMLDivElement> = useCallback(
|
||||
(event): void => {
|
||||
async (event) => {
|
||||
setIsBeingDraggedOver(false)
|
||||
const draggedTagUuid = event.dataTransfer.getData(TagDragDataFormat)
|
||||
if (!draggedTagUuid) {
|
||||
return
|
||||
}
|
||||
if (!navigationController.isValidTagParent(tag, { uuid: draggedTagUuid } as SNTag)) {
|
||||
return
|
||||
}
|
||||
if (!hasFolders) {
|
||||
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
|
||||
return
|
||||
}
|
||||
const draggedNoteUuid = event.dataTransfer.getData(NoteDragDataFormat)
|
||||
if (draggedTagUuid) {
|
||||
if (!navigationController.isValidTagParent(tag, { uuid: draggedTagUuid } as SNTag)) {
|
||||
return
|
||||
}
|
||||
if (!hasFolders) {
|
||||
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
void navigationController.assignParent(draggedTagUuid, tag.uuid)
|
||||
return
|
||||
} else if (draggedNoteUuid) {
|
||||
const currentTag = navigationController.selected
|
||||
const shouldSwapTags = currentTag instanceof SNTag && currentTag.uuid !== tag.uuid
|
||||
const note = application.items.findSureItem<SNNote>(draggedNoteUuid)
|
||||
await linkingController.linkItems(note, tag)
|
||||
if (shouldSwapTags) {
|
||||
await linkingController.unlinkItems(note, currentTag)
|
||||
}
|
||||
return
|
||||
}
|
||||
},
|
||||
[hasFolders, navigationController, premiumModal, tag],
|
||||
[application.items, hasFolders, linkingController, navigationController, premiumModal, tag],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ module.exports = {
|
||||
minHeight: {
|
||||
1: '0.25rem',
|
||||
2: '0.5rem',
|
||||
11: '2.75rem',
|
||||
},
|
||||
maxHeight: {
|
||||
110: '27.5rem',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user