Compare commits

...
Author SHA1 Message Date
StandardNotes CI 8755001f7e chore(release): publish
- @standardnotes/[email protected].9
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2022-11-23 02:16:46 +00:00
Mo 7c2e832065 feat: display file backup status in file context menu (#2044) (skip e2e)
* feat: show file backup status in context menu

* feat: show backup status in list cell

* feat: mapping cache

* feat: add to linking menu + date format

* fix: types
2022-11-22 19:58:48 -06:00
Aman Harwara 096d82f7af fix: lazy load embedded files in super editor (#2043) 2022-11-23 00:55:20 +05:30
Aman Harwara 8c8f045b9a fix: super editor popover menus (#2041) 2022-11-22 19:16:59 +05:30
StandardNotes CI b3140f1623 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].1
2022-11-22 10:15:26 +00:00
Karol Sójko c5e104f90b fix(snjs): add handling errors on creating websocket connection 2022-11-22 10:47:54 +01:00
StandardNotes CI 09b8f00eed chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].0
2022-11-19 15:17:36 +00:00
Mo c39c72da7a feat: ability to drag super list items; secure password generation blocks (#2039)
* feat: ability to drag list item nodes

* fix: issue where editor focus would scroll to bottom

* fix: improve drag icon and prevent from interfering with selection

* fix(super): add 'current' as keyword for bringing up date block options

* fix(super): issue with autocomplete menu width on large screens

* feat(super): ability to generate secure random passwords
2022-11-19 08:53:30 -06:00
StandardNotes CI 7f28876047 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-11-19 03:21:08 +00:00
Mo 24ff87c306 fix: improved error reporting in super; improved draggable block icon padding 2022-11-18 20:54:03 -06:00
StandardNotes CI e8b758214d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-18 21:15:30 +00:00
Mo e58c563795 fix: issue where converting empty plain note to super would result in error 2022-11-18 14:53:18 -06:00
StandardNotes CI 328bbb797d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].0
2022-11-18 16:10:59 +00:00
Aman Harwara dab4f678f0 feat(web): enable block drag'n'drop in super editor (#2029) 2022-11-18 21:15:18 +05:30
StandardNotes CI c847cc1d16 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2022-11-18 15:30:47 +00:00
Mo f49ba6bd4d feat: keyboard shortcuts for primary actions (#2030) 2022-11-18 09:01:48 -06:00
StandardNotes CI 0309912f98 chore(release): publish
- @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-11-18 12:15:29 +00:00
Mo 0c253dc46a chore: make models package public for consumption by server 2022-11-18 05:51:08 -06:00
StandardNotes CI 4b081890fd chore(release): publish
- @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-18 04:18:24 +00:00
Mo 87f86693a6 fix(snjs): keep apply payload timestamp when using keep base conflict strategy (#2031) 2022-11-17 21:51:06 -06:00
StandardNotes CI bc12663c7f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-11-17 17:25:29 +00:00
Mo f793673875 feat: click saving indicator to toggle visibility (#2028) 2022-11-17 10:57:23 -06:00
175 changed files with 2472 additions and 978 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/api
## [1.20.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/api
## [1.20.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/api
## [1.20.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.20.6",
"version": "1.20.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -1,4 +1,4 @@
import { Invitation } from '@standardnotes/models'
import { type Invitation } from '@standardnotes/models'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
@@ -1,5 +1,5 @@
import { ProtocolVersion, UserRequestType } from '@standardnotes/common'
import { RootKeyParamsInterface } from '@standardnotes/models'
import { type RootKeyParamsInterface } from '@standardnotes/models'
import { UserDeletionResponse } from '../../Response/User/UserDeletionResponse'
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
@@ -1,4 +1,4 @@
import { RootKeyParamsInterface } from '@standardnotes/models'
import { type RootKeyParamsInterface } from '@standardnotes/models'
import { UserRequestType } from '@standardnotes/common'
import { ErrorMessage } from '../../Error/ErrorMessage'
@@ -1,5 +1,5 @@
import { UserRequestType, Uuid } from '@standardnotes/common'
import { RootKeyParamsInterface } from '@standardnotes/models'
import { type RootKeyParamsInterface } from '@standardnotes/models'
import { UserDeletionResponse } from '../../Response/User/UserDeletionResponse'
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
@@ -1,4 +1,4 @@
import { Invitation } from '@standardnotes/models'
import { type Invitation } from '@standardnotes/models'
export type SubscriptionInviteListResponseBody = {
invitations: Array<Invitation>
@@ -1,4 +1,4 @@
import { Invitation } from '@standardnotes/models'
import { type Invitation } from '@standardnotes/models'
import { ApiVersion } from '../../Api'
import { HttpServiceInterface } from '../../Http'
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.7.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
### Features
* ability to drag super list items; secure password generation blocks ([#2039](https://github.com/standardnotes/app/issues/2039)) ([c39c72d](https://github.com/standardnotes/app/commit/c39c72da7a4fb85f4da9aa4e6f8e9f7ba4486a94))
## [1.6.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
### Bug Fixes
* improved error reporting in super; improved draggable block icon padding ([24ff87c](https://github.com/standardnotes/app/commit/24ff87c30625432393ffedeada14478ebb625292))
## [1.6.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Bug Fixes
* issue where converting empty plain note to super would result in error ([e58c563](https://github.com/standardnotes/app/commit/e58c5637951f928efcd8c8427f06d0b6c69b5e55))
# [1.6.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Features
* **web:** enable block drag'n'drop in super editor ([#2029](https://github.com/standardnotes/app/issues/2029)) ([dab4f67](https://github.com/standardnotes/app/commit/dab4f678f04aee2c7dfbe7f416e7fc34e8d870cf))
## [1.5.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/blocks-editor
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/blocks-editor",
"version": "1.5.3",
"version": "1.7.0",
"private": true,
"main": "./src/index.ts",
"scripts": {
@@ -31,8 +31,6 @@ import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugi
import {truncateString} from './Utils';
import {SuperEditorContentId} from './Constants';
const BlockDragEnabled = false;
type BlocksEditorProps = {
onChange: (value: string, preview: string) => void;
className?: string;
@@ -69,8 +67,14 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
});
previewText = truncateString(previewText, previewLength);
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
onChange(stringifiedEditorState, previewText);
try {
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
onChange(stringifiedEditorState, previewText);
} catch (error) {
window.alert(
`An invalid change was made inside the Super editor. Your change was not saved. Please report this error to the team: ${error}`,
);
}
});
},
[onChange, didIgnoreFirstChange],
@@ -94,7 +98,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<div className="editor" ref={onRef}>
<ContentEditable
id={SuperEditorContentId}
className={`ContentEditable__root ${className}`}
className={`ContentEditable__root overflow-y-auto ${className}`}
spellCheck={spellcheck}
/>
</div>
@@ -130,11 +134,9 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin />
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
</>
)}
{floatingAnchorElem && BlockDragEnabled && (
<>{<DraggableBlockPlugin anchorElem={floatingAnchorElem} />}</>
)}
</>
);
};
@@ -5,7 +5,7 @@ import {BlockEditorNodes} from '../Lexical/Nodes/AllNodes';
import {Klass, LexicalNode} from 'lexical';
type BlocksEditorComposerProps = {
initialValue: string;
initialValue: string | undefined;
children: React.ReactNode;
nodes?: Array<Klass<LexicalNode>>;
readonly?: boolean;
@@ -1,19 +1,21 @@
.draggable-block-menu {
border-radius: 4px;
padding: 2px 1px;
padding: 3px 1px;
cursor: grab;
opacity: 0;
position: absolute;
left: 0;
top: 0;
will-change: transform;
transition: opacity 0.3s;
}
.draggable-block-menu .icon {
width: 16px;
height: 16px;
opacity: 0.3;
background-image: url(#{$blocks-editor-icons-path}/draggable-block-menu.svg);
width: 0.8rem;
height: 1.1rem;
opacity: 0.2;
padding-left: 4.75px;
padding-top: 2px;
}
.draggable-block-menu:active {
@@ -21,16 +23,17 @@
}
.draggable-block-menu:hover {
background-color: #efefef;
background-color: var(--sn-stylekit-contrast-background-color);
}
.draggable-block-target-line {
pointer-events: none;
background: deepskyblue;
height: 4px;
background: var(--sn-stylekit-info-color);
height: 0.25rem;
position: absolute;
left: 0;
top: 0;
opacity: 0;
will-change: transform;
will-change: transform, opacity;
transition: opacity 0.15s;
}
@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*
*/
import {$createListNode, $isListNode} from '@lexical/list';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {eventFiles} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils';
@@ -19,16 +20,17 @@ import {
} from 'lexical';
import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {BlockIcon} from '@standardnotes/icons';
import {isHTMLElement} from '../../Utils/guard';
import {Point} from '../../Utils/point';
import {Rect} from '../../Utils/rect';
import {ContainsPointReturn, Rect} from '../../Utils/rect';
const SPACE = 4;
const TARGET_LINE_HALF_HEIGHT = 2;
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
const TEXT_BOX_HORIZONTAL_PADDING = 28;
const TEXT_BOX_HORIZONTAL_PADDING = 24;
const Downward = 1;
const Upward = -1;
@@ -52,12 +54,56 @@ function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
return root ? root.__children : [];
}
function elementContainingEventLocation(
anchorElem: HTMLElement,
element: HTMLElement,
event: MouseEvent,
): {contains: ContainsPointReturn; element: HTMLElement} {
const anchorElementRect = anchorElem.getBoundingClientRect();
const eventLocation = new Point(event.x, event.y);
const elementDomRect = Rect.fromDOM(element);
const {marginTop, marginBottom} = window.getComputedStyle(element);
const rect = elementDomRect.generateNewRect({
bottom: elementDomRect.bottom + parseFloat(marginBottom),
left: anchorElementRect.left,
right: anchorElementRect.right,
top: elementDomRect.top - parseFloat(marginTop),
});
const children = Array.from(element.children);
const shouldRecurseIntoChildren = ['UL', 'OL', 'LI'].includes(
element.tagName,
);
if (shouldRecurseIntoChildren) {
for (const child of children) {
const isLeaf = child.children.length === 0;
if (isLeaf) {
continue;
}
const childResult = elementContainingEventLocation(
anchorElem,
child as HTMLElement,
event,
);
if (childResult.contains.result) {
return childResult;
}
}
}
return {contains: rect.contains(eventLocation), element: element};
}
function getBlockElement(
anchorElem: HTMLElement,
editor: LexicalEditor,
event: MouseEvent,
): HTMLElement | null {
const anchorElementRect = anchorElem.getBoundingClientRect();
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
let blockElem: HTMLElement | null = null;
@@ -72,32 +118,22 @@ function getBlockElement(
if (elem === null) {
break;
}
const point = new Point(event.x, event.y);
const domRect = Rect.fromDOM(elem);
const {marginTop, marginBottom} = window.getComputedStyle(elem);
const {contains, element} = elementContainingEventLocation(
anchorElem,
elem,
event,
);
const rect = domRect.generateNewRect({
bottom: domRect.bottom + parseFloat(marginBottom),
left: anchorElementRect.left,
right: anchorElementRect.right,
top: domRect.top - parseFloat(marginTop),
});
const {
result,
reason: {isOnTopSide, isOnBottomSide},
} = rect.contains(point);
if (result) {
blockElem = elem;
if (contains.result) {
blockElem = element;
prevIndex = index;
break;
}
if (direction === Indeterminate) {
if (isOnTopSide) {
if (contains.reason.isOnTopSide) {
direction = Upward;
} else if (isOnBottomSide) {
} else if (contains.reason.isOnBottomSide) {
direction = Downward;
} else {
// stop search block element
@@ -123,7 +159,6 @@ function setMenuPosition(
) {
if (!targetElem) {
floatingElem.style.opacity = '0';
floatingElem.style.transform = 'translate(-10000px, -10000px)';
return;
}
@@ -185,13 +220,12 @@ function setTargetLine(
targetLineElem.style.width = `${
anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2
}px`;
targetLineElem.style.opacity = '.4';
targetLineElem.style.opacity = '.6';
}
function hideTargetLine(targetLineElem: HTMLElement | null) {
if (targetLineElem) {
targetLineElem.style.opacity = '0';
targetLineElem.style.transform = 'translate(-10000px, -10000px)';
}
}
@@ -283,18 +317,30 @@ function useDraggableBlockMenu(
return false;
}
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
if (!targetNode) {
return false;
}
if (targetNode === draggedNode) {
return true;
}
let nodeToInsert = draggedNode;
const targetParent = targetNode.getParent();
const sourceParent = draggedNode.getParent();
if ($isListNode(sourceParent) && !$isListNode(targetParent)) {
const newList = $createListNode(sourceParent.getListType());
newList.append(draggedNode);
nodeToInsert = newList;
}
const {top, height} = targetBlockElem.getBoundingClientRect();
const shouldInsertAfter = pageY - top > height / 2;
if (shouldInsertAfter) {
targetNode.insertAfter(draggedNode);
targetNode.insertAfter(nodeToInsert);
} else {
targetNode.insertBefore(draggedNode);
targetNode.insertBefore(nodeToInsert);
}
setDraggableBlockElem(null);
@@ -347,7 +393,9 @@ function useDraggableBlockMenu(
draggable={true}
onDragStart={onDragStart}
onDragEnd={onDragEnd}>
<div className={isEditable ? 'icon' : ''} />
<div className={isEditable ? 'icon' : ''}>
<BlockIcon className="text-text pointer-events-none" />
</div>
</div>
<div className="draggable-block-target-line" ref={targetLineRef} />
</>,
@@ -110,8 +110,8 @@ export function InsertTableDialog({
return (
<>
<TextInput label="No of rows" onChange={setRows} value={rows} />
<TextInput label="No of columns" onChange={setColumns} value={columns} />
<TextInput label="Number of rows" onChange={setRows} value={rows} />
<TextInput label="Number of columns" onChange={setColumns} value={columns} />
<DialogActions data-test-id="table-model-confirm-insert">
<Button onClick={onClick}>Confirm</Button>
</DialogActions>
@@ -13,13 +13,13 @@
padding-left: 15px;
padding-right: 15px;
border: 0px;
background-color: #eee;
border-radius: 5px;
background-color: var(--sn-stylekit-contrast-background-color);
cursor: pointer;
font-size: 14px;
}
.Button__root:hover {
background-color: #ddd;
background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
}
.Button__small {
padding-top: 5px;
@@ -32,5 +32,5 @@
cursor: not-allowed;
}
.Button__disabled:hover {
background-color: #eee;
background-color: var(--sn-stylekit-secondary-background-color);
}
@@ -17,16 +17,17 @@
display: flex;
flex: 1;
color: #666;
margin-right: 20px;
}
.Input__input {
display: flex;
flex: 2;
border: 1px solid #999;
border: 1px solid var(--sn-stylekit-contrast-border-color);
background-color: var(--sn-stylekit-contrast-background-color);
padding-top: 7px;
padding-bottom: 7px;
padding-left: 10px;
padding-right: 10px;
font-size: 16px;
border-radius: 5px;
min-width: 0;
}
@@ -17,7 +17,7 @@
bottom: 0px;
left: 0px;
right: 0px;
background-color: rgba(40, 40, 40, 0.6);
background-color: rgba(0, 0, 0, 0.7);
flex-grow: 0px;
flex-shrink: 1px;
z-index: 100;
@@ -28,22 +28,23 @@
min-width: 300px;
display: flex;
flex-grow: 0px;
background-color: #fff;
background-color: var(--sn-stylekit-background-color);
flex-direction: column;
position: relative;
box-shadow: 0 0 20px 0 #444;
border-radius: 10px;
box-shadow: 0 0px 0 var(--sn-stylekit-shadow-color);
border-radius: 0px;
}
.Modal__title {
color: #444;
color:var(--sn-stylekit-foreground-color);
margin: 0px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
padding-bottom: 15px;
border-bottom: 1px solid var(--sn-stylekit-border-color);
}
.Modal__closeButton {
border: 0px;
position: absolute;
right: 20px;
top: 15px;
border-radius: 20px;
justify-content: center;
align-items: center;
@@ -52,10 +53,11 @@
height: 30px;
text-align: center;
cursor: pointer;
background-color: #eee;
background-color: var(--sn-stylekit-contrast-background-color);
}
.Modal__closeButton:hover {
background-color: #ddd;
background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
}
.Modal__content {
padding-top: 20px;
@@ -73,7 +73,7 @@ function PortalImpl({
aria-label="Close modal"
type="button"
onClick={onClose}>
X
</button>
<div className="Modal__content">{children}</div>
</div>
@@ -7,7 +7,7 @@
*/
import {isPoint, Point} from './point';
type ContainsPointReturn = {
export type ContainsPointReturn = {
result: boolean;
reason: {
isOnTopSide: boolean;
+42
View File
@@ -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.101.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
### Features
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
## [3.100.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/desktop
## [3.100.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/desktop
@@ -1,4 +1,4 @@
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { FileBackupRecord, FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { AppState } from 'app/AppState'
import { shell } from 'electron'
import { StoreKeys } from '../Store/StoreKeys'
@@ -120,6 +120,10 @@ export class FilesBackupManager implements FileBackupsDevice {
return this.defaultMappingFileValue()
}
for (const entry of Object.values(data.files)) {
entry.backedUpOn = new Date(entry.backedUpOn)
}
return data
}
@@ -129,6 +133,10 @@ export class FilesBackupManager implements FileBackupsDevice {
void shell.openPath(location)
}
async openFileBackup(record: FileBackupRecord): Promise<void> {
void shell.openPath(record.absolutePath)
}
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getMappingFileLocation(), file)
@@ -5,7 +5,7 @@ import { StoreKeys } from '../Store/StoreKeys'
const path = require('path')
const rendererPath = path.join('file://', __dirname, '/renderer.js')
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { FileBackupsDevice, FileBackupsMapping, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
import { app, BrowserWindow } from 'electron'
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
import { KeychainInterface } from '../Keychain/KeychainInterface'
@@ -64,6 +64,7 @@ export class RemoteBridge implements CrossProcessBridge {
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
openFileBackup: this.openFileBackup.bind(this),
}
}
@@ -202,4 +203,8 @@ export class RemoteBridge implements CrossProcessBridge {
public openFilesBackupsLocation(): Promise<void> {
return this.fileBackups.openFilesBackupsLocation()
}
public openFileBackup(record: FileBackupRecord): Promise<void> {
return this.fileBackups.openFileBackup(record)
}
}
@@ -3,6 +3,7 @@ import {
Environment,
FileBackupsMapping,
RawKeychainValue,
FileBackupRecord,
} from '@web/Application/Device/DesktopSnjsExports'
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
import { Component } from '../Main/Packages/PackageManagerInterface'
@@ -132,6 +133,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
return this.remoteBridge.openFilesBackupsLocation()
}
openFileBackup(record: FileBackupRecord): Promise<void> {
return this.remoteBridge.openFileBackup(record)
}
async saveFilesBackupsFile(
uuid: string,
metaFile: string,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.100.9",
"version": "3.101.0",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/encryption
## [1.19.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/encryption
## [1.19.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/encryption
## [1.19.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.19.14",
"version": "1.19.17",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.26.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
### Features
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
## [1.25.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.25.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.25.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/filepicker
+4 -11
View File
@@ -1,24 +1,17 @@
{
"name": "@standardnotes/filepicker",
"version": "1.25.14",
"version": "1.26.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
"description": "Web filepicker for Standard Notes projects",
"main": "dist/index.js",
"main": "./src/index.ts",
"author": "Standard Notes",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"types": "./src/index.ts",
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
"clean": "rm -fr dist",
"prestart": "yarn clean",
"start": "tsc -p tsconfig.json --watch",
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"build": "echo 'Empty build script required for yarn topological install'",
"lint": "eslint src --ext .ts",
"test": "jest"
},
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
"extends": "../../UILib.tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"rootDir": "./src",
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.13.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
### Features
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
## [1.12.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/files
## [1.12.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/files
## [1.12.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/files
+5 -9
View File
@@ -1,26 +1,22 @@
{
"name": "@standardnotes/files",
"version": "1.12.14",
"version": "1.13.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
"description": "Client-side files library",
"main": "dist/index.js",
"main": "./src/index.ts",
"author": "Standard Notes",
"types": "dist/index.d.ts",
"types": "./src/index.ts",
"files": [
"dist"
],
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
"clean": "rm -fr dist",
"prestart": "yarn clean",
"start": "tsc -p tsconfig.json --watch",
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint src --ext .ts",
"test": "jest"
"test": "jest",
"build": "echo 'Empty build script required for yarn topological install'"
},
"devDependencies": {
"@types/jest": "^29.2.3",
@@ -1,5 +1,5 @@
import { Uuid } from '@standardnotes/common'
import { FileBackupsMapping } from './FileBackupsMapping'
import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping'
export interface FileBackupsDevice {
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
@@ -18,4 +18,5 @@ export interface FileBackupsDevice {
changeFilesBackupsLocation(): Promise<string | undefined>
getFilesBackupsLocation(): Promise<string>
openFilesBackupsLocation(): Promise<void>
openFileBackup(record: FileBackupRecord): Promise<void>
}
@@ -1,17 +1,16 @@
import { Uuid } from '@standardnotes/common'
import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'
export type FileBackupRecord = {
backedUpOn: Date
absolutePath: string
relativePath: string
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
version: typeof FileBackupsConstantsV1.Version
}
export interface FileBackupsMapping {
version: typeof FileBackupsConstantsV1.Version
files: Record<
Uuid,
{
backedUpOn: Date
absolutePath: string
relativePath: string
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
version: typeof FileBackupsConstantsV1.Version
}
>
files: Record<Uuid, FileBackupRecord>
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
"extends": "../../UILib.tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"rootDir": "./src",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.6.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
### Features
* ability to drag super list items; secure password generation blocks ([#2039](https://github.com/standardnotes/app/issues/2039)) ([c39c72d](https://github.com/standardnotes/app/commit/c39c72da7a4fb85f4da9aa4e6f8e9f7ba4486a94))
# [1.5.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Features
* **web:** enable block drag'n'drop in super editor ([#2029](https://github.com/standardnotes/app/issues/2029)) ([dab4f67](https://github.com/standardnotes/app/commit/dab4f678f04aee2c7dfbe7f416e7fc34e8d870cf))
## [1.4.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-16)
**Note:** Version bump only for package @standardnotes/icons
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/icons",
"version": "1.4.10",
"version": "1.6.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
+7
View File
@@ -0,0 +1,7 @@
<svg width="9px" height="14px" viewBox="0 0 9 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2" fill="currentColor" fill-rule="nonzero">
<path d="M1.6666,3.33333 C1.2246,3.33333 0.8007,3.15774 0.4881,2.84518 C0.1755,2.53262 0,2.1087 0,1.66667 C0,1.22464 0.1755,0.80072 0.4881,0.48816 C0.8007,0.1756 1.2246,0 1.6666,0 C2.1086,0 2.5326,0.1756 2.8451,0.48816 C3.1577,0.80072 3.3333,1.22464 3.3333,1.66667 C3.3333,2.1087 3.1577,2.53262 2.8451,2.84518 C2.5326,3.15774 2.1086,3.33333 1.6666,3.33333 Z M1.6666,8.33333 C1.2246,8.33333 0.8007,8.15773 0.4881,7.84513 C0.1755,7.53263 0,7.10873 0,6.66663 C0,6.22463 0.1755,5.80073 0.4881,5.48813 C0.8007,5.17563 1.2246,5.00003 1.6666,5.00003 C2.1086,5.00003 2.5326,5.17563 2.8451,5.48813 C3.1577,5.80073 3.3333,6.22463 3.3333,6.66663 C3.3333,7.10873 3.1577,7.53263 2.8451,7.84513 C2.5326,8.15773 2.1086,8.33333 1.6666,8.33333 Z M1.6666,13.33333 C1.2246,13.33333 0.8007,13.15773 0.4881,12.84513 C0.1755,12.53263 0,12.10873 0,11.66663 C0,11.22463 0.1755,10.80073 0.4881,10.48813 C0.8007,10.17563 1.2246,10.00003 1.6666,10.00003 C2.1086,10.00003 2.5326,10.17563 2.8451,10.48813 C3.1577,10.80073 3.3333,11.22463 3.3333,11.66663 C3.3333,12.10873 3.1577,12.53263 2.8451,12.84513 C2.5326,13.15773 2.1086,13.33333 1.6666,13.33333 L1.6666,13.33333 Z M6.6666,3.33333 C6.2246,3.33333 5.8007,3.15774 5.4881,2.84518 C5.1755,2.53262 5,2.1087 5,1.66667 C5,1.22464 5.1755,0.80072 5.4881,0.48816 C5.8007,0.1756 6.2246,0 6.6666,0 C7.1086,0 7.5326,0.1756 7.8451,0.48816 C8.1577,0.80072 8.3333,1.22464 8.3333,1.66667 C8.3333,2.1087 8.1577,2.53262 7.8451,2.84518 C7.5326,3.15774 7.1086,3.33333 6.6666,3.33333 Z M6.6666,8.33333 C6.2246,8.33333 5.8007,8.15773 5.4881,7.84513 C5.1755,7.53263 5,7.10873 5,6.66663 C5,6.22463 5.1755,5.80073 5.4881,5.48813 C5.8007,5.17563 6.2246,5.00003 6.6666,5.00003 C7.1086,5.00003 7.5326,5.17563 7.8451,5.48813 C8.1577,5.80073 8.3333,6.22463 8.3333,6.66663 C8.3333,7.10873 8.1577,7.53263 7.8451,7.84513 C7.5326,8.15773 7.1086,8.33333 6.6666,8.33333 Z M6.6666,13.33333 C6.2246,13.33333 5.8007,13.15773 5.4881,12.84513 C5.1755,12.53263 5,12.10873 5,11.66663 C5,11.22463 5.1755,10.80073 5.4881,10.48813 C5.8007,10.17563 6.2246,10.00003 6.6666,10.00003 C7.1086,10.00003 7.5326,10.17563 7.8451,10.48813 C8.1577,10.80073 8.3333,11.22463 8.3333,11.66663 C8.3333,12.10873 8.1577,12.53263 7.8451,12.84513 C7.5326,13.15773 7.1086,13.33333 6.6666,13.33333 L6.6666,13.33333 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+2
View File
@@ -21,6 +21,7 @@ import AttachmentFileIcon from './ic-attachment-file.svg'
import AuthenticatorIcon from './ic-authenticator.svg'
import AuthenticatorVariantIcon from './ic-authenticator-variant.svg'
import BackIosIcon from './ic-back-ios.svg'
import BlockIcon from './ic-block.svg'
import BlueDotIcon from './blue-dot.svg'
import BoldIcon from './ic-bold.svg'
import BoxFilledIcon from './ic-box-filled.svg'
@@ -224,6 +225,7 @@ export {
AuthenticatorIcon,
AuthenticatorVariantIcon,
BackIosIcon,
BlockIcon,
BlueDotIcon,
BoldIcon,
BoxFilledIcon,
+2
View File
@@ -27,6 +27,7 @@ import TypeSubscript from './type-subscript.svg'
import TypeSuperscript from './type-superscript.svg'
import TypeUnderline from './type-underline.svg'
import LexicalPencilFill from './pencil-fill.svg'
import LexicalDraggableBlockMenu from './draggable-block-menu.svg'
export {
LexicalCaretRightFill,
@@ -58,4 +59,5 @@ export {
TypeSuperscript,
TypeUnderline,
LexicalPencilFill,
LexicalDraggableBlockMenu,
}
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.46.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/mobile
## [3.46.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.46.19",
"version": "3.46.29",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/models
## [1.37.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/models
## [1.37.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Bug Fixes
* **snjs:** keep apply payload timestamp when using keep base conflict strategy ([#2031](https://github.com/standardnotes/app/issues/2031)) ([87f8669](https://github.com/standardnotes/app/commit/87f86693a67a36972f0e44a883b18a7129c36893))
## [1.37.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/models
+11 -4
View File
@@ -1,16 +1,23 @@
{
"name": "@standardnotes/models",
"version": "1.37.3",
"version": "1.37.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
"private": true,
"publishConfig": {
"access": "public"
},
"description": "Models used in SNJS library",
"author": "Standard Notes",
"main": "./src/index.ts",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "AGPL-3.0-or-later",
"scripts": {
"tsc": "tsc -p tsconfig.json",
"clean": "rm -fr dist",
"prestart": "yarn clean",
"start": "tsc -p tsconfig.json --watch",
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint src --ext .ts",
"test": "jest"
},
@@ -99,4 +99,22 @@ describe('conflict delta', () => {
expect(delta.getConflictStrategy()).toBe(ConflictStrategy.KeepApply)
})
it('if keep base strategy, always use the apply payloads updated_at_timestamp', () => {
const basePayload = createDecryptedItemsKey('123', 'secret', 2)
const baseCollection = createBaseCollection(basePayload)
const applyPayload = createDecryptedItemsKey('123', 'other secret', 1)
const delta = new ConflictDelta(baseCollection, basePayload, applyPayload, historyMap)
expect(delta.getConflictStrategy()).toBe(ConflictStrategy.KeepBaseDuplicateApply)
const result = delta.result()
expect(result.emits).toHaveLength(1)
expect(result.emits[0].updated_at_timestamp).toEqual(applyPayload.updated_at_timestamp)
})
})
@@ -1,4 +1,4 @@
import { greaterOfTwoDates, uniqCombineObjArrays } from '@standardnotes/utils'
import { uniqCombineObjArrays } from '@standardnotes/utils'
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { CreateDecryptedItemFromPayload, CreateItemFromPayload } from '../../Utilities/Item/ItemGenerator'
import { HistoryMap, historyMapFunctions } from '../History/HistoryMap'
@@ -114,9 +114,9 @@ export class ConflictDelta {
}
private handleKeepBaseStrategy(): SyncResolvedPayload[] {
const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt)
const updatedAt = this.applyPayload.serverUpdatedAt
const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp)
const updatedAtTimestamp = this.applyPayload.updated_at_timestamp
const leftPayload = this.basePayload.copyAsSyncResolved(
{
@@ -146,9 +146,9 @@ export class ConflictDelta {
}
private handleKeepBaseDuplicateApplyStrategy(): SyncResolvedPayload[] {
const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt)
const updatedAt = this.applyPayload.serverUpdatedAt
const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp)
const updatedAtTimestamp = this.applyPayload.updated_at_timestamp
const leftPayload = this.basePayload.copyAsSyncResolved(
{
@@ -201,9 +201,9 @@ export class ConflictDelta {
'content_type',
])
const updatedAt = greaterOfTwoDates(this.basePayload.serverUpdatedAt, this.applyPayload.serverUpdatedAt)
const updatedAt = this.applyPayload.serverUpdatedAt
const updatedAtTimestamp = Math.max(this.basePayload.updated_at_timestamp, this.applyPayload.updated_at_timestamp)
const updatedAtTimestamp = this.applyPayload.updated_at_timestamp
const payload = this.basePayload.copyAsSyncResolved(
{
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../UILib.tsconfig.json",
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"rootDir": "./src",
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.255](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.254](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.253](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.252](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.251](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.250](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.249](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.248](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.247](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.246](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.245](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.3.245",
"version": "1.3.255",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.46.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
### Features
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
## [1.45.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/services
## [1.45.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/services
## [1.45.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.45.4",
"version": "1.46.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -2,7 +2,13 @@ import { ContentType, Uuid } from '@standardnotes/common'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files'
import {
FilesApiInterface,
FileBackupMetadataFile,
FileBackupsDevice,
FileBackupsMapping,
FileBackupRecord,
} from '@standardnotes/files'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AbstractService } from '../Service/AbstractService'
@@ -11,6 +17,7 @@ import { StatusServiceInterface } from '../Status/StatusServiceInterface'
export class FilesBackupService extends AbstractService {
private itemsObserverDisposer: () => void
private pendingFiles = new Set<Uuid>()
private mappingCache?: FileBackupsMapping['files']
constructor(
private items: ItemManagerInterface,
@@ -75,8 +82,30 @@ export class FilesBackupService extends AbstractService {
return this.device.openFilesBackupsLocation()
}
private async getBackupsMapping(): Promise<FileBackupsMapping['files']> {
return (await this.device.getFilesBackupsMappingFile()).files
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files']> {
const result = (await this.device.getFilesBackupsMappingFile()).files
this.mappingCache = result
return result
}
private invalidateMappingCache(): void {
this.mappingCache = undefined
}
private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files']> {
return this.mappingCache ?? (await this.getBackupsMappingFromDisk())
}
public async getFileBackupInfo(file: FileItem): Promise<FileBackupRecord | undefined> {
const mapping = await this.getBackupsMappingFromCache()
const record = mapping[file.uuid]
return record
}
public async openFileBackup(record: FileBackupRecord): Promise<void> {
await this.device.openFileBackup(record)
}
private async handleChangedFiles(files: FileItem[]): Promise<void> {
@@ -88,7 +117,7 @@ export class FilesBackupService extends AbstractService {
return
}
const mapping = await this.getBackupsMapping()
const mapping = await this.getBackupsMappingFromDisk()
for (const file of files) {
if (this.pendingFiles.has(file.uuid)) {
@@ -105,6 +134,8 @@ export class FilesBackupService extends AbstractService {
this.pendingFiles.delete(file.uuid)
}
}
this.invalidateMappingCache()
}
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
+20
View File
@@ -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.
## [2.151.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/snjs
## [2.151.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
### Bug Fixes
* **snjs:** add handling errors on creating websocket connection ([c5e104f](https://github.com/standardnotes/app/commit/c5e104f90bcd0d494b9d4adc0b27f2f942dfe71c))
## [2.151.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/snjs
## [2.151.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Bug Fixes
* **snjs:** keep apply payload timestamp when using keep base conflict strategy ([#2031](https://github.com/standardnotes/app/issues/2031)) ([87f8669](https://github.com/standardnotes/app/commit/87f86693a67a36972f0e44a883b18a7129c36893))
## [2.151.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
### Bug Fixes
@@ -70,6 +70,11 @@ export class SNWebSocketsService extends AbstractService<WebSocketsServiceEvent,
private async createWebSocketConnectionToken(): Promise<string | undefined> {
try {
const response = await this.webSocketApiService.createConnectionToken()
if (response.data.error) {
console.error(response.data.error)
return undefined
}
return response.data.token
} catch (error) {
@@ -885,6 +885,38 @@ describe('online conflict handling', function () {
await this.sharedFinalAssertions()
})
it('conflict where server updated_at_timestamp is less than base updated_at should not result in infinite loop', async function () {
/**
* While this shouldn't happen, I've seen this happen locally where a single UserPrefs object has a timestamp of A
* on the server, and A + 10 on the client side. Somehow the client had a newer timestamp than the server. The
* server rejects any change if the timestamp is not exactly equal. When we use the KeepBase strategy during conflict
* resolution, we keep the base item, but give it the timestamp of the server item, so that the server accepts it.
* However, RemoteDataConflict would only take the server's timestamp if it was greater than the base's timestamp.
* Because this was not the case, the client kept sending up its own base timestamp and the server kept rejecting it,
* and it never resolved. The fix made here was to take the server's timestamp no matter what, even if it is less than client's.
*/
const note = await Factory.createSyncedNote(this.application)
this.expectedItemCount++
/** First modify the item without saving so that our local contents digress from the server's */
await this.application.mutator.changeItem(note, (mutator) => {
mutator.title = `${Math.random()}`
})
const modified = note.payload.copy({
updated_at_timestamp: note.payload.updated_at_timestamp + 1,
content: {
...note.content,
title: Math.random(),
},
dirty: true,
})
this.expectedItemCount++
await this.application.itemManager.emitItemFromPayload(modified)
await this.application.sync.sync()
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
await this.sharedFinalAssertions()
})
it('conflicting should not over resolve', async function () {
/**
* Before refactoring to use dirtyIndex instead of dirtiedDate, sometimes an item could be dirtied
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.151.5",
"version": "2.151.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-16)
**Note:** Version bump only for package @standardnotes/toast
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/toast",
"version": "1.3.9",
"version": "1.3.11",
"private": true,
"main": "./src/index.ts",
"scripts": {
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.14.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.14.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.14.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/ui-services
# [1.14.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Features
* keyboard shortcuts for primary actions ([#2030](https://github.com/standardnotes/app/issues/2030)) ([f49ba6b](https://github.com/standardnotes/app/commit/f49ba6bd4d38e4026bd88630014126a6e0086351))
## [1.13.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.13.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.13.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.13.16",
"version": "1.14.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
-212
View File
@@ -1,212 +0,0 @@
import { removeFromArray } from '@standardnotes/utils'
export enum KeyboardKey {
Tab = 'Tab',
Backspace = 'Backspace',
Up = 'ArrowUp',
Down = 'ArrowDown',
Left = 'ArrowLeft',
Right = 'ArrowRight',
Enter = 'Enter',
Escape = 'Escape',
Home = 'Home',
End = 'End',
}
export enum KeyboardModifier {
Shift = 'Shift',
Ctrl = 'Control',
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = 'Meta',
Alt = 'Alt',
}
enum KeyboardKeyEvent {
Down = 'KeyEventDown',
Up = 'KeyEventUp',
}
type KeyboardObserver = {
key?: KeyboardKey | string
modifiers?: KeyboardModifier[]
onKeyDown?: (event: KeyboardEvent) => void
onKeyUp?: (event: KeyboardEvent) => void
element?: HTMLElement
elements?: HTMLElement[]
notElement?: HTMLElement
notElementIds?: string[]
notTags?: string[]
}
export class IOService {
readonly activeModifiers = new Set<KeyboardModifier>()
private observers: KeyboardObserver[] = []
constructor(private isMac: boolean) {
window.addEventListener('keydown', this.handleKeyDown)
window.addEventListener('keyup', this.handleKeyUp)
window.addEventListener('blur', this.handleWindowBlur)
}
public deinit() {
this.observers.length = 0
window.removeEventListener('keydown', this.handleKeyDown)
window.removeEventListener('keyup', this.handleKeyUp)
window.removeEventListener('blur', this.handleWindowBlur)
;(this.handleKeyDown as unknown) = undefined
;(this.handleKeyUp as unknown) = undefined
;(this.handleWindowBlur as unknown) = undefined
}
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
if (!modifier) {
return
}
switch (modifier) {
case KeyboardModifier.Meta: {
if (this.isMac) {
this.activeModifiers.add(modifier)
}
break
}
case KeyboardModifier.Ctrl: {
if (!this.isMac) {
this.activeModifiers.add(modifier)
}
break
}
default: {
this.activeModifiers.add(modifier)
break
}
}
}
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
if (!modifier) {
return
}
this.activeModifiers.delete(modifier)
}
public cancelAllKeyboardModifiers = (): void => {
this.activeModifiers.clear()
}
public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
this.addActiveModifier(modifier)
}
public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
this.removeActiveModifier(modifier)
}
private handleKeyDown = (event: KeyboardEvent): void => {
this.updateAllModifiersFromEvent(event)
this.notifyObserver(event, KeyboardKeyEvent.Down)
}
private handleKeyUp = (event: KeyboardEvent): void => {
this.updateAllModifiersFromEvent(event)
this.notifyObserver(event, KeyboardKeyEvent.Up)
}
private updateAllModifiersFromEvent(event: KeyboardEvent): void {
for (const modifier of Object.values(KeyboardModifier)) {
if (event.getModifierState(modifier)) {
this.addActiveModifier(modifier)
} else {
this.removeActiveModifier(modifier)
}
}
}
handleWindowBlur = (): void => {
for (const modifier of this.activeModifiers) {
this.activeModifiers.delete(modifier)
}
}
modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] {
const allModifiers = Object.values(KeyboardModifier)
const eventModifiers = allModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches =
((event.ctrlKey || event.key === KeyboardModifier.Ctrl) && modifier === KeyboardModifier.Ctrl) ||
((event.metaKey || event.key === KeyboardModifier.Meta) && modifier === KeyboardModifier.Meta) ||
((event.altKey || event.key === KeyboardModifier.Alt) && modifier === KeyboardModifier.Alt) ||
((event.shiftKey || event.key === KeyboardModifier.Shift) && modifier === KeyboardModifier.Shift)
return matches
})
return eventModifiers
}
private eventMatchesKeyAndModifiers(
event: KeyboardEvent,
key: KeyboardKey | string | undefined,
modifiers: KeyboardModifier[] = [],
): boolean {
const eventModifiers = this.modifiersForEvent(event)
if (eventModifiers.length !== modifiers.length) {
return false
}
for (const modifier of modifiers) {
if (!eventModifiers.includes(modifier)) {
return false
}
}
// Modifers match, check key
if (!key) {
return true
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() === event.key.toLowerCase()
}
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
const target = event.target as HTMLElement
for (const observer of this.observers) {
if (observer.element && event.target !== observer.element) {
continue
}
if (observer.elements && !observer.elements.includes(target)) {
continue
}
if (observer.notElement && observer.notElement === event.target) {
continue
}
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
continue
}
if (observer.notTags && observer.notTags.includes(target.tagName)) {
continue
}
if (this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp
if (callback) {
callback(event)
}
}
}
}
addKeyObserver(observer: KeyboardObserver): () => void {
this.observers.push(observer)
const thislessObservers = this.observers
return () => {
removeFromArray(thislessObservers, observer)
}
}
}
@@ -0,0 +1,12 @@
import { KeyboardCommand } from './KeyboardCommands'
export type KeyboardCommandHandler = {
command: KeyboardCommand
onKeyDown?: (event: KeyboardEvent) => boolean | void
onKeyUp?: (event: KeyboardEvent) => boolean | void
element?: HTMLElement
elements?: HTMLElement[]
notElement?: HTMLElement
notElementIds?: string[]
notTags?: string[]
}
@@ -0,0 +1,26 @@
export type KeyboardCommand = symbol
function createKeyboardCommand(type: string): KeyboardCommand {
return Symbol(type)
}
export const TOGGLE_LIST_PANE_KEYBOARD_COMMAND = createKeyboardCommand('TOGGLE_LIST_PANE_KEYBOARD_COMMAND')
export const TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND = createKeyboardCommand('TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND')
export const CREATE_NEW_NOTE_KEYBOARD_COMMAND = createKeyboardCommand('CREATE_NEW_NOTE_KEYBOARD_COMMAND')
export const NEXT_LIST_ITEM_KEYBOARD_COMMAND = createKeyboardCommand('NEXT_LIST_ITEM_KEYBOARD_COMMAND')
export const PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND = createKeyboardCommand('PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND')
export const SEARCH_KEYBOARD_COMMAND = createKeyboardCommand('SEARCH_KEYBOARD_COMMAND')
export const CANCEL_SEARCH_COMMAND = createKeyboardCommand('CANCEL_SEARCH_COMMAND')
export const SELECT_ALL_ITEMS_KEYBOARD_COMMAND = createKeyboardCommand('SELECT_ALL_ITEMS_KEYBOARD_COMMAND')
export const SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND = createKeyboardCommand('SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND')
export const DELETE_NOTE_KEYBOARD_COMMAND = createKeyboardCommand('DELETE_NOTE_KEYBOARD_COMMAND')
export const TAB_COMMAND = createKeyboardCommand('PLAIN_EDITOR_INSERT_TAB_KEYBOARD_COMMAND')
export const ESCAPE_COMMAND = createKeyboardCommand('ESCAPE_COMMAND')
export const TOGGLE_FOCUS_MODE_COMMAND = createKeyboardCommand('TOGGLE_FOCUS_MODE_COMMAND')
export const CHANGE_EDITOR_COMMAND = createKeyboardCommand('CHANGE_EDITOR_COMMAND')
export const FOCUS_TAGS_INPUT_COMMAND = createKeyboardCommand('FOCUS_TAGS_INPUT_COMMAND')
export const CREATE_NEW_TAG_COMMAND = createKeyboardCommand('CREATE_NEW_TAG_COMMAND')
export const OPEN_NOTE_HISTORY_COMMAND = createKeyboardCommand('OPEN_NOTE_HISTORY_COMMAND')
export const CAPTURE_SAVE_COMMAND = createKeyboardCommand('CAPTURE_SAVE_COMMAND')
export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND')
export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND')
@@ -0,0 +1,12 @@
export enum KeyboardKey {
Tab = 'Tab',
Backspace = 'Backspace',
Up = 'ArrowUp',
Down = 'ArrowDown',
Left = 'ArrowLeft',
Right = 'ArrowRight',
Enter = 'Enter',
Escape = 'Escape',
Home = 'Home',
End = 'End',
}
@@ -0,0 +1,4 @@
export enum KeyboardKeyEvent {
Down = 'KeyEventDown',
Up = 'KeyEventUp',
}
@@ -0,0 +1,7 @@
export enum KeyboardModifier {
Shift = 'Shift',
Ctrl = 'Control',
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = 'Meta',
Alt = 'Alt',
}
@@ -0,0 +1,203 @@
import { Environment, Platform } from '@standardnotes/snjs'
import { removeFromArray } from '@standardnotes/utils'
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
import { KeyboardCommand } from './KeyboardCommands'
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
import { KeyboardModifier } from './KeyboardModifier'
import { KeyboardCommandHandler } from './KeyboardCommandHandler'
import { KeyboardShortcut, PlatformedKeyboardShortcut } from './KeyboardShortcut'
import { getKeyboardShortcuts } from './getKeyboardShortcuts'
export class KeyboardService {
readonly activeModifiers = new Set<KeyboardModifier>()
private commandHandlers: KeyboardCommandHandler[] = []
private commandMap = new Map<KeyboardCommand, KeyboardShortcut>()
constructor(private platform: Platform, environment: Environment) {
window.addEventListener('keydown', this.handleKeyDown)
window.addEventListener('keyup', this.handleKeyUp)
window.addEventListener('blur', this.handleWindowBlur)
const shortcuts = getKeyboardShortcuts(platform, environment)
for (const shortcut of shortcuts) {
this.registerShortcut(shortcut)
}
}
get isMac() {
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
}
public deinit() {
this.commandHandlers.length = 0
window.removeEventListener('keydown', this.handleKeyDown)
window.removeEventListener('keyup', this.handleKeyUp)
window.removeEventListener('blur', this.handleWindowBlur)
;(this.handleKeyDown as unknown) = undefined
;(this.handleKeyUp as unknown) = undefined
;(this.handleWindowBlur as unknown) = undefined
}
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
if (!modifier) {
return
}
switch (modifier) {
case KeyboardModifier.Meta: {
if (this.isMac) {
this.activeModifiers.add(modifier)
}
break
}
case KeyboardModifier.Ctrl: {
if (!this.isMac) {
this.activeModifiers.add(modifier)
}
break
}
default: {
this.activeModifiers.add(modifier)
break
}
}
}
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
if (!modifier) {
return
}
this.activeModifiers.delete(modifier)
}
public cancelAllKeyboardModifiers = (): void => {
this.activeModifiers.clear()
}
public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
this.addActiveModifier(modifier)
}
public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
this.removeActiveModifier(modifier)
}
private handleKeyDown = (event: KeyboardEvent): void => {
this.updateAllModifiersFromEvent(event)
this.handleKeyboardEvent(event, KeyboardKeyEvent.Down)
}
private handleKeyUp = (event: KeyboardEvent): void => {
this.updateAllModifiersFromEvent(event)
this.handleKeyboardEvent(event, KeyboardKeyEvent.Up)
}
private updateAllModifiersFromEvent(event: KeyboardEvent): void {
for (const modifier of Object.values(KeyboardModifier)) {
if (event.getModifierState(modifier)) {
this.addActiveModifier(modifier)
} else {
this.removeActiveModifier(modifier)
}
}
}
handleWindowBlur = (): void => {
for (const modifier of this.activeModifiers) {
this.activeModifiers.delete(modifier)
}
}
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
for (const command of this.commandMap.keys()) {
const shortcut = this.commandMap.get(command)
if (!shortcut) {
continue
}
if (eventMatchesKeyAndModifiers(event, shortcut)) {
if (shortcut.preventDefault) {
event.preventDefault()
}
this.handleCommand(command, event, keyEvent)
}
}
}
private handleCommand(command: KeyboardCommand, event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
const target = event.target as HTMLElement
for (const observer of this.commandHandlers) {
if (observer.command !== command) {
continue
}
if (observer.element && event.target !== observer.element) {
continue
}
if (observer.elements && !observer.elements.includes(target)) {
continue
}
if (observer.notElement && observer.notElement === event.target) {
continue
}
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
continue
}
if (observer.notTags && observer.notTags.includes(target.tagName)) {
continue
}
const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp
if (callback) {
const exclusive = callback(event)
if (exclusive) {
return
}
}
}
}
registerShortcut(shortcut: KeyboardShortcut): void {
this.commandMap.set(shortcut.command, shortcut)
}
addCommandHandler(observer: KeyboardCommandHandler): () => void {
this.commandHandlers.push(observer)
const thislessObservers = this.commandHandlers
return () => {
observer.onKeyDown = undefined
observer.onKeyDown = undefined
removeFromArray(thislessObservers, observer)
}
}
addCommandHandlers(handlers: KeyboardCommandHandler[]): () => void {
const disposers = handlers.map((handler) => this.addCommandHandler(handler))
return () => {
for (const disposer of disposers) {
disposer()
}
}
}
keyboardShortcutForCommand(command: KeyboardCommand): PlatformedKeyboardShortcut | undefined {
const shortcut = this.commandMap.get(command)
if (!shortcut) {
return undefined
}
return {
platform: this.platform,
...shortcut,
}
}
}
@@ -0,0 +1,20 @@
import { Platform } from '@standardnotes/snjs'
import { KeyboardCommand } from './KeyboardCommands'
import { KeyboardKey } from './KeyboardKey'
import { KeyboardModifier } from './KeyboardModifier'
export type KeyboardShortcut = {
command: KeyboardCommand
modifiers?: KeyboardModifier[]
key?: KeyboardKey | string
/**
* Alternative to using key, if the key can be affected by alt + shift. For example, if you want alt + shift + n,
* use code 'KeyN' instead of key 'n', as the modifiers would turn n into '˜' on Mac.
*/
code?: string
preventDefault?: boolean
}
export type PlatformedKeyboardShortcut = KeyboardShortcut & {
platform: Platform
}
@@ -0,0 +1,27 @@
import { KeyboardShortcut } from './KeyboardShortcut'
import { modifiersForEvent } from './modifiersForEvent'
export function eventMatchesKeyAndModifiers(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean {
const eventModifiers = modifiersForEvent(event)
const shortcutModifiers = shortcut.modifiers ?? []
if (eventModifiers.length !== shortcutModifiers.length) {
return false
}
for (const modifier of shortcutModifiers) {
if (!eventModifiers.includes(modifier)) {
return false
}
}
if (!shortcut.key && !shortcut.code) {
return true
}
if (shortcut.key) {
return shortcut.key.toLowerCase() === event.key.toLowerCase()
} else {
return shortcut.code === event.code
}
}
@@ -0,0 +1,136 @@
import { Environment, Platform } from '@standardnotes/snjs'
import { isMacPlatform } from './platformCheck'
import {
CREATE_NEW_NOTE_KEYBOARD_COMMAND,
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
NEXT_LIST_ITEM_KEYBOARD_COMMAND,
PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
SEARCH_KEYBOARD_COMMAND,
SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
DELETE_NOTE_KEYBOARD_COMMAND,
TAB_COMMAND,
ESCAPE_COMMAND,
CANCEL_SEARCH_COMMAND,
TOGGLE_FOCUS_MODE_COMMAND,
CHANGE_EDITOR_COMMAND,
FOCUS_TAGS_INPUT_COMMAND,
CREATE_NEW_TAG_COMMAND,
OPEN_NOTE_HISTORY_COMMAND,
CAPTURE_SAVE_COMMAND,
STAR_NOTE_COMMAND,
PIN_NOTE_COMMAND,
} from './KeyboardCommands'
import { KeyboardKey } from './KeyboardKey'
import { KeyboardModifier } from './KeyboardModifier'
import { KeyboardShortcut } from './KeyboardShortcut'
export function getKeyboardShortcuts(platform: Platform, _environment: Environment): KeyboardShortcut[] {
const isMac = isMacPlatform(platform)
const primaryModifier = isMac ? KeyboardModifier.Meta : KeyboardModifier.Ctrl
return [
{
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
key: 'l',
modifiers: [primaryModifier, KeyboardModifier.Shift],
},
{
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
key: 'e',
modifiers: [primaryModifier, KeyboardModifier.Shift],
},
{
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
code: 'KeyN',
modifiers: [KeyboardModifier.Alt, KeyboardModifier.Shift],
},
{
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
key: KeyboardKey.Down,
},
{
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
key: KeyboardKey.Up,
},
{
command: SEARCH_KEYBOARD_COMMAND,
code: 'KeyF',
modifiers: [KeyboardModifier.Alt, KeyboardModifier.Shift],
},
{
command: CANCEL_SEARCH_COMMAND,
key: KeyboardKey.Escape,
},
{
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
key: 'a',
modifiers: [primaryModifier],
},
{
command: SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
modifiers: [KeyboardModifier.Alt],
},
{
command: DELETE_NOTE_KEYBOARD_COMMAND,
key: KeyboardKey.Backspace,
modifiers: [primaryModifier],
},
{
command: TAB_COMMAND,
key: KeyboardKey.Tab,
},
{
command: ESCAPE_COMMAND,
key: KeyboardKey.Escape,
},
{
command: TOGGLE_FOCUS_MODE_COMMAND,
key: 'f',
modifiers: [primaryModifier, KeyboardModifier.Shift],
},
{
command: CHANGE_EDITOR_COMMAND,
key: '/',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
{
command: FOCUS_TAGS_INPUT_COMMAND,
code: 'KeyT',
modifiers: [primaryModifier, KeyboardModifier.Alt],
preventDefault: true,
},
{
command: CREATE_NEW_TAG_COMMAND,
code: 'KeyN',
modifiers: [primaryModifier, KeyboardModifier.Alt],
},
{
command: OPEN_NOTE_HISTORY_COMMAND,
key: 'h',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
{
command: CAPTURE_SAVE_COMMAND,
key: 's',
modifiers: [primaryModifier],
preventDefault: true,
},
{
command: STAR_NOTE_COMMAND,
key: 's',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
{
command: PIN_NOTE_COMMAND,
key: 'p',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
]
}
@@ -0,0 +1,22 @@
import { Platform } from '@standardnotes/snjs'
import { KeyboardModifier } from './KeyboardModifier'
function isMacPlatform(platform: Platform) {
return platform === Platform.MacDesktop || platform === Platform.MacWeb
}
export function keyboardCharacterForModifier(modifier: KeyboardModifier, platform: Platform) {
const isMac = isMacPlatform(platform)
if (modifier === KeyboardModifier.Meta) {
return isMac ? '⌘' : '⊞'
} else if (modifier === KeyboardModifier.Ctrl) {
return isMac ? '⌃' : 'Ctrl'
} else if (modifier === KeyboardModifier.Alt) {
return isMac ? '⌥' : 'Alt'
} else if (modifier === KeyboardModifier.Shift) {
return isMac ? '⇧' : 'Shift'
} else {
return KeyboardModifier[modifier]
}
}
@@ -0,0 +1,29 @@
import { isMacPlatform } from '@standardnotes/ui-services'
import { keyboardCharacterForModifier } from './keyboardCharacterForModifier'
import { PlatformedKeyboardShortcut } from './KeyboardShortcut'
function stringForCode(code = ''): string {
return code.replace('Key', '').replace('Digit', '')
}
export function keyboardStringForShortcut(shortcut: PlatformedKeyboardShortcut | undefined) {
if (!shortcut) {
return ''
}
const key = shortcut.key?.toUpperCase() || stringForCode(shortcut.code)
if (!shortcut.modifiers || shortcut.modifiers.length === 0) {
return key
}
const modifierCharacters = shortcut.modifiers.map((modifier) =>
keyboardCharacterForModifier(modifier, shortcut.platform),
)
if (isMacPlatform(shortcut.platform)) {
return `${modifierCharacters.join('')}${key}`
} else {
return `${modifierCharacters.join('+')}+${key}`
}
}
@@ -0,0 +1,18 @@
import { KeyboardModifier } from './KeyboardModifier'
export function modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] {
const allModifiers = Object.values(KeyboardModifier)
const eventModifiers = allModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches =
((event.ctrlKey || event.key === KeyboardModifier.Ctrl) && modifier === KeyboardModifier.Ctrl) ||
((event.metaKey || event.key === KeyboardModifier.Meta) && modifier === KeyboardModifier.Meta) ||
((event.altKey || event.key === KeyboardModifier.Alt) && modifier === KeyboardModifier.Alt) ||
((event.shiftKey || event.key === KeyboardModifier.Shift) && modifier === KeyboardModifier.Shift)
return matches
})
return eventModifiers
}
@@ -0,0 +1,9 @@
import { Platform } from '@standardnotes/snjs'
export function isMacPlatform(platform: Platform) {
return platform === Platform.MacDesktop || platform === Platform.MacWeb
}
export function isWindowsPlatform(platform: Platform) {
return platform === Platform.WindowsDesktop || platform === Platform.WindowsWeb
}
+8 -1
View File
@@ -1,7 +1,14 @@
export * from './Alert/Functions'
export * from './Alert/WebAlertService'
export * from './Archive/ArchiveManager'
export * from './IO/IOService'
export * from './Keyboard/KeyboardService'
export * from './Keyboard/KeyboardShortcut'
export * from './Keyboard/KeyboardCommands'
export * from './Keyboard/platformCheck'
export * from './Keyboard/KeyboardKey'
export * from './Keyboard/KeyboardModifier'
export * from './Keyboard/keyboardCharacterForModifier'
export * from './Keyboard/keyboardStringForShortcut'
export * from './Preferences/PreferenceId'
export * from './Route/Params/DemoParams'
export * from './Route/Params/OnboardingParams'
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.12.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
### Features
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
## [1.11.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
**Note:** Version bump only for package @standardnotes/utils
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/utils",
"version": "1.11.3",
"version": "1.12.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -41,6 +41,7 @@
"eslint-plugin-prettier": "*",
"jest": "^29.3.1",
"jsdom": "^20.0.2",
"ts-jest": "^29.0.3"
"ts-jest": "^29.0.3",
"typescript": "*"
}
}
+59
View File
@@ -3,6 +3,65 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.105.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
### Bug Fixes
* lazy load embedded files in super editor ([#2043](https://github.com/standardnotes/app/issues/2043)) ([096d82f](https://github.com/standardnotes/app/commit/096d82f7af73613a3d8bc994bec2ec10a92a527c))
* super editor popover menus ([#2041](https://github.com/standardnotes/app/issues/2041)) ([8c8f045](https://github.com/standardnotes/app/commit/8c8f045b9a53d910bcbf1dc5ee19cf92addf252e))
### Features
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
## [3.104.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
**Note:** Version bump only for package @standardnotes/web
# [3.104.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
### Features
* ability to drag super list items; secure password generation blocks ([#2039](https://github.com/standardnotes/app/issues/2039)) ([c39c72d](https://github.com/standardnotes/app/commit/c39c72da7a4fb85f4da9aa4e6f8e9f7ba4486a94))
## [3.103.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
### Bug Fixes
* improved error reporting in super; improved draggable block icon padding ([24ff87c](https://github.com/standardnotes/app/commit/24ff87c30625432393ffedeada14478ebb625292))
## [3.103.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Bug Fixes
* issue where converting empty plain note to super would result in error ([e58c563](https://github.com/standardnotes/app/commit/e58c5637951f928efcd8c8427f06d0b6c69b5e55))
# [3.103.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Features
* **web:** enable block drag'n'drop in super editor ([#2029](https://github.com/standardnotes/app/issues/2029)) ([dab4f67](https://github.com/standardnotes/app/commit/dab4f678f04aee2c7dfbe7f416e7fc34e8d870cf))
# [3.102.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
### Features
* keyboard shortcuts for primary actions ([#2030](https://github.com/standardnotes/app/issues/2030)) ([f49ba6b](https://github.com/standardnotes/app/commit/f49ba6bd4d38e4026bd88630014126a6e0086351))
## [3.101.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/web
## [3.101.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
**Note:** Version bump only for package @standardnotes/web
# [3.101.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
### Features
* click saving indicator to toggle visibility ([#2028](https://github.com/standardnotes/app/issues/2028)) ([f793673](https://github.com/standardnotes/app/commit/f7936738759d4c2aa9d5a4a370649a20d758d3f9))
## [3.100.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
### Bug Fixes
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.100.8",
"version": "3.105.0",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -99,9 +99,6 @@
"prettier-plugin-tailwindcss": "^0.1.13",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-refresh": "^0.14.0",
"sass-loader": "*",
@@ -24,7 +24,7 @@
--sn-stylekit-accessory-tint-color-1: #941648;
--sn-stylekit-shadow-color: var(--background-2);
--sn-stylekit-shadow-color: #c4b89f;
--sn-stylekit-background-color: var(--background-1);
--sn-stylekit-foreground-color: var(--foreground-color);
--sn-stylekit-border-color: var(--border-color);
@@ -29,7 +29,8 @@ import { DesktopManager } from './Device/DesktopManager'
import {
ArchiveManager,
AutolockService,
IOService,
KeyboardService,
PreferenceId,
RouteService,
RouteServiceInterface,
ThemeManager,
@@ -85,19 +86,17 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.itemControllerGroup = new ItemGroupController(this)
this.routeService = new RouteService(this, internalEventBus)
const viewControllerManager = new ViewControllerManager(this, deviceInterface)
const archiveService = new ArchiveManager(this)
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
const themeService = new ThemeManager(this, internalEventBus)
this.setWebServices({
viewControllerManager,
archiveService,
desktopService: isDesktopDevice(deviceInterface) ? new DesktopManager(this, deviceInterface) : undefined,
io,
autolockService: this.isNativeMobileWeb() ? undefined : new AutolockService(this, internalEventBus),
themeService,
})
this.webServices = {} as WebServices
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
this.webServices.archiveService = new ArchiveManager(this)
this.webServices.themeService = new ThemeManager(this, internalEventBus)
this.webServices.autolockService = this.isNativeMobileWeb()
? undefined
: new AutolockService(this, internalEventBus)
this.webServices.desktopService = isDesktopDevice(deviceInterface)
? new DesktopManager(this, deviceInterface)
: undefined
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
if (this.isNativeMobileWeb()) {
this.mobileWebReceiver = new MobileWebReceiver(this)
@@ -152,10 +151,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
}
setWebServices(services: WebServices): void {
this.webServices = services
}
public addWebEventObserver(observer: WebEventObserver): () => void {
this.webEventObservers.push(observer)
@@ -194,6 +189,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.webServices.archiveService
}
public get paneController() {
return this.webServices.viewControllerManager.paneController
}
public get desktopDevice(): DesktopDeviceInterface | undefined {
if (isDesktopDevice(this.deviceInterface)) {
return this.deviceInterface
@@ -221,8 +220,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.webServices.themeService
}
public get io() {
return this.webServices.io
public get keyboardService() {
return this.webServices.keyboardService
}
async checkForSecurityUpdate() {
@@ -389,4 +388,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
FeatureIdentifier.PlainEditor
)
}
openPreferences(pane?: PreferenceId): void {
this.getViewControllerManager().preferencesController.openPreferences()
if (pane) {
this.getViewControllerManager().preferencesController.setCurrentPane(pane)
}
}
}
@@ -6,4 +6,5 @@ export {
DesktopClientRequiresWebMethods,
FileBackupsMapping,
FileBackupsDevice,
FileBackupRecord,
} from '@standardnotes/snjs'
@@ -1,6 +1,6 @@
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { DesktopManager } from './Device/DesktopManager'
import { ArchiveManager, AutolockService, IOService, ThemeManager } from '@standardnotes/ui-services'
import { ArchiveManager, AutolockService, KeyboardService, ThemeManager } from '@standardnotes/ui-services'
export type WebServices = {
viewControllerManager: ViewControllerManager
@@ -8,5 +8,5 @@ export type WebServices = {
autolockService?: AutolockService
archiveService: ArchiveManager
themeService: ThemeManager
io: IOService
keyboardService: KeyboardService
}
@@ -30,6 +30,7 @@ import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModa
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
import ApplicationProvider from './ApplicationProvider'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import CommandProvider from './CommandProvider'
type Props = {
application: WebApplication
@@ -196,85 +197,90 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
return (
<ApplicationProvider application={application}>
<AndroidBackHandlerProvider application={application}>
<DarkModeHandler application={application} />
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<PremiumModalProvider application={application} featuresController={viewControllerManager.featuresController}>
<div className={platformString + ' main-ui-view sn-component h-full'}>
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
<FileDragNDropProvider
application={application}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<Navigation application={application} />
<ContentListView
<CommandProvider service={application.keyboardService}>
<AndroidBackHandlerProvider application={application}>
<DarkModeHandler application={application} />
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<PremiumModalProvider
application={application}
featuresController={viewControllerManager.featuresController}
>
<div className={platformString + ' main-ui-view sn-component h-full'}>
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
<FileDragNDropProvider
application={application}
accountMenuController={viewControllerManager.accountMenuController}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
itemListController={viewControllerManager.itemListController}
navigationController={viewControllerManager.navigationController}
noAccountWarningController={viewControllerManager.noAccountWarningController}
>
<Navigation application={application} />
<ContentListView
application={application}
accountMenuController={viewControllerManager.accountMenuController}
filesController={viewControllerManager.filesController}
itemListController={viewControllerManager.itemListController}
navigationController={viewControllerManager.navigationController}
noAccountWarningController={viewControllerManager.noAccountWarningController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
/>
<ErrorBoundary>
<NoteGroupView application={application} />
</ErrorBoundary>
</FileDragNDropProvider>
</div>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
<RevisionHistoryModal
application={application}
historyModalController={viewControllerManager.historyModalController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
subscriptionController={viewControllerManager.subscriptionController}
/>
<ErrorBoundary>
<NoteGroupView application={application} />
</ErrorBoundary>
</FileDragNDropProvider>
</>
{renderChallenges()}
<>
<NotesContextMenu
application={application}
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
/>
<TagContextMenuWrapper
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController}
/>
<FileContextMenuWrapper
filesController={viewControllerManager.filesController}
selectionController={viewControllerManager.selectionController}
/>
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
application={application}
/>
<ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
<PermissionsModalWrapper application={application} />
<ConfirmDeleteAccountContainer
application={application}
viewControllerManager={viewControllerManager}
/>
</>
</div>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
<RevisionHistoryModal
application={application}
historyModalController={viewControllerManager.historyModalController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
subscriptionController={viewControllerManager.subscriptionController}
/>
</>
{renderChallenges()}
<>
<NotesContextMenu
application={application}
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
/>
<TagContextMenuWrapper
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController}
/>
<FileContextMenuWrapper
filesController={viewControllerManager.filesController}
selectionController={viewControllerManager.selectionController}
/>
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
application={application}
/>
<ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
<PermissionsModalWrapper application={application} />
<ConfirmDeleteAccountContainer
application={application}
viewControllerManager={viewControllerManager}
/>
</>
</div>
</PremiumModalProvider>
</ResponsivePaneProvider>
</AndroidBackHandlerProvider>
</PremiumModalProvider>
</ResponsivePaneProvider>
</AndroidBackHandlerProvider>
</CommandProvider>
</ApplicationProvider>
)
}
@@ -0,0 +1,36 @@
import { ReactNode, createContext, useContext, memo } from 'react'
import { observer } from 'mobx-react-lite'
import { KeyboardService } from '@standardnotes/ui-services'
const CommandServiceContext = createContext<KeyboardService | undefined>(undefined)
export const useCommandService = () => {
const value = useContext(CommandServiceContext)
if (!value) {
throw new Error('Component must be a child of <CommandServiceProvider />')
}
return value
}
type ChildrenProps = {
children: ReactNode
}
type ProviderProps = {
service: KeyboardService
} & ChildrenProps
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
const CommandServiceProvider = ({ service, children }: ProviderProps) => {
return (
<CommandServiceContext.Provider value={service}>
<MemoizedChildren children={children} />
</CommandServiceContext.Provider>
)
}
export default observer(CommandServiceProvider)
@@ -13,7 +13,7 @@ type Props = {
}
const RoundIconButton = forwardRef(
({ onClick, className, icon: iconType, iconClassName, id }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
({ onClick, className, icon: iconType, iconClassName, id, label }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
const click: MouseEventHandler = (e) => {
e.preventDefault()
onClick()
@@ -29,6 +29,8 @@ const RoundIconButton = forwardRef(
onClick={click}
ref={ref}
id={id}
title={label}
aria-label={label}
>
<Icon type={iconType} className={iconClassName} />
</button>
@@ -1,11 +1,12 @@
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ChangeEditorMenu from './ChangeEditorMenu'
import Popover from '../Popover/Popover'
import RoundIconButton from '../Button/RoundIconButton'
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
import { CHANGE_EDITOR_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
type Props = {
application: WebApplication
@@ -40,10 +41,24 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
setIsClickOutsideDisabled(true)
}, [])
useEffect(() => {
return application.keyboardService.addCommandHandler({
command: CHANGE_EDITOR_COMMAND,
onKeyDown: () => {
void toggleMenu()
},
})
}, [application, toggleMenu])
const shortcut = useMemo(
() => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_COMMAND),
[application],
)
return (
<div ref={containerRef}>
<RoundIconButton
label="Change note type"
label={`Change note type (${shortcut && keyboardStringForShortcut(shortcut)})`}
onClick={toggleMenu}
ref={buttonRef}
icon={selectedEditorIcon}
@@ -143,10 +143,10 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
const removeActionObserver = componentViewer.addActionObserver((action, data) => {
switch (action) {
case ComponentAction.KeyDown:
application.io.handleComponentKeyDown(data.keyboardModifier)
application.keyboardService.handleComponentKeyDown(data.keyboardModifier)
break
case ComponentAction.KeyUp:
application.io.handleComponentKeyUp(data.keyboardModifier)
application.keyboardService.handleComponentKeyUp(data.keyboardModifier)
break
case ComponentAction.Click:
application.getViewControllerManager().notesController.setContextMenuOpen(false)
@@ -9,7 +9,7 @@ import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { ContentType, SNTag } from '@standardnotes/snjs'
@@ -1,4 +1,12 @@
import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
import {
CANCEL_SEARCH_COMMAND,
CREATE_NEW_NOTE_KEYBOARD_COMMAND,
keyboardStringForShortcut,
NEXT_LIST_ITEM_KEYBOARD_COMMAND,
PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
SEARCH_KEYBOARD_COMMAND,
SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
} from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
import { FileItem, PrefKey } from '@standardnotes/snjs'
@@ -12,7 +20,7 @@ import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { FilesController } from '@/Controllers/FilesController'
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
import { NotesController } from '@/Controllers/NotesController'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { ElementIds } from '@/Constants/ElementIDs'
import ContentListHeader from './Header/ContentListHeader'
@@ -110,7 +118,6 @@ const ContentListView: FunctionComponent<Props> = ({
panelWidth,
renderedItems,
items,
searchBarElement,
isCurrentNoteTemplate,
} = itemListController
@@ -142,80 +149,69 @@ const ContentListView: FunctionComponent<Props> = ({
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
useEffect(() => {
const searchBarElement = document.getElementById(ElementIds.SearchBar)
/**
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
* use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent.
*/
const disposeNewNoteKeyObserver = application.io.addKeyObserver({
key: 'n',
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
onKeyDown: (event) => {
event.preventDefault()
void addNewItem()
return application.keyboardService.addCommandHandlers([
{
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
void addNewItem()
},
},
})
const disposeNextNoteKeyObserver = application.io.addKeyObserver({
key: KeyboardKey.Down,
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
onKeyDown: () => {
if (searchBarElement === document.activeElement) {
searchBarElement?.blur()
}
selectNextItem()
{
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
onKeyDown: () => {
if (searchBarElement === document.activeElement) {
searchBarElement?.blur()
}
selectNextItem()
},
},
})
const disposePreviousNoteKeyObserver = application.io.addKeyObserver({
key: KeyboardKey.Up,
element: document.body,
onKeyDown: () => {
selectPreviousItem()
{
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
element: document.body,
onKeyDown: () => {
selectPreviousItem()
},
},
})
const disposeSearchKeyObserver = application.io.addKeyObserver({
key: 'f',
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift],
onKeyDown: () => {
if (searchBarElement) {
searchBarElement.focus()
}
{
command: SEARCH_KEYBOARD_COMMAND,
onKeyDown: (event) => {
if (searchBarElement) {
event.preventDefault()
searchBarElement.focus()
}
},
},
})
const disposeSelectAllKeyObserver = application.io.addKeyObserver({
key: 'a',
modifiers: [KeyboardModifier.Ctrl],
onKeyDown: (event) => {
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
if (!isTargetInsideContentList) {
return
}
event.preventDefault()
selectionController.selectAll()
{
command: CANCEL_SEARCH_COMMAND,
onKeyDown: () => {
if (searchBarElement) {
searchBarElement.blur()
}
},
},
})
{
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
onKeyDown: (event) => {
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
return () => {
disposeNewNoteKeyObserver()
disposeNextNoteKeyObserver()
disposePreviousNoteKeyObserver()
disposeSearchKeyObserver()
disposeSelectAllKeyObserver()
}
}, [
addNewItem,
application.io,
createNewNote,
searchBarElement,
selectNextItem,
selectPreviousItem,
selectionController,
])
if (!isTargetInsideContentList) {
return
}
event.preventDefault()
selectionController.selectAll()
},
},
])
}, [addNewItem, application.keyboardService, createNewNote, selectNextItem, selectPreviousItem, selectionController])
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
@@ -229,11 +225,17 @@ const ContentListView: FunctionComponent<Props> = ({
[application, selectedAsTag, navigationController],
)
const addButtonLabel = useMemo(
() => (isFilesSmartView ? 'Upload file' : 'Create a new note in the selected tag'),
[isFilesSmartView],
const shortcutForCreate = useMemo(
() => application.keyboardService.keyboardShortcutForCommand(CREATE_NEW_NOTE_KEYBOARD_COMMAND),
[application],
)
const addButtonLabel = useMemo(() => {
return isFilesSmartView
? 'Upload file'
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
}, [isFilesSmartView, shortcutForCreate])
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
@@ -1,6 +1,6 @@
import { FileItem } from '@standardnotes/snjs'
import { FileItem, FileBackupRecord } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef } from 'react'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { getFileIconComponent } from '../FilePreview/getFileIconComponent'
import ListItemConflictIndicator from './ListItemConflictIndicator'
import ListItemTags from './ListItemTags'
@@ -12,19 +12,28 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import Icon from '../Icon/Icon'
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
filesController,
hideDate,
hideIcon,
hideTags,
item,
item: file,
onSelect,
selected,
sortBy,
tags,
}) => {
const { toggleAppPane } = useResponsiveAppPane()
const application = useApplication()
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
useEffect(() => {
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
}, [application, file])
const listItemRef = useRef<HTMLDivElement>(null)
@@ -45,7 +54,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
let shouldOpenContextMenu = selected
if (!selected) {
const { didSelect } = await onSelect(item)
const { didSelect } = await onSelect(file)
if (didSelect) {
shouldOpenContextMenu = true
}
@@ -55,18 +64,18 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
openFileContextMenu(posX, posY)
}
},
[selected, onSelect, item, openFileContextMenu],
[selected, onSelect, file, openFileContextMenu],
)
const onClick = useCallback(async () => {
const { didSelect } = await onSelect(item, true)
const { didSelect } = await onSelect(file, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item, onSelect, toggleAppPane])
}, [file, onSelect, toggleAppPane])
const IconComponent = () =>
getFileIconComponent(getIconForFileType((item as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
getFileIconComponent(getIconForFileType((file as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
useContextMenuEvent(listItemRef, openContextMenu)
@@ -74,7 +83,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
<div
ref={listItemRef}
className={classNames('flex max-h-[300px] w-[190px] cursor-pointer px-1 pt-2 text-text md:w-[200px]')}
id={item.uuid}
id={file.uuid}
onClick={onClick}
>
<div
@@ -92,11 +101,11 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
)}
<div className="min-w-0 flex-grow py-4 px-0">
<div className="line-clamp-2 overflow-hidden text-editor font-semibold">
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{item.title}</div>
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{file.title}</div>
</div>
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
<ListItemMetadata item={file} hideDate={hideDate} sortBy={sortBy} />
<ListItemTags hideTags={hideTags} tags={tags} />
<ListItemConflictIndicator item={item} />
<ListItemConflictIndicator item={file} />
</div>
</div>
<div
@@ -105,7 +114,14 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
selected ? 'bg-info text-info-contrast' : 'bg-passive-4 text-neutral',
)}
>
{formatSizeToReadableString(item.decryptedSize)}
<div className="flex justify-between">
{formatSizeToReadableString(file.decryptedSize)}
{backupInfo && (
<div title="File is backed up locally">
<Icon type="check-circle" />
</div>
)}
</div>
</div>
</div>
</div>
@@ -1,6 +1,6 @@
import { WebApplication } from '@/Application/Application'
import { FilesController } from '@/Controllers/FilesController'
import { NotesController } from '@/Controllers/NotesController'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs'
import { ListableContentItem } from './ListableContentItem'

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