mirror of
https://github.com/standardnotes/app
synced 2026-09-20 12:13:48 -04:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dc9086257 | ||
|
|
d149b500ee | ||
|
|
ad70bea368 | ||
|
|
3bcd969f83 | ||
|
|
d4af612fea | ||
|
|
b068b814f2 | ||
|
|
51de012236 | ||
|
|
2694c2f1a9 | ||
|
|
d3609853c6 | ||
|
|
a7ac715499 | ||
|
|
5ac93a6bd8 | ||
|
|
be4cc4e605 | ||
|
|
2fa3fec9ab | ||
|
|
059ee9ea36 | ||
|
|
66d08895fe | ||
|
|
518643dc71 | ||
|
|
5a0c005e8c | ||
|
|
45cdab6f6c | ||
|
|
46245b5569 | ||
|
|
8b103307cb | ||
|
|
a70d1009a3 | ||
|
|
a5b1ff9595 | ||
|
|
d691d7a3b3 |
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.20.18",
|
||||
"version": "1.20.19",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.10.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [1.9.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve variance between Super header font sizes and weights ([8b10330](https://github.com/standardnotes/app/commit/8b103307cba41855713b8627cf5d66a6fef6ff52))
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/blocks-editor",
|
||||
"version": "1.9.6",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
+120
-7
@@ -11,7 +11,11 @@ import './index.css';
|
||||
import {$isCodeHighlightNode} from '@lexical/code';
|
||||
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
mergeRegister,
|
||||
$findMatchingParent,
|
||||
$getNearestNodeOfType,
|
||||
} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
@@ -20,7 +24,17 @@ import {
|
||||
FORMAT_TEXT_COMMAND,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
$isRootOrShadowRoot,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
} from 'lexical';
|
||||
import {$isHeadingNode} from '@lexical/rich-text';
|
||||
import {
|
||||
INSERT_UNORDERED_LIST_COMMAND,
|
||||
REMOVE_LIST_COMMAND,
|
||||
$isListNode,
|
||||
ListNode,
|
||||
INSERT_ORDERED_LIST_COMMAND,
|
||||
} from '@lexical/list';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
@@ -36,9 +50,26 @@ import {
|
||||
LinkIcon,
|
||||
SuperscriptIcon,
|
||||
SubscriptIcon,
|
||||
ListBulleted,
|
||||
ListNumbered,
|
||||
} from '@standardnotes/icons';
|
||||
import {IconComponent} from '../../Theme/IconComponent';
|
||||
|
||||
const blockTypeToBlockName = {
|
||||
bullet: 'Bulleted List',
|
||||
check: 'Check List',
|
||||
code: 'Code Block',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
number: 'Numbered List',
|
||||
paragraph: 'Normal',
|
||||
quote: 'Quote',
|
||||
};
|
||||
|
||||
const IconSize = 15;
|
||||
|
||||
function TextFormatFloatingToolbar({
|
||||
@@ -52,6 +83,8 @@ function TextFormatFloatingToolbar({
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
isBulletedList,
|
||||
isNumberedList,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
anchorElem: HTMLElement;
|
||||
@@ -63,6 +96,8 @@ function TextFormatFloatingToolbar({
|
||||
isSubscript: boolean;
|
||||
isSuperscript: boolean;
|
||||
isUnderline: boolean;
|
||||
isBulletedList: boolean;
|
||||
isNumberedList: boolean;
|
||||
}): JSX.Element {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -74,6 +109,22 @@ function TextFormatFloatingToolbar({
|
||||
}
|
||||
}, [editor, isLink]);
|
||||
|
||||
const formatBulletList = useCallback(() => {
|
||||
if (!isBulletedList) {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}, [isBulletedList]);
|
||||
|
||||
const formatNumberedList = useCallback(() => {
|
||||
if (!isNumberedList) {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}, [isNumberedList]);
|
||||
|
||||
const updateTextFormatFloatingToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
@@ -226,6 +277,22 @@ function TextFormatFloatingToolbar({
|
||||
<LinkIcon />
|
||||
</IconComponent>
|
||||
</button>
|
||||
<button
|
||||
onClick={formatBulletList}
|
||||
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
|
||||
aria-label="Insert bulleted list">
|
||||
<IconComponent size={IconSize}>
|
||||
<ListBulleted />
|
||||
</IconComponent>
|
||||
</button>
|
||||
<button
|
||||
onClick={formatNumberedList}
|
||||
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
|
||||
aria-label="Insert numbered list">
|
||||
<IconComponent size={IconSize}>
|
||||
<ListNumbered />
|
||||
</IconComponent>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,6 +303,7 @@ function useFloatingTextFormatToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
): JSX.Element | null {
|
||||
const [activeEditor, setActiveEditor] = useState(editor);
|
||||
const [isText, setIsText] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
@@ -245,6 +313,8 @@ function useFloatingTextFormatToolbar(
|
||||
const [isSubscript, setIsSubscript] = useState(false);
|
||||
const [isSuperscript, setIsSuperscript] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [blockType, setBlockType] =
|
||||
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
||||
|
||||
const updatePopup = useCallback(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
@@ -270,6 +340,42 @@ function useFloatingTextFormatToolbar(
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
let element =
|
||||
anchorNode.getKey() === 'root'
|
||||
? anchorNode
|
||||
: $findMatchingParent(anchorNode, (e) => {
|
||||
const parent = e.getParent();
|
||||
return parent !== null && $isRootOrShadowRoot(parent);
|
||||
});
|
||||
|
||||
if (element === null) {
|
||||
element = anchorNode.getTopLevelElementOrThrow();
|
||||
}
|
||||
|
||||
const elementKey = element.getKey();
|
||||
const elementDOM = activeEditor.getElementByKey(elementKey);
|
||||
|
||||
if (elementDOM !== null) {
|
||||
if ($isListNode(element)) {
|
||||
const parentList = $getNearestNodeOfType<ListNode>(
|
||||
anchorNode,
|
||||
ListNode,
|
||||
);
|
||||
const type = parentList
|
||||
? parentList.getListType()
|
||||
: element.getListType();
|
||||
setBlockType(type);
|
||||
} else {
|
||||
const type = $isHeadingNode(element)
|
||||
? element.getTag()
|
||||
: element.getType();
|
||||
if (type in blockTypeToBlockName) {
|
||||
setBlockType(type as keyof typeof blockTypeToBlockName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const node = getSelectedNode(selection);
|
||||
|
||||
// Update text format
|
||||
@@ -298,14 +404,19 @@ function useFloatingTextFormatToolbar(
|
||||
setIsText(false);
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
}, [editor, activeEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('selectionchange', updatePopup);
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', updatePopup);
|
||||
};
|
||||
}, [updatePopup]);
|
||||
return editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
setActiveEditor(newEditor);
|
||||
updatePopup();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
);
|
||||
}, [editor, updatePopup]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
@@ -336,6 +447,8 @@ function useFloatingTextFormatToolbar(
|
||||
isSuperscript={isSuperscript}
|
||||
isUnderline={isUnderline}
|
||||
isCode={isCode}
|
||||
isBulletedList={blockType === 'bullet'}
|
||||
isNumberedList={blockType === 'number'}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
|
||||
@@ -20,19 +20,20 @@
|
||||
padding-left: 16px;
|
||||
}
|
||||
.Lexical__h1 {
|
||||
font-size: 24px;
|
||||
font-size: 26px;
|
||||
color: var(--sn-stylekit-editor-foreground-color);
|
||||
font-weight: 400;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__h2 {
|
||||
font-size: 15px;
|
||||
font-size: 22px;
|
||||
color: var(--sn-stylekit-editor-foreground-color);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__h3 {
|
||||
font-size: 12px;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.Lexical__textBold {
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.104.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.104.13",
|
||||
"version": "3.104.19",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/docs
|
||||
|
||||
## [0.2.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/docs
|
||||
|
||||
@@ -90,7 +90,7 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
|
||||
--sn-stylekit-sans-serif-font: -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
|
||||
--sn-stylekit-grey-1: #72767e;
|
||||
--sn-stylekit-grey-2: #bbbec4;
|
||||
--sn-stylekit-grey-3: #dfe1e4;
|
||||
@@ -98,7 +98,7 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
|
||||
--sn-stylekit-grey-4-opacity-variant: #bbbec43d;
|
||||
--sn-stylekit-grey-5: #f4f5f7;
|
||||
--sn-stylekit-grey-6: #e5e5e5;
|
||||
|
||||
|
||||
--sn-stylekit-accessory-tint-color-1: #086dd6;
|
||||
--sn-stylekit-accessory-tint-color-2: #ea6595;
|
||||
--sn-stylekit-accessory-tint-color-3: #ebad00;
|
||||
@@ -108,6 +108,53 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Overrides
|
||||
|
||||
```css
|
||||
--modal-background-color: var(--sn-stylekit-background-color);
|
||||
|
||||
--editor-header-bar-background-color: var(--sn-stylekit-background-color);
|
||||
--editor-background-color: var(--sn-stylekit-editor-background-color);
|
||||
--editor-foreground-color: var(--sn-stylekit-editor-foreground-color);
|
||||
--editor-title-bar-border-bottom-color: var(--sn-stylekit-border-color);
|
||||
--editor-title-input-color: var(--sn-stylekit-editor-foreground-color);
|
||||
--editor-pane-background-color: var(--sn-stylekit-background-color);
|
||||
--editor-pane-editor-background-color: var(--sn-stylekit-editor-background-color);
|
||||
--editor-pane-editor-foreground-color: var(--sn-stylekit-editor-foreground-color);
|
||||
--editor-pane-component-stack-item-background-color: var(--sn-stylekit-background-color);
|
||||
|
||||
--text-selection-color: var(--sn-stylekit-info-contrast-color);
|
||||
--text-selection-background-color: var(--sn-stylekit-info-color);
|
||||
|
||||
--note-preview-progress-color: var(--sn-stylekit-info-color);
|
||||
--note-preview-progress-background-color: var(--sn-stylekit-passive-color-4-opacity-variant);
|
||||
|
||||
--note-preview-selected-progress-color: var(--sn-stylekit-secondary-background-color);
|
||||
--note-preview-selected-progress-background-color: var(--sn-stylekit-passive-color-4-opacity-variant);
|
||||
|
||||
--items-column-background-color: var(--sn-stylekit-background-color);
|
||||
--items-column-items-background-color: var(--sn-stylekit-background-color);
|
||||
--items-column-border-left-color: var(--sn-stylekit-border-color);
|
||||
--items-column-border-right-color: var(--sn-stylekit-border-color);
|
||||
--items-column-search-background-color: var(--sn-stylekit-contrast-background-color);
|
||||
--item-cell-selected-background-color: var(--sn-stylekit-contrast-background-color);
|
||||
--item-cell-selected-border-left-color: var(--sn-stylekit-info-color);
|
||||
|
||||
--navigation-column-background-color: var(--sn-stylekit-secondary-background-color);
|
||||
--navigation-section-title-color: var(--sn-stylekit-secondary-foreground-color);
|
||||
--navigation-item-text-color: var(--sn-stylekit-secondary-foreground-color);
|
||||
--navigation-item-count-color: var(--sn-stylekit-neutral-color);
|
||||
--navigation-item-selected-background-color: rgb(253, 253, 253);
|
||||
|
||||
--preferences-navigation-icon-color: var(--sn-stylekit-neutral-color);
|
||||
--preferences-navigation-selected-background-color: var(--sn-stylekit-info-backdrop-color);
|
||||
|
||||
--dropdown-menu-radio-button-inactive-color: var(--sn-stylekit-passive-color-1);
|
||||
|
||||
--panel-resizer-background-color: var(--sn-stylekit-secondary-contrast-background-color);
|
||||
--link-element-color: var(--sn-stylekit-info-color);
|
||||
```
|
||||
|
||||
In order to get SN to display a dock icon for your theme (a circle in the lower right corner of the app that allows you to quickly toggle themes), add the following payload into the your ext.json file when [publishing your theme](/extensions/publishing):
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
---
|
||||
id: auth
|
||||
title: Auth API
|
||||
sidebar_label: Auth
|
||||
description: Authentication portion of the Standard Notes API.
|
||||
keywords:
|
||||
- standard notes
|
||||
- docs
|
||||
- notes app
|
||||
- end-to-end encryption
|
||||
- sync
|
||||
image: /img/logo.png
|
||||
hide_title: false
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
### POST /auth
|
||||
|
||||
**Registers a new user**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| created | `string` | Integer timestamp representing the date the client generated the account's encryption keys. Example "1622494310383" |
|
||||
| email | `string` | The account email. |
|
||||
| ephemeral | `boolean` | Whether the initial session created for this account is ephemeral ("Stay signed in" unchecked). |
|
||||
| identifier | `string` | Should equal the account email. |
|
||||
| origination | `string` | Should equal "registration" |
|
||||
| password | `string` | The server password generated by the client from the user's master password. |
|
||||
| pw_nonce | `string` | The nonce generated by the client for the user's encryption key. |
|
||||
| version | `string` | The protocol version the client used to generate the user's encryption key. Latest is "004". |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"session": {
|
||||
"access_token": "1:457b1f4b-88c2-4328-bf32-fe7dd9431d62:WwKgTEDUoEhxECu6",
|
||||
"refresh_token": "1:457b1f4b-88c2-4328-bf32-fe7dd9431d62:G-qEoZsFtoj~RLL-",
|
||||
"access_expiration": 1627678312000,
|
||||
"refresh_expiration": 1654051238000
|
||||
},
|
||||
"key_params": {
|
||||
"created": "1622494310383",
|
||||
"identifier": "[email protected]",
|
||||
"origination": "registration",
|
||||
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
|
||||
"version": "004"
|
||||
},
|
||||
"user": {
|
||||
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /auth/sign_in
|
||||
|
||||
**Authenticates a user and returns a session.**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| email | `string` | The account email. |
|
||||
| ephemeral | `boolean` | Whether the session created for this account is ephemeral ("Stay signed in" unchecked). |
|
||||
| password | `string` | The server password generated by the client from the user's master password. |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"session": {
|
||||
"access_token": "1:ee03808d-dd05-417c-9c87-d471e49bcc49:Q~-UoIpDhdtkii_t",
|
||||
"refresh_token": "1:ee03808d-dd05-417c-9c87-d471e49bcc49:Jn8UWzeHx2H5nZI5",
|
||||
"access_expiration": 1627678779000,
|
||||
"refresh_expiration": 1654051705000
|
||||
},
|
||||
"key_params": {
|
||||
"created": "1622494310383",
|
||||
"identifier": "[email protected]",
|
||||
"origination": "registration",
|
||||
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
|
||||
"version": "004"
|
||||
},
|
||||
"user": {
|
||||
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /auth/params
|
||||
|
||||
**Queries the parameters used for key generation for an email address. Queried before signing into an account.**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| email | `string` | The account email. |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"identifier": "[email protected]",
|
||||
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
|
||||
"version": "004"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /auth/change_pw
|
||||
|
||||
**Changes a user's password.**
|
||||
|
||||
| | | |
|
||||
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
|
||||
| created | `string` | Integer timestamp representing the date the client generated the account's new encryption keys. Example "1622494310383" |
|
||||
| identifier | `string` | The account email. |
|
||||
| origination | `string` | Should equal "password-change" |
|
||||
| current_password | `string` | The old server password generated by the client from the user's master password. |
|
||||
| new_password | `string` | The new server password generated by the client from the user's master password. |
|
||||
| pw_nonce | `string` | The nonce generated by the client for the user's encryption key. |
|
||||
| version | `string` | The protocol version the client used to generate the user's encryption key. Latest is "004". |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"session": {
|
||||
"access_token": "1:27d4fd8f-b730-4e0a-afd3-1600fb466aaa:HMgmZwV5k5ePt0vj",
|
||||
"refresh_token": "1:27d4fd8f-b730-4e0a-afd3-1600fb466aaa:wHuTeOB-qWGP3AE3",
|
||||
"access_expiration": 1627679129000,
|
||||
"refresh_expiration": 1654052055000
|
||||
},
|
||||
"key_params": {
|
||||
"created": "1622494310383",
|
||||
"identifier": "[email protected]",
|
||||
"origination": "password-change",
|
||||
"pw_nonce": "be1974ff6fb1c541aa8c71fd3c66851b6492cf224b661c72daf44e0bef3096bb",
|
||||
"version": "004"
|
||||
},
|
||||
"user": {
|
||||
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
### Session Model
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|-----------|---------------------------------------------------|
|
||||
| uuid | `string` | Unique identifier of the session |
|
||||
| user_uuid | `string` | Unique identifier of the user |
|
||||
| user_agent | `string` | The user agent used to create the session |
|
||||
| api_version | `string` | The server API version used to create the session |
|
||||
| access_token | `string` | The access token used to authenticate requests |
|
||||
| refresh_token | `string` | The refresh token used to extend tokens |
|
||||
| access_expiration | `datetime` | The expiration time of the access token |
|
||||
| refresh_expiration | `datetime` | The expiration time of the refresh token |
|
||||
| created_at | `datetime` | Date and time of creation of the session |
|
||||
| updated_at | `datetime` | Last updated date and time of the session |
|
||||
|
||||
- Each `session` includes the API version they were created with. This way we can deny sessions for a given API version if we detect a vulnerability with that version in the future
|
||||
- Sessions are created in the following cases:
|
||||
- When a user signs in
|
||||
- When a user registers a new account
|
||||
|
||||
---
|
||||
|
||||
### Authenticated requests
|
||||
|
||||
The `Authorization` request header field is used by clients to make authenticated request. `Bearer` is the only authentication scheme allowed.
|
||||
|
||||
The client must send the `access` token generated by the session, in the `Authorization` header. For example:
|
||||
|
||||
```
|
||||
GET /sessions HTTP/1.1
|
||||
Host: sync.standardnotes.org
|
||||
Authorization: Bearer <access token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Below is a list of endpoints related to session management:
|
||||
|
||||
| Method | URL | Params | Description | Successful response code |
|
||||
|-----------|--------------------------|---------------------|--------------------------------------------------------|--------------------------|
|
||||
|`POST` | /auth/sign_out | *None* | Terminates the current session | `204` |
|
||||
|`DELETE` | /session | **uuid** | Terminates the specified session by UUID | `204` |
|
||||
|`DELETE` | /sessions | *None* | Terminates all sessions, except the current one | `204` |
|
||||
|`GET` | /sessions | *None* | Lists all sessions active sessions | `200` |
|
||||
|`POST` | /session/token/refresh | **refresh_token** | Obtains new pair of `access_token` and `refresh_token` | `200` |
|
||||
|
||||
A successful request to `GET /sessions` returns the following JSON response:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": [
|
||||
{
|
||||
"uuid": "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
|
||||
"user_agent": "<product> / <product-version> <comment>",
|
||||
"api_version": "xxxxyyzz",
|
||||
"current": "<boolean>",
|
||||
"created_at": "2020-01-01T00:00:00.000Z"
|
||||
}
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Obtaining tokens
|
||||
|
||||
Tokens can be obtained every time the user performs any of the following actions:
|
||||
|
||||
1. When a user signs in
|
||||
1. When a user registers an account
|
||||
1. When the tokens are refreshed
|
||||
|
||||
#### Refreshing tokens
|
||||
|
||||
When an expired `access_token` is provided in the `Authorization` HTTP header, the following JSON response is returned:
|
||||
|
||||
HTTP Status Code: `498 Expired Access Token`
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"tag": "expired-access-token",
|
||||
"message": "The provided access token has expired."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To continue accessing resources, the `access_token` must be refreshed. That is, the current `access_token` is replaced with a new one with an extended expiration date.
|
||||
|
||||
To refresh an `access_token`, a valid `refresh_token` is needed. This `refresh_token` must meet the following requirements:
|
||||
|
||||
- It should belong to the session of the `access_token`
|
||||
- It should not be expired
|
||||
|
||||
Since the `refresh_token` is of single-use, a new `refresh_token` is obtained when the `access_token` is refreshed.
|
||||
|
||||
Refreshing tokens is a process that is transparent to the user, meaning that the client will perform the requests to keep valid tokens without user intervention.
|
||||
|
||||
Here's how to refresh tokens:
|
||||
|
||||
1. Send a `POST` request to `/session/token/refresh`. The body should contain a JSON paylaod with the `refresh_token`:
|
||||
```
|
||||
POST /session/token/refresh HTTP/1.1
|
||||
Host: sync.standardnotes.org
|
||||
Authorization: Bearer <access token>
|
||||
|
||||
{
|
||||
"refresh_token": "R_xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
1. The `refresh_token` is validated on the server. Depending of the circumstances, there should be two outcomes:
|
||||
1. The provided `refresh_token` is valid. If so, a new pair of tokens is generated and the following JSON response is returned:
|
||||
```json
|
||||
{
|
||||
"token": "xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
|
||||
"session": {
|
||||
"refresh_expiration": 1583020800,
|
||||
"refresh_token": "xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
|
||||
}
|
||||
}
|
||||
```
|
||||
1. The provided `refresh_token` is expired. If so, the following JSON response is returned:
|
||||
|
||||
HTTP Status Code: `400 Bad Request`
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"tag": "expired-refresh-token",
|
||||
"message": "The refresh token has expired."
|
||||
}
|
||||
}
|
||||
```
|
||||
User must start a new session by re-entering their credentials.
|
||||
|
||||
### Expiration
|
||||
|
||||
#### Sessions
|
||||
|
||||
Sessions should be terminated after a period of inactivity. This is for best security practices.
|
||||
|
||||
`Long-lived sessions` are a good choice for our use case, because it can build a better user experience than expiring sessions for a short idle-timeout.
|
||||
|
||||
- A `session` that remains inactive for `1 year` will be terminated, along with all `tokens`
|
||||
|
||||
#### Tokens
|
||||
|
||||
| Name | Type | Expiration |
|
||||
|-----------|-------------|---------------|
|
||||
| `access` | Short-lived | `60 days` |
|
||||
| `refresh` | Long-lived | `1 year` |
|
||||
|
||||
- A `refresh` token is of single-use, while an `access` token can be used as long as it is not expired
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
id: sync
|
||||
title: Sync API
|
||||
sidebar_label: Sync
|
||||
description: Specification for the Standard Notes Sync API.
|
||||
keywords:
|
||||
- standard notes
|
||||
- docs
|
||||
- notes app
|
||||
- end-to-end encryption
|
||||
- sync
|
||||
image: /img/logo.png
|
||||
hide_title: false
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
This document outlines the client-server communication of the Standard Notes ecosystem.
|
||||
|
||||
## Item
|
||||
|
||||
An `Item` is largely the only model that both clients and server are concerned wtih. The `content` field stores an encrypted object that can be anything the client needs to operate.
|
||||
|
||||
Relationships between items are handled by the client, and not the server, and are stored encrypted in the `content` field as `references`.
|
||||
|
||||
Items have the following properties:
|
||||
|
||||
| name | type | description |
|
||||
| ------------ | ----------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| uuid | `string` (or uuid for some databases) | The unique identifier for this item. |
|
||||
| content | `text` | An encrypted string generated by the client. |
|
||||
| content_type | `string` | The type of data contained in the `content` field. (i.e Note, Tag, SN|Component, etc.) |
|
||||
| enc_item_key | `text` | The key used to encrypt `content`, itself also encrypted. |
|
||||
| deleted | `boolean` | Whether the model has been deleted. |
|
||||
| created_at_timestamp | `integer` | Timestamp representing when the item was created, with microsecond precision. |
|
||||
| updated_at_timestamp | `integer` | Timestamp representing when the item was updated _by the server_, with microsecond precision. |
|
||||
|
||||
## Content
|
||||
|
||||
Data generated by the client is stored in the `content` field of the `Item`. An item's `content` is "sticky" and can have any property the client chooses, as well as the following:
|
||||
|
||||
| name | type | description |
|
||||
| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| appData | `dictionary` | A domain based dictionary whose content must be preserved between platforms. `appData[domain]` contains data the client stores in the item. |
|
||||
| references | `array` | A metadata array of other `Item` uuids this item is related to and their respective `content_type`. See sample below. |
|
||||
|
||||
`references` array structure:
|
||||
|
||||
```JSON
|
||||
[
|
||||
{uuid: xxxx, content_type: "Tag"},
|
||||
{uuid: xxxxx, content_type: "Tag"},
|
||||
]
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
General:
|
||||
|
||||
1. All requests after initial authentication should be authenticated with a session token in the `Authorization` header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <access token>
|
||||
|
||||
```
|
||||
|
||||
### POST items/sync
|
||||
|
||||
| | | |
|
||||
|-------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115". |
|
||||
| compute_integrity | `boolean` | Should be false in most cases. Pass true if you plan to read the `integrity_hash` result (integrity can be optionally checked once on app launch). |
|
||||
| items | `array` | An array of Items. |
|
||||
| limit | `integer` | The maximum number of items to return per request. Official clients use 150. |
|
||||
| sync_token | `string` | An opaque token returned by the server from a previous sync request. This lets the server know to pull in only new changes. |
|
||||
| cursor_token | `string` | An opaque pagination token returned by the server from a previous multi-page sync request. Send this value back only if the server returned this value from a previous request. |
|
||||
|
||||
#### Response
|
||||
|
||||
```JSON
|
||||
{
|
||||
"retrieved_items": Array<Item>,
|
||||
"saved_items": Array<Item>,
|
||||
"conflicts": Array<Conflict>,
|
||||
"sync_token": string,
|
||||
"cursor_token": string?,
|
||||
"integrity_hash": string?,
|
||||
}
|
||||
```
|
||||
|
||||
| | |
|
||||
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| retrieved_items | Items that are new to the client or have been modified since last sync and should be merged with local values or created locally. |
|
||||
| saved_items | Items which have been successfully saved. Clients should merge *only* the resulting `updated_at_timestamp` with local values. Do not update local value of `content` from remote `saved_items` value. |
|
||||
| conflicts | Items which have not been saved by the server and returned to client. |
|
||||
| sync_token | An opaque token which should be sent back to server in subsequent sync request. |
|
||||
| cursor_token | An opaque token which should be sent back to server in subsequent sync request (if present). |
|
||||
|
||||
#### Conflicts
|
||||
|
||||
```typescript
|
||||
type Conflict = {
|
||||
server_item?: Item
|
||||
unsaved_item?: Item
|
||||
type: 'sync_conflict' | 'uuid_conflict'
|
||||
}
|
||||
```
|
||||
|
||||
A `sync_conflict` occurs when the item the client is attempting to save has a newer change on the server. The server is able to determine this by reading the `updated_at_timestamp` value from both the incoming payload and the server payload. If the incoming payload's `updated_at_timestamp` != to the server's `updated_at_timestamp` for that item, the save is rejected and returned to client as a `sync_conflict`. These types of conflicts are not uncommon.
|
||||
|
||||
To resolve a `sync_conflict`, the client must choose a winner between server_item and unsaved_item then re-sync. The winning item should have its updated_at_timestamp value set to the server's value.
|
||||
|
||||
A `uuid_conflict` occurs when the UUID the client has choosen for an item for X user already belongs to an item for Y user. This should mostly only occur when a user registers a new account and imports a data backup from another account.
|
||||
|
||||
To resolve a `uuid_conflict`, change the UUID of the item attempting to be saved to a new UUID, then re-sync.
|
||||
|
||||
#### Deletion
|
||||
|
||||
To delete an item permanently, set `deleted` to `true` on the `Item` and sync. When receiving an item that is `deleted`, remove it from the local database immediately.
|
||||
@@ -23,8 +23,6 @@ This documentation explains how to get started with using Standard Notes, publis
|
||||
- [Self-hosting a Syncing Server](/self-hosting/getting-started)
|
||||
- [Developing Extensions](/extensions/intro)
|
||||
- [Client Encryption API](/specification/encryption)
|
||||
- [Sync API](/specification/sync)
|
||||
- [Auth API](/specification/auth)
|
||||
|
||||
## Staying informed
|
||||
|
||||
|
||||
@@ -43,11 +43,6 @@ module.exports = {
|
||||
label: 'Client Encryption API',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
to: '/specification/sync',
|
||||
label: 'Server API',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
to: '/extensions/intro',
|
||||
label: 'Build an Extension',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/docs",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -14,11 +14,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
{
|
||||
API: [
|
||||
'specification/encryption',
|
||||
'specification/sync',
|
||||
'specification/auth',
|
||||
],
|
||||
API: ['specification/encryption'],
|
||||
},
|
||||
{
|
||||
Extensions: [
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.19.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.19.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.19.26",
|
||||
"version": "1.19.27",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.27.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.27.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.27.3",
|
||||
"version": "1.27.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.14.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.14.3",
|
||||
"version": "1.14.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/icons",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 4.16667H17.4999V5.83333H5.83325V4.16667ZM5.83325 10.8333V9.16667H17.4999V10.8333H5.83325ZM3.33325 3.75C3.66477 3.75 3.98272 3.8817 4.21714 4.11612C4.45156 4.35054 4.58325 4.66848 4.58325 5C4.58325 5.33152 4.45156 5.64946 4.21714 5.88388C3.98272 6.1183 3.66477 6.25 3.33325 6.25C3.00173 6.25 2.68379 6.1183 2.44937 5.88388C2.21495 5.64946 2.08325 5.33152 2.08325 5C2.08325 4.66848 2.21495 4.35054 2.44937 4.11612C2.68379 3.8817 3.00173 3.75 3.33325 3.75ZM3.33325 8.75C3.66477 8.75 3.98272 8.8817 4.21714 9.11612C4.45156 9.35054 4.58325 9.66848 4.58325 10C4.58325 10.3315 4.45156 10.6495 4.21714 10.8839C3.98272 11.1183 3.66477 11.25 3.33325 11.25C3.00173 11.25 2.68379 11.1183 2.44937 10.8839C2.21495 10.6495 2.08325 10.3315 2.08325 10C2.08325 9.66848 2.21495 9.35054 2.44937 9.11612C2.68379 8.8817 3.00173 8.75 3.33325 8.75ZM5.83325 15.8333V14.1667H17.4999V15.8333H5.83325ZM3.33325 13.75C3.66477 13.75 3.98272 13.8817 4.21714 14.1161C4.45156 14.3505 4.58325 14.6685 4.58325 15C4.58325 15.3315 4.45156 15.6495 4.21714 15.8839C3.98272 16.1183 3.66477 16.25 3.33325 16.25C3.00173 16.25 2.68379 16.1183 2.44937 15.8839C2.21495 15.6495 2.08325 15.3315 2.08325 15C2.08325 14.6685 2.21495 14.3505 2.44937 14.1161C2.68379 13.8817 3.00173 13.75 3.33325 13.75Z"/>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 3.16667H17.4999V4.83333H5.83325V3.16667ZM5.83325 10.8333V9.16667H17.4999V10.8333H5.83325ZM3.33325 2.75C3.66477 2.75 3.98272 2.8817 4.21714 3.11612C4.45156 3.35054 4.58325 3.66848 4.58325 4C4.58325 4.33152 4.45156 4.64946 4.21714 4.88388C3.98272 5.1183 3.66477 5.25 3.33325 5.25C3.00173 5.25 2.68379 5.1183 2.44937 4.88388C2.21495 4.64946 2.08325 4.33152 2.08325 4C2.08325 3.66848 2.21495 3.35054 2.44937 3.11612C2.68379 2.8817 3.00173 2.75 3.33325 2.75ZM3.33325 8.75C3.66477 8.75 3.98272 8.8817 4.21714 9.11612C4.45156 9.35054 4.58325 9.66848 4.58325 10C4.58325 10.3315 4.45156 10.6495 4.21714 10.8839C3.98272 11.1183 3.66477 11.25 3.33325 11.25C3.00173 11.25 2.68379 11.1183 2.44937 10.8839C2.21495 10.6495 2.08325 10.3315 2.08325 10C2.08325 9.66848 2.21495 9.35054 2.44937 9.11612C2.68379 8.8817 3.00173 8.75 3.33325 8.75ZM5.83325 16.8333V15.1667H17.4999V16.8333H5.83325ZM3.33325 14.75C3.66477 14.75 3.98272 14.8817 4.21714 15.1161C4.45156 15.3505 4.58325 15.6685 4.58325 16C4.58325 16.3315 4.45156 16.6495 4.21714 16.8839C3.98272 17.1183 3.66477 17.25 3.33325 17.25C3.00173 17.25 2.68379 17.1183 2.44937 16.8839C2.21495 16.6495 2.08325 16.3315 2.08325 16C2.08325 15.6685 2.21495 15.3505 2.44937 15.1161C2.68379 14.8817 3.00173 14.75 3.33325 14.75Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 3.16675H17.4999V4.83341H5.83325V3.16675ZM5.83325 10.8334V9.16675H17.4999V10.8334H5.83325ZM5.83325 16.8334V15.1667H17.4999V16.8334H5.83325Z" />
|
||||
<path d="M4.02415 2.36364V6H3.03693V3.27273H3.01562L2.22017 3.74858V2.91051L3.11506 2.36364H4.02415Z" />
|
||||
<path d="M2.24148 12V11.2898L3.59801 10.1676C3.68797 10.093 3.76491 10.0232 3.82884 9.9581C3.89394 9.89181 3.94366 9.82375 3.97798 9.75391C4.01349 9.68407 4.03125 9.60653 4.03125 9.52131C4.03125 9.42779 4.01113 9.34789 3.97088 9.28161C3.93182 9.21532 3.87737 9.16442 3.80753 9.12891C3.73769 9.09221 3.6572 9.07386 3.56605 9.07386C3.47491 9.07386 3.39441 9.09221 3.32457 9.12891C3.25592 9.1656 3.20265 9.21946 3.16477 9.29048C3.12689 9.36151 3.10795 9.44792 3.10795 9.54972H2.17045C2.17045 9.29403 2.22786 9.07386 2.34268 8.8892C2.4575 8.70455 2.61967 8.5625 2.82919 8.46307C3.03871 8.36364 3.28433 8.31392 3.56605 8.31392C3.85724 8.31392 4.10938 8.36068 4.32244 8.45419C4.5367 8.54652 4.70182 8.67673 4.81783 8.84482C4.93501 9.0129 4.99361 9.20999 4.99361 9.43608C4.99361 9.57576 4.96461 9.71484 4.90661 9.85334C4.8486 9.99065 4.74444 10.1428 4.59411 10.3097C4.44377 10.4766 4.23011 10.6754 3.95312 10.9062L3.61222 11.1903V11.2116H5.03267V12H2.24148Z" />
|
||||
<path d="M3.66193 18.0497C3.37547 18.0497 3.12098 18.0006 2.89844 17.9023C2.67708 17.8029 2.50308 17.6662 2.37642 17.4922C2.24976 17.3182 2.18584 17.1181 2.18466 16.892H3.17898C3.18016 16.9619 3.20147 17.0246 3.2429 17.0803C3.28551 17.1347 3.34351 17.1773 3.4169 17.2081C3.49029 17.2389 3.57434 17.2543 3.66903 17.2543C3.76018 17.2543 3.84067 17.2383 3.91051 17.2063C3.98035 17.1732 4.0348 17.1276 4.07386 17.0696C4.11293 17.0116 4.13187 16.9453 4.13068 16.8707C4.13187 16.7973 4.10938 16.7322 4.06321 16.6754C4.01823 16.6186 3.9549 16.5742 3.87322 16.5423C3.79155 16.5103 3.69744 16.4943 3.59091 16.4943H3.22159V15.8125H3.59091C3.68916 15.8125 3.77557 15.7965 3.85014 15.7646C3.9259 15.7326 3.98449 15.6882 4.02592 15.6314C4.06854 15.5746 4.08925 15.5095 4.08807 15.4361C4.08925 15.3651 4.07209 15.3023 4.03658 15.2479C4.00107 15.1934 3.95135 15.1508 3.88743 15.12C3.82469 15.0893 3.75189 15.0739 3.66903 15.0739C3.57907 15.0739 3.49858 15.0898 3.42756 15.1218C3.35772 15.1538 3.30268 15.1982 3.26243 15.255C3.22218 15.3118 3.20147 15.3769 3.20028 15.4503H2.25568C2.25687 15.2277 2.31783 15.0312 2.43857 14.8608C2.56049 14.6903 2.72739 14.5566 2.93928 14.4595C3.15116 14.3625 3.39441 14.3139 3.66903 14.3139C3.93774 14.3139 4.17507 14.3595 4.38104 14.4506C4.58819 14.5418 4.74976 14.6673 4.86577 14.8271C4.98295 14.9857 5.04096 15.1674 5.03977 15.3722C5.04214 15.5758 4.9723 15.7427 4.83026 15.8729C4.68939 16.0031 4.51065 16.08 4.29403 16.1037V16.1321C4.58759 16.1641 4.80836 16.254 4.95632 16.402C5.10429 16.5488 5.17708 16.7334 5.17472 16.956C5.1759 17.169 5.11198 17.3578 4.98295 17.5224C4.85511 17.6869 4.67696 17.8159 4.44851 17.9094C4.22124 18.003 3.95904 18.0497 3.66193 18.0497Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9999 8.6665C11.5579 8.6665 11.134 8.49091 10.8214 8.17835C10.5088 7.86579 10.3333 7.44187 10.3333 6.99984C10.3333 6.55781 10.5088 6.13389 10.8214 5.82133C11.134 5.50877 11.5579 5.33317 11.9999 5.33317C12.4419 5.33317 12.8659 5.50877 13.1784 5.82133C13.491 6.13389 13.6666 6.55781 13.6666 6.99984C13.6666 7.44187 13.491 7.86579 13.1784 8.17835C12.8659 8.49091 12.4419 8.6665 11.9999 8.6665ZM11.9999 13.6665C11.5579 13.6665 11.134 13.4909 10.8214 13.1783C10.5088 12.8658 10.3333 12.4419 10.3333 11.9998C10.3333 11.5578 10.5088 11.1339 10.8214 10.8213C11.134 10.5088 11.5579 10.3332 11.9999 10.3332C12.4419 10.3332 12.8659 10.5088 13.1784 10.8213C13.491 11.1339 13.6666 11.5578 13.6666 11.9998C13.6666 12.4419 13.491 12.8658 13.1784 13.1783C12.8659 13.4909 12.4419 13.6665 11.9999 13.6665ZM11.9999 18.6665C11.5579 18.6665 11.134 18.4909 10.8214 18.1783C10.5088 17.8658 10.3333 17.4419 10.3333 16.9998C10.3333 16.5578 10.5088 16.1339 10.8214 15.8213C11.134 15.5088 11.5579 15.3332 11.9999 15.3332C12.4419 15.3332 12.8659 15.5088 13.1784 15.8213C13.491 16.1339 13.6666 16.5578 13.6666 16.9998C13.6666 17.4419 13.491 17.8658 13.1784 18.1783C12.8659 18.4909 12.4419 18.6665 11.9999 18.6665Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -112,6 +112,7 @@ import LineWidthIcon from './ic-line-width.svg'
|
||||
import LinkIcon from './ic-link.svg'
|
||||
import LinkOffIcon from './ic-link-off.svg'
|
||||
import ListBulleted from './ic-list-bulleted.svg'
|
||||
import ListNumbered from './ic-list-numbered.svg'
|
||||
import ListedFilledIcon from './ic-listed-filled.svg'
|
||||
import ListedIcon from './ic-listed.svg'
|
||||
import LockFilledIcon from './ic-lock-filled.svg'
|
||||
@@ -318,6 +319,7 @@ export {
|
||||
ListBulleted,
|
||||
ListedFilledIcon,
|
||||
ListedIcon,
|
||||
ListNumbered,
|
||||
LockFilledIcon,
|
||||
LockIcon,
|
||||
MarkdownIcon,
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.49.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.49.12",
|
||||
"version": "3.49.18",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.39.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.39.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.39.2",
|
||||
"version": "1.39.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.4.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.36",
|
||||
"version": "1.4.42",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.50.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.50.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.50.4",
|
||||
"version": "1.50.5",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.156.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
# [2.156.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.156.0",
|
||||
"version": "2.156.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
## [1.3.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
## [1.3.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/toast",
|
||||
"version": "1.3.12",
|
||||
"version": "1.3.14",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -75,8 +75,10 @@ export const ToastTimer: FunctionComponent<Props> = ({ toast, index }) => {
|
||||
toastElement.addEventListener('mouseenter', handleMouseEnter)
|
||||
toastElement.addEventListener('mouseleave', handleMouseLeave)
|
||||
}
|
||||
window.addEventListener('focus', handlePageFocus)
|
||||
window.addEventListener('blur', handlePageBlur)
|
||||
if (toast.pauseOnWindowBlur) {
|
||||
window.addEventListener('focus', handlePageFocus)
|
||||
window.addEventListener('blur', handlePageBlur)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimer()
|
||||
@@ -84,8 +86,10 @@ export const ToastTimer: FunctionComponent<Props> = ({ toast, index }) => {
|
||||
toastElement.removeEventListener('mouseenter', handleMouseEnter)
|
||||
toastElement.removeEventListener('mouseleave', handleMouseLeave)
|
||||
}
|
||||
window.removeEventListener('focus', handlePageFocus)
|
||||
window.removeEventListener('blur', handlePageBlur)
|
||||
if (toast.pauseOnWindowBlur) {
|
||||
window.removeEventListener('focus', handlePageFocus)
|
||||
window.removeEventListener('blur', handlePageBlur)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
clearTimer,
|
||||
|
||||
@@ -61,10 +61,11 @@ export const addToast = action(toastStore, 'addToast', (store: WritableAtom<Toas
|
||||
If you want to update an existing toast, use the \`updateToast()\` function instead.`)
|
||||
}
|
||||
|
||||
const toast = {
|
||||
const toast: Toast = {
|
||||
...options,
|
||||
id,
|
||||
dismissed: false,
|
||||
pauseOnWindowBlur: options.pauseOnWindowBlur ?? true,
|
||||
}
|
||||
|
||||
store.set([...existingToasts, toast])
|
||||
|
||||
@@ -12,6 +12,7 @@ type CommonToastProperties = {
|
||||
progress?: number
|
||||
autoClose?: boolean
|
||||
duration?: number
|
||||
pauseOnWindowBlur?: boolean
|
||||
}
|
||||
|
||||
export type Toast = CommonToastProperties & {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.19.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.19.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.19.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.19.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed workspace menu item rename issue ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))
|
||||
|
||||
## [1.19.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.19.2",
|
||||
"version": "1.19.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -9,4 +9,5 @@ export enum KeyboardKey {
|
||||
Escape = 'Escape',
|
||||
Home = 'Home',
|
||||
End = 'End',
|
||||
Space = ' ',
|
||||
}
|
||||
|
||||
@@ -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.15.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* Add current Writing Streak to Daily Notebooks ([#2093](https://github.com/standardnotes/app/issues/2093)) ([d149b50](https://github.com/standardnotes/app/commit/d149b500ee3f13f63a1f831e6e0ba29a53e3e416))
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/utils",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -687,3 +687,7 @@ export function useBoolean(value: boolean | undefined, defaultValue: boolean): b
|
||||
export function spaceSeparatedStrings(...strings: string[]): string {
|
||||
return strings.join(' ')
|
||||
}
|
||||
|
||||
export function pluralize(count: number, singular: string, plural: string): string {
|
||||
return count === 1 ? singular : plural
|
||||
}
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.124.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* Add current Writing Streak to Daily Notebooks ([#2093](https://github.com/standardnotes/app/issues/2093)) ([d149b50](https://github.com/standardnotes/app/commit/d149b500ee3f13f63a1f831e6e0ba29a53e3e416))
|
||||
|
||||
## [3.123.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where passcode lock setting was not visible on mobile ([d4af612](https://github.com/standardnotes/app/commit/d4af612fea6960a5233404682aeb1819754b9b8b))
|
||||
|
||||
# [3.123.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* alignment of Loading text in What's New screen ([d360985](https://github.com/standardnotes/app/commit/d3609853c6c6121a5633d72a685723498099edba))
|
||||
|
||||
### Features
|
||||
|
||||
* Moments will now create a link with the current open note, so you can look back on what you were working on during a moment ([a7ac715](https://github.com/standardnotes/app/commit/a7ac715499fdf3f15a736f68af4bf5a78454742f))
|
||||
|
||||
# [3.122.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [3.121.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where note UUID was not selectable in context menu ([5a0c005](https://github.com/standardnotes/app/commit/5a0c005e8c0373e2f47fa613fc5bfec0e84cdb37))
|
||||
* Fixed issue where workspaces could not be renamed ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))
|
||||
|
||||
# [3.121.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where Dark theme could be called 'Focus' in Appearance preferences ([d691d7a](https://github.com/standardnotes/app/commit/d691d7a3b34f7f4249138baa4c3f27b79a9f8e6c))
|
||||
* Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu ([a5b1ff9](https://github.com/standardnotes/app/commit/a5b1ff95957a9d407663d28429785457aef3a1b4))
|
||||
|
||||
### Features
|
||||
|
||||
* add Indent/Outdent options in Super when on mobile ([a70d100](https://github.com/standardnotes/app/commit/a70d1009a3d8d6be1395bf148a46c3cb2260dcdc))
|
||||
|
||||
## [3.120.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,5 +1,101 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.124.0",
|
||||
"title": "[3.124.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Add current Writing Streak to Daily Notebooks ([#2093](https://github.com/standardnotes/app/issues/2093)) ([d149b50](https://github.com/standardnotes/app/commit/d149b500ee3f13f63a1f831e6e0ba29a53e3e416))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Add current Writing Streak to Daily Notebooks (#2093) (d149b50)"
|
||||
],
|
||||
"Features": [
|
||||
"Add current Writing Streak to Daily Notebooks (#2093) (d149b50)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.123.1",
|
||||
"title": "[3.123.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where passcode lock setting was not visible on mobile ([d4af612](https://github.com/standardnotes/app/commit/d4af612fea6960a5233404682aeb1819754b9b8b))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue where passcode lock setting was not visible on mobile (d4af612)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue where passcode lock setting was not visible on mobile (d4af612)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.123.0",
|
||||
"title": "[3.123.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* alignment of Loading text in What's New screen ([d360985](https://github.com/standardnotes/app/commit/d3609853c6c6121a5633d72a685723498099edba))\n\n### Features\n\n* Moments will now create a link with the current open note, so you can look back on what you were working on during a moment ([a7ac715](https://github.com/standardnotes/app/commit/a7ac715499fdf3f15a736f68af4bf5a78454742f))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"alignment of Loading text in What's New screen (d360985)",
|
||||
"Moments will now create a link with the current open note, so you can look back on what you were working on during a moment (a7ac715)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"alignment of Loading text in What's New screen (d360985)"
|
||||
],
|
||||
"Features": [
|
||||
"Moments will now create a link with the current open note, so you can look back on what you were working on during a moment (a7ac715)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.122.0",
|
||||
"title": "[3.122.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"add ability to convert selection in Super to bulleted or numbered list (be4cc4e)"
|
||||
],
|
||||
"Features": [
|
||||
"add ability to convert selection in Super to bulleted or numbered list (be4cc4e)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.121.1",
|
||||
"title": "[3.121.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where note UUID was not selectable in context menu ([5a0c005](https://github.com/standardnotes/app/commit/5a0c005e8c0373e2f47fa613fc5bfec0e84cdb37))\n* Fixed issue where workspaces could not be renamed ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue where note UUID was not selectable in context menu (5a0c005)",
|
||||
"Fixed issue where workspaces could not be renamed (45cdab6)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue where note UUID was not selectable in context menu (5a0c005)",
|
||||
"Fixed issue where workspaces could not be renamed (45cdab6)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.121.0",
|
||||
"title": "[3.121.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where Dark theme could be called 'Focus' in Appearance preferences ([d691d7a](https://github.com/standardnotes/app/commit/d691d7a3b34f7f4249138baa4c3f27b79a9f8e6c))\n* Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu ([a5b1ff9](https://github.com/standardnotes/app/commit/a5b1ff95957a9d407663d28429785457aef3a1b4))\n\n### Features\n\n* add Indent/Outdent options in Super when on mobile ([a70d100](https://github.com/standardnotes/app/commit/a70d1009a3d8d6be1395bf148a46c3cb2260dcdc))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue where Dark theme could be called 'Focus' in Appearance preferences (d691d7a)",
|
||||
"Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu (a5b1ff9)",
|
||||
"add Indent/Outdent options in Super when on mobile (a70d100)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue where Dark theme could be called 'Focus' in Appearance preferences (d691d7a)",
|
||||
"Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu (a5b1ff9)"
|
||||
],
|
||||
"Features": [
|
||||
"add Indent/Outdent options in Super when on mobile (a70d100)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.120.3",
|
||||
"title": "[3.120.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.120.3",
|
||||
"version": "3.124.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes",
|
||||
|
||||
+47
-45
@@ -51,54 +51,56 @@ const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
const handleInputBlur: FocusEventHandler<HTMLInputElement> = useCallback(() => {
|
||||
renameDescriptor(inputValue)
|
||||
setIsRenaming(false)
|
||||
setInputValue('')
|
||||
}, [inputValue, renameDescriptor])
|
||||
|
||||
return (
|
||||
<MenuRadioButtonItem
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={onClick}
|
||||
checked={descriptor.primary}
|
||||
>
|
||||
<div className="ml-2 flex w-full items-center justify-between">
|
||||
{isRenaming ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
) : (
|
||||
<div>{descriptor.label}</div>
|
||||
)}
|
||||
{descriptor.primary && !hideOptions && (
|
||||
<div className="flex items-center">
|
||||
<a
|
||||
role="button"
|
||||
className="mr-3 h-5 w-5 cursor-pointer border-0 bg-transparent p-0 hover:bg-contrast"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsRenaming((isRenaming) => !isRenaming)
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="text-neutral" size="medium" />
|
||||
</a>
|
||||
<a
|
||||
role="button"
|
||||
className="h-5 w-5 cursor-pointer border-0 bg-transparent p-0 hover:bg-contrast"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="text-danger" size="medium" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MenuRadioButtonItem>
|
||||
<div className="relative">
|
||||
<MenuRadioButtonItem
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={onClick}
|
||||
checked={descriptor.primary}
|
||||
>
|
||||
<div className="ml-2 flex w-full items-center justify-between">
|
||||
{!isRenaming && <div>{descriptor.label}</div>}
|
||||
{descriptor.primary && !hideOptions && (
|
||||
<div className="flex items-center">
|
||||
<a
|
||||
role="button"
|
||||
className="mr-3 flex h-5 w-5 cursor-pointer items-center justify-center border-0 bg-transparent p-0 hover:bg-contrast"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsRenaming((isRenaming) => !isRenaming)
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="text-neutral" size="medium" />
|
||||
</a>
|
||||
<a
|
||||
role="button"
|
||||
className="flex h-5 w-5 cursor-pointer items-center justify-center border-0 bg-transparent p-0 hover:bg-contrast"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="text-danger" size="medium" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MenuRadioButtonItem>
|
||||
{isRenaming && (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute top-1/2 left-11 -translate-y-1/2 bg-default"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FunctionComponent } from 'react'
|
||||
|
||||
const DotOrgNotice: FunctionComponent = () => {
|
||||
return (
|
||||
<div className="align-center z-modal flex h-30 w-full w-full items-center bg-danger text-center text-info-contrast">
|
||||
<div className="z-modal flex h-30 w-full w-full items-center bg-danger text-center text-info-contrast">
|
||||
<div className="w-full text-center text-xl font-bold">
|
||||
app.standardnotes.org is no longer maintained. Please switch to{' '}
|
||||
<a className="underline" href="https://app.standardnotes.com">
|
||||
|
||||
+3
-1
@@ -18,6 +18,7 @@ type Props = {
|
||||
selectedDay?: Date
|
||||
selectedDayType?: 'item' | 'template'
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export type InfiniteCalendarInterface = {
|
||||
@@ -27,7 +28,7 @@ export type InfiniteCalendarInterface = {
|
||||
const PageSize = 2
|
||||
|
||||
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
({ activities, onDateSelect, selectedDay, className }: Props, ref) => {
|
||||
({ activities, onDateSelect, selectedDay, className, children }: Props, ref) => {
|
||||
const [expanded, setExpanded] = useState(isMobileScreen() ? false : true)
|
||||
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
||||
const scrollerRef = useRef<InfiniteScrollerInterface | null>(null)
|
||||
@@ -216,6 +217,7 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
})}
|
||||
</InfinteScroller>
|
||||
)}
|
||||
{expanded && children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ import { AppPaneId } from '../../Panes/AppPaneMetadata'
|
||||
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
||||
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||
import { DailyItemCell } from './DailyItemCell'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { SNTag, pluralize } from '@standardnotes/snjs'
|
||||
import { CalendarActivity } from '../Calendar/CalendarActivity'
|
||||
import { dateToDailyDayIdentifier } from './Utils'
|
||||
import { dateToDailyDayIdentifier, getDailyWritingStreak } from './Utils'
|
||||
import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar'
|
||||
import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller'
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
@@ -50,6 +50,11 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
return createItemsByDateMapping(items)
|
||||
}, [items])
|
||||
|
||||
const currentStreak = useMemo(
|
||||
() => getDailyWritingStreak(todayItem, itemsByDateMapping),
|
||||
[todayItem, itemsByDateMapping],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay)
|
||||
}, [dailyItems])
|
||||
@@ -182,7 +187,16 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
selectedDayType={!selectedDay ? undefined : hasItemsOnSelectedDay ? 'item' : 'template'}
|
||||
ref={calendarRef}
|
||||
className={'flex-column flex'}
|
||||
/>
|
||||
>
|
||||
{currentStreak > 0 && (
|
||||
<div className="flex w-full items-center justify-center border-t border-solid border-border bg-secondary-background p-2">
|
||||
<span className="opacity-50">Current Streak</span>
|
||||
<span className="ml-1.5 font-bold">
|
||||
{currentStreak} {pluralize(currentStreak, 'Day', 'Days')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteCalendar>
|
||||
|
||||
<InfinteScroller
|
||||
paginateFront={paginateTop}
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
import { addDaysToDate } from '@standardnotes/utils'
|
||||
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||
|
||||
export function dateToDailyDayIdentifier(date: Date): string {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
export function getDailyWritingStreak(
|
||||
todayItem: DailyItemsDay | undefined,
|
||||
itemsByDateMapping: Record<string, ListableContentItem[]>,
|
||||
) {
|
||||
if (!todayItem) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const startDay = todayItem.date
|
||||
let checkingDayOffsetFromToday = -1
|
||||
let keepLooping = true
|
||||
let streak = 0
|
||||
|
||||
while (keepLooping) {
|
||||
const checkingDay = addDaysToDate(startDay, checkingDayOffsetFromToday)
|
||||
const items = itemsByDateMapping[dateToDailyDayIdentifier(checkingDay)]
|
||||
if (!items || items?.length === 0) {
|
||||
keepLooping = false
|
||||
break
|
||||
}
|
||||
|
||||
streak++
|
||||
checkingDayOffsetFromToday--
|
||||
}
|
||||
|
||||
const hasEntryForToday = itemsByDateMapping[dateToDailyDayIdentifier(todayItem.date)]?.length > 0
|
||||
|
||||
return streak + (hasEntryForToday ? 1 : 0)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
||||
import { getItemTitleInContextOfLinkBubble } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
|
||||
|
||||
type Props = {
|
||||
link: ItemLink
|
||||
@@ -117,7 +118,7 @@ const LinkedItemBubble = ({
|
||||
{link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && (
|
||||
<span className={!isBidirectional ? 'hidden group-focus:block' : ''}>Linked By:</span>
|
||||
)}
|
||||
{link.item.title}
|
||||
{getItemTitleInContextOfLinkBubble(link.item)}
|
||||
</span>
|
||||
</span>
|
||||
{showUnlinkButton && (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { getItemTitleInContextOfLinkBubble } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
|
||||
|
||||
type Props = {
|
||||
item: LinkableItem
|
||||
@@ -16,7 +17,7 @@ const LinkedItemMeta = ({ item, searchQuery }: Props) => {
|
||||
const application = useApplication()
|
||||
const [icon, className] = getIconForItem(item, application)
|
||||
const tagTitle = getTitleForLinkedTag(item, application)
|
||||
const title = item.title ?? ''
|
||||
const title = getItemTitleInContextOfLinkBubble(item)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CSSProperties, forwardRef, KeyboardEventHandler, ReactNode, Ref, useCallback, useEffect, useRef } from 'react'
|
||||
import { CSSProperties, forwardRef, KeyboardEventHandler, ReactNode, Ref, useCallback, useRef } from 'react'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||
@@ -23,7 +23,6 @@ const Menu = forwardRef(
|
||||
style,
|
||||
a11yLabel,
|
||||
closeMenu,
|
||||
isOpen,
|
||||
initialFocus,
|
||||
onKeyDown,
|
||||
shouldAutoFocus = true,
|
||||
@@ -44,15 +43,7 @@ const Menu = forwardRef(
|
||||
[closeMenu, onKeyDown],
|
||||
)
|
||||
|
||||
useListKeyboardNavigation(menuElementRef, initialFocus)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && shouldAutoFocus) {
|
||||
setTimeout(() => {
|
||||
menuElementRef.current?.focus()
|
||||
}, 5)
|
||||
}
|
||||
}, [isOpen, shouldAutoFocus])
|
||||
useListKeyboardNavigation(menuElementRef, initialFocus, shouldAutoFocus)
|
||||
|
||||
return (
|
||||
<menu
|
||||
|
||||
@@ -905,9 +905,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{editorMode !== 'super' && (
|
||||
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
|
||||
)}
|
||||
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+7
-1
@@ -23,9 +23,12 @@ import Popover from '@/Components/Popover/Popover'
|
||||
import { PopoverClassNames } from '../ClassNames'
|
||||
import { GetDatetimeBlocks } from './Blocks/DateTime'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { GetIndentOutdentBlocks } from './Blocks/IndentOutdent'
|
||||
|
||||
export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const application = useApplication()
|
||||
const [modal, showModal] = useModal()
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
|
||||
@@ -36,9 +39,12 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
const [popoverOpen, setPopoverOpen] = useState(true)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const indentOutdentOptions = application.isNativeMobileWeb() ? GetIndentOutdentBlocks(editor) : []
|
||||
|
||||
const baseOptions = [
|
||||
GetParagraphBlock(editor),
|
||||
...GetHeadingsBlocks(editor),
|
||||
...indentOutdentOptions,
|
||||
GetTableBlock(() =>
|
||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||
),
|
||||
@@ -70,7 +76,7 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
}),
|
||||
]
|
||||
: baseOptions
|
||||
}, [editor, queryString, showModal])
|
||||
}, [editor, queryString, showModal, application])
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, LexicalEditor } from 'lexical'
|
||||
|
||||
export function GetIndentOutdentBlocks(editor: LexicalEditor) {
|
||||
return [
|
||||
new BlockPickerOption('Indent', {
|
||||
iconName: 'arrow-right',
|
||||
keywords: ['indent'],
|
||||
onSelect: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
||||
}),
|
||||
new BlockPickerOption('Outdent', {
|
||||
iconName: 'arrow-left',
|
||||
keywords: ['outdent'],
|
||||
onSelect: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
||||
}),
|
||||
]
|
||||
}
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { TextNode } from 'lexical'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
|
||||
@@ -12,6 +12,7 @@ import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands'
|
||||
import { useLinkingController } from '../../../../../Controllers/LinkingControllerProvider'
|
||||
import { PopoverClassNames } from '../ClassNames'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { useTypeaheadAllowingSpacesAndPunctuation } from './useTypeaheadAllowingSpacesAndPunctuation'
|
||||
|
||||
type Props = {
|
||||
currentNote: SNNote
|
||||
@@ -26,7 +27,7 @@ export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) =
|
||||
|
||||
const [queryString, setQueryString] = useState<string | null>('')
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', {
|
||||
const checkForTriggerMatch = useTypeaheadAllowingSpacesAndPunctuation('@', {
|
||||
minLength: 0,
|
||||
})
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export type QueryMatch = {
|
||||
leadOffset: number
|
||||
matchingString: string
|
||||
replaceableString: string
|
||||
}
|
||||
type TriggerFn = (text: string, editor: LexicalEditor) => QueryMatch | null
|
||||
|
||||
/**
|
||||
* Derived from
|
||||
* https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx#L545
|
||||
*/
|
||||
export function useTypeaheadAllowingSpacesAndPunctuation(
|
||||
trigger: string,
|
||||
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
|
||||
): TriggerFn {
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const validChars = '[^' + trigger + ']'
|
||||
const TypeaheadTriggerRegex = new RegExp(
|
||||
'(^|\\s|\\()(' + '[' + trigger + ']' + '((?:' + validChars + '){0,' + maxLength + '})' + ')$',
|
||||
)
|
||||
const match = TypeaheadTriggerRegex.exec(text)
|
||||
if (match !== null) {
|
||||
const maybeLeadingWhitespace = match[1]
|
||||
const matchingString = match[3]
|
||||
if (matchingString.length >= minLength) {
|
||||
return {
|
||||
leadOffset: match.index + maybeLeadingWhitespace.length,
|
||||
matchingString,
|
||||
replaceableString: match[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
[maxLength, minLength, trigger],
|
||||
)
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
>
|
||||
<BlocksEditor
|
||||
onChange={handleChange}
|
||||
ignoreFirstChange={false}
|
||||
ignoreFirstChange={controller.isTemplateNote}
|
||||
className={classNames(
|
||||
'relative h-full resize-none px-4 py-4 focus:shadow-none focus:outline-none',
|
||||
lineHeight && `leading-${lineHeight.toLowerCase()}`,
|
||||
|
||||
+1
-14
@@ -8,7 +8,6 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { useBeforeUnload } from '@/Hooks/useBeforeUnload'
|
||||
import ChangeEmailForm from './ChangeEmailForm'
|
||||
import ChangeEmailSuccess from './ChangeEmailSuccess'
|
||||
import { isEmailValid } from '@/Utils'
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
@@ -57,18 +56,6 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
return success
|
||||
}
|
||||
|
||||
const validateNewEmail = async () => {
|
||||
if (!isEmailValid(newEmail)) {
|
||||
applicationAlertService
|
||||
.alert('The email you entered has an invalid format. Please review your input and try again.')
|
||||
.catch(console.error)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default)
|
||||
setIsContinuing(false)
|
||||
@@ -110,7 +97,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
setIsContinuing(true)
|
||||
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys)
|
||||
|
||||
const valid = (await validateCurrentPassword()) && (await validateNewEmail())
|
||||
const valid = await validateCurrentPassword()
|
||||
|
||||
if (!valid) {
|
||||
resetProgressState()
|
||||
|
||||
@@ -4,11 +4,19 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ContentType, FeatureIdentifier, PrefKey, GetFeatures, SNTheme } from '@standardnotes/snjs'
|
||||
import {
|
||||
ContentType,
|
||||
FeatureIdentifier,
|
||||
PrefKey,
|
||||
GetFeatures,
|
||||
SNTheme,
|
||||
FindNativeFeature,
|
||||
FeatureStatus,
|
||||
naturalSort,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useEffect, useState } from 'react'
|
||||
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { sortThemes } from '@/Utils/SortThemes'
|
||||
import PreferencesPane from '../PreferencesComponents/PreferencesPane'
|
||||
import PreferencesGroup from '../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../PreferencesComponents/PreferencesSegment'
|
||||
@@ -37,9 +45,8 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
useEffect(() => {
|
||||
const themesAsItems: DropdownItem[] = application.items
|
||||
.getDisplayableComponents()
|
||||
.filter((component) => component.isTheme())
|
||||
.filter((component) => !(component as SNTheme).isLayerable())
|
||||
.sort(sortThemes)
|
||||
.filter((component) => component.isTheme() && !(component as SNTheme).isLayerable())
|
||||
.filter((theme) => !FindNativeFeature(theme.identifier))
|
||||
.map((theme) => {
|
||||
return {
|
||||
label: theme.name,
|
||||
@@ -50,13 +57,14 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
GetFeatures()
|
||||
.filter((feature) => feature.content_type === ContentType.Theme && !feature.layerable)
|
||||
.forEach((theme) => {
|
||||
if (themesAsItems.findIndex((item) => item.value === theme.identifier) === -1) {
|
||||
themesAsItems.push({
|
||||
label: theme.name as string,
|
||||
value: theme.identifier,
|
||||
icon: PremiumFeatureIconName,
|
||||
})
|
||||
}
|
||||
themesAsItems.push({
|
||||
label: theme.name as string,
|
||||
value: theme.identifier,
|
||||
icon:
|
||||
application.features.getFeatureStatus(theme.identifier) !== FeatureStatus.Entitled
|
||||
? PremiumFeatureIconName
|
||||
: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
themesAsItems.unshift({
|
||||
@@ -64,7 +72,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
value: 'Default',
|
||||
})
|
||||
|
||||
setThemeItems(themesAsItems)
|
||||
setThemeItems(naturalSort(themesAsItems, 'label'))
|
||||
}, [application])
|
||||
|
||||
const toggleUseDeviceSettings = () => {
|
||||
|
||||
@@ -186,10 +186,6 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => {
|
||||
|
||||
const autolockService = application.getAutolockService()
|
||||
|
||||
if (!autolockService) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
@@ -246,7 +242,7 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => {
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
{hasPasscode && !isNativeMobileWeb && (
|
||||
{hasPasscode && autolockService && (
|
||||
<>
|
||||
<div className="min-h-3" />
|
||||
<PreferencesGroup>
|
||||
|
||||
@@ -42,7 +42,11 @@ const WhatsNew = ({ application }: { application: WebApplication }) => {
|
||||
}, [changelog, application])
|
||||
|
||||
if (!changelog) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<div className="flex h-full w-full items-center text-center">
|
||||
<span className="w-full font-bold">Loading...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
export async function awaitVideoReady(video: HTMLVideoElement) {
|
||||
return new Promise((resolve) => {
|
||||
video.addEventListener('canplaythrough', () => {
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function stopCameraStream(canvas: HTMLCanvasElement, video: HTMLVideoElement, stream: MediaStream) {
|
||||
video.pause()
|
||||
video.parentElement?.removeChild(video)
|
||||
canvas.parentElement?.removeChild(canvas)
|
||||
video.remove()
|
||||
canvas.remove()
|
||||
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
}
|
||||
|
||||
export async function takePhoto(
|
||||
filename: string,
|
||||
canvas: HTMLCanvasElement,
|
||||
video: HTMLVideoElement,
|
||||
width: number,
|
||||
height: number,
|
||||
): Promise<File | undefined> {
|
||||
const context = canvas.getContext('2d')
|
||||
context?.drawImage(video, 0, 0, width, height)
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
|
||||
const isFailedImage = dataUrl.length < 100000
|
||||
|
||||
if (isFailedImage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const res: Response = await fetch(dataUrl)
|
||||
const blob: Blob = await res.blob()
|
||||
const file = new File([blob], filename, { type: 'image/png' })
|
||||
return file
|
||||
}
|
||||
|
||||
export async function preparePhotoOperation() {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
|
||||
const video = document.createElement('video')
|
||||
video.playsInline = true
|
||||
video.style.position = 'absolute'
|
||||
video.style.display = 'none'
|
||||
const canvas = document.createElement('canvas')
|
||||
document.body.append(video)
|
||||
video.srcObject = stream
|
||||
await video.play()
|
||||
await awaitVideoReady(video)
|
||||
|
||||
const videoTrack = stream.getVideoTracks()[0]
|
||||
const settings = videoTrack.getSettings()
|
||||
const width = settings.width ?? 1280
|
||||
const height = settings.height ?? 720
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
return { canvas, video, stream, width, height }
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import { ApplicationEvent, InternalEventBus, StorageKey } from '@standardnotes/s
|
||||
import { isDev } from '@/Utils'
|
||||
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
||||
import { FilesController } from '../FilesController'
|
||||
import { preparePhotoOperation, takePhoto, stopCameraStream } from './CameraUtils'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
||||
import { PhotoRecorder } from './PhotoRecorder'
|
||||
|
||||
const EVERY_HALF_HOUR = 1000 * 60 * 30
|
||||
const EVERY_TEN_SECONDS = 1000 * 10
|
||||
@@ -100,18 +100,18 @@ export class MomentsService extends AbstractViewController {
|
||||
}
|
||||
}
|
||||
|
||||
const { canvas, video, stream, width, height } = await preparePhotoOperation()
|
||||
|
||||
const filename = `Moment ${dateToStringStyle1(new Date())}.png`
|
||||
const camera = new PhotoRecorder(filename)
|
||||
await camera.initialize()
|
||||
|
||||
if (this.application.isMobileDevice) {
|
||||
await sleep(DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS)
|
||||
}
|
||||
|
||||
let file = await takePhoto(filename, canvas, video, width, height)
|
||||
let file = await camera.takePhoto()
|
||||
if (!file) {
|
||||
await sleep(1000)
|
||||
file = await takePhoto(filename, canvas, video, width, height)
|
||||
file = await camera.takePhoto()
|
||||
if (!file) {
|
||||
return undefined
|
||||
}
|
||||
@@ -121,12 +121,16 @@ export class MomentsService extends AbstractViewController {
|
||||
|
||||
const uploadedFile = await this.filesController.uploadNewFile(file)
|
||||
|
||||
const defaultTag = this.getDefaultTag()
|
||||
if (defaultTag && uploadedFile) {
|
||||
void this.application.linkingController.linkItems(uploadedFile, defaultTag)
|
||||
if (uploadedFile) {
|
||||
void this.application.linkingController.linkItemToSelectedItem(uploadedFile)
|
||||
|
||||
const defaultTag = this.getDefaultTag()
|
||||
if (defaultTag) {
|
||||
void this.application.linkingController.linkItems(uploadedFile, defaultTag)
|
||||
}
|
||||
}
|
||||
|
||||
stopCameraStream(canvas, video, stream)
|
||||
camera.finish()
|
||||
|
||||
return uploadedFile
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
export class PhotoRecorder {
|
||||
public video!: HTMLVideoElement
|
||||
|
||||
private canvas!: HTMLCanvasElement
|
||||
private width!: number
|
||||
private height!: number
|
||||
private stream!: MediaStream
|
||||
|
||||
constructor(private fileName: string) {}
|
||||
|
||||
public async initialize() {
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
|
||||
|
||||
this.video = document.createElement('video')
|
||||
this.video.playsInline = true
|
||||
this.video.style.position = 'absolute'
|
||||
this.video.style.display = 'none'
|
||||
|
||||
this.canvas = document.createElement('canvas')
|
||||
|
||||
document.body.append(this.video)
|
||||
this.video.srcObject = this.stream
|
||||
|
||||
await this.video.play()
|
||||
await this.awaitVideoReady(this.video)
|
||||
|
||||
const videoTrack = this.stream.getVideoTracks()[0]
|
||||
const settings = videoTrack.getSettings()
|
||||
this.width = settings.width ?? 1280
|
||||
this.height = settings.height ?? 720
|
||||
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
}
|
||||
|
||||
public async takePhoto(): Promise<File | undefined> {
|
||||
const context = this.canvas.getContext('2d')
|
||||
context?.drawImage(this.video, 0, 0, this.width, this.height)
|
||||
const dataUrl = this.canvas.toDataURL('image/png')
|
||||
|
||||
const isFailedImage = dataUrl.length < 100000
|
||||
|
||||
if (isFailedImage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const res: Response = await fetch(dataUrl)
|
||||
const blob: Blob = await res.blob()
|
||||
const file = new File([blob], this.fileName, { type: 'image/png' })
|
||||
return file
|
||||
}
|
||||
|
||||
public finish() {
|
||||
this.video.pause()
|
||||
|
||||
this.video.parentElement?.removeChild(this.video)
|
||||
this.canvas.parentElement?.removeChild(this.canvas)
|
||||
|
||||
this.video.remove()
|
||||
this.canvas.remove()
|
||||
|
||||
this.stream.getTracks().forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
}
|
||||
|
||||
private async awaitVideoReady(video: HTMLVideoElement) {
|
||||
return new Promise((resolve) => {
|
||||
video.addEventListener('canplaythrough', () => {
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Deferred } from '@standardnotes/snjs'
|
||||
|
||||
export class VideoRecorder {
|
||||
public video!: HTMLVideoElement
|
||||
|
||||
private canvas!: HTMLCanvasElement
|
||||
private width!: number
|
||||
private height!: number
|
||||
private stream!: MediaStream
|
||||
private recorder!: MediaRecorder
|
||||
|
||||
private dataReadyPromise = Deferred<File>()
|
||||
|
||||
constructor(private fileName: string) {}
|
||||
|
||||
public async initialize() {
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
this.recorder = new MediaRecorder(this.stream)
|
||||
|
||||
this.video = document.createElement('video')
|
||||
this.video.playsInline = true
|
||||
this.video.style.position = 'absolute'
|
||||
this.video.style.display = 'none'
|
||||
this.video.volume = 0
|
||||
|
||||
this.canvas = document.createElement('canvas')
|
||||
|
||||
document.body.append(this.video)
|
||||
this.video.srcObject = this.stream
|
||||
|
||||
await this.video.play()
|
||||
await this.awaitVideoReady(this.video)
|
||||
|
||||
const videoTrack = this.stream.getVideoTracks()[0]
|
||||
const settings = videoTrack.getSettings()
|
||||
this.width = settings.width ?? 1280
|
||||
this.height = settings.height ?? 720
|
||||
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
}
|
||||
|
||||
public async startRecording(): Promise<void> {
|
||||
this.recorder.start()
|
||||
|
||||
this.recorder.ondataavailable = this.onData
|
||||
}
|
||||
|
||||
private onData = async (event: BlobEvent) => {
|
||||
const blob = new Blob([event.data], { type: 'video/mp4' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const res: Response = await fetch(url)
|
||||
const responseBlob: Blob = await res.blob()
|
||||
const file = new File([responseBlob], this.fileName, { type: 'video/mp4' })
|
||||
|
||||
this.dataReadyPromise.resolve(file)
|
||||
}
|
||||
|
||||
public async stop(): Promise<File> {
|
||||
this.video.pause()
|
||||
this.recorder.stop()
|
||||
|
||||
this.video.parentElement?.removeChild(this.video)
|
||||
this.canvas.parentElement?.removeChild(this.canvas)
|
||||
|
||||
this.video.remove()
|
||||
this.canvas.remove()
|
||||
|
||||
this.stream.getTracks().forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
|
||||
return this.dataReadyPromise.promise
|
||||
}
|
||||
|
||||
private async awaitVideoReady(video: HTMLVideoElement) {
|
||||
return new Promise((resolve) => {
|
||||
video.addEventListener('canplaythrough', () => {
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@ import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { useCallback, useEffect, RefObject, useRef } from 'react'
|
||||
|
||||
export const useListKeyboardNavigation = (container: RefObject<HTMLElement | null>, initialFocus = 0) => {
|
||||
export const useListKeyboardNavigation = (
|
||||
container: RefObject<HTMLElement | null>,
|
||||
initialFocus = 0,
|
||||
shouldAutoFocus = false,
|
||||
) => {
|
||||
const listItems = useRef<HTMLButtonElement[]>([])
|
||||
const focusedItemIndex = useRef<number>(initialFocus)
|
||||
|
||||
@@ -69,13 +73,17 @@ export const useListKeyboardNavigation = (container: RefObject<HTMLElement | nul
|
||||
}, FIRST_ITEM_FOCUS_TIMEOUT)
|
||||
}, [container, focusItemWithIndex, initialFocus])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoFocus) {
|
||||
containerFocusHandler()
|
||||
}
|
||||
}, [containerFocusHandler, shouldAutoFocus])
|
||||
|
||||
useEffect(() => {
|
||||
const containerElement = container.current
|
||||
containerElement?.addEventListener('focus', containerFocusHandler)
|
||||
containerElement?.addEventListener('keydown', keyDownHandler)
|
||||
|
||||
return () => {
|
||||
containerElement?.removeEventListener('focus', containerFocusHandler)
|
||||
containerElement?.removeEventListener('keydown', keyDownHandler)
|
||||
}
|
||||
}, [container, containerFocusHandler, keyDownHandler])
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { SNTag, WebApplicationInterface, DecryptedItemInterface, ItemContent } from '@standardnotes/snjs'
|
||||
import { WebApplicationInterface, DecryptedItemInterface, ItemContent, isNote, isTag } from '@standardnotes/snjs'
|
||||
|
||||
export function getItemTitleInContextOfLinkBubble(item: DecryptedItemInterface<ItemContent>) {
|
||||
return item.title && item.title.length > 0 ? item.title : isNote(item) ? item.preview_plain : ''
|
||||
}
|
||||
|
||||
function getItemSearchableString(item: DecryptedItemInterface<ItemContent>, application: WebApplicationInterface) {
|
||||
if (isNote(item)) {
|
||||
return item.title.length > 0 ? item.title : item.preview_plain
|
||||
} else if (isTag(item)) {
|
||||
return application.items.getTagLongTitle(item)
|
||||
}
|
||||
|
||||
return item.title ?? ''
|
||||
}
|
||||
|
||||
export function doesItemMatchSearchQuery(
|
||||
item: DecryptedItemInterface<ItemContent>,
|
||||
searchQuery: string,
|
||||
application: WebApplicationInterface,
|
||||
) {
|
||||
const title = item instanceof SNTag ? application.items.getTagLongTitle(item) : item.title ?? ''
|
||||
const matchesQuery = title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const title = getItemSearchableString(item, application).toLowerCase()
|
||||
const matchesQuery = title.includes(searchQuery.toLowerCase())
|
||||
const isArchivedOrTrashed = item.archived || item.trashed
|
||||
const isValidSearchResult = matchesQuery && !isArchivedOrTrashed
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ module.exports = {
|
||||
foreground: 'var(--sn-stylekit-foreground-color)',
|
||||
contrast: 'var(--sn-stylekit-contrast-background-color)',
|
||||
'secondary-contrast': 'var(--sn-stylekit-secondary-contrast-border-color)',
|
||||
'secondary-background': 'var(--sn-stylekit-secondary-background-color)',
|
||||
text: 'var(--sn-stylekit-contrast-foreground-color)',
|
||||
border: 'var(--sn-stylekit-border-color)',
|
||||
'secondary-border': 'var(--sn-stylekit-secondary-border-color)',
|
||||
|
||||
Reference in New Issue
Block a user