Merge pull request #552 from standardnotes/feature/multiple-selection
feat: multiple selection
4
app/assets/icons/ic-archive.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.4444 12.3333H12.3333C12.3333 12.9522 12.0875 13.5457 11.6499 13.9832C11.2123 14.4208 10.6188 14.6667 10 14.6667C9.38116 14.6667 8.78767 14.4208 8.35008 13.9832C7.9125 13.5457 7.66667 12.9522 7.66667 12.3333H4.55556V4.55556H15.4444V12.3333ZM15.4444 3H4.55556C3.69222 3 3 3.7 3 4.55556V15.4444C3 15.857 3.16389 16.2527 3.45561 16.5444C3.74733 16.8361 4.143 17 4.55556 17H15.4444C15.857 17 16.2527 16.8361 16.5444 16.5444C16.8361 16.2527 17 15.857 17 15.4444V4.55556C17 4.143 16.8361 3.74733 16.5444 3.45561C16.2527 3.16389 15.857 3 15.4444 3Z" />
|
||||
<path d="M13.1111 8.44442H11.5556V6.11108H8.44447V8.44442H6.88892L10 11.5555L13.1111 8.44442Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 736 B |
3
app/assets/icons/ic-chevron-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.90918 14.0667L10.7342 10.2417L6.90918 6.4167L8.09251 5.2417L13.0925 10.2417L8.09251 15.2417L6.90918 14.0667Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 205 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2459 5.92925C15.5704 5.60478 15.5704 5.07872 15.2459 4.75425C14.9214 4.42978 14.3954 4.42978 14.0709 4.75425L10.0001 8.82508L5.92925 4.75425C5.60478 4.42978 5.07872 4.42978 4.75425 4.75425C4.42978 5.07872 4.42978 5.60478 4.75425 5.92925L8.82508 10.0001L4.75425 14.0709C4.42978 14.3954 4.42978 14.9214 4.75425 15.2459C5.07872 15.5704 5.60478 15.5704 5.92925 15.2459L10.0001 11.1751L14.0709 15.2459C14.3954 15.5704 14.9214 15.5704 15.2459 15.2459C15.5704 14.9214 15.5704 14.3954 15.2459 14.0709L11.1751 10.0001L15.2459 5.92925Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 623 B |
3
app/assets/icons/ic-hashtag.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill-rule="evenodd" clip-rule="evenodd" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.6 3.5H7V6.9L3.5 6.9V8.5H7V11.4H3.5V13H7V16.5H8.6V13L11.5 13V16.5H13.1V13H16.5V11.4H13.1V8.5H16.5V6.9L13.1 6.9V3.5H11.5V6.9L8.6 6.9V3.5ZM8.6 8.5V11.4L11.5 11.4V8.5L8.6 8.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
3
app/assets/icons/ic-more.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3333 9.99992C13.3333 9.55789 13.5088 9.13397 13.8214 8.82141C14.134 8.50885 14.5579 8.33325 14.9999 8.33325C15.4419 8.33325 15.8659 8.50885 16.1784 8.82141C16.491 9.13397 16.6666 9.55789 16.6666 9.99992C16.6666 10.4419 16.491 10.8659 16.1784 11.1784C15.8659 11.491 15.4419 11.6666 14.9999 11.6666C14.5579 11.6666 14.134 11.491 13.8214 11.1784C13.5088 10.8659 13.3333 10.4419 13.3333 9.99992ZM8.33325 9.99992C8.33325 9.55789 8.50885 9.13397 8.82141 8.82141C9.13397 8.50885 9.55789 8.33325 9.99992 8.33325C10.4419 8.33325 10.8659 8.50885 11.1784 8.82141C11.491 9.13397 11.6666 9.55789 11.6666 9.99992C11.6666 10.4419 11.491 10.8659 11.1784 11.1784C10.8659 11.491 10.4419 11.6666 9.99992 11.6666C9.55789 11.6666 9.13397 11.491 8.82141 11.1784C8.50885 10.8659 8.33325 10.4419 8.33325 9.99992ZM3.33325 9.99992C3.33325 9.55789 3.50885 9.13397 3.82141 8.82141C4.13397 8.50885 4.55789 8.33325 4.99992 8.33325C5.44195 8.33325 5.86587 8.50885 6.17843 8.82141C6.49099 9.13397 6.66658 9.55789 6.66658 9.99992C6.66658 10.4419 6.49099 10.8659 6.17843 11.1784C5.86587 11.491 5.44195 11.6666 4.99992 11.6666C4.55789 11.6666 4.13397 11.491 3.82141 11.1784C3.50885 10.8659 3.33325 10.4419 3.33325 9.99992Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
app/assets/icons/ic-pencil-off.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.25 7.9L12.25 4.9L15.1 7.75L12.1 10.75L11.05 9.7L13 7.675L12.325 7L10.375 8.95L9.25 7.9ZM17.275 4.45L15.55 2.725C15.4 2.575 15.175 2.5 15.025 2.5C14.875 2.5 14.65 2.575 14.5 2.725L13.15 4.075L16 6.925L17.275 5.5C17.575 5.275 17.575 4.75 17.275 4.45ZM16 16.525L15.025 17.5L10.15 12.625L6.85 16H4V13.15L7.375 9.775L2.5 4.975L3.475 4L16 16.525ZM9.1 11.575L8.425 10.9L5.5 13.825V14.5H6.175L9.1 11.575Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
3
app/assets/icons/ic-pin-off.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 9.68991L1.66675 4.35665L2.84526 3.17813L16.9874 17.3203L15.8089 18.4988L10.6 13.2899V17.5H9.4V13H4.50001V11.5L7 10V9.68991ZM8.8101 11.5H6.60001L8.09971 10.7896L8.8101 11.5ZM11.5 4V9.47623L15.0238 13H15.5V11.5L13 10V4H13.75V2.5H6.25001V4H7V4.97623L8.5 6.47623V4H11.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 403 B |
3
app/assets/icons/ic-pin.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 10V4H13.75V2.5H6.25V4H7V10L4.5 11.5V13H9.4V17.5H10.6V13H15.5V11.5L13 10ZM6.6 11.5L8.5 10.6V4H11.5V10.6L13.4 11.5H6.6Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 215 B |
3
app/assets/icons/ic-restore.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 2.5C8.01088 2.5 6.10322 3.29018 4.6967 4.6967C3.29018 6.10322 2.5 8.01088 2.5 10H0L3.33333 13.3333L6.66667 10H4.16667C4.16667 8.4529 4.78125 6.96917 5.87521 5.87521C6.96917 4.78125 8.4529 4.16667 10 4.16667C11.5471 4.16667 13.0308 4.78125 14.1248 5.87521C15.2188 6.96917 15.8333 8.4529 15.8333 10C15.8333 11.5471 15.2188 13.0308 14.1248 14.1248C13.0308 15.2188 11.5471 15.8333 10 15.8333C8.75 15.8333 7.575 15.4167 6.61667 14.75L5.41667 15.95C6.7 16.9167 8.28333 17.5 10 17.5C11.9891 17.5 13.8968 16.7098 15.3033 15.3033C16.7098 13.8968 17.5 11.9891 17.5 10C17.5 8.01088 16.7098 6.10322 15.3033 4.6967C13.8968 3.29018 11.9891 2.5 10 2.5ZM11.6667 10C11.6667 9.55797 11.4911 9.13405 11.1785 8.82149C10.8659 8.50893 10.442 8.33333 10 8.33333C9.55797 8.33333 9.13405 8.50893 8.82149 8.82149C8.50893 9.13405 8.33333 9.55797 8.33333 10C8.33333 10.442 8.50893 10.866 8.82149 11.1785C9.13405 11.4911 9.55797 11.6667 10 11.6667C10.442 11.6667 10.8659 11.4911 11.1785 11.1785C11.4911 10.866 11.6667 10.442 11.6667 10Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
app/assets/icons/ic-text-rich.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 7.49984H7.5V12.4998H2.5V7.49984ZM2.5 4.1665H17.5V5.83317H2.5V4.1665ZM17.5 7.49984V9.1665H9.16667V7.49984H17.5ZM17.5 10.8332V12.4998H9.16667V10.8332H17.5ZM2.5 14.1665H14.1667V15.8332H2.5V14.1665Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
3
app/assets/icons/ic-textbox-password.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.2 7.8H20.4V16.2H16.2V17.88C16.2 18.1028 16.2885 18.3164 16.446 18.474C16.6035 18.6315 16.8172 18.72 17.04 18.72H18.72V20.4H16.62C16.158 20.4 15.36 20.022 15.36 19.56C15.36 20.022 14.562 20.4 14.1 20.4H12V18.72H13.68C13.9028 18.72 14.1164 18.6315 14.2739 18.474C14.4315 18.3164 14.52 18.1028 14.52 17.88V6.12C14.52 5.89722 14.4315 5.68356 14.2739 5.52603C14.1164 5.3685 13.9028 5.28 13.68 5.28H12V3.6H14.1C14.562 3.6 15.36 3.978 15.36 4.44C15.36 3.978 16.158 3.6 16.62 3.6H18.72V5.28H17.04C16.8172 5.28 16.6035 5.3685 16.446 5.52603C16.2885 5.68356 16.2 5.89722 16.2 6.12V7.8ZM3.59998 7.8H12.84V9.48H5.27998V14.52H12.84V16.2H3.59998V7.8ZM18.72 14.52V9.48H16.2V14.52H18.72ZM9.05998 12C9.05998 11.6658 8.92723 11.3453 8.69093 11.109C8.45463 10.8727 8.13415 10.74 7.79998 10.74C7.4658 10.74 7.14532 10.8727 6.90902 11.109C6.67273 11.3453 6.53998 11.6658 6.53998 12C6.53998 12.3342 6.67273 12.6547 6.90902 12.891C7.14532 13.1272 7.4658 13.26 7.79998 13.26C8.13415 13.26 8.45463 13.1272 8.69093 12.891C8.92723 12.6547 9.05998 12.3342 9.05998 12ZM12.84 11.0676C12.3276 10.5972 11.5296 10.6392 11.0592 11.16C10.5888 11.664 10.6308 12.462 11.16 12.9324C11.622 13.3692 12.3612 13.3692 12.84 12.9324V11.0676Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
app/assets/icons/ic-trash-sweep.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5001 13.3333H15.8334V14.9999H12.5001V13.3333ZM12.5001 6.66658H18.3334V8.33325H12.5001V6.66658ZM12.5001 9.99992H17.5001V11.6666H12.5001V9.99992ZM9.16675 8.33325V14.9999H4.16675V8.33325H9.16675ZM10.8334 6.66658H2.50008V14.9999C2.50008 15.4419 2.67568 15.8659 2.98824 16.1784C3.3008 16.491 3.72472 16.6666 4.16675 16.6666H9.16675C9.60878 16.6666 10.0327 16.491 10.3453 16.1784C10.6578 15.8659 10.8334 15.4419 10.8334 14.9999V6.66658ZM11.6667 4.16659H9.16675L8.33342 3.33325H5.00008L4.16675 4.16659H1.66675V5.83325H11.6667V4.16659Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
3
app/assets/icons/ic-trash.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.49992 2.5V3.33333H3.33325V5H4.16659V15.8333C4.16659 16.2754 4.34218 16.6993 4.65474 17.0118C4.9673 17.3244 5.39122 17.5 5.83325 17.5H14.1666C14.6086 17.5 15.0325 17.3244 15.3451 17.0118C15.6577 16.6993 15.8333 16.2754 15.8333 15.8333V5H16.6666V3.33333H12.4999V2.5H7.49992ZM5.83325 5H14.1666V15.8333H5.83325V5ZM7.49992 6.66667V14.1667H9.16658V6.66667H7.49992ZM10.8333 6.66667V14.1667H12.4999V6.66667H10.8333Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 14.1667V15.8333H7.5V14.1667H2.5ZM2.5 4.16667V5.83333H10.8333V4.16667H2.5ZM10.8333 17.5V15.8333H17.5V14.1667H10.8333V12.5H9.16667V17.5H10.8333ZM5.83333 7.5V9.16667H2.5V10.8333H5.83333V12.5H7.5V7.5H5.83333ZM17.5 10.8333V9.16667H9.16667V10.8333H17.5ZM12.5 7.5H14.1667V5.83333H17.5V4.16667H14.1667V2.5H12.5V7.5Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 406 B |
3
app/assets/icons/ic-unarchive.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3333 12.3333H15.4444V4.55556H4.55556V12.3333H7.66667C7.66667 12.9522 7.9125 13.5457 8.35008 13.9832C8.78767 14.4208 9.38116 14.6667 10 14.6667C10.6188 14.6667 11.2123 14.4208 11.6499 13.9832C12.0875 13.5457 12.3333 12.9522 12.3333 12.3333ZM4.55556 3H15.4444C15.857 3 16.2527 3.16389 16.5444 3.45561C16.8361 3.74733 17 4.143 17 4.55556V15.4444C17 15.857 16.8361 16.2527 16.5444 16.5444C16.2527 16.8361 15.857 17 15.4444 17H4.55556C4.143 17 3.74733 16.8361 3.45561 16.5444C3.16389 16.2527 3 15.857 3 15.4444V4.55556C3 3.7 3.69222 3 4.55556 3ZM8.44442 8.91667L6.88886 8.91667L9.99997 5.80555L13.1111 8.91667H11.5555V11.25H8.44442V8.91667Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 773 B |
35
app/assets/icons/il-notes.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="60" fill="#F4F5F7"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<rect x="21.5051" y="49.3528" width="40" height="40" rx="4" transform="rotate(-15 21.5051 49.3528)" fill="white"/>
|
||||
</g>
|
||||
<rect x="29.8889" y="57.4591" width="28" height="4" rx="2" transform="rotate(-15 29.8889 57.4591)" fill="#BBBEC4"/>
|
||||
<rect x="31.9595" y="65.1865" width="28" height="4" rx="2" transform="rotate(-15 31.9595 65.1865)" fill="#BBBEC4"/>
|
||||
<rect x="34.03" y="72.9139" width="16" height="4" rx="2" transform="rotate(-15 34.03 72.9139)" fill="#BBBEC4"/>
|
||||
<g filter="url(#filter1_d)">
|
||||
<rect x="40" y="32" width="56" height="56" rx="4" fill="white"/>
|
||||
</g>
|
||||
<rect x="48" y="46" width="40" height="6" rx="3" fill="#BBBEC4"/>
|
||||
<rect x="48" y="57" width="40" height="6" rx="3" fill="#BBBEC4"/>
|
||||
<rect x="48" y="68" width="22" height="6" rx="3" fill="#BBBEC4"/>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="10.4031" y="31.8979" width="71.1938" height="71.1938" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="28" y="24" width="80" height="80" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -60,6 +60,10 @@ import { NoAccountWarningDirective } from './components/NoAccountWarning';
|
||||
import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning';
|
||||
import { SearchOptionsDirective } from './components/SearchOptions';
|
||||
import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal';
|
||||
import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes';
|
||||
import { NotesContextMenuDirective } from './components/NotesContextMenu';
|
||||
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
|
||||
import { IconDirective } from './components/Icon';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
@@ -149,7 +153,11 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('noAccountWarning', NoAccountWarningDirective)
|
||||
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
|
||||
.directive('searchOptions', SearchOptionsDirective)
|
||||
.directive('confirmSignout', ConfirmSignoutDirective);
|
||||
.directive('confirmSignout', ConfirmSignoutDirective)
|
||||
.directive('multipleSelectedNotesPanel', MultipleSelectedNotesDirective)
|
||||
.directive('notesContextMenu', NotesContextMenuDirective)
|
||||
.directive('notesOptionsPanel', NotesOptionsPanelDirective)
|
||||
.directive('icon', IconDirective);
|
||||
|
||||
// Filters
|
||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||
|
||||
@@ -83,14 +83,14 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||
)}
|
||||
<div className="flex my-1 mt-4">
|
||||
<button
|
||||
className="sn-button neutral"
|
||||
className="sn-button small neutral"
|
||||
ref={cancelRef}
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="sn-button danger ml-2"
|
||||
className="sn-button small danger ml-2"
|
||||
onClick={() => {
|
||||
if (deleteLocalBackups) {
|
||||
application.signOutAndDeleteLocalBackups();
|
||||
|
||||
52
app/assets/javascripts/components/Icon.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
||||
import RichTextIcon from '../../icons/ic-text-rich.svg';
|
||||
import TrashIcon from '../../icons/ic-trash.svg';
|
||||
import PinIcon from '../../icons/ic-pin.svg';
|
||||
import UnpinIcon from '../../icons/ic-pin-off.svg';
|
||||
import ArchiveIcon from '../../icons/ic-archive.svg';
|
||||
import UnarchiveIcon from '../../icons/ic-unarchive.svg';
|
||||
import HashtagIcon from '../../icons/ic-hashtag.svg';
|
||||
import ChevronRightIcon from '../../icons/ic-chevron-right.svg';
|
||||
import RestoreIcon from '../../icons/ic-restore.svg';
|
||||
import CloseIcon from '../../icons/ic-close.svg';
|
||||
import PasswordIcon from '../../icons/ic-textbox-password.svg';
|
||||
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
||||
import MoreIcon from '../../icons/ic-more.svg';
|
||||
import TuneIcon from '../../icons/ic-tune.svg';
|
||||
import { toDirective } from './utils';
|
||||
|
||||
const ICONS = {
|
||||
'pencil-off': PencilOffIcon,
|
||||
'rich-text': RichTextIcon,
|
||||
'trash': TrashIcon,
|
||||
'pin': PinIcon,
|
||||
'unpin': UnpinIcon,
|
||||
'archive': ArchiveIcon,
|
||||
'unarchive': UnarchiveIcon,
|
||||
'hashtag': HashtagIcon,
|
||||
'chevron-right': ChevronRightIcon,
|
||||
'restore': RestoreIcon,
|
||||
'close': CloseIcon,
|
||||
'password': PasswordIcon,
|
||||
'trash-sweep': TrashSweepIcon,
|
||||
'more': MoreIcon,
|
||||
'tune': TuneIcon,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type: keyof (typeof ICONS);
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const Icon: React.FC<Props> = ({ type, className }) => {
|
||||
const IconComponent = ICONS[type];
|
||||
return <IconComponent className={`sn-icon ${className}`} />;
|
||||
};
|
||||
|
||||
export const IconDirective = toDirective<Props>(
|
||||
Icon,
|
||||
{
|
||||
type: '@',
|
||||
className: '@',
|
||||
}
|
||||
);
|
||||
35
app/assets/javascripts/components/MultipleSelectedNotes.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { toDirective } from './utils';
|
||||
import NotesIcon from '../../icons/il-notes.svg';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptionsPanel } from './NotesOptionsPanel';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const MultipleSelectedNotes = observer(({ appState }: Props) => {
|
||||
const count = appState.notes.selectedNotesCount;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center">
|
||||
<div className="flex items-center justify-between p-4 w-full">
|
||||
<h1 className="sk-h1 font-bold m-0">{count} selected notes</h1>
|
||||
<NotesOptionsPanel appState={appState} />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col justify-center items-center w-full max-w-md">
|
||||
<NotesIcon className="block" />
|
||||
<h2 className="text-lg m-0 text-center mt-4">
|
||||
{count} selected notes
|
||||
</h2>
|
||||
<p className="text-sm mt-2 text-center max-w-60">
|
||||
Actions will be performed on all selected notes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const MultipleSelectedNotesDirective = toDirective<Props>(
|
||||
MultipleSelectedNotes
|
||||
);
|
||||
@@ -1,13 +1,12 @@
|
||||
import { toDirective, useAutorunValue } from './utils';
|
||||
import Close from '../../icons/ic_close.svg';
|
||||
import { toDirective } from './utils';
|
||||
import { Icon } from './Icon';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = { appState: AppState };
|
||||
|
||||
function NoAccountWarning({ appState }: Props) {
|
||||
const canShow = useAutorunValue(() => appState.noAccountWarning.show, [
|
||||
appState,
|
||||
]);
|
||||
const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
const canShow = appState.noAccountWarning.show;
|
||||
if (!canShow) {
|
||||
return null;
|
||||
}
|
||||
@@ -18,7 +17,7 @@ function NoAccountWarning({ appState }: Props) {
|
||||
Sign in or register to back up your notes.
|
||||
</p>
|
||||
<button
|
||||
className="sn-button info mt-3 col-start-1 col-end-3 justify-self-start"
|
||||
className="sn-button small info mt-3 col-start-1 col-end-3 justify-self-start"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
appState.accountMenu.setShow(true);
|
||||
@@ -35,10 +34,10 @@ function NoAccountWarning({ appState }: Props) {
|
||||
style="height: 20px"
|
||||
className="border-0 m-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1 color-neutral hover:color-info"
|
||||
>
|
||||
<Close className="fill-current block" />
|
||||
<Icon type="close" className="block" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const NoAccountWarningDirective = toDirective<Props>(NoAccountWarning);
|
||||
|
||||
@@ -13,14 +13,14 @@ function NoProtectionsNoteWarning({ appState, onViewNote }: Props) {
|
||||
</p>
|
||||
<div className="mt-4 flex gap-3">
|
||||
<button
|
||||
className="sn-button info"
|
||||
className="sn-button small info"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setShow(true);
|
||||
}}
|
||||
>
|
||||
Open account menu
|
||||
</button>
|
||||
<button className="sn-button outlined" onClick={onViewNote}>
|
||||
<button className="sn-button small outlined" onClick={onViewNote}>
|
||||
View note
|
||||
</button>
|
||||
</div>
|
||||
|
||||
45
app/assets/javascripts/components/NotesContextMenu.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptions } from './NotesOptions';
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const NotesContextMenu = observer(({ appState }: Props) => {
|
||||
const contextMenuRef = useRef<HTMLDivElement>();
|
||||
const [closeOnBlur] = useCloseOnBlur(
|
||||
contextMenuRef,
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
const closeOnClickOutside = useCallback((event: MouseEvent) => {
|
||||
if (!contextMenuRef.current?.contains(event.target as Node)) {
|
||||
appState.notes.setContextMenuOpen(false);
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', closeOnClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', closeOnClickOutside);
|
||||
};
|
||||
}, [closeOnClickOutside]);
|
||||
|
||||
return appState.notes.contextMenuOpen ? (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="sn-dropdown max-w-80 flex flex-col py-2"
|
||||
style={{ position: 'absolute', ...appState.notes.contextMenuPosition }}
|
||||
>
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
|
||||
export const NotesContextMenuDirective = toDirective<Props>(NotesContextMenu);
|
||||
298
app/assets/javascripts/components/NotesOptions.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { Icon } from './Icon';
|
||||
import { Switch } from './Switch';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useRef, useState, useEffect } from 'preact/hooks';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { SNNote } from '@standardnotes/snjs/dist/@types';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
onSubmenuChange?: (submenuOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const NotesOptions = observer(
|
||||
({ appState, closeOnBlur, onSubmenuChange }: Props) => {
|
||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||
const [tagsMenuPosition, setTagsMenuPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
});
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>('auto');
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
const notesMatchingAttribute = notes.filter(condition);
|
||||
const notesNotMatchingAttribute = notes.filter(
|
||||
(note) => !condition(note)
|
||||
);
|
||||
return notesMatchingAttribute.length > notesNotMatchingAttribute.length;
|
||||
};
|
||||
|
||||
const notes = Object.values(appState.notes.selectedNotes);
|
||||
const hidePreviews = toggleOn((note) => note.hidePreview);
|
||||
const locked = toggleOn((note) => note.locked);
|
||||
const protect = toggleOn((note) => note.protected);
|
||||
const archived = notes.some((note) => note.archived);
|
||||
const unarchived = notes.some((note) => !note.archived);
|
||||
const trashed = notes.some((note) => note.trashed);
|
||||
const notTrashed = notes.some((note) => !note.trashed);
|
||||
const pinned = notes.some((note) => note.pinned);
|
||||
const unpinned = notes.some((note) => !note.pinned);
|
||||
|
||||
const tagsButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const iconClass = 'color-neutral mr-2';
|
||||
const buttonClass =
|
||||
'flex items-center border-0 focus:inner-ring-info ' +
|
||||
'cursor-pointer hover:bg-contrast color-text bg-transparent px-3 ' +
|
||||
'text-left';
|
||||
|
||||
useEffect(() => {
|
||||
if (onSubmenuChange) {
|
||||
onSubmenuChange(tagsMenuOpen);
|
||||
}
|
||||
}, [tagsMenuOpen, onSubmenuChange]);
|
||||
|
||||
const openTagsMenu = () => {
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
).fontSize;
|
||||
const maxTagsMenuSize = parseFloat(defaultFontSize) * 20;
|
||||
const { clientWidth, clientHeight } = document.body;
|
||||
const buttonRect = tagsButtonRef.current.getBoundingClientRect();
|
||||
const { offsetTop, offsetWidth } = tagsButtonRef.current;
|
||||
const footerHeight = 32;
|
||||
|
||||
if (buttonRect.top + maxTagsMenuSize > clientHeight - footerHeight) {
|
||||
setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2);
|
||||
}
|
||||
|
||||
setTagsMenuPosition({
|
||||
top: offsetTop,
|
||||
right:
|
||||
buttonRect.right + maxTagsMenuSize >
|
||||
clientWidth
|
||||
? offsetWidth
|
||||
: -offsetWidth,
|
||||
});
|
||||
|
||||
setTagsMenuOpen(!tagsMenuOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
onBlur={closeOnBlur}
|
||||
className="px-3 py-1.5"
|
||||
checked={locked}
|
||||
onChange={() => {
|
||||
appState.notes.setLockSelectedNotes(!locked);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="pencil-off" className={iconClass} />
|
||||
Prevent editing
|
||||
</span>
|
||||
</Switch>
|
||||
<Switch
|
||||
onBlur={closeOnBlur}
|
||||
className="px-3 py-1.5"
|
||||
checked={!hidePreviews}
|
||||
onChange={() => {
|
||||
appState.notes.setHideSelectedNotePreviews(!hidePreviews);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="rich-text" className={iconClass} />
|
||||
Show preview
|
||||
</span>
|
||||
</Switch>
|
||||
<Switch
|
||||
onBlur={closeOnBlur}
|
||||
className="px-3 py-1.5"
|
||||
checked={protect}
|
||||
onChange={() => {
|
||||
appState.notes.setProtectSelectedNotes(!protect);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className={iconClass} />
|
||||
Protect
|
||||
</span>
|
||||
</Switch>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
{appState.tags.tagsCount > 0 && (
|
||||
<Disclosure
|
||||
open={tagsMenuOpen}
|
||||
onChange={openTagsMenu}
|
||||
>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={tagsButtonRef}
|
||||
className={`${buttonClass} py-1.5 justify-between`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className={iconClass} />
|
||||
{'Add tag'}
|
||||
</div>
|
||||
<Icon
|
||||
type="chevron-right"
|
||||
className="color-neutral"
|
||||
/>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
tagsButtonRef.current.focus();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...tagsMenuPosition,
|
||||
maxHeight: tagsMenuMaxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown-anchor-right flex flex-col py-2 max-h-80 overflow-y-scroll"
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
key={tag.title}
|
||||
className={`${buttonClass} py-2`}
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? appState.notes.removeTagFromSelectedNotes(tag)
|
||||
: appState.notes.addTagToSelectedNotes(tag);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
? 'font-bold'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{tag.title}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
)}
|
||||
{unpinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={() => {
|
||||
appState.notes.setPinSelectedNotes(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="pin" className={iconClass} />
|
||||
Pin to top
|
||||
</button>
|
||||
)}
|
||||
{pinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={() => {
|
||||
appState.notes.setPinSelectedNotes(false);
|
||||
}}
|
||||
>
|
||||
<Icon type="unpin" className={iconClass} />
|
||||
Unpin
|
||||
</button>
|
||||
)}
|
||||
{unarchived && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={() => {
|
||||
appState.notes.setArchiveSelectedNotes(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="archive" className={iconClass} />
|
||||
Archive
|
||||
</button>
|
||||
)}
|
||||
{archived && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={() => {
|
||||
appState.notes.setArchiveSelectedNotes(false);
|
||||
}}
|
||||
>
|
||||
<Icon type="unarchive" className={iconClass} />
|
||||
Unarchive
|
||||
</button>
|
||||
)}
|
||||
{notTrashed && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className={iconClass} />
|
||||
Move to Trash
|
||||
</button>
|
||||
)}
|
||||
{trashed && (
|
||||
<>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(false);
|
||||
}}
|
||||
>
|
||||
<Icon type="restore" className={iconClass} />
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={async () => {
|
||||
await appState.notes.deleteNotesPermanently();
|
||||
}}
|
||||
>
|
||||
<Icon type="close" className="color-danger mr-2" />
|
||||
<span className="color-danger">Delete permanently</span>
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
onClick={async () => {
|
||||
await appState.notes.emptyTrash();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<Icon
|
||||
type="trash-sweep"
|
||||
className="color-danger mr-2"
|
||||
/>
|
||||
<div className="flex-row">
|
||||
<div className="color-danger">Empty Trash</div>
|
||||
<div className="text-xs">
|
||||
{appState.notes.trashedNotesCount} notes in Trash
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
88
app/assets/javascripts/components/NotesOptionsPanel.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { Icon } from './Icon';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { Portal } from '@reach/portal';
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptions } from './NotesOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
});
|
||||
const buttonRef = useRef<HTMLButtonElement>();
|
||||
const panelRef = useRef<HTMLDivElement>();
|
||||
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
||||
const [submenuOpen, setSubmenuOpen] = useState(false);
|
||||
|
||||
const onSubmenuChange = (open: boolean) => {
|
||||
setSubmenuOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
open={open}
|
||||
onChange={() => {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
});
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="sn-icon-button"
|
||||
>
|
||||
<VisuallyHidden>Actions</VisuallyHidden>
|
||||
<Icon type="more" className="block" />
|
||||
</DisclosureButton>
|
||||
<Portal>
|
||||
<div className="sn-component">
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
}}
|
||||
className="sn-dropdown flex flex-col py-2"
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
</Portal>
|
||||
</Disclosure>
|
||||
);
|
||||
});
|
||||
|
||||
export const NotesOptionsPanelDirective = toDirective<Props>(NotesOptionsPanel);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { toDirective, useAutorunValue } from './utils';
|
||||
import { Icon } from './Icon';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
@@ -8,56 +9,35 @@ import {
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { FocusEvent } from 'react';
|
||||
import { Switch } from './Switch';
|
||||
import TuneIcon from '../../icons/ic_tune.svg';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
function SearchOptions({ appState }: Props) {
|
||||
const SearchOptions = observer(({ appState }: Props) => {
|
||||
const { searchOptions } = appState;
|
||||
|
||||
const {
|
||||
includeProtectedContents,
|
||||
includeArchived,
|
||||
includeTrashed,
|
||||
} = useAutorunValue(
|
||||
() => ({
|
||||
includeProtectedContents: searchOptions.includeProtectedContents,
|
||||
includeArchived: searchOptions.includeArchived,
|
||||
includeTrashed: searchOptions.includeTrashed,
|
||||
}),
|
||||
[searchOptions]
|
||||
);
|
||||
|
||||
const [
|
||||
togglingIncludeProtectedContents,
|
||||
setTogglingIncludeProtectedContents,
|
||||
] = useState(false);
|
||||
|
||||
async function toggleIncludeProtectedContents() {
|
||||
setTogglingIncludeProtectedContents(true);
|
||||
try {
|
||||
await searchOptions.toggleIncludeProtectedContents();
|
||||
} finally {
|
||||
setTogglingIncludeProtectedContents(false);
|
||||
}
|
||||
}
|
||||
} = searchOptions;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [optionsPanelTop, setOptionsPanelTop] = useState(0);
|
||||
const buttonRef = useRef<HTMLButtonElement>();
|
||||
const panelRef = useRef<HTMLDivElement>();
|
||||
const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
||||
|
||||
function closeOnBlur(event: FocusEvent<HTMLElement>) {
|
||||
if (
|
||||
!togglingIncludeProtectedContents &&
|
||||
!panelRef.current.contains(event.relatedTarget as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
async function toggleIncludeProtectedContents() {
|
||||
setLockCloseOnBlur(true);
|
||||
try {
|
||||
await searchOptions.toggleIncludeProtectedContents();
|
||||
} finally {
|
||||
setLockCloseOnBlur(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +53,10 @@ function SearchOptions({ appState }: Props) {
|
||||
<DisclosureButton
|
||||
ref={buttonRef}
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-icon-button color-neutral hover:color-info"
|
||||
className="border-0 p-0 bg-transparent cursor-pointer color-neutral hover:color-info"
|
||||
>
|
||||
<VisuallyHidden>Search options</VisuallyHidden>
|
||||
<TuneIcon className="fill-current block" />
|
||||
<Icon type="tune" className="block" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={panelRef}
|
||||
@@ -86,6 +66,7 @@ function SearchOptions({ appState }: Props) {
|
||||
className="sn-dropdown sn-dropdown-anchor-right grid gap-2 py-2"
|
||||
>
|
||||
<Switch
|
||||
className="h-10"
|
||||
checked={includeProtectedContents}
|
||||
onChange={toggleIncludeProtectedContents}
|
||||
onBlur={closeOnBlur}
|
||||
@@ -93,6 +74,7 @@ function SearchOptions({ appState }: Props) {
|
||||
<p className="capitalize">Include protected contents</p>
|
||||
</Switch>
|
||||
<Switch
|
||||
className="h-10"
|
||||
checked={includeArchived}
|
||||
onChange={searchOptions.toggleIncludeArchived}
|
||||
onBlur={closeOnBlur}
|
||||
@@ -100,6 +82,7 @@ function SearchOptions({ appState }: Props) {
|
||||
<p className="capitalize">Include archived notes</p>
|
||||
</Switch>
|
||||
<Switch
|
||||
className="h-10"
|
||||
checked={includeTrashed}
|
||||
onChange={searchOptions.toggleIncludeTrashed}
|
||||
onBlur={closeOnBlur}
|
||||
@@ -109,6 +92,6 @@ function SearchOptions({ appState }: Props) {
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const SearchOptionsDirective = toDirective<Props>(SearchOptions);
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
import { toDirective, useAutorunValue } from './utils';
|
||||
import { toDirective } from './utils';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Session = RemoteSession & {
|
||||
revoking?: true;
|
||||
@@ -171,7 +172,7 @@ const SessionsModal: FunctionComponent<{
|
||||
{formatter.format(session.updated_at)}
|
||||
</p>
|
||||
<button
|
||||
className="sn-button danger sk-label"
|
||||
className="sn-button small danger sk-label"
|
||||
disabled={session.revoking}
|
||||
onClick={() =>
|
||||
setRevokingSessionUuid(session.uuid)
|
||||
@@ -212,14 +213,14 @@ const SessionsModal: FunctionComponent<{
|
||||
</AlertDialogDescription>
|
||||
<div className="flex my-1 gap-2">
|
||||
<button
|
||||
className="sn-button neutral sk-label"
|
||||
className="sn-button small neutral sk-label"
|
||||
ref={cancelRevokeRef}
|
||||
onClick={closeRevokeSessionAlert}
|
||||
>
|
||||
<span>{SessionStrings.RevokeCancelButton}</span>
|
||||
</button>
|
||||
<button
|
||||
className="sn-button danger sk-label"
|
||||
className="sn-button small danger sk-label"
|
||||
onClick={() => {
|
||||
closeRevokeSessionAlert();
|
||||
revokeSession(confirmRevokingSessionUuid);
|
||||
@@ -242,16 +243,12 @@ const SessionsModal: FunctionComponent<{
|
||||
const Sessions: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
}> = ({ appState, application }) => {
|
||||
const showModal = useAutorunValue(() => appState.isSessionsModalVisible, [
|
||||
appState,
|
||||
]);
|
||||
|
||||
if (showModal) {
|
||||
}> = observer(({ appState, application }) => {
|
||||
if (appState.isSessionsModalVisible) {
|
||||
return <SessionsModal application={application} appState={appState} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const SessionsModalDirective = toDirective(Sessions);
|
||||
|
||||
@@ -11,6 +11,7 @@ import '@reach/checkbox/styles.css';
|
||||
export type SwitchProps = HTMLProps<HTMLInputElement> & {
|
||||
checked?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
className?: string;
|
||||
children: ComponentChildren;
|
||||
};
|
||||
|
||||
@@ -19,8 +20,9 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
) => {
|
||||
const [checkedState, setChecked] = useState(props.checked || false);
|
||||
const checked = props.checked ?? checkedState;
|
||||
const className = props.className ?? '';
|
||||
return (
|
||||
<label className="sn-component flex justify-between items-center cursor-pointer hover:bg-contrast py-2 px-3">
|
||||
<label className={`sn-component flex justify-between items-center cursor-pointer hover:bg-contrast px-3 ${className}`}>
|
||||
{props.children}
|
||||
<CustomCheckboxContainer
|
||||
checked={checked}
|
||||
@@ -28,11 +30,12 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
setChecked(event.target.checked);
|
||||
props.onChange(event.target.checked);
|
||||
}}
|
||||
className={`sn-switch ${checked ? 'bg-info' : 'bg-secondary-contrast'}`}
|
||||
className={`sn-switch ${checked ? 'bg-info' : 'bg-neutral'}`}
|
||||
>
|
||||
<CustomCheckboxInput
|
||||
{...({
|
||||
...props,
|
||||
className: undefined,
|
||||
children: undefined,
|
||||
} as CustomCheckboxInputProps)}
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
import { autorun } from 'mobx';
|
||||
import { FunctionComponent, h, render } from 'preact';
|
||||
import { Inputs, useEffect, useState } from 'preact/hooks';
|
||||
import { StateUpdater, useCallback, useState } from 'preact/hooks';
|
||||
|
||||
export function useAutorunValue<T>(query: () => T, inputs: Inputs): T {
|
||||
const [value, setValue] = useState(query);
|
||||
useEffect(() => {
|
||||
return autorun(() => {
|
||||
setValue(query());
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, inputs);
|
||||
return value;
|
||||
/**
|
||||
* @returns a callback that will close a dropdown if none of its children has
|
||||
* focus. Must be set as the onBlur callback of children that need to be
|
||||
* monitored.
|
||||
*/
|
||||
export function useCloseOnBlur(
|
||||
container: { current: HTMLDivElement },
|
||||
setOpen: (open: boolean) => void
|
||||
): [
|
||||
(event: { relatedTarget: EventTarget | null }) => void,
|
||||
StateUpdater<boolean>
|
||||
] {
|
||||
const [locked, setLocked] = useState(false);
|
||||
return [
|
||||
useCallback(
|
||||
function onBlur(event: { relatedTarget: EventTarget | null }) {
|
||||
if (
|
||||
!locked &&
|
||||
!container.current?.contains(event.relatedTarget as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
[container, setOpen, locked]
|
||||
),
|
||||
setLocked,
|
||||
];
|
||||
}
|
||||
|
||||
export function toDirective<Props>(
|
||||
component: FunctionComponent<Props>,
|
||||
scope: Record<string, '=' | '&'> = {}
|
||||
scope: Record<string, '=' | '&' | '@'> = {}
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
return function () {
|
||||
|
||||
@@ -4,7 +4,6 @@ import template from '%/directives/account-menu.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import {
|
||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
STRING_SIGN_OUT_CONFIRMATION,
|
||||
STRING_E2E_ENABLED,
|
||||
STRING_LOCAL_ENC_ENABLED,
|
||||
STRING_ENC_NOT_ENABLED,
|
||||
@@ -18,17 +17,15 @@ import {
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
||||
STRING_UNSUPPORTED_BACKUP_FILE_VERSION,
|
||||
Strings,
|
||||
StringUtils,
|
||||
} from '@/strings';
|
||||
import { PasswordWizardType } from '@/types';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
BackupFile,
|
||||
ContentType,
|
||||
Platform,
|
||||
} from '@standardnotes/snjs';
|
||||
import { confirmDialog, alertDialog } from '@/services/alertService';
|
||||
import { autorun, IReactionDisposer } from 'mobx';
|
||||
import { storage, StorageKey } from '@/services/localStorage';
|
||||
import {
|
||||
disableErrorReporting,
|
||||
@@ -84,8 +81,6 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
||||
public appVersion: string;
|
||||
/** @template */
|
||||
private closeFunction?: () => void;
|
||||
private removeBetaWarningListener?: IReactionDisposer;
|
||||
private removeSyncObserver?: IReactionDisposer;
|
||||
private removeProtectionLengthObserver?: () => void;
|
||||
|
||||
public passcodeInput!: JQLite;
|
||||
@@ -114,7 +109,7 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
||||
storage.get(StorageKey.DisableErrorReporting) === false,
|
||||
showSessions: false,
|
||||
errorReportingId: errorReportingId(),
|
||||
keyStorageInfo: Strings.keyStorageInfo(this.application),
|
||||
keyStorageInfo: StringUtils.keyStorageInfo(this.application),
|
||||
importData: null,
|
||||
syncInProgress: false,
|
||||
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
|
||||
@@ -154,13 +149,13 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
||||
});
|
||||
|
||||
const sync = this.appState.sync;
|
||||
this.removeSyncObserver = autorun(() => {
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
syncInProgress: sync.inProgress,
|
||||
syncError: sync.errorMessage,
|
||||
});
|
||||
});
|
||||
this.removeBetaWarningListener = autorun(() => {
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
showBetaWarning: this.appState.showBetaWarning,
|
||||
});
|
||||
@@ -177,8 +172,6 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeSyncObserver?.();
|
||||
this.removeBetaWarningListener?.();
|
||||
this.removeProtectionLengthObserver?.();
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { SNItem, Action, SNActionsExtension, UuidString } from '@standardnotes/snjs';
|
||||
import { ActionResponse } from '@standardnotes/snjs';
|
||||
import { ActionsExtensionMutator } from '@standardnotes/snjs';
|
||||
import { autorun, IReactionDisposer } from 'mobx';
|
||||
|
||||
type ActionsMenuScope = {
|
||||
application: WebApplication
|
||||
@@ -43,7 +42,6 @@ type ActionsMenuState = {
|
||||
class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements ActionsMenuScope {
|
||||
application!: WebApplication
|
||||
item!: SNItem
|
||||
private removeHiddenExtensionsListener?: IReactionDisposer;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -58,17 +56,13 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
item: this.item
|
||||
});
|
||||
this.loadExtensions();
|
||||
this.removeHiddenExtensionsListener = autorun(() => {
|
||||
this.autorun(() => {
|
||||
this.rebuildMenu({
|
||||
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeHiddenExtensionsListener?.();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getInitialState() {
|
||||
const extensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
|
||||
|
||||
@@ -146,8 +146,18 @@ class ComponentViewCtrl implements ComponentViewScope {
|
||||
identifier: 'component-view-' + Math.random(),
|
||||
areas: [this.component.area],
|
||||
actionHandler: (component, action, data) => {
|
||||
if (action === ComponentAction.SetSize) {
|
||||
this.application.componentManager!.handleSetSizeEvent(component, data);
|
||||
switch (action) {
|
||||
case (ComponentAction.SetSize):
|
||||
this.application.componentManager!.handleSetSizeEvent(component, data);
|
||||
break;
|
||||
case (ComponentAction.KeyDown):
|
||||
this.application.io.handleComponentKeyDown(data.keyboardModifier);
|
||||
break;
|
||||
case (ComponentAction.KeyUp):
|
||||
this.application.io.handleComponentKeyUp(data.keyboardModifier);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export { AlertService } from './alertService';
|
||||
export { ArchiveManager } from './archiveManager';
|
||||
export { DesktopManager } from './desktopManager';
|
||||
export { KeyboardManager } from './keyboardManager';
|
||||
export { AutolockService } from './autolock_service';
|
||||
export { NativeExtManager } from './nativeExtManager';
|
||||
export { StatusManager } from './statusManager';
|
||||
export { ThemeManager } from './themeManager';
|
||||
202
app/assets/javascripts/services/ioService.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { removeFromArray } from '@standardnotes/snjs';
|
||||
export enum KeyboardKey {
|
||||
Tab = 'Tab',
|
||||
Backspace = 'Backspace',
|
||||
Up = 'ArrowUp',
|
||||
Down = 'ArrowDown',
|
||||
}
|
||||
|
||||
export enum KeyboardModifier {
|
||||
Shift = 'Shift',
|
||||
Ctrl = 'Control',
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta = 'Meta',
|
||||
Alt = 'Alt',
|
||||
}
|
||||
|
||||
enum KeyboardKeyEvent {
|
||||
Down = 'KeyEventDown',
|
||||
Up = 'KeyEventUp',
|
||||
}
|
||||
|
||||
type KeyboardObserver = {
|
||||
key?: KeyboardKey | string;
|
||||
modifiers?: KeyboardModifier[];
|
||||
onKeyDown?: (event: KeyboardEvent) => void;
|
||||
onKeyUp?: (event: KeyboardEvent) => void;
|
||||
element?: HTMLElement;
|
||||
elements?: HTMLElement[];
|
||||
notElement?: HTMLElement;
|
||||
notElementIds?: string[];
|
||||
};
|
||||
|
||||
export class IOService {
|
||||
readonly activeModifiers = new Set<KeyboardModifier>();
|
||||
private observers: KeyboardObserver[] = [];
|
||||
|
||||
constructor(private isMac: boolean) {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
window.addEventListener('blur', this.handleWindowBlur);
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.observers.length = 0;
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
window.removeEventListener('blur', this.handleWindowBlur);
|
||||
(this.handleKeyDown as unknown) = undefined;
|
||||
(this.handleKeyUp as unknown) = undefined;
|
||||
(this.handleWindowBlur as unknown) = undefined;
|
||||
}
|
||||
|
||||
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return;
|
||||
}
|
||||
switch (modifier) {
|
||||
case KeyboardModifier.Meta: {
|
||||
if (this.isMac) {
|
||||
this.activeModifiers.add(modifier);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case KeyboardModifier.Ctrl: {
|
||||
if (!this.isMac) {
|
||||
this.activeModifiers.add(modifier);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.activeModifiers.add(modifier);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return;
|
||||
}
|
||||
this.activeModifiers.delete(modifier);
|
||||
}
|
||||
|
||||
handleKeyDown = (event: KeyboardEvent): void => {
|
||||
for (const modifier of this.modifiersForEvent(event)) {
|
||||
this.addActiveModifier(modifier);
|
||||
}
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Down);
|
||||
};
|
||||
|
||||
handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.addActiveModifier(modifier);
|
||||
}
|
||||
|
||||
handleKeyUp = (event: KeyboardEvent): void => {
|
||||
for (const modifier of this.modifiersForEvent(event)) {
|
||||
this.removeActiveModifier(modifier);
|
||||
}
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Up);
|
||||
};
|
||||
|
||||
handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.removeActiveModifier(modifier);
|
||||
}
|
||||
|
||||
handleWindowBlur = (): void => {
|
||||
for (const modifier of this.activeModifiers) {
|
||||
this.activeModifiers.delete(modifier);
|
||||
}
|
||||
};
|
||||
|
||||
modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] {
|
||||
const allModifiers = Object.values(KeyboardModifier);
|
||||
const eventModifiers = allModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
const matches =
|
||||
((event.ctrlKey || event.key === KeyboardModifier.Ctrl) &&
|
||||
modifier === KeyboardModifier.Ctrl) ||
|
||||
((event.metaKey || event.key === KeyboardModifier.Meta) &&
|
||||
modifier === KeyboardModifier.Meta) ||
|
||||
((event.altKey || event.key === KeyboardModifier.Alt) &&
|
||||
modifier === KeyboardModifier.Alt) ||
|
||||
((event.shiftKey || event.key === KeyboardModifier.Shift) &&
|
||||
modifier === KeyboardModifier.Shift);
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
return eventModifiers;
|
||||
}
|
||||
|
||||
eventMatchesKeyAndModifiers(
|
||||
event: KeyboardEvent,
|
||||
key: KeyboardKey | string,
|
||||
modifiers: KeyboardModifier[] = []
|
||||
): boolean {
|
||||
const eventModifiers = this.modifiersForEvent(event);
|
||||
if (eventModifiers.length !== modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
for (const modifier of modifiers) {
|
||||
if (!eventModifiers.includes(modifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Modifers match, check key
|
||||
if (!key) {
|
||||
return true;
|
||||
}
|
||||
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
|
||||
// In our case we don't differentiate between the two.
|
||||
return key.toLowerCase() === event.key.toLowerCase();
|
||||
}
|
||||
|
||||
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
const target = event.target as HTMLElement;
|
||||
for (const observer of this.observers) {
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
observer.notElementIds &&
|
||||
observer.notElementIds.includes(target.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
this.eventMatchesKeyAndModifiers(
|
||||
event,
|
||||
observer.key!,
|
||||
observer.modifiers
|
||||
)
|
||||
) {
|
||||
const callback =
|
||||
keyEvent === KeyboardKeyEvent.Down
|
||||
? observer.onKeyDown
|
||||
: observer.onKeyUp;
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeyObserver(observer: KeyboardObserver): () => void {
|
||||
this.observers.push(observer);
|
||||
return () => {
|
||||
removeFromArray(this.observers, observer);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { removeFromArray } from '@standardnotes/snjs';
|
||||
export enum KeyboardKey {
|
||||
Tab = "Tab",
|
||||
Backspace = "Backspace",
|
||||
Up = "ArrowUp",
|
||||
Down = "ArrowDown",
|
||||
}
|
||||
|
||||
export enum KeyboardModifier {
|
||||
Shift = "Shift",
|
||||
Ctrl = "Control",
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta = "Meta",
|
||||
Alt = "Alt",
|
||||
}
|
||||
|
||||
enum KeyboardKeyEvent {
|
||||
Down = "KeyEventDown",
|
||||
Up = "KeyEventUp"
|
||||
}
|
||||
|
||||
type KeyboardObserver = {
|
||||
key?: KeyboardKey | string
|
||||
modifiers?: KeyboardModifier[]
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onKeyUp?: (event: KeyboardEvent) => void
|
||||
element?: HTMLElement
|
||||
elements?: HTMLElement[]
|
||||
notElement?: HTMLElement
|
||||
notElementIds?: string[]
|
||||
}
|
||||
|
||||
export class KeyboardManager {
|
||||
|
||||
private observers: KeyboardObserver[] = []
|
||||
private handleKeyDown: any
|
||||
private handleKeyUp: any
|
||||
|
||||
constructor() {
|
||||
this.handleKeyDown = (event: KeyboardEvent) => {
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Down);
|
||||
};
|
||||
this.handleKeyUp = (event: KeyboardEvent) => {
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Up);
|
||||
};
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.observers.length = 0;
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.handleKeyDown = undefined;
|
||||
this.handleKeyUp = undefined;
|
||||
}
|
||||
|
||||
modifiersForEvent(event: KeyboardEvent) {
|
||||
const allModifiers = Object.values(KeyboardModifier);
|
||||
const eventModifiers = allModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
const matches = (
|
||||
(
|
||||
(event.ctrlKey || event.key === KeyboardModifier.Ctrl)
|
||||
&& modifier === KeyboardModifier.Ctrl
|
||||
) ||
|
||||
(
|
||||
(event.metaKey || event.key === KeyboardModifier.Meta)
|
||||
&& modifier === KeyboardModifier.Meta
|
||||
) ||
|
||||
(
|
||||
(event.altKey || event.key === KeyboardModifier.Alt)
|
||||
&& modifier === KeyboardModifier.Alt
|
||||
) ||
|
||||
(
|
||||
(event.shiftKey || event.key === KeyboardModifier.Shift)
|
||||
&& modifier === KeyboardModifier.Shift
|
||||
)
|
||||
);
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
return eventModifiers;
|
||||
}
|
||||
|
||||
eventMatchesKeyAndModifiers(
|
||||
event: KeyboardEvent,
|
||||
key: KeyboardKey | string,
|
||||
modifiers: KeyboardModifier[] = []
|
||||
) {
|
||||
const eventModifiers = this.modifiersForEvent(event);
|
||||
if (eventModifiers.length !== modifiers.length) {
|
||||
return false;
|
||||
}
|
||||
for (const modifier of modifiers) {
|
||||
if (!eventModifiers.includes(modifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Modifers match, check key
|
||||
if (!key) {
|
||||
return true;
|
||||
}
|
||||
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
|
||||
// In our case we don't differentiate between the two.
|
||||
return key.toLowerCase() === event.key.toLowerCase();
|
||||
}
|
||||
|
||||
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
for (const observer of this.observers) {
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.eventMatchesKeyAndModifiers(event, observer.key!, observer.modifiers)) {
|
||||
const callback = keyEvent === KeyboardKeyEvent.Down
|
||||
? observer.onKeyDown
|
||||
: observer.onKeyUp;
|
||||
if (callback) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeyObserver(observer: KeyboardObserver) {
|
||||
this.observers.push(observer);
|
||||
return () => {
|
||||
removeFromArray(this.observers, observer);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -35,13 +35,13 @@ export const STRING_GENERIC_SAVE_ERROR =
|
||||
export const STRING_DELETE_PLACEHOLDER_ATTEMPT =
|
||||
'This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.';
|
||||
export const STRING_ARCHIVE_LOCKED_ATTEMPT =
|
||||
"This note is locked. If you'd like to archive it, unlock it, and try again.";
|
||||
"This note has editing disabled. If you'd like to archive it, enable editing, and try again.";
|
||||
export const STRING_UNARCHIVE_LOCKED_ATTEMPT =
|
||||
"This note is locked. If you'd like to archive it, unlock it, and try again.";
|
||||
"This note has editing disabled. If you'd like to unarchive it, enable editing, and try again.";
|
||||
export const STRING_DELETE_LOCKED_ATTEMPT =
|
||||
"This note is locked. If you'd like to delete it, unlock it, and try again.";
|
||||
"This note had editing disabled. If you'd like to delete it, enable editing, and try again.";
|
||||
export const STRING_EDIT_LOCKED_ATTEMPT =
|
||||
"This note is locked. If you'd like to edit its options, unlock it, and try again.";
|
||||
"This note has editing disabled. If you'd like to edit its options, enable editing, and try again.";
|
||||
export function StringDeleteNote(title: string, permanently: boolean) {
|
||||
return permanently
|
||||
? `Are you sure you want to permanently delete ${title}?`
|
||||
@@ -109,6 +109,13 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
|
||||
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';
|
||||
|
||||
export const Strings = {
|
||||
protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.',
|
||||
openAccountMenu: 'Open Account Menu',
|
||||
trashNotesTitle: 'Move to Trash',
|
||||
trashNotesText: 'Are you sure you want to move these notes to the trash?',
|
||||
};
|
||||
|
||||
export const StringUtils = {
|
||||
keyStorageInfo(application: SNApplication): string | null {
|
||||
if (!isDesktopApplication()) {
|
||||
return null;
|
||||
@@ -125,6 +132,26 @@ export const Strings = {
|
||||
: 'password manager';
|
||||
return `Your keys are currently stored in your operating system's ${keychainName}. Adding a passcode prevents even your operating system from reading them.`;
|
||||
},
|
||||
protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.',
|
||||
openAccountMenu: 'Open Account Menu'
|
||||
};
|
||||
deleteNotes(permanently: boolean, notesCount = 1, title?: string): string {
|
||||
if (notesCount === 1) {
|
||||
return permanently
|
||||
? `Are you sure you want to permanently delete ${title}?`
|
||||
: `Are you sure you want to move ${title} to the trash?`;
|
||||
} else {
|
||||
return permanently
|
||||
? `Are you sure you want to permanently delete these notes?`
|
||||
: `Are you sure you want to move these notes to the trash?`;
|
||||
}
|
||||
},
|
||||
archiveLockedNotesAttempt(archive: boolean, notesCount = 1): string {
|
||||
const archiveString = archive ? 'archive' : 'unarchive';
|
||||
return notesCount === 1
|
||||
? `This note has editing disabled. If you'd like to ${archiveString} it, enable editing, and try again.`
|
||||
: `One or more of these notes have editing disabled. If you'd like to ${archiveString} them, make sure editing is enabled on all of them, and try again.`;
|
||||
},
|
||||
deleteLockedNotesAttempt(notesCount = 1): string {
|
||||
return notesCount === 1
|
||||
? "This note has editing disabled. If you'd like to delete it, enable editing, and try again."
|
||||
: "One or more of these notes have editing disabled. If you'd like to delete them, make sure editing is enabled on all of them, and try again.";
|
||||
},
|
||||
};
|
||||
@@ -19,6 +19,8 @@ import { ActionsMenuState } from './actions_menu_state';
|
||||
import { NoAccountWarningState } from './no_account_warning_state';
|
||||
import { SyncState } from './sync_state';
|
||||
import { SearchOptionsState } from './search_options_state';
|
||||
import { NotesState } from './notes_state';
|
||||
import { TagsState } from './tags_state';
|
||||
|
||||
export enum AppStateEvent {
|
||||
TagChanged,
|
||||
@@ -62,7 +64,9 @@ export class AppState {
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
readonly noAccountWarning: NoAccountWarningState;
|
||||
readonly sync = new SyncState();
|
||||
readonly searchOptions;
|
||||
readonly searchOptions: SearchOptionsState;
|
||||
readonly notes: NotesState;
|
||||
readonly tags: TagsState;
|
||||
isSessionsModalVisible = false;
|
||||
|
||||
private appEventObserverRemovers: (() => void)[] = [];
|
||||
@@ -77,6 +81,17 @@ export class AppState {
|
||||
this.$timeout = $timeout;
|
||||
this.$rootScope = $rootScope;
|
||||
this.application = application;
|
||||
this.notes = new NotesState(
|
||||
this.application,
|
||||
async () => {
|
||||
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
|
||||
},
|
||||
this.appEventObserverRemovers,
|
||||
);
|
||||
this.tags = new TagsState(
|
||||
application,
|
||||
this.appEventObserverRemovers,
|
||||
),
|
||||
this.noAccountWarning = new NoAccountWarningState(
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
@@ -175,28 +190,6 @@ export class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
async openEditor(noteUuid: string): Promise<void> {
|
||||
if (this.getActiveEditor()?.note?.uuid === noteUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const note = this.application.findItem(noteUuid) as SNNote;
|
||||
if (!note) {
|
||||
console.warn('Tried accessing a non-existant note of UUID ' + noteUuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.application.authorizeNoteAccess(note)) {
|
||||
const activeEditor = this.getActiveEditor();
|
||||
if (!activeEditor) {
|
||||
this.application.editorGroup.createEditor(noteUuid);
|
||||
} else {
|
||||
activeEditor.setNote(note);
|
||||
}
|
||||
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
|
||||
}
|
||||
}
|
||||
|
||||
getActiveEditor() {
|
||||
return this.application.editorGroup.editors[0];
|
||||
}
|
||||
|
||||
366
app/assets/javascripts/ui_models/app_state/notes_state.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import { KeyboardModifier } from '@/services/ioService';
|
||||
import { StringEmptyTrash, Strings, StringUtils } from '@/strings';
|
||||
import {
|
||||
UuidString,
|
||||
SNNote,
|
||||
NoteMutator,
|
||||
ContentType,
|
||||
SNTag,
|
||||
ChallengeReason,
|
||||
} from '@standardnotes/snjs';
|
||||
import {
|
||||
makeObservable,
|
||||
observable,
|
||||
action,
|
||||
computed,
|
||||
runInAction,
|
||||
} from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
import { Editor } from '../editor';
|
||||
|
||||
export class NotesState {
|
||||
lastSelectedNote: SNNote | undefined;
|
||||
selectedNotes: Record<UuidString, SNNote> = {};
|
||||
contextMenuOpen = false;
|
||||
contextMenuPosition: { top?: number; left: number; bottom?: number } = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
};
|
||||
showProtectedWarning = false;
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private onActiveEditorChanged: () => Promise<void>,
|
||||
appEventListeners: (() => void)[]
|
||||
) {
|
||||
makeObservable(this, {
|
||||
selectedNotes: observable,
|
||||
contextMenuOpen: observable,
|
||||
contextMenuPosition: observable,
|
||||
showProtectedWarning: observable,
|
||||
|
||||
selectedNotesCount: computed,
|
||||
trashedNotesCount: computed,
|
||||
|
||||
setContextMenuOpen: action,
|
||||
setContextMenuPosition: action,
|
||||
setShowProtectedWarning: action,
|
||||
unselectNotes: action,
|
||||
});
|
||||
|
||||
appEventListeners.push(
|
||||
application.streamItems(ContentType.Note, (notes) => {
|
||||
runInAction(() => {
|
||||
for (const note of notes) {
|
||||
if (this.selectedNotes[note.uuid]) {
|
||||
this.selectedNotes[note.uuid] = note as SNNote;
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get activeEditor(): Editor | undefined {
|
||||
return this.application.editorGroup.editors[0];
|
||||
}
|
||||
|
||||
get selectedNotesCount(): number {
|
||||
return Object.keys(this.selectedNotes).length;
|
||||
}
|
||||
|
||||
get trashedNotesCount(): number {
|
||||
return this.application.getTrashedItems().length;
|
||||
}
|
||||
|
||||
private async selectNotesRange(selectedNote: SNNote): Promise<void> {
|
||||
const notes = this.application.getDisplayableItems(
|
||||
ContentType.Note
|
||||
) as SNNote[];
|
||||
const lastSelectedNoteIndex = notes.findIndex(
|
||||
(note) => note.uuid == this.lastSelectedNote?.uuid
|
||||
);
|
||||
const selectedNoteIndex = notes.findIndex(
|
||||
(note) => note.uuid == selectedNote.uuid
|
||||
);
|
||||
|
||||
let notesToSelect = [];
|
||||
if (selectedNoteIndex > lastSelectedNoteIndex) {
|
||||
notesToSelect = notes.slice(lastSelectedNoteIndex, selectedNoteIndex + 1);
|
||||
} else {
|
||||
notesToSelect = notes.slice(selectedNoteIndex, lastSelectedNoteIndex + 1);
|
||||
}
|
||||
|
||||
const authorizedNotes =
|
||||
await this.application.authorizeProtectedActionForNotes(
|
||||
notesToSelect,
|
||||
ChallengeReason.SelectProtectedNote
|
||||
);
|
||||
|
||||
for (const note of authorizedNotes) {
|
||||
runInAction(() => {
|
||||
this.selectedNotes[note.uuid] = note;
|
||||
this.lastSelectedNote = note;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async selectNote(uuid: UuidString): Promise<void> {
|
||||
const note = this.application.findItem(uuid) as SNNote;
|
||||
|
||||
if (note) {
|
||||
if (
|
||||
this.io.activeModifiers.has(KeyboardModifier.Meta) ||
|
||||
this.io.activeModifiers.has(KeyboardModifier.Ctrl)
|
||||
) {
|
||||
if (this.selectedNotes[uuid]) {
|
||||
delete this.selectedNotes[uuid];
|
||||
} else if (await this.application.authorizeNoteAccess(note)) {
|
||||
runInAction(() => {
|
||||
this.selectedNotes[uuid] = note;
|
||||
this.lastSelectedNote = note;
|
||||
});
|
||||
}
|
||||
} else if (this.io.activeModifiers.has(KeyboardModifier.Shift)) {
|
||||
await this.selectNotesRange(note);
|
||||
} else {
|
||||
const shouldSelectNote =
|
||||
this.selectedNotesCount > 1 || !this.selectedNotes[uuid];
|
||||
if (
|
||||
shouldSelectNote &&
|
||||
(await this.application.authorizeNoteAccess(note))
|
||||
) {
|
||||
runInAction(() => {
|
||||
this.selectedNotes = {
|
||||
[note.uuid]: note,
|
||||
};
|
||||
this.lastSelectedNote = note;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectedNotesCount === 1) {
|
||||
await this.openEditor(Object.keys(this.selectedNotes)[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async openEditor(noteUuid: string): Promise<void> {
|
||||
if (this.activeEditor?.note?.uuid === noteUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const note = this.application.findItem(noteUuid) as SNNote | undefined;
|
||||
if (!note) {
|
||||
console.warn('Tried accessing a non-existant note of UUID ' + noteUuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.activeEditor) {
|
||||
this.application.editorGroup.createEditor(noteUuid);
|
||||
} else {
|
||||
this.activeEditor.setNote(note);
|
||||
}
|
||||
await this.onActiveEditorChanged();
|
||||
|
||||
if (note.waitingForKey) {
|
||||
this.application.presentKeyRecoveryWizard();
|
||||
}
|
||||
}
|
||||
|
||||
setContextMenuOpen(open: boolean): void {
|
||||
this.contextMenuOpen = open;
|
||||
}
|
||||
|
||||
setContextMenuPosition(position: {
|
||||
top?: number;
|
||||
left: number;
|
||||
bottom?: number;
|
||||
}): void {
|
||||
this.contextMenuPosition = position;
|
||||
}
|
||||
|
||||
async changeSelectedNotes(
|
||||
mutate: (mutator: NoteMutator) => void
|
||||
): Promise<void> {
|
||||
await this.application.changeItems(
|
||||
Object.keys(this.selectedNotes),
|
||||
mutate,
|
||||
false
|
||||
);
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
setHideSelectedNotePreviews(hide: boolean): void {
|
||||
this.changeSelectedNotes((mutator) => {
|
||||
mutator.hidePreview = hide;
|
||||
});
|
||||
}
|
||||
|
||||
setLockSelectedNotes(lock: boolean): void {
|
||||
this.changeSelectedNotes((mutator) => {
|
||||
mutator.locked = lock;
|
||||
});
|
||||
}
|
||||
|
||||
async setTrashSelectedNotes(trashed: boolean): Promise<void> {
|
||||
if (trashed) {
|
||||
const notesDeleted = await this.deleteNotes(false);
|
||||
if (notesDeleted) {
|
||||
runInAction(() => {
|
||||
this.unselectNotes();
|
||||
this.contextMenuOpen = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.changeSelectedNotes((mutator) => {
|
||||
mutator.trashed = trashed;
|
||||
});
|
||||
runInAction(() => {
|
||||
this.unselectNotes();
|
||||
this.contextMenuOpen = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async deleteNotesPermanently(): Promise<void> {
|
||||
await this.deleteNotes(true);
|
||||
}
|
||||
|
||||
async deleteNotes(permanently: boolean): Promise<boolean> {
|
||||
if (Object.values(this.selectedNotes).some((note) => note.locked)) {
|
||||
const text = StringUtils.deleteLockedNotesAttempt(
|
||||
this.selectedNotesCount
|
||||
);
|
||||
this.application.alertService.alert(text);
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = Strings.trashNotesTitle;
|
||||
let noteTitle = undefined;
|
||||
if (this.selectedNotesCount === 1) {
|
||||
const selectedNote = Object.values(this.selectedNotes)[0];
|
||||
noteTitle = selectedNote.safeTitle().length
|
||||
? `'${selectedNote.title}'`
|
||||
: 'this note';
|
||||
}
|
||||
const text = StringUtils.deleteNotes(
|
||||
permanently,
|
||||
this.selectedNotesCount,
|
||||
noteTitle
|
||||
);
|
||||
|
||||
if (
|
||||
await confirmDialog({
|
||||
title,
|
||||
text,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
if (permanently) {
|
||||
for (const note of Object.values(this.selectedNotes)) {
|
||||
await this.application.deleteItem(note);
|
||||
}
|
||||
} else {
|
||||
await this.changeSelectedNotes((mutator) => {
|
||||
mutator.trashed = true;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setPinSelectedNotes(pinned: boolean): void {
|
||||
this.changeSelectedNotes((mutator) => {
|
||||
mutator.pinned = pinned;
|
||||
});
|
||||
}
|
||||
|
||||
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
||||
if (Object.values(this.selectedNotes).some((note) => note.locked)) {
|
||||
this.application.alertService.alert(
|
||||
StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.changeSelectedNotes((mutator) => {
|
||||
mutator.archived = archived;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.selectedNotes = {};
|
||||
this.contextMenuOpen = false;
|
||||
});
|
||||
}
|
||||
|
||||
async setProtectSelectedNotes(protect: boolean): Promise<void> {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
if (protect) {
|
||||
await this.application.protectNotes(selectedNotes);
|
||||
if (!this.application.hasProtectionSources()) {
|
||||
this.setShowProtectedWarning(true);
|
||||
}
|
||||
} else {
|
||||
await this.application.unprotectNotes(selectedNotes);
|
||||
this.setShowProtectedWarning(false);
|
||||
}
|
||||
}
|
||||
|
||||
unselectNotes(): void {
|
||||
this.selectedNotes = {};
|
||||
}
|
||||
|
||||
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
for (const note of selectedNotes) {
|
||||
mutator.addItemAsRelationship(note);
|
||||
}
|
||||
});
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
async removeTagFromSelectedNotes(tag: SNTag): Promise<void> {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
await this.application.changeItem(tag.uuid, (mutator) => {
|
||||
for (const note of selectedNotes) {
|
||||
mutator.removeItemAsRelationship(note);
|
||||
}
|
||||
});
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
isTagInSelectedNotes(tag: SNTag): boolean {
|
||||
const selectedNotes = Object.values(this.selectedNotes);
|
||||
return selectedNotes.every((note) =>
|
||||
this.application
|
||||
.getAppState()
|
||||
.getNoteTags(note)
|
||||
.find((noteTag) => noteTag.uuid === tag.uuid)
|
||||
);
|
||||
}
|
||||
|
||||
setShowProtectedWarning(show: boolean): void {
|
||||
this.showProtectedWarning = show;
|
||||
}
|
||||
|
||||
async emptyTrash(): Promise<void> {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: StringEmptyTrash(this.trashedNotesCount),
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
this.application.emptyTrash();
|
||||
this.application.sync();
|
||||
}
|
||||
}
|
||||
|
||||
private get io() {
|
||||
return this.application.io;
|
||||
}
|
||||
}
|
||||
38
app/assets/javascripts/ui_models/app_state/tags_state.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ContentType, SNSmartTag, SNTag } from '@standardnotes/snjs';
|
||||
import { computed, makeObservable, observable, runInAction } from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
|
||||
export class TagsState {
|
||||
tags: SNTag[] = [];
|
||||
smartTags: SNSmartTag[] = [];
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
appEventListeners: (() => void)[]
|
||||
) {
|
||||
makeObservable(this, {
|
||||
tags: observable,
|
||||
smartTags: observable,
|
||||
|
||||
tagsCount: computed,
|
||||
});
|
||||
|
||||
appEventListeners.push(
|
||||
this.application.streamItems(
|
||||
[ContentType.Tag, ContentType.SmartTag],
|
||||
() => {
|
||||
runInAction(() => {
|
||||
this.tags = this.application.getDisplayableItems(
|
||||
ContentType.Tag
|
||||
) as SNTag[];
|
||||
this.smartTags = this.application.getSmartTags();
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get tagsCount(): number {
|
||||
return this.tags.length;
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,21 @@ import {
|
||||
SNComponent,
|
||||
PermissionDialog,
|
||||
DeinitSource,
|
||||
Platform,
|
||||
} from '@standardnotes/snjs';
|
||||
import angular from 'angular';
|
||||
import { getPlatform } from '@/utils';
|
||||
import { AlertService } from '@/services/alertService';
|
||||
import { WebDeviceInterface } from '@/web_device_interface';
|
||||
import {
|
||||
DesktopManager,
|
||||
AutolockService,
|
||||
ArchiveManager,
|
||||
NativeExtManager,
|
||||
StatusManager,
|
||||
ThemeManager,
|
||||
KeyboardManager
|
||||
} from '@/services';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { Bridge } from '@/services/bridge';
|
||||
import { WebCrypto } from '@/crypto';
|
||||
import { AlertService } from '@/services/alertService';
|
||||
import { AutolockService } from '@/services/autolock_service';
|
||||
import { ArchiveManager } from '@/services/archiveManager';
|
||||
import { DesktopManager } from '@/services/desktopManager';
|
||||
import { IOService } from '@/services/ioService';
|
||||
import { NativeExtManager } from '@/services/nativeExtManager';
|
||||
import { StatusManager } from '@/services/statusManager';
|
||||
import { ThemeManager } from '@/services/themeManager';
|
||||
|
||||
type WebServices = {
|
||||
appState: AppState;
|
||||
@@ -35,7 +33,7 @@ type WebServices = {
|
||||
nativeExtService: NativeExtManager;
|
||||
statusManager: StatusManager;
|
||||
themeService: ThemeManager;
|
||||
keyboardService: KeyboardManager;
|
||||
io: IOService;
|
||||
}
|
||||
|
||||
export class WebApplication extends SNApplication {
|
||||
@@ -49,6 +47,7 @@ export class WebApplication extends SNApplication {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
deviceInterface: WebDeviceInterface,
|
||||
platform: Platform,
|
||||
identifier: string,
|
||||
private $compile: angular.ICompileService,
|
||||
scope: angular.IScope,
|
||||
@@ -57,7 +56,7 @@ export class WebApplication extends SNApplication {
|
||||
) {
|
||||
super(
|
||||
bridge.environment,
|
||||
getPlatform(),
|
||||
platform,
|
||||
deviceInterface,
|
||||
WebCrypto,
|
||||
new AlertService(),
|
||||
@@ -139,8 +138,8 @@ export class WebApplication extends SNApplication {
|
||||
return this.webServices.themeService;
|
||||
}
|
||||
|
||||
public getKeyboardService() {
|
||||
return this.webServices.keyboardService;
|
||||
public get io() {
|
||||
return this.webServices.io;
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { WebDeviceInterface } from '@/web_device_interface';
|
||||
import { WebApplication } from './application';
|
||||
import { ApplicationDescriptor, SNApplicationGroup, DeviceInterface } from '@standardnotes/snjs';
|
||||
import {
|
||||
ArchiveManager,
|
||||
DesktopManager,
|
||||
KeyboardManager,
|
||||
AutolockService,
|
||||
NativeExtManager,
|
||||
StatusManager,
|
||||
ThemeManager
|
||||
} from '@/services';
|
||||
ApplicationDescriptor,
|
||||
SNApplicationGroup,
|
||||
DeviceInterface,
|
||||
Platform,
|
||||
} from '@standardnotes/snjs';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { Bridge } from '@/services/bridge';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { getPlatform, isDesktopApplication } from '@/utils';
|
||||
import { ArchiveManager } from '@/services/archiveManager';
|
||||
import { DesktopManager } from '@/services/desktopManager';
|
||||
import { IOService } from '@/services/ioService';
|
||||
import { AutolockService } from '@/services/autolock_service';
|
||||
import { StatusManager } from '@/services/statusManager';
|
||||
import { NativeExtManager } from '@/services/nativeExtManager';
|
||||
import { ThemeManager } from '@/services/themeManager';
|
||||
|
||||
export class ApplicationGroup extends SNApplicationGroup {
|
||||
|
||||
$compile: ng.ICompileService
|
||||
$rootScope: ng.IRootScopeService
|
||||
$timeout: ng.ITimeoutService
|
||||
$compile: ng.ICompileService;
|
||||
$rootScope: ng.IRootScopeService;
|
||||
$timeout: ng.ITimeoutService;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -26,75 +28,72 @@ export class ApplicationGroup extends SNApplicationGroup {
|
||||
$rootScope: ng.IRootScopeService,
|
||||
$timeout: ng.ITimeoutService,
|
||||
private defaultSyncServerHost: string,
|
||||
private bridge: Bridge,
|
||||
private bridge: Bridge
|
||||
) {
|
||||
super(new WebDeviceInterface(
|
||||
$timeout,
|
||||
bridge
|
||||
));
|
||||
super(new WebDeviceInterface($timeout, bridge));
|
||||
this.$compile = $compile;
|
||||
this.$timeout = $timeout;
|
||||
this.$rootScope = $rootScope;
|
||||
}
|
||||
|
||||
async initialize(callback?: any) {
|
||||
async initialize(callback?: any): Promise<void> {
|
||||
await super.initialize({
|
||||
applicationCreator: this.createApplication
|
||||
applicationCreator: this.createApplication,
|
||||
});
|
||||
|
||||
if (isDesktopApplication()) {
|
||||
Object.defineProperty(window, 'desktopManager', {
|
||||
get: () => (this.primaryApplication as WebApplication).getDesktopService()
|
||||
get: () =>
|
||||
(this.primaryApplication as WebApplication).getDesktopService(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createApplication = (descriptor: ApplicationDescriptor, deviceInterface: DeviceInterface) => {
|
||||
private createApplication = (
|
||||
descriptor: ApplicationDescriptor,
|
||||
deviceInterface: DeviceInterface
|
||||
) => {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
const platform = getPlatform();
|
||||
const application = new WebApplication(
|
||||
deviceInterface as WebDeviceInterface,
|
||||
platform,
|
||||
descriptor.identifier,
|
||||
this.$compile,
|
||||
scope,
|
||||
this.defaultSyncServerHost,
|
||||
this.bridge,
|
||||
this.bridge
|
||||
);
|
||||
const appState = new AppState(
|
||||
this.$rootScope,
|
||||
this.$timeout,
|
||||
application,
|
||||
this.bridge,
|
||||
);
|
||||
const archiveService = new ArchiveManager(
|
||||
application
|
||||
this.bridge
|
||||
);
|
||||
const archiveService = new ArchiveManager(application);
|
||||
const desktopService = new DesktopManager(
|
||||
this.$rootScope,
|
||||
this.$timeout,
|
||||
application,
|
||||
this.bridge,
|
||||
this.bridge
|
||||
);
|
||||
const keyboardService = new KeyboardManager();
|
||||
const autolockService = new AutolockService(
|
||||
application
|
||||
);
|
||||
const nativeExtService = new NativeExtManager(
|
||||
application
|
||||
);
|
||||
const statusService = new StatusManager();
|
||||
const themeService = new ThemeManager(
|
||||
application,
|
||||
const io = new IOService(
|
||||
platform === Platform.MacWeb || platform === Platform.MacDesktop
|
||||
);
|
||||
const autolockService = new AutolockService(application);
|
||||
const nativeExtService = new NativeExtManager(application);
|
||||
const statusManager = new StatusManager();
|
||||
const themeService = new ThemeManager(application);
|
||||
application.setWebServices({
|
||||
appState,
|
||||
archiveService,
|
||||
desktopService,
|
||||
keyboardService,
|
||||
io,
|
||||
autolockService,
|
||||
nativeExtService,
|
||||
statusManager: statusService,
|
||||
themeService
|
||||
statusManager,
|
||||
themeService,
|
||||
});
|
||||
return application;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApplicationEvent } from '@standardnotes/snjs';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx';
|
||||
|
||||
export type CtrlState = Partial<Record<string, any>>
|
||||
export type CtrlProps = Partial<Record<string, any>>
|
||||
@@ -17,6 +19,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
* no Angular handlebars/syntax render in the UI before display data is ready.
|
||||
*/
|
||||
protected templateReady = false
|
||||
private reactionDisposers: IReactionDisposer[] = [];
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -26,7 +29,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
$onInit(): void {
|
||||
this.state = {
|
||||
...this.getInitialState(),
|
||||
...this.state,
|
||||
@@ -36,9 +39,13 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
this.templateReady = true;
|
||||
}
|
||||
|
||||
deinit() {
|
||||
deinit(): void {
|
||||
this.unsubApp();
|
||||
this.unsubState();
|
||||
for (const disposer of this.reactionDisposers) {
|
||||
disposer();
|
||||
}
|
||||
this.reactionDisposers.length = 0;
|
||||
this.unsubApp = undefined;
|
||||
this.unsubState = undefined;
|
||||
if (this.stateTimeout) {
|
||||
@@ -46,16 +53,16 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
}
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
$onDestroy(): void {
|
||||
this.deinit();
|
||||
}
|
||||
|
||||
public get appState() {
|
||||
return this.application!.getAppState();
|
||||
public get appState(): AppState {
|
||||
return this.application.getAppState();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
async resetState() {
|
||||
async resetState(): Promise<void> {
|
||||
this.state = this.getInitialState();
|
||||
await this.setState(this.state);
|
||||
}
|
||||
@@ -65,7 +72,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
async setState(state: Partial<S>) {
|
||||
async setState(state: Partial<S>): Promise<void> {
|
||||
if (!this.$timeout) {
|
||||
return;
|
||||
}
|
||||
@@ -88,17 +95,21 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
}
|
||||
|
||||
/** @returns a promise that resolves after the UI has been updated. */
|
||||
flushUI() {
|
||||
flushUI(): angular.IPromise<void> {
|
||||
return this.$timeout();
|
||||
}
|
||||
|
||||
initProps(props: CtrlProps) {
|
||||
initProps(props: CtrlProps): void {
|
||||
if (Object.keys(this.props).length > 0) {
|
||||
throw 'Already init-ed props.';
|
||||
}
|
||||
this.props = Object.freeze(Object.assign({}, this.props, props));
|
||||
}
|
||||
|
||||
autorun(view: (r: IReactionPublic) => void): void {
|
||||
this.reactionDisposers.push(autorun(view));
|
||||
}
|
||||
|
||||
addAppStateObserver() {
|
||||
this.unsubState = this.application!.getAppState().addObserver(
|
||||
async (eventName, data) => {
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
)
|
||||
tags-view(application='self.application')
|
||||
notes-view(application='self.application')
|
||||
editor-group-view(
|
||||
application='self.application'
|
||||
)
|
||||
editor-group-view.flex-grow(application='self.application')
|
||||
|
||||
footer-view(
|
||||
ng-if='!self.state.needsUnlock && self.state.ready'
|
||||
@@ -35,3 +33,6 @@
|
||||
challenge="challenge"
|
||||
on-dismiss="self.removeChallenge(challenge)"
|
||||
)
|
||||
notes-context-menu(
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
@@ -281,7 +281,7 @@ function ChallengeModalView({ ctrl }: { ctrl: ChallengeModalCtrl }) {
|
||||
<div className="sk-panel-footer extra-padding">
|
||||
<button
|
||||
className={
|
||||
'sn-button w-full py-3 text-base ' +
|
||||
'sn-button w-full ' +
|
||||
(ctrl.state.processing ? 'neutral' : 'info')
|
||||
}
|
||||
disabled={ctrl.state.processing}
|
||||
@@ -293,7 +293,7 @@ function ChallengeModalView({ ctrl }: { ctrl: ChallengeModalCtrl }) {
|
||||
<>
|
||||
<div className="sk-panel-row"></div>
|
||||
<a
|
||||
className="sk-panel-row sk-a info centered"
|
||||
className="sk-panel-row sk-a info centered text-sm"
|
||||
onClick={() => ctrl.cancel()}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -1,62 +1,73 @@
|
||||
#editor-column.section.editor.sn-component(aria-label='Note')
|
||||
protected-note-panel.h-full.flex.justify-center.items-center(
|
||||
ng-if='self.state.showProtectedWarning'
|
||||
ng-if='self.appState.notes.showProtectedWarning'
|
||||
app-state='self.appState'
|
||||
on-view-note='self.dismissProtectedWarning()'
|
||||
)
|
||||
.flex-grow.flex.flex-col(
|
||||
ng-if='!self.state.showProtectedWarning'
|
||||
ng-if='!self.appState.notes.showProtectedWarning'
|
||||
)
|
||||
.sn-component
|
||||
.sk-app-bar.no-edges(
|
||||
ng-if='self.noteLocked',
|
||||
ng-init="self.lockText = 'Note Locked'",
|
||||
ng-mouseleave="self.lockText = 'Note Locked'",
|
||||
ng-mouseover="self.lockText = 'Unlock'"
|
||||
ng-init="self.lockText = 'Note Editing Disabled'; self.showLockedIcon = true",
|
||||
ng-mouseleave="self.lockText = 'Note Editing Disabled'; self.showLockedIcon = true",
|
||||
ng-mouseover="self.lockText = 'Enable editing'; self.showLockedIcon = false"
|
||||
)
|
||||
.left
|
||||
.sk-app-bar-item(ng-click='self.toggleLockNote()')
|
||||
.sk-label.warning
|
||||
i.icon.ion-locked
|
||||
| {{self.lockText}}
|
||||
#editor-title-bar.section-title-bar(
|
||||
ng-class="{'locked' : self.noteLocked}",
|
||||
.sk-app-bar-item(
|
||||
ng-click='self.appState.notes.setLockSelectedNotes(!self.noteLocked)'
|
||||
)
|
||||
.sk-label.warning.flex.items-center
|
||||
icon.flex(
|
||||
type="pencil-off"
|
||||
class-name="fill-current mr-2"
|
||||
ng-if="self.showLockedIcon"
|
||||
)
|
||||
| {{self.lockText}}
|
||||
#editor-title-bar.section-title-bar.flex.items-center.justify-between.w-full(
|
||||
ng-show='self.note && !self.note.errorDecrypting'
|
||||
)
|
||||
.title
|
||||
input#note-title-editor.input(
|
||||
ng-blur='self.onTitleBlur()',
|
||||
ng-change='self.onTitleChange()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-focus='self.onTitleFocus()',
|
||||
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
|
||||
ng-model='self.editorValues.title',
|
||||
select-on-focus='true',
|
||||
spellcheck='false'
|
||||
)
|
||||
#save-status
|
||||
.message(
|
||||
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
|
||||
) {{self.state.noteStatus.message}}
|
||||
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
|
||||
.editor-tags
|
||||
#note-tags-component-container(ng-if='self.state.tagsComponent && !self.note.errorDecrypting')
|
||||
component-view.component-view(
|
||||
component-uuid='self.state.tagsComponent.uuid',
|
||||
ng-class="{'locked' : self.noteLocked}",
|
||||
ng-style="self.noteLocked && {'pointer-events' : 'none'}",
|
||||
application='self.application'
|
||||
div.flex-grow(
|
||||
ng-class="{'locked' : self.noteLocked}"
|
||||
)
|
||||
.title
|
||||
input#note-title-editor.input(
|
||||
ng-blur='self.onTitleBlur()',
|
||||
ng-change='self.onTitleChange()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-focus='self.onTitleFocus()',
|
||||
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
|
||||
ng-model='self.editorValues.title',
|
||||
select-on-focus='true',
|
||||
spellcheck='false'
|
||||
)
|
||||
input.tags-input(
|
||||
ng-blur='self.onTagsInputBlur()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-if='!self.state.tagsComponent',
|
||||
ng-keyup='$event.keyCode == 13 && $event.target.blur();',
|
||||
ng-model='self.editorValues.tagsInputValue',
|
||||
placeholder='#tags',
|
||||
spellcheck='false',
|
||||
type='text'
|
||||
)
|
||||
.editor-tags
|
||||
#note-tags-component-container(ng-if='self.state.tagsComponent && !self.note.errorDecrypting')
|
||||
component-view.component-view(
|
||||
component-uuid='self.state.tagsComponent.uuid',
|
||||
ng-style="self.notesLocked && {'pointer-events' : 'none'}",
|
||||
application='self.application'
|
||||
)
|
||||
input.tags-input(
|
||||
ng-blur='self.onTagsInputBlur()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-if='!self.state.tagsComponent',
|
||||
ng-keyup='$event.keyCode == 13 && $event.target.blur();',
|
||||
ng-model='self.editorValues.tagsInputValue',
|
||||
placeholder='#tags',
|
||||
spellcheck='false',
|
||||
type='text'
|
||||
)
|
||||
div.flex.items-center
|
||||
#save-status
|
||||
.message(
|
||||
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
|
||||
) {{self.state.noteStatus.message}}
|
||||
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
|
||||
notes-options-panel(
|
||||
app-state='self.appState',
|
||||
ng-if='self.appState.notes.selectedNotesCount > 0'
|
||||
)
|
||||
.sn-component(ng-if='self.note')
|
||||
#editor-menu-bar.sk-app-bar.no-edges
|
||||
.left
|
||||
@@ -68,72 +79,6 @@
|
||||
)
|
||||
.sk-label Options
|
||||
.sk-menu-panel.dropdown-menu(ng-if='self.state.showOptionsMenu')
|
||||
.sk-menu-panel-section
|
||||
.sk-menu-panel-header
|
||||
.sk-menu-panel-header-title Note Options
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.togglePin()'
|
||||
desc="'Pin or unpin a note from the top of your list'",
|
||||
label="self.note.pinned ? 'Unpin' : 'Pin'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.toggleArchiveNote()'
|
||||
desc="'Archive or unarchive a note from your Archived system tag'",
|
||||
label="self.note.archived ? 'Unarchive' : 'Archive'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.toggleLockNote()'
|
||||
desc="'Locking notes prevents unintentional editing'",
|
||||
label="self.noteLocked ? 'Unlock' : 'Lock'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.toggleProtectNote()'
|
||||
desc=`'Protecting a note will require credentials to view it'`,
|
||||
label="self.note.protected ? 'Unprotect' : 'Protect'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.toggleNotePreview()'
|
||||
circle="self.note.hidePreview ? 'danger' : 'success'",
|
||||
circle-align="'right'",
|
||||
desc="'Hide or unhide the note preview from the list of notes'",
|
||||
label="'Preview'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(); self.deleteNote()'
|
||||
desc="'Send this note to the trash'",
|
||||
label="'Move to Trash'",
|
||||
ng-show='!self.state.altKeyDown && !self.note.trashed && !self.note.errorDecrypting',
|
||||
stylekit-class="'warning'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(); self.deleteNotePermanantely()'
|
||||
desc="'Delete this note permanently from all your devices'",
|
||||
label="'Delete Permanently'",
|
||||
ng-show='!self.note.trashed && self.note.errorDecrypting',
|
||||
stylekit-class="'danger'"
|
||||
)
|
||||
div(ng-if='self.note.trashed || self.state.altKeyDown')
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.restoreTrashedNote()'
|
||||
desc="'Undelete this note and restore it back into your notes'",
|
||||
label="'Restore'",
|
||||
ng-show='self.note.trashed',
|
||||
stylekit-class="'info'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.deleteNotePermanantely()'
|
||||
desc="'Delete this note permanently from all your devices'",
|
||||
label="'Delete Permanently'",
|
||||
stylekit-class="'danger'"
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.emptyTrash()'
|
||||
desc="'Permanently delete all notes in the trash'",
|
||||
label="'Empty Trash'",
|
||||
ng-show='self.note.trashed || !self.state.altKeyDown',
|
||||
stylekit-class="'danger'",
|
||||
subtitle="self.getTrashCount() + ' notes in trash'"
|
||||
)
|
||||
.sk-menu-panel-section
|
||||
.sk-menu-panel-header
|
||||
.sk-menu-panel-header-title Global Display
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '@standardnotes/snjs';
|
||||
import find from 'lodash/find';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
|
||||
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
|
||||
import template from './editor-view.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { EventSource } from '@/ui_models/app_state';
|
||||
@@ -85,7 +85,6 @@ type EditorState = {
|
||||
textareaUnloading: boolean;
|
||||
/** Fields that can be directly mutated by the template */
|
||||
mutable: any;
|
||||
showProtectedWarning: boolean;
|
||||
};
|
||||
|
||||
type EditorValues = {
|
||||
@@ -241,7 +240,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
mutable: {
|
||||
tagsString: '',
|
||||
},
|
||||
showProtectedWarning: false,
|
||||
} as EditorState;
|
||||
}
|
||||
|
||||
@@ -291,8 +289,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
async handleEditorNoteChange() {
|
||||
this.cancelPendingSetStatus();
|
||||
const note = this.editor.note;
|
||||
const showProtectedWarning =
|
||||
note.protected && !this.application.hasProtectionSources();
|
||||
const showProtectedWarning = note.protected && !this.application.hasProtectionSources();
|
||||
this.setShowProtectedWarning(showProtectedWarning);
|
||||
await this.setState({
|
||||
showActionsMenu: false,
|
||||
showOptionsMenu: false,
|
||||
@@ -300,7 +298,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
showHistoryMenu: false,
|
||||
altKeyDown: false,
|
||||
noteStatus: undefined,
|
||||
showProtectedWarning,
|
||||
});
|
||||
this.editorValues.title = note.title;
|
||||
this.editorValues.text = note.text;
|
||||
@@ -318,9 +315,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
async dismissProtectedWarning() {
|
||||
await this.setState({
|
||||
showProtectedWarning: false,
|
||||
});
|
||||
this.setShowProtectedWarning(false);
|
||||
this.focusTitle();
|
||||
}
|
||||
|
||||
@@ -396,6 +391,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
toggleMenu(menu: keyof EditorState) {
|
||||
this.setMenuState(menu, !this.state[menu]);
|
||||
this.application.getAppState().notes.setContextMenuOpen(false);
|
||||
}
|
||||
|
||||
closeAllMenus(exclude?: string) {
|
||||
@@ -657,6 +653,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
}
|
||||
|
||||
setShowProtectedWarning(show: boolean) {
|
||||
this.application.getAppState().notes.setShowProtectedWarning(show);
|
||||
}
|
||||
|
||||
async deleteNote(permanently: boolean) {
|
||||
if (this.editor.isTemplateNote) {
|
||||
this.application.alertService!.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT);
|
||||
@@ -697,120 +697,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.application.deleteItem(note);
|
||||
}
|
||||
|
||||
restoreTrashedNote() {
|
||||
this.save(
|
||||
this.note,
|
||||
copyEditorValues(this.editorValues),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.trashed = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
deleteNotePermanantely() {
|
||||
this.deleteNote(true);
|
||||
}
|
||||
|
||||
getTrashCount() {
|
||||
return this.application.getTrashedItems().length;
|
||||
}
|
||||
|
||||
async emptyTrash() {
|
||||
const count = this.getTrashCount();
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: StringEmptyTrash(count),
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
this.application.emptyTrash();
|
||||
this.application.sync();
|
||||
}
|
||||
}
|
||||
|
||||
togglePin() {
|
||||
const note = this.note;
|
||||
this.save(
|
||||
note,
|
||||
copyEditorValues(this.editorValues),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.pinned = !note.pinned;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleLockNote() {
|
||||
const note = this.note;
|
||||
this.save(
|
||||
note,
|
||||
copyEditorValues(this.editorValues),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.locked = !note.locked;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async toggleProtectNote() {
|
||||
if (this.note.protected) {
|
||||
void this.application.unprotectNote(this.note);
|
||||
} else {
|
||||
const note = await this.application.protectNote(this.note);
|
||||
if (note?.protected && !this.application.hasProtectionSources()) {
|
||||
this.setState({
|
||||
showProtectedWarning: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleNotePreview() {
|
||||
const note = this.note;
|
||||
this.save(
|
||||
note,
|
||||
copyEditorValues(this.editorValues),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.hidePreview = !note.hidePreview;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleArchiveNote() {
|
||||
const note = this.note;
|
||||
if (note.locked) {
|
||||
alertDialog({
|
||||
text: note.archived
|
||||
? STRING_UNARCHIVE_LOCKED_ATTEMPT
|
||||
: STRING_ARCHIVE_LOCKED_ATTEMPT,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.save(
|
||||
note,
|
||||
copyEditorValues(this.editorValues),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.archived = !note.archived;
|
||||
},
|
||||
/** If we are unarchiving, and we are in the archived tag, close the editor */
|
||||
note.archived && this.appState.selectedTag?.isArchiveTag
|
||||
);
|
||||
}
|
||||
|
||||
async reloadTags() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
@@ -1168,7 +1054,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
this.removeAltKeyObserver = this.application
|
||||
.getKeyboardService()
|
||||
.io
|
||||
.addKeyObserver({
|
||||
modifiers: [KeyboardModifier.Alt],
|
||||
onKeyDown: () => {
|
||||
@@ -1184,7 +1070,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
});
|
||||
|
||||
this.removeTrashKeyObserver = this.application
|
||||
.getKeyboardService()
|
||||
.io
|
||||
.addKeyObserver({
|
||||
key: KeyboardKey.Backspace,
|
||||
notElementIds: [ElementIds.NoteTextEditor, ElementIds.NoteTitleEditor],
|
||||
@@ -1209,7 +1095,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
ElementIds.NoteTextEditor
|
||||
)! as HTMLInputElement;
|
||||
this.removeTabObserver = this.application
|
||||
.getKeyboardService()
|
||||
.io
|
||||
.addKeyObserver({
|
||||
element: editor,
|
||||
key: KeyboardKey.Tab,
|
||||
@@ -1247,7 +1133,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
* (and not when our controller is destroyed.)
|
||||
*/
|
||||
angular.element(editor).one('$destroy', () => {
|
||||
this.removeTabObserver();
|
||||
this.removeTabObserver?.();
|
||||
this.removeTabObserver = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
.flex-grow(
|
||||
ng-repeat='editor in self.editors'
|
||||
)
|
||||
editor-view(
|
||||
application='self.application'
|
||||
editor='editor'
|
||||
.h-full
|
||||
multiple-selected-notes-panel.h-full(
|
||||
app-state='self.appState'
|
||||
ng-if='self.state.showMultipleSelectedNotes'
|
||||
)
|
||||
.flex-grow.h-full(
|
||||
ng-if='!self.state.showMultipleSelectedNotes'
|
||||
ng-repeat='editor in self.editors'
|
||||
)
|
||||
editor-view(
|
||||
application='self.application'
|
||||
editor='editor'
|
||||
)
|
||||
|
||||
@@ -2,16 +2,31 @@ import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from './editor-group-view.pug';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { PureViewCtrl } from '../abstract/pure_view_ctrl';
|
||||
|
||||
class EditorGroupViewCtrl {
|
||||
class EditorGroupViewCtrl extends PureViewCtrl<unknown, {
|
||||
showMultipleSelectedNotes: boolean
|
||||
}> {
|
||||
|
||||
private application!: WebApplication
|
||||
public editors: Editor[] = []
|
||||
|
||||
/* @ngInject */
|
||||
constructor($timeout: ng.ITimeoutService,) {
|
||||
super($timeout);
|
||||
this.state = {
|
||||
showMultipleSelectedNotes: false
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.application.editorGroup.addChangeObserver(() => {
|
||||
this.editors = this.application.editorGroup.editors;
|
||||
});
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
showMultipleSelectedNotes: this.appState.notes.selectedNotesCount > 1
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +35,6 @@ export class EditorGroupView extends WebDirective {
|
||||
super();
|
||||
this.template = template;
|
||||
this.controller = EditorGroupViewCtrl;
|
||||
this.replace = true;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from '@/strings';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { alertDialog, confirmDialog } from '@/services/alertService';
|
||||
import { autorun, IReactionDisposer } from 'mobx';
|
||||
|
||||
/**
|
||||
* Disable before production release.
|
||||
@@ -75,7 +74,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
private observerRemovers: Array<() => void> = [];
|
||||
private completedInitialSync = false;
|
||||
private showingDownloadStatus = false;
|
||||
private autorunDisposer?: IReactionDisposer;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -103,7 +101,6 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
this.rootScopeListener2 = undefined;
|
||||
(this.closeAccountMenu as any) = undefined;
|
||||
(this.toggleSyncResolutionMenu as any) = undefined;
|
||||
this.autorunDisposer?.();
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -115,7 +112,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
});
|
||||
});
|
||||
this.loadAccountSwitcherState();
|
||||
this.autorunDisposer = autorun(() => {
|
||||
this.autorun(() => {
|
||||
const showBetaWarning = this.appState.showBetaWarning;
|
||||
this.showAccountMenu = this.appState.accountMenu.show;
|
||||
this.setState({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#notes-column.sn-component.section.notes(aria-label='Notes')
|
||||
.content
|
||||
#notes-title-bar.section-title-bar
|
||||
.p-4.pt-0
|
||||
.p-4
|
||||
.section-title-bar-header
|
||||
.sk-h2.font-semibold.title {{self.state.panelTitle}}
|
||||
.sk-button.contrast.wide(
|
||||
@@ -127,8 +127,9 @@
|
||||
threshold='200'
|
||||
)
|
||||
.note(
|
||||
ng-attr-id='note-{{note.uuid}}'
|
||||
ng-repeat='note in self.state.renderedNotes track by note.uuid'
|
||||
ng-class="{'selected' : self.activeEditorNote.uuid == note.uuid}"
|
||||
ng-class="{'selected' : self.isNoteSelected(note.uuid) }"
|
||||
ng-click='self.selectNote(note)'
|
||||
)
|
||||
.note-flags(ng-show='self.noteFlags[note.uuid].length > 0')
|
||||
|
||||
@@ -14,17 +14,17 @@ import {
|
||||
} from '@standardnotes/snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { AppStateEvent } from '@/ui_models/app_state';
|
||||
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
|
||||
import { KeyboardKey, KeyboardModifier } from '@/services/ioService';
|
||||
import {
|
||||
PANEL_NAME_NOTES
|
||||
} from '@/views/constants';
|
||||
import { autorun, IReactionDisposer } from 'mobx';
|
||||
|
||||
type NotesState = {
|
||||
type NotesCtrlState = {
|
||||
panelTitle: string
|
||||
notes: SNNote[]
|
||||
renderedNotes: SNNote[]
|
||||
renderedNotesTags: string[],
|
||||
selectedNotes: Record<UuidString, SNNote>,
|
||||
sortBy?: string
|
||||
sortReverse?: boolean
|
||||
showArchived?: boolean
|
||||
@@ -65,7 +65,7 @@ const DEFAULT_LIST_NUM_NOTES = 20;
|
||||
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
|
||||
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
|
||||
|
||||
class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
|
||||
private panelPuppet?: PanelPuppet
|
||||
private reloadNotesPromise?: any
|
||||
@@ -78,7 +78,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
private searchKeyObserver: any
|
||||
private noteFlags: Partial<Record<UuidString, NoteFlag[]>> = {}
|
||||
private removeObservers: Array<() => void> = [];
|
||||
private appStateObserver?: IReactionDisposer;
|
||||
private rightClickListeners: Map<UuidString, (e: MouseEvent) => void> = new Map();
|
||||
|
||||
/* @ngInject */
|
||||
constructor($timeout: ng.ITimeoutService,) {
|
||||
@@ -95,7 +95,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
this.onPanelResize = this.onPanelResize.bind(this);
|
||||
window.addEventListener('resize', this.onWindowResize, true);
|
||||
this.registerKeyboardShortcuts();
|
||||
this.appStateObserver = autorun(async () => {
|
||||
this.autorun(async () => {
|
||||
const {
|
||||
includeProtectedContents,
|
||||
includeArchived,
|
||||
@@ -113,6 +113,11 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
this.reloadNotes();
|
||||
}
|
||||
});
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
selectedNotes: this.appState.notes.selectedNotes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
@@ -122,6 +127,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
deinit() {
|
||||
for (const remove of this.removeObservers) remove();
|
||||
this.removeObservers.length = 0;
|
||||
this.removeRightClickListeners();
|
||||
this.panelPuppet!.onReady = undefined;
|
||||
this.panelPuppet = undefined;
|
||||
window.removeEventListener('resize', this.onWindowResize, true);
|
||||
@@ -131,7 +137,6 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
this.nextNoteKeyObserver();
|
||||
this.previousNoteKeyObserver();
|
||||
this.searchKeyObserver();
|
||||
this.appStateObserver?.();
|
||||
this.newNoteKeyObserver = undefined;
|
||||
this.nextNoteKeyObserver = undefined;
|
||||
this.previousNoteKeyObserver = undefined;
|
||||
@@ -139,15 +144,16 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
async setNotesState(state: Partial<NotesState>) {
|
||||
async setNotesState(state: Partial<NotesCtrlState>) {
|
||||
return this.setState(state);
|
||||
}
|
||||
|
||||
getInitialState(): NotesState {
|
||||
getInitialState(): NotesCtrlState {
|
||||
return {
|
||||
notes: [],
|
||||
renderedNotes: [],
|
||||
renderedNotesTags: [],
|
||||
selectedNotes: {},
|
||||
mutable: { showMenu: false },
|
||||
noteFilter: {
|
||||
text: '',
|
||||
@@ -180,9 +186,13 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
}
|
||||
}
|
||||
|
||||
private get activeEditorNote() {
|
||||
return this.appState.notes.activeEditor?.note;
|
||||
}
|
||||
|
||||
/** @template */
|
||||
public get activeEditorNote() {
|
||||
return this.appState?.getActiveEditor()?.note;
|
||||
public isNoteSelected(uuid: UuidString) {
|
||||
return !!this.state.selectedNotes[uuid];
|
||||
}
|
||||
|
||||
public get editorNotes() {
|
||||
@@ -262,13 +272,17 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
* we dont need to reload display options */
|
||||
await this.reloadNotes();
|
||||
const activeNote = this.activeEditorNote;
|
||||
if (activeNote) {
|
||||
const discarded = activeNote.deleted || activeNote.trashed;
|
||||
if (discarded && !this.appState?.selectedTag?.isTrashTag) {
|
||||
this.selectNextOrCreateNew();
|
||||
if (this.application.getAppState().notes.selectedNotesCount < 2) {
|
||||
if (activeNote) {
|
||||
const discarded = activeNote.deleted || activeNote.trashed;
|
||||
if (discarded && !this.appState?.selectedTag?.isTrashTag) {
|
||||
this.selectNextOrCreateNew();
|
||||
} else if (!this.state.selectedNotes[activeNote.uuid]) {
|
||||
this.selectNote(activeNote);
|
||||
}
|
||||
} else {
|
||||
this.selectFirstNote();
|
||||
}
|
||||
} else {
|
||||
this.selectFirstNote();
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -288,12 +302,65 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
));
|
||||
}
|
||||
|
||||
async selectNote(note: SNNote) {
|
||||
await this.appState.openEditor(note.uuid);
|
||||
if (note.waitingForKey) {
|
||||
this.application.presentKeyRecoveryWizard();
|
||||
private async openNotesContextMenu(e: MouseEvent, note: SNNote) {
|
||||
e.preventDefault();
|
||||
if (!this.state.selectedNotes[note.uuid]) {
|
||||
await this.selectNote(note);
|
||||
}
|
||||
this.reloadNotes();
|
||||
if (this.state.selectedNotes[note.uuid]) {
|
||||
const clientHeight = document.documentElement.clientHeight;
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
).fontSize;
|
||||
const maxContextMenuHeight = parseFloat(defaultFontSize) * 20;
|
||||
if (e.clientY > clientHeight - maxContextMenuHeight) {
|
||||
this.application.getAppState().notes.setContextMenuPosition({
|
||||
bottom: clientHeight - e.clientY,
|
||||
left: e.clientX,
|
||||
});
|
||||
} else {
|
||||
this.application.getAppState().notes.setContextMenuPosition({
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
});
|
||||
}
|
||||
this.application.getAppState().notes.setContextMenuOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
private removeRightClickListeners() {
|
||||
for (const [noteUuid, listener] of this.rightClickListeners.entries()) {
|
||||
document
|
||||
.getElementById(`note-${noteUuid}`)
|
||||
?.removeEventListener('contextmenu', listener);
|
||||
}
|
||||
this.rightClickListeners.clear();
|
||||
}
|
||||
|
||||
private addRightClickListeners() {
|
||||
for (const [noteUuid, listener] of this.rightClickListeners.entries()) {
|
||||
if (!this.state.renderedNotes.find(note => note.uuid === noteUuid)) {
|
||||
document
|
||||
.getElementById(`note-${noteUuid}`)
|
||||
?.removeEventListener('contextmenu', listener);
|
||||
this.rightClickListeners.delete(noteUuid);
|
||||
}
|
||||
}
|
||||
for (const note of this.state.renderedNotes) {
|
||||
if (!this.rightClickListeners.has(note.uuid)) {
|
||||
const listener = async (e: MouseEvent): Promise<void> => {
|
||||
return await this.openNotesContextMenu(e, note);
|
||||
};
|
||||
document
|
||||
.getElementById(`note-${note.uuid}`)
|
||||
?.addEventListener('contextmenu', listener);
|
||||
this.rightClickListeners.set(note.uuid, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async selectNote(note: SNNote): Promise<void> {
|
||||
await this.appState.notes.selectNote(note.uuid);
|
||||
}
|
||||
|
||||
async createNewNote() {
|
||||
@@ -410,6 +477,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
renderedNotes,
|
||||
});
|
||||
this.reloadPanelTitle();
|
||||
this.addRightClickListeners();
|
||||
}
|
||||
|
||||
private notesTagsList(notes: SNNote[]): string[] {
|
||||
@@ -461,7 +529,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
}
|
||||
|
||||
async reloadPreferences() {
|
||||
const viewOptions = {} as NotesState;
|
||||
const viewOptions = {} as NotesCtrlState;
|
||||
const prevSortValue = this.state.sortBy;
|
||||
let sortBy = this.application.getPreference(
|
||||
PrefKey.SortNotesBy,
|
||||
@@ -621,7 +689,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
}
|
||||
if (note.locked) {
|
||||
flags.push({
|
||||
text: "Locked",
|
||||
text: "Editing Disabled",
|
||||
class: 'neutral'
|
||||
});
|
||||
}
|
||||
@@ -673,7 +741,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
selectNextNote() {
|
||||
const displayableNotes = this.state.notes;
|
||||
const currentIndex = displayableNotes.findIndex((candidate) => {
|
||||
return candidate.uuid === this.activeEditorNote.uuid;
|
||||
return candidate.uuid === this.activeEditorNote?.uuid;
|
||||
});
|
||||
if (currentIndex + 1 < displayableNotes.length) {
|
||||
this.selectNote(displayableNotes[currentIndex + 1]);
|
||||
@@ -791,7 +859,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
* use Control modifier as well. These rules don't apply to desktop, but
|
||||
* probably better to be consistent.
|
||||
*/
|
||||
this.newNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
this.newNoteKeyObserver = this.application.io.addKeyObserver({
|
||||
key: 'n',
|
||||
modifiers: [
|
||||
KeyboardModifier.Meta,
|
||||
@@ -803,7 +871,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
}
|
||||
});
|
||||
|
||||
this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
this.nextNoteKeyObserver = this.application.io.addKeyObserver({
|
||||
key: KeyboardKey.Down,
|
||||
elements: [
|
||||
document.body,
|
||||
@@ -818,7 +886,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
}
|
||||
});
|
||||
|
||||
this.previousNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
this.previousNoteKeyObserver = this.application.io.addKeyObserver({
|
||||
key: KeyboardKey.Up,
|
||||
element: document.body,
|
||||
onKeyDown: () => {
|
||||
@@ -826,7 +894,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
}
|
||||
});
|
||||
|
||||
this.searchKeyObserver = this.application.getKeyboardService().addKeyObserver({
|
||||
this.searchKeyObserver = this.application.io.addKeyObserver({
|
||||
key: "f",
|
||||
modifiers: [
|
||||
KeyboardModifier.Meta,
|
||||
|
||||
@@ -41,14 +41,10 @@ $heading-height: 75px;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
|
||||
$title-width: 70%;
|
||||
$save-status-width: 30%;
|
||||
|
||||
> .title {
|
||||
.title {
|
||||
font-size: var(--sn-stylekit-font-size-h1);
|
||||
font-weight: bold;
|
||||
padding-top: 0px;
|
||||
width: $title-width;
|
||||
padding-right: 20px; /* make room for save status */
|
||||
|
||||
> .input {
|
||||
@@ -71,15 +67,10 @@ $heading-height: 75px;
|
||||
}
|
||||
|
||||
#save-status {
|
||||
width: $save-status-width;
|
||||
float: right;
|
||||
position: absolute;
|
||||
|
||||
right: 20px;
|
||||
margin-right: 20px;
|
||||
font-size: calc(var(--sn-stylekit-base-font-size) - 2px);
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
margin-top: 4px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
$z-index-editor-content: 10;
|
||||
|
||||
$z-index-editor-title-bar: 100;
|
||||
$z-index-dropdown-menu: 100;
|
||||
$z-index-dropdown-menu: 1002;
|
||||
|
||||
$z-index-resizer-overlay: 1000;
|
||||
$z-index-panel-resizer: 1001;
|
||||
@@ -15,6 +15,14 @@ $z-index-footer-bar-item-panel: 2000;
|
||||
$z-index-lock-screen: 10000;
|
||||
$z-index-modal: 10000;
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: var(--sn-stylekit-base-font-size);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
@@ -26,7 +34,6 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
font-size: var(--sn-stylekit-base-font-size);
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
color: var(--sn-stylekit-foreground-color);
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
.sk-panel-content {
|
||||
padding-top: 1.1rem;
|
||||
padding-top: 0.89375rem;
|
||||
}
|
||||
.sk-panel-footer {
|
||||
padding-bottom: 1.4rem;
|
||||
padding-bottom: 1.1375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
.header .subtitle {
|
||||
font-size: 1.1rem;
|
||||
font-size: 0.89375rem;
|
||||
}
|
||||
|
||||
.sk-modal {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
}
|
||||
|
||||
#notes-title-bar {
|
||||
padding-top: 16px;
|
||||
font-weight: normal;
|
||||
|
||||
.section-title-bar-header .title {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
align-self: center;
|
||||
}
|
||||
.sn-component .sk-panel-content {
|
||||
padding-bottom: 1.6rem;
|
||||
padding-bottom: 1.3rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,37 +1,99 @@
|
||||
/* Components and utilities that are good candidates for extraction to StyleKit. */
|
||||
|
||||
.outline-none {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.border-2 {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border-b-1 {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-background {
|
||||
border-color: var(--sn-stylekit-background-color);
|
||||
}
|
||||
|
||||
.focus-within\:border-background:focus-within {
|
||||
border-color: var(--sn-stylekit-background-color);
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.border-transparent {
|
||||
border-color: var(--sn-stylekit-background-color);
|
||||
.border-info {
|
||||
border-color: var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
.border-neutral {
|
||||
border-color: var(--sn-stylekit-neutral-color);
|
||||
}
|
||||
|
||||
.bg-border {
|
||||
background-color: var(--sn-stylekit-border-color);
|
||||
}
|
||||
|
||||
.bg-clip-padding {
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.bg-neutral {
|
||||
background-color: var(--sn-stylekit-neutral-color);
|
||||
}
|
||||
|
||||
.focus-within\:border-background:focus-within {
|
||||
border-color: var(--sn-stylekit-background-color);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.py-1\.5 {
|
||||
padding-top: 0.375rem;
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.outline-none {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.color-foreground {
|
||||
color: var(--sn-stylekit-foreground-color);
|
||||
}
|
||||
|
||||
.color-danger {
|
||||
color: var(--sn-stylekit-danger-color);
|
||||
}
|
||||
|
||||
.ring-info {
|
||||
box-shadow: 0 0 0 2px var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
.inner-ring-info {
|
||||
box-shadow: inset 0 0 0 2px var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
.focus\:bg-contrast:focus {
|
||||
@extend .bg-contrast;
|
||||
}
|
||||
|
||||
.hover\:color-text:hover {
|
||||
@extend .color-text;
|
||||
}
|
||||
|
||||
.focus\:color-text:focus {
|
||||
@extend .color-text;
|
||||
}
|
||||
|
||||
.focus\:inner-ring-info:focus {
|
||||
@extend .inner-ring-info;
|
||||
}
|
||||
|
||||
.focus\:ring-info:focus {
|
||||
@extend .ring-info;
|
||||
}
|
||||
@@ -40,8 +102,53 @@
|
||||
@extend .ring-info;
|
||||
}
|
||||
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.max-w-60 {
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
.h-1px {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-10 {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.max-h-80 {
|
||||
max-height: 20rem;
|
||||
}
|
||||
|
||||
.overflow-y-scroll {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,8 +156,14 @@
|
||||
* is almost no style overlap.
|
||||
*/
|
||||
.sn-icon-button {
|
||||
@extend .border-2;
|
||||
@extend .border-transparent;
|
||||
@extend .w-8;
|
||||
@extend .h-8;
|
||||
@extend .flex;
|
||||
@extend .justify-center;
|
||||
@extend .items-center;
|
||||
@extend .border-solid;
|
||||
@extend .border-1;
|
||||
@extend .border-neutral;
|
||||
@extend .bg-clip-padding;
|
||||
@extend .m-0;
|
||||
@extend .p-0;
|
||||
@@ -58,17 +171,26 @@
|
||||
@extend .cursor-pointer;
|
||||
@extend .rounded-full;
|
||||
@extend .color-neutral;
|
||||
@extend .hover\:color-text;
|
||||
@extend .focus\:color-text;
|
||||
@extend .hover\:bg-contrast;
|
||||
@extend .focus\:bg-contrast;
|
||||
@extend .focus\:outline-none;
|
||||
@extend .focus\:ring-info;
|
||||
}
|
||||
|
||||
.sn-icon {
|
||||
@extend .h-5;
|
||||
@extend .w-5;
|
||||
@extend .fill-current;
|
||||
}
|
||||
|
||||
.sn-dropdown {
|
||||
@extend .absolute;
|
||||
@extend .bg-default;
|
||||
@extend .min-w-80;
|
||||
@extend .transition-transform;
|
||||
@extend .duration-150;
|
||||
@extend .grid;
|
||||
@extend .gap-2;
|
||||
@extend .slide-down-animation;
|
||||
@extend .rounded;
|
||||
@extend .box-shadow;
|
||||
@@ -129,3 +251,7 @@ $border-width: 2px;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.sn-component .sk-app-bar .sk-app-bar-item {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:root {
|
||||
--sn-stylekit-font-size-editor: 1.154rem;
|
||||
--sn-stylekit-font-size-editor: 0.9375rem;
|
||||
}
|
||||
|
||||
.sn-component {
|
||||
@@ -22,10 +22,10 @@
|
||||
|
||||
.sk-app-bar {
|
||||
&.dynamic-height {
|
||||
min-height: 2rem !important;
|
||||
min-height: 1.625rem !important;
|
||||
height: inherit !important;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.40625rem;
|
||||
padding-bottom: 0.40625rem;
|
||||
}
|
||||
|
||||
&.no-top-edge {
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.sk-horizontal-group.tight > *:not(:first-child) {
|
||||
margin-left: 0.3rem;
|
||||
margin-left: 0.24375rem;
|
||||
}
|
||||
|
||||
.sk-horizontal-group {
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
.sk-panel-section {
|
||||
&:last-child {
|
||||
padding-bottom: 1rem;
|
||||
padding-bottom: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,3 +109,7 @@ input:focus {
|
||||
box-shadow: 0 0 0 2px var(--sn-stylekit-background-color),
|
||||
0 0 0 4px var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
.sk-button:focus-visible, button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -115,26 +115,26 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: .25rem;
|
||||
margin-top: .203125rem;
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: .5rem;
|
||||
margin-top: .40625rem;
|
||||
}
|
||||
.mt-3 {
|
||||
margin-top: .75rem;
|
||||
margin-top: .609375rem;
|
||||
}
|
||||
.mt-5 {
|
||||
margin-top: 1.25rem;
|
||||
margin-top: 1.015625rem;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0rem;
|
||||
}
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
padding: 0.8125rem;
|
||||
}
|
||||
.p-5 {
|
||||
padding: 1.25rem;
|
||||
padding: 1.015625rem;
|
||||
}
|
||||
|
||||
.pt-0 {
|
||||
@@ -142,13 +142,13 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: .75rem;
|
||||
padding-right: .75rem;
|
||||
padding-left: .609375rem;
|
||||
padding-right: .609375rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
padding-top: .40625rem;
|
||||
padding-bottom: .40625rem;
|
||||
}
|
||||
|
||||
.border-0 {
|
||||
@@ -159,7 +159,7 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
border-radius: var(--sn-stylekit-general-border-radius);
|
||||
}
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
border-radius: 0.3046875rem;
|
||||
}
|
||||
|
||||
.bg-main {
|
||||
@@ -209,18 +209,9 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.grid-template-cols-1fr {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.col-span-all {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.grid-col-2 {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
|
||||
@@ -175,14 +175,14 @@
|
||||
| or revoking an active session, require additional authentication
|
||||
| like entering your account password or application passcode.
|
||||
.sk-panel-row(ng-if="self.state.protectionsDisabledUntil")
|
||||
button.sn-button.info(ng-click="self.enableProtections()")
|
||||
button.sn-button.small.info(ng-click="self.enableProtections()")
|
||||
| Enable protections
|
||||
.sk-panel-section
|
||||
.sk-panel-section-title Passcode Lock
|
||||
div(ng-if='!self.state.hasPasscode')
|
||||
div(ng-if='self.state.canAddPasscode')
|
||||
.sk-panel-row(ng-if='!self.state.formData.showPasscodeForm')
|
||||
button.sn-button.info(
|
||||
button.sn-button.small.info(
|
||||
ng-click='self.addPasscodeClicked(); $event.stopPropagation();'
|
||||
) Add Passcode
|
||||
p.sk-p
|
||||
@@ -212,8 +212,8 @@
|
||||
placeholder='Confirm Passcode',
|
||||
type='password'
|
||||
)
|
||||
button.sn-button.info.mt-2(type='submit') Set Passcode
|
||||
button.sn-button.outlined.ml-2(
|
||||
button.sn-button.small.info.mt-2(type='submit') Set Passcode
|
||||
button.sn-button.small.outlined.ml-2(
|
||||
ng-click='self.state.formData.showPasscodeForm = false'
|
||||
) Cancel
|
||||
div(ng-if='self.state.hasPasscode && !self.state.formData.showPasscodeForm')
|
||||
@@ -264,9 +264,9 @@
|
||||
p.sk-p Decrypted
|
||||
.sk-panel-row
|
||||
.flex
|
||||
button.sn-button.info(ng-click='self.downloadDataArchive()')
|
||||
button.sn-button.small.info(ng-click='self.downloadDataArchive()')
|
||||
| Download Backup
|
||||
label.sn-button.info.ml-2
|
||||
label.sn-button.small.flex.items-center.info.ml-2
|
||||
input(
|
||||
file-change='->',
|
||||
handler='self.importFileSelected(files)',
|
||||
@@ -294,7 +294,7 @@
|
||||
| local storage, and a new identifier will be created should you
|
||||
| decide to enable error reporting again in the future.
|
||||
.sk-panel-row
|
||||
button(ng-click="self.toggleErrorReportingEnabled()").sn-button.info
|
||||
button(ng-click="self.toggleErrorReportingEnabled()").sn-button.small.info
|
||||
| {{ self.state.errorReportingEnabled ? 'Disable' : 'Enable'}} Error Reporting
|
||||
.sk-panel-row
|
||||
a(ng-click="self.openErrorReportingDialog()").sk-a What data is being sent?
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.sk-label.warning There was an issue loading {{ctrl.component.name}}.
|
||||
.right
|
||||
.sk-app-bar-item(ng-click='ctrl.reloadIframe()')
|
||||
button.sn-button.info Reload
|
||||
button.sn-button.small.info Reload
|
||||
.sn-component(ng-if='ctrl.expired')
|
||||
.sk-app-bar.no-edges.no-top-edge.dynamic-height
|
||||
.left
|
||||
@@ -25,10 +25,10 @@
|
||||
| Extensions are in a read-only state.
|
||||
.right
|
||||
.sk-app-bar-item(ng-click='ctrl.reloadStatus(true)')
|
||||
button.sn-button.info Reload
|
||||
button.sn-button.small.info Reload
|
||||
.sk-app-bar-item
|
||||
.sk-app-bar-item-column
|
||||
a.sn-button.warning(
|
||||
a.sn-button.small.warning(
|
||||
href='https://standardnotes.org/help/41/expired',
|
||||
rel='noopener',
|
||||
target='_blank'
|
||||
@@ -54,7 +54,7 @@
|
||||
li.sk-p
|
||||
strong Use the Desktop application.
|
||||
.sk-panel-row
|
||||
button.sn-button.info(
|
||||
button.sn-button.small.info(
|
||||
ng-click='ctrl.reloadStatus()',
|
||||
ng-if='!ctrl.reloading'
|
||||
) Reload
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
| Please ensure you are running the latest version of Standard Notes
|
||||
| on all platforms to ensure maximum compatibility.
|
||||
.sk-panel-footer
|
||||
button.sn-button.info(
|
||||
button.sn-button.small.info(
|
||||
ng-click='ctrl.nextStep()',
|
||||
ng-disabled='ctrl.state.lockContinue'
|
||||
) {{ctrl.state.continueTitle}}
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
| perform a full account sync resolution.
|
||||
.sk-panel-row
|
||||
.flex.gap-2
|
||||
button.sn-button.info(ng-click='ctrl.downloadBackup(true)') Encrypted
|
||||
button.sn-button.info(ng-click='ctrl.downloadBackup(false)') Decrypted
|
||||
button.sn-button.danger(ng-click='ctrl.skipBackup()') Skip
|
||||
button.sn-button.small.info(ng-click='ctrl.downloadBackup(true)') Encrypted
|
||||
button.sn-button.small.info(ng-click='ctrl.downloadBackup(false)') Decrypted
|
||||
button.sn-button.small.danger(ng-click='ctrl.skipBackup()') Skip
|
||||
div(ng-if='ctrl.status.backupFinished')
|
||||
.sk-panel-row(ng-if='!ctrl.status.resolving && !ctrl.status.attemptedResolution')
|
||||
button.sn-button.info(ng-click='ctrl.performSyncResolution()')
|
||||
button.sn-button.small.info(ng-click='ctrl.performSyncResolution()')
|
||||
| Perform Sync Resolution
|
||||
.sk-panel-row.justify-left(ng-if='ctrl.status.resolving')
|
||||
.sk-horizontal-group
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"pug-loader": "^2.4.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"serve-static": "^1.14.1",
|
||||
"sn-stylekit": "^4.0.3",
|
||||
"sn-stylekit": "5.1.0",
|
||||
"ts-loader": "^8.0.17",
|
||||
"typescript": "^4.1.5",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@reach/checkbox": "^0.13.2",
|
||||
"@reach/dialog": "^0.13.0",
|
||||
"@standardnotes/sncrypto-web": "1.2.10",
|
||||
"@standardnotes/snjs": "2.1.1",
|
||||
"@standardnotes/snjs": "2.2.4",
|
||||
"mobx": "^6.1.6",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"preact": "^10.5.12"
|
||||
|
||||
16
yarn.lock
@@ -1936,10 +1936,10 @@
|
||||
"@standardnotes/sncrypto-common" "^1.2.7"
|
||||
libsodium-wrappers "^0.7.8"
|
||||
|
||||
"@standardnotes/snjs@2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.1.1.tgz#789bb492e76ee4fe5816ed0de01c9b774244034d"
|
||||
integrity sha512-GeOPZGX5K2YBdzIWVmS/z4wdQJLLz4Yo2lje8rjep2eLlUiQhRO5BVWaOroYq0uaIamtfSr8m+twbYUkCkPSIQ==
|
||||
"@standardnotes/snjs@2.2.4":
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.2.4.tgz#a096dbfd0da29dbdf55f5b8362bf261916a2d660"
|
||||
integrity sha512-VYxT+m7BVHFGgE8rfGsVy2ua/HZkhq1Bb4yH8wymLgNqcMDN//yuEZzBStOLJzRMQJVz5n79dfwHwdmdQET8AQ==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "^2.0.0"
|
||||
"@standardnotes/sncrypto-common" "^1.2.9"
|
||||
@@ -7815,10 +7815,10 @@ slice-ansi@^4.0.0:
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
sn-stylekit@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-4.0.3.tgz#f8b86a68286bf5237dfc035c43a31464625b010c"
|
||||
integrity sha512-cKsq3XndExpzJnP6qjd5khGbNhKlA724MLbxbMbgUv3aYQWZddHudzV84Lb5gx87mbUQJQc0lh0nPWZBpa2hUA==
|
||||
sn-stylekit@5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.1.0.tgz#97ce7323834ff7f3645ed4463beb3ad4e42f7e7e"
|
||||
integrity sha512-SjKJYGRnR1iCVtllqJyW9/gWV7V56MJrkXHEo5+C9Ch3syRRlG3k+AwC0vC8ms6PWxpHJUdCHLQROvFOBPQuCw==
|
||||
|
||||
snapdragon-node@^2.0.1:
|
||||
version "2.1.1"
|
||||
|
||||