feat: New account menu and text input with icon & toggle (#665)
* feat: Add new icons * Revert "feat: Add new icons" This reverts commit 0acb403fe846dbb2e48fd22de35c3568c3cb4453. * feat: Add new icons for account menu * feat: Add new Icons * feat: Add "currentPane" state to prefs view * feat: Update account menu to new design * feat: Add input component with icon & toggle * fix: sync icon & function * fix: Fix eye icon * feat: Create re-usable checkbox feat: Add "merge local" option * feat: Allow using className on IconButton * feat: Add disabled state on input feat: Make toggle circle * refactor: Move checkbox to components * feat: Handle invalid email/password error * feat: Implement new design for Create Account * feat: Implement new account menu design * feat: Add disabled option to IconButton * feat: Set account menu pane from other component * feat: Add 2fa account menu pane feat: Add lock icon * feat: Remove unnecessary 2FA menu pane feat: Reset current menu pane on clickOutside * feat: Change "Log in" to "Sign in" * feat: Remove sync from footer * feat: Change "Login" to "Sign in" feat: Add spinner to "Syncing..." refactor: Use then-catch-finally for sync * feat: Use common enableCustomServer state * feat: Animate account menu closing * fix: Reset menu pane only after it's closed * feat: Add keyDown handler to InputWithIcon * feat: Handle Enter press in inputs * Update app/assets/javascripts/components/InputWithIcon.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * Update app/assets/javascripts/components/InputWithIcon.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * refactor: Use server state from AccountMenuState * Update app/assets/javascripts/components/AccountMenu/CreateAccount.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * Update app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * feat: Use common AdvancedOptions * feat: Add "eye-off" icon and toggle state * feat: Allow undefined values * refactor: Remove enableCustomServer state * feat: Persist server option state * feat: Add bottom-100 and cursor-auto util classes refactor: Use bottom-100 and cursor-auto classes * refactor: Invert ternary operator * refactor: Remove unused imports * refactor: Use toggled as prop instead of state * refactor: Change "Log in/out" to "Sign in/out" * refactor: Change "Login" to "Sign in" * refactor: Remove hardcoded width/height * refactor: Use success class * feat: Remove hardcoded width & height from svg * fix: Fix chevron-down icon Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
@@ -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="M17.5 7.50008H12.5V18.3334H10.8333V13.3334H9.16667V18.3334H7.5V7.50008H2.5V5.83342H17.5V7.50008ZM10 1.66675C10.442 1.66675 10.866 1.84234 11.1785 2.1549C11.4911 2.46746 11.6667 2.89139 11.6667 3.33342C11.6667 3.77544 11.4911 4.19937 11.1785 4.51193C10.866 4.82449 10.442 5.00008 10 5.00008C9.075 5.00008 8.33333 4.25008 8.33333 3.33342C8.33333 2.40841 9.075 1.66675 10 1.66675Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 485 B |
4
app/assets/icons/ic-arrow-left.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6668 9.16663V10.8333L6.66677 10.8333L10.6584 14.825C10.9852 15.1517 10.9852 15.6815 10.6584 16.0083C10.3317 16.3351 9.80187 16.3351 9.4751 16.0083L3.46677 9.99996L9.4751 3.99163C9.80187 3.66486 10.3317 3.66486 10.6584 3.99163C10.9852 4.3184 10.9852 4.84819 10.6584 5.17496L6.66677 9.16663L16.6668 9.16663Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 407 B |
4
app/assets/icons/ic-check-circle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 1.66666C5.41669 1.66666 1.66669 5.41666 1.66669 9.99999C1.66669 14.5833 5.41669 18.3333 10 18.3333C14.5834 18.3333 18.3334 14.5833 18.3334 9.99999C18.3334 5.41666 14.5834 1.66666 10 1.66666ZM10 16.6667C6.32502 16.6667 3.33335 13.675 3.33335 9.99999C3.33335 6.32499 6.32502 3.33332 10 3.33332C13.675 3.33332 16.6667 6.32499 16.6667 9.99999C16.6667 13.675 13.675 16.6667 10 16.6667ZM13.825 6.31666L8.33335 11.8083L6.17502 9.65832L5.00002 10.8333L8.33335 14.1667L15 7.49999L13.825 6.31666Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 588 B |
4
app/assets/icons/ic-chevron-down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.17622 7.15015L10.0012 10.9751L13.8262 7.15015L15.0012 8.33348L10.0012 13.3335L5.00122 8.33348L6.17622 7.15015Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 225 B |
4
app/assets/icons/ic-cloud-off.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.44167 8.33334L13.1083 15H5C4.11594 15 3.2681 14.6488 2.64298 14.0237C2.01786 13.3986 1.66667 12.5507 1.66667 11.6667C1.66667 10.7826 2.01786 9.93478 2.64298 9.30965C3.2681 8.68453 4.11594 8.33334 5 8.33334H6.44167ZM2.5 4.39168L4.79167 6.66668C2.13333 6.79168 0 8.97501 0 11.6667C0 12.9928 0.526784 14.2645 1.46447 15.2022C2.40215 16.1399 3.67392 16.6667 5 16.6667H14.775L16.4417 18.3333L17.5 17.275L3.55833 3.33334L2.5 4.39168ZM16.125 8.35834C15.5583 5.49168 13.0333 3.33334 10 3.33334C8.75 3.33334 7.625 3.69168 6.66667 4.30834L7.875 5.52501C8.50833 5.19168 9.23333 5.00001 10 5.00001C11.2156 5.00001 12.3814 5.4829 13.2409 6.34244C14.1004 7.20198 14.5833 8.36777 14.5833 9.58334V10H15.8333C16.4964 10 17.1323 10.2634 17.6011 10.7322C18.0699 11.2011 18.3333 11.837 18.3333 12.5C18.3333 13.4417 17.8 14.2583 17.0333 14.6833L18.2417 15.8917C19.3 15.1333 20 13.9 20 12.5C20 10.3 18.2917 8.51668 16.125 8.35834Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1009 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="M1.66724 3.66626C1.66724 2.56169 2.56267 1.66626 3.66724 1.66626H11.3339C12.4385 1.66626 13.3339 2.56169 13.3339 3.66626V13.3329H3.66724C2.56267 13.3329 1.66724 12.4375 1.66724 11.3329V3.66626ZM16.3339 6.66626C17.4385 6.66626 18.3339 7.56169 18.3339 8.66626V16.3329C18.3339 17.4375 17.4385 18.3329 16.3339 18.3329H8.66724C7.56267 18.3329 6.66724 17.4375 6.66724 16.3329V14.9996H15.0006V6.66626H16.3339ZM3.3339 3.33293V11.6663H11.6672V3.33293H3.3339Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 557 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="M11 3.5V9.5H12.17L10 11.67L7.83 9.5H9V3.5H11ZM13 1.5H7V7.5H3L10 14.5L17 7.5H13V1.5ZM17 16.5H3V18.5H17V16.5Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 215 B |
4
app/assets/icons/ic-email.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33317 3.33331H16.6665C17.1085 3.33331 17.5325 3.50891 17.845 3.82147C18.1576 4.13403 18.3332 4.55795 18.3332 4.99998V15C18.3332 15.442 18.1576 15.8659 17.845 16.1785C17.5325 16.491 17.1085 16.6666 16.6665 16.6666H3.33317C2.40817 16.6666 1.6665 15.9166 1.6665 15V4.99998C1.6665 4.07498 2.40817 3.33331 3.33317 3.33331ZM9.99984 9.16665L16.6665 4.99998H3.33317L9.99984 9.16665ZM3.33317 15H16.6665V6.97498L9.99984 11.1333L3.33317 6.97498V15Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 538 B |
4
app/assets/icons/ic-eye-off.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66659 4.39159L2.73325 3.33325L16.6666 17.2666L15.6082 18.3333L13.0416 15.7666C12.0833 16.0833 11.0666 16.2499 9.99992 16.2499C5.83325 16.2499 2.27492 13.6583 0.833252 9.99992C1.40825 8.53325 2.32492 7.24159 3.49158 6.21659L1.66659 4.39159ZM9.99992 7.49992C10.663 7.49992 11.2988 7.76331 11.7677 8.23215C12.2365 8.70099 12.4999 9.33688 12.4999 9.99992C12.4999 10.2916 12.4499 10.5749 12.3582 10.8333L9.16658 7.64159C9.42492 7.54992 9.70825 7.49992 9.99992 7.49992ZM9.99992 3.74992C14.1666 3.74992 17.7249 6.34159 19.1666 9.99992C18.4832 11.7333 17.3249 13.2333 15.8332 14.3249L14.6499 13.1333C15.7832 12.3499 16.7166 11.2833 17.3499 9.99992C15.9749 7.19992 13.1333 5.41659 9.99992 5.41659C9.09158 5.41659 8.19992 5.56659 7.36658 5.83325L6.08325 4.55825C7.28325 4.04159 8.60825 3.74992 9.99992 3.74992ZM2.64992 9.99992C4.02492 12.7999 6.86658 14.5833 9.99992 14.5833C10.5749 14.5833 11.1416 14.5249 11.6666 14.4083L9.76658 12.4999C8.57492 12.3749 7.62492 11.4249 7.49992 10.2333L4.66658 7.39158C3.84158 8.09992 3.14992 8.98325 2.64992 9.99992Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
app/assets/icons/ic-eye.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99984 3C8.53027 3 9.03898 3.21071 9.41405 3.58579C9.78912 3.96086 9.99984 4.46957 9.99984 5C9.99984 5.53043 9.78912 6.03914 9.41405 6.41421C9.03898 6.78929 8.53027 7 7.99984 7C7.4694 7 6.9607 6.78929 6.58562 6.41421C6.21055 6.03914 5.99984 5.53043 5.99984 5C5.99984 4.46957 6.21055 3.96086 6.58562 3.58579C6.9607 3.21071 7.4694 3 7.99984 3ZM7.99984 0C11.3332 0 14.1798 2.07333 15.3332 5C14.1798 7.92667 11.3332 10 7.99984 10C4.6665 10 1.81984 7.92667 0.666504 5C1.81984 2.07333 4.6665 0 7.99984 0ZM2.11984 5C3.21984 7.24 5.49317 8.66667 7.99984 8.66667C10.5065 8.66667 12.7798 7.24 13.8798 5C12.7798 2.76 10.5065 1.33333 7.99984 1.33333C5.49317 1.33333 3.21984 2.76 2.11984 5Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 777 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="M9.16675 15.0001H10.8334V13.3334H9.16675V15.0001ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10753 4.10753C2.54472 5.67033 1.66675 7.78995 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10753 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 8.90573 18.1179 7.8221 17.6991 6.81105C17.2803 5.80001 16.6665 4.88135 15.8926 4.10753C15.1188 3.3337 14.2002 2.71987 13.1891 2.30109C12.1781 1.8823 11.0944 1.66675 10.0001 1.66675ZM10.0001 16.6668C6.32508 16.6668 3.33342 13.6751 3.33342 10.0001C3.33342 6.32508 6.32508 3.33342 10.0001 3.33342C13.6751 3.33342 16.6668 6.32508 16.6668 10.0001C16.6668 13.6751 13.6751 16.6668 10.0001 16.6668ZM10.0001 5.00008C9.11603 5.00008 8.26818 5.35127 7.64306 5.97639C7.01794 6.60151 6.66675 7.44936 6.66675 8.33342H8.33342C8.33342 7.89139 8.50901 7.46747 8.82157 7.1549C9.13413 6.84234 9.55806 6.66675 10.0001 6.66675C10.4421 6.66675 10.866 6.84234 11.1786 7.1549C11.4912 7.46747 11.6667 7.89139 11.6667 8.33342C11.6667 10.0001 9.16675 9.79175 9.16675 12.5001H10.8334C10.8334 10.6251 13.3334 10.4167 13.3334 8.33342C13.3334 7.44936 12.9822 6.60151 12.3571 5.97639C11.732 5.35127 10.8841 5.00008 10.0001 5.00008Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -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="M9.16675 7.50008H10.8334V5.83342H9.16675V7.50008ZM10.0001 16.6667C6.32508 16.6667 3.33341 13.6751 3.33341 10.0001C3.33341 6.32508 6.32508 3.33341 10.0001 3.33341C13.6751 3.33341 16.6667 6.32508 16.6667 10.0001C16.6667 13.6751 13.6751 16.6667 10.0001 16.6667ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10752 4.10752C2.54472 5.67033 1.66675 7.78994 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10752 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 8.90573 18.1179 7.8221 17.6991 6.81105C17.2803 5.80001 16.6665 4.88135 15.8926 4.10752C15.1188 3.3337 14.2002 2.71987 13.1891 2.30109C12.1781 1.8823 11.0944 1.66675 10.0001 1.66675ZM9.16675 14.1667H10.8334V9.16675H9.16675V14.1667Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1001 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="M3.33341 4.16675C2.89139 4.16675 2.46746 4.34234 2.1549 4.6549C1.84234 4.96746 1.66675 5.39139 1.66675 5.83341V14.1667C1.66675 14.6088 1.84234 15.0327 2.1549 15.3453C2.46746 15.6578 2.89139 15.8334 3.33341 15.8334H16.6667C17.1088 15.8334 17.5327 15.6578 17.8453 15.3453C18.1578 15.0327 18.3334 14.6088 18.3334 14.1667V5.83341C18.3334 5.39139 18.1578 4.96746 17.8453 4.6549C17.5327 4.34234 17.1088 4.16675 16.6667 4.16675H3.33341ZM3.33341 5.83341H16.6667V14.1667H3.33341V5.83341ZM4.16675 6.66675V8.33342H5.83341V6.66675H4.16675ZM6.66675 6.66675V8.33342H8.33341V6.66675H6.66675ZM9.16675 6.66675V8.33342H10.8334V6.66675H9.16675ZM11.6667 6.66675V8.33342H13.3334V6.66675H11.6667ZM14.1667 6.66675V8.33342H15.8334V6.66675H14.1667ZM4.16675 9.16675V10.8334H5.83341V9.16675H4.16675ZM6.66675 9.16675V10.8334H8.33341V9.16675H6.66675ZM9.16675 9.16675V10.8334H10.8334V9.16675H9.16675ZM11.6667 9.16675V10.8334H13.3334V9.16675H11.6667ZM14.1667 9.16675V10.8334H15.8334V9.16675H14.1667ZM6.66675 11.6667V13.3334H13.3334V11.6667H6.66675Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -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 fill-rule="evenodd" clip-rule="evenodd" d="M4.28571 3.6H15.7143C16.093 3.6 16.4 3.907 16.4 4.28571V15.7143C16.4 16.093 16.093 16.4 15.7143 16.4H4.28571C3.907 16.4 3.6 16.093 3.6 15.7143V4.28571C3.6 3.907 3.907 3.6 4.28571 3.6ZM2 4.28571C2 3.02335 3.02335 2 4.28571 2H15.7143C16.9767 2 18 3.02335 18 4.28571V15.7143C18 16.9767 16.9767 18 15.7143 18H4.28571C3.02335 18 2 16.9767 2 15.7143V4.28571ZM9.8045 6.31638C9.94745 6.27119 10.1703 6.24105 10.473 6.22599V6C10.0105 6.0226 9.35045 6.0339 8.49279 6.0339C7.59309 6.0339 6.92883 6.0226 6.5 6V6.22599C6.77748 6.24105 6.97928 6.27119 7.10541 6.31638C7.23994 6.36158 7.32823 6.44821 7.37027 6.57627C7.42072 6.70433 7.44595 6.91149 7.44595 7.19774V12.8023C7.44595 13.0885 7.42072 13.2957 7.37027 13.4237C7.32823 13.5518 7.23994 13.6384 7.10541 13.6836C6.97928 13.7288 6.77748 13.7589 6.5 13.774V14C7.40811 13.9774 8.77868 13.9661 10.6117 13.9661C11.9655 13.9661 12.9282 13.9774 13.5 14C13.4411 13.533 13.4117 12.9379 13.4117 12.2147C13.4117 11.8079 13.4243 11.4765 13.4495 11.2203H13.1595C13.0333 11.9812 12.7601 12.5913 12.3396 13.0508C11.9276 13.5104 11.4357 13.7401 10.864 13.7401H10.2459C10.0105 13.7401 9.83814 13.7213 9.72883 13.6836C9.62793 13.646 9.55646 13.5744 9.51441 13.4689C9.47237 13.3559 9.45135 13.1789 9.45135 12.9379V7.19774C9.45135 6.91149 9.47658 6.70433 9.52703 6.57627C9.57748 6.44821 9.66997 6.36158 9.8045 6.31638Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
4
app/assets/icons/ic-lock.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0002 14.1666C9.07516 14.1666 8.3335 13.4166 8.3335 12.5C8.3335 11.575 9.07516 10.8333 10.0002 10.8333C10.4422 10.8333 10.8661 11.0089 11.1787 11.3215C11.4912 11.634 11.6668 12.058 11.6668 12.5C11.6668 12.942 11.4912 13.3659 11.1787 13.6785C10.8661 13.9911 10.4422 14.1666 10.0002 14.1666ZM15.0002 16.6666V8.33331H5.00016V16.6666H15.0002ZM15.0002 6.66665C15.4422 6.66665 15.8661 6.84224 16.1787 7.1548C16.4912 7.46736 16.6668 7.89129 16.6668 8.33331V16.6666C16.6668 17.1087 16.4912 17.5326 16.1787 17.8452C15.8661 18.1577 15.4422 18.3333 15.0002 18.3333H5.00016C4.07516 18.3333 3.3335 17.5833 3.3335 16.6666V8.33331C3.3335 7.40831 4.07516 6.66665 5.00016 6.66665H5.8335V4.99998C5.8335 3.89491 6.27248 2.8351 7.05388 2.0537C7.83529 1.2723 8.89509 0.833313 10.0002 0.833313C10.5473 0.833313 11.0892 0.941087 11.5947 1.15048C12.1002 1.35988 12.5595 1.66679 12.9464 2.0537C13.3333 2.44061 13.6403 2.89994 13.8497 3.40547C14.0591 3.91099 14.1668 4.45281 14.1668 4.99998V6.66665H15.0002ZM10.0002 2.49998C9.33712 2.49998 8.70124 2.76337 8.23239 3.23221C7.76355 3.70105 7.50016 4.33694 7.50016 4.99998V6.66665H12.5002V4.99998C12.5002 4.33694 12.2368 3.70105 11.7679 3.23221C11.2991 2.76337 10.6632 2.49998 10.0002 2.49998Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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="M10 10H15.4444C15.0322 12.9891 12.8933 15.6582 10 16.4873V10H4.55556V5.85455L10 3.59273V10ZM10 2L3 4.90909V9.27273C3 13.3091 5.98667 17.0764 10 18C14.0133 17.0764 17 13.3091 17 9.27273V4.90909L10 2Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 329 B After Width: | Height: | Size: 306 B |
4
app/assets/icons/ic-server.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.8333 15.8333H11.6666C11.8876 15.8333 12.0996 15.9211 12.2559 16.0774C12.4122 16.2337 12.5 16.4457 12.5 16.6667H18.3333V18.3333H12.5C12.5 18.5543 12.4122 18.7663 12.2559 18.9226C12.0996 19.0789 11.8876 19.1667 11.6666 19.1667H8.33329C8.11228 19.1667 7.90032 19.0789 7.74404 18.9226C7.58776 18.7663 7.49996 18.5543 7.49996 18.3333H1.66663V16.6667H7.49996C7.49996 16.4457 7.58776 16.2337 7.74404 16.0774C7.90032 15.9211 8.11228 15.8333 8.33329 15.8333H9.16663V14.1667H3.33329C3.11228 14.1667 2.90032 14.0789 2.74404 13.9226C2.58776 13.7663 2.49996 13.5543 2.49996 13.3333V10C2.49996 9.77899 2.58776 9.56702 2.74404 9.41074C2.90032 9.25446 3.11228 9.16667 3.33329 9.16667H16.6666C16.8876 9.16667 17.0996 9.25446 17.2559 9.41074C17.4122 9.56702 17.5 9.77899 17.5 10V13.3333C17.5 13.5543 17.4122 13.7663 17.2559 13.9226C17.0996 14.0789 16.8876 14.1667 16.6666 14.1667H10.8333V15.8333ZM3.33329 2.5H16.6666C16.8876 2.5 17.0996 2.5878 17.2559 2.74408C17.4122 2.90036 17.5 3.11232 17.5 3.33333V6.66667C17.5 6.88768 17.4122 7.09964 17.2559 7.25592C17.0996 7.4122 16.8876 7.5 16.6666 7.5H3.33329C3.11228 7.5 2.90032 7.4122 2.74404 7.25592C2.58776 7.09964 2.49996 6.88768 2.49996 6.66667V3.33333C2.49996 3.11232 2.58776 2.90036 2.74404 2.74408C2.90032 2.5878 3.11228 2.5 3.33329 2.5ZM7.49996 5.83333H8.33329V4.16667H7.49996V5.83333ZM7.49996 12.5H8.33329V10.8333H7.49996V12.5ZM4.16663 4.16667V5.83333H5.83329V4.16667H4.16663ZM4.16663 10.8333V12.5H5.83329V10.8333H4.16663Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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="M10.0001 6.66675C10.8842 6.66675 11.732 7.01794 12.3571 7.64306C12.9823 8.26818 13.3334 9.11603 13.3334 10.0001C13.3334 10.8841 12.9823 11.732 12.3571 12.3571C11.732 12.9822 10.8842 13.3334 10.0001 13.3334C9.11606 13.3334 8.26821 12.9822 7.64309 12.3571C7.01797 11.732 6.66678 10.8841 6.66678 10.0001C6.66678 9.11603 7.01797 8.26818 7.64309 7.64306C8.26821 7.01794 9.11606 6.66675 10.0001 6.66675ZM10.0001 8.33342C9.55808 8.33342 9.13416 8.50901 8.8216 8.82157C8.50904 9.13413 8.33344 9.55805 8.33344 10.0001C8.33344 10.4421 8.50904 10.866 8.8216 11.1786C9.13416 11.4912 9.55808 11.6667 10.0001 11.6667C10.4421 11.6667 10.8661 11.4912 11.1786 11.1786C11.4912 10.866 11.6668 10.4421 11.6668 10.0001C11.6668 9.55805 11.4912 9.13413 11.1786 8.82157C10.8661 8.50901 10.4421 8.33342 10.0001 8.33342ZM8.33344 18.3334C8.12511 18.3334 7.95011 18.1834 7.91678 17.9834L7.60844 15.7751C7.08344 15.5667 6.63344 15.2834 6.20011 14.9501L4.12511 15.7917C3.94178 15.8584 3.71678 15.7917 3.61678 15.6084L1.95011 12.7251C1.84178 12.5417 1.89178 12.3167 2.05011 12.1917L3.80844 10.8084L3.75011 10.0001L3.80844 9.16675L2.05011 7.80841C1.89178 7.68341 1.84178 7.45841 1.95011 7.27508L3.61678 4.39175C3.71678 4.20841 3.94178 4.13341 4.12511 4.20842L6.20011 5.04175C6.63344 4.71675 7.08344 4.43341 7.60844 4.22508L7.91678 2.01675C7.95011 1.81675 8.12511 1.66675 8.33344 1.66675H11.6668C11.8751 1.66675 12.0501 1.81675 12.0834 2.01675L12.3918 4.22508C12.9168 4.43341 13.3668 4.71675 13.8001 5.04175L15.8751 4.20842C16.0584 4.13341 16.2834 4.20841 16.3834 4.39175L18.0501 7.27508C18.1584 7.45841 18.1084 7.68341 17.9501 7.80841L16.1918 9.16675L16.2501 10.0001L16.1918 10.8334L17.9501 12.1917C18.1084 12.3167 18.1584 12.5417 18.0501 12.7251L16.3834 15.6084C16.2834 15.7917 16.0584 15.8667 15.8751 15.7917L13.8001 14.9584C13.3668 15.2834 12.9168 15.5667 12.3918 15.7751L12.0834 17.9834C12.0501 18.1834 11.8751 18.3334 11.6668 18.3334H8.33344ZM9.37511 3.33341L9.06678 5.50841C8.06678 5.71675 7.18344 6.25008 6.54178 6.99175L4.53344 6.12508L3.90844 7.20841L5.66678 8.50008C5.33344 9.47508 5.33344 10.5334 5.66678 11.5001L3.90011 12.8001L4.52511 13.8834L6.55011 13.0167C7.19178 13.7501 8.06678 14.2834 9.05844 14.4834L9.36678 16.6667H10.6334L10.9418 14.4917C11.9334 14.2834 12.8084 13.7501 13.4501 13.0167L15.4751 13.8834L16.1001 12.8001L14.3334 11.5084C14.6668 10.5334 14.6668 9.47508 14.3334 8.50008L16.0918 7.20841L15.4668 6.12508L13.4584 6.99175C12.8168 6.25008 11.9334 5.71675 10.9334 5.51675L10.6251 3.33341H9.37511Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
4
app/assets/icons/ic-signin.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.8333 2.5H4.16667C3.24167 2.5 2.5 3.24167 2.5 4.16667V7.5H4.16667V4.16667H15.8333V15.8333H4.16667V12.5H2.5V15.8333C2.5 16.2754 2.67559 16.6993 2.98816 17.0118C3.30072 17.3244 3.72464 17.5 4.16667 17.5H15.8333C16.2754 17.5 16.6993 17.3244 17.0118 17.0118C17.3244 16.6993 17.5 16.2754 17.5 15.8333V4.16667C17.5 3.24167 16.75 2.5 15.8333 2.5ZM8.4 12.9833L9.58333 14.1667L13.75 10L9.58333 5.83333L8.4 7.00833L10.5583 9.16667H2.5V10.8333H10.5583L8.4 12.9833Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 554 B |
4
app/assets/icons/ic-signout.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.7333 12.9917L13.8917 10.8333H5.83333V9.16667H13.8917L11.7333 7.00833L12.9167 5.83333L17.0833 10L12.9167 14.1667L11.7333 12.9917ZM15.8333 2.5C16.2754 2.5 16.6993 2.67559 17.0118 2.98816C17.3244 3.30072 17.5 3.72464 17.5 4.16667V8.05833L15.8333 6.39167V4.16667H4.16667V15.8333H15.8333V13.6083L17.5 11.9417V15.8333C17.5 16.2754 17.3244 16.6993 17.0118 17.0118C16.6993 17.3244 16.2754 17.5 15.8333 17.5H4.16667C3.24167 17.5 2.5 16.75 2.5 15.8333V4.16667C2.5 3.24167 3.24167 2.5 4.16667 2.5H15.8333Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 596 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="M10.1035 12.8875C10.0399 12.8491 9.96028 12.8491 9.89672 12.8875L7.27153 14.4724C7.12022 14.5637 6.93347 14.4283 6.9733 14.2561L7.66462 11.2674C7.68131 11.1952 7.65676 11.1197 7.60082 11.0712L5.283 9.06056C5.14938 8.94465 5.22096 8.72509 5.39722 8.7102L8.45479 8.45191C8.52877 8.44566 8.5932 8.39895 8.62214 8.33058L9.8159 5.51022C9.88478 5.3475 10.1154 5.3475 10.1843 5.51022L11.378 8.33058C11.407 8.39895 11.4714 8.44566 11.5454 8.45191L14.6029 8.7102C14.7792 8.72509 14.8508 8.94465 14.7172 9.06057L12.3993 11.0712C12.3434 11.1197 12.3189 11.1952 12.3355 11.2674L13.0269 14.2561C13.0667 14.4283 12.8799 14.5637 12.7286 14.4724L10.1035 12.8875ZM17.9751 8.01046C18.1089 7.89462 18.0374 7.67496 17.8611 7.66001L12.4619 7.20194C12.388 7.19567 12.3236 7.149 12.2947 7.08071L10.1842 2.10122C10.1153 1.93862 9.88486 1.93862 9.81594 2.10122L7.70548 7.08071C7.67653 7.149 7.61216 7.19567 7.53824 7.20194L2.13848 7.66006C1.96228 7.67501 1.89074 7.89448 2.02429 8.01039L6.11748 11.5628C6.17343 11.6114 6.19795 11.6869 6.18122 11.759L4.95694 17.0392C4.91701 17.2114 5.10377 17.347 5.25512 17.2556L9.89674 14.4541C9.96029 14.4158 10.0399 14.4158 10.1034 14.4541L14.7443 17.2552C14.8957 17.3466 15.0826 17.2108 15.0424 17.0385L13.8108 11.7593C13.794 11.687 13.8185 11.6113 13.8747 11.5627L17.9751 8.01046Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
4
app/assets/icons/ic-sync.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99992 15C8.67384 15 7.40207 14.4732 6.46438 13.5355C5.5267 12.5978 4.99992 11.3261 4.99992 9.99998C4.99992 9.16665 5.20825 8.35831 5.58325 7.66665L4.36659 6.44998C3.71659 7.47498 3.33325 8.69165 3.33325 9.99998C3.33325 11.7681 4.03563 13.4638 5.28587 14.714C6.53612 15.9643 8.23181 16.6666 9.99992 16.6666V19.1666L13.3333 15.8333L9.99992 12.5V15ZM9.99992 3.33331V0.833313L6.66658 4.16665L9.99992 7.49998V4.99998C11.326 4.99998 12.5978 5.52676 13.5355 6.46445C14.4731 7.40213 14.9999 8.6739 14.9999 9.99998C14.9999 10.8333 14.7916 11.6416 14.4166 12.3333L15.6333 13.55C16.2833 12.525 16.6666 11.3083 16.6666 9.99998C16.6666 8.23187 15.9642 6.53618 14.714 5.28593C13.4637 4.03569 11.768 3.33331 9.99992 3.33331Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 810 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="M10 18C8.94943 18 7.90914 17.7931 6.93853 17.391C5.96793 16.989 5.08601 16.3997 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C14.4 2 18 5.2 18 9.2C18 10.473 17.4943 11.6939 16.5941 12.5941C15.6939 13.4943 14.473 14 13.2 14H11.76C11.52 14 11.36 14.16 11.36 14.4C11.36 14.48 11.44 14.56 11.44 14.64C11.76 15.04 11.92 15.52 11.92 16C12 17.12 11.12 18 10 18ZM10 3.6C8.30261 3.6 6.67475 4.27428 5.47452 5.47452C4.27428 6.67475 3.6 8.30261 3.6 10C3.6 11.6974 4.27428 13.3253 5.47452 14.5255C6.67475 15.7257 8.30261 16.4 10 16.4C10.24 16.4 10.4 16.24 10.4 16C10.4 15.84 10.32 15.76 10.32 15.68C10 15.28 9.84 14.88 9.84 14.4C9.84 13.28 10.72 12.4 11.84 12.4H13.2C14.0487 12.4 14.8626 12.0629 15.4627 11.4627C16.0629 10.8626 16.4 10.0487 16.4 9.2C16.4 6.08 13.52 3.6 10 3.6ZM5.6 8.4C6.24 8.4 6.8 8.96 6.8 9.6C6.8 10.24 6.24 10.8 5.6 10.8C4.96 10.8 4.4 10.24 4.4 9.6C4.4 8.96 4.96 8.4 5.6 8.4ZM8 5.2C8.64 5.2 9.2 5.76 9.2 6.4C9.2 7.04 8.64 7.6 8 7.6C7.36 7.6 6.8 7.04 6.8 6.4C6.8 5.76 7.36 5.2 8 5.2ZM12 5.2C12.64 5.2 13.2 5.76 13.2 6.4C13.2 7.04 12.64 7.6 12 7.6C11.36 7.6 10.8 7.04 10.8 6.4C10.8 5.76 11.36 5.2 12 5.2ZM14.4 8.4C15.04 8.4 15.6 8.96 15.6 9.6C15.6 10.24 15.04 10.8 14.4 10.8C13.76 10.8 13.2 10.24 13.2 9.6C13.2 8.96 13.76 8.4 14.4 8.4Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -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="M10 3C10.9283 3 11.8185 3.36875 12.4749 4.02513C13.1313 4.6815 13.5 5.57174 13.5 6.5C13.5 7.42826 13.1313 8.3185 12.4749 8.97487C11.8185 9.63125 10.9283 10 10 10C9.07174 10 8.1815 9.63125 7.52513 8.97487C6.86875 8.3185 6.5 7.42826 6.5 6.5C6.5 5.57174 6.86875 4.6815 7.52513 4.02513C8.1815 3.36875 9.07174 3 10 3ZM10 4.75C9.53587 4.75 9.09075 4.93437 8.76256 5.26256C8.43437 5.59075 8.25 6.03587 8.25 6.5C8.25 6.96413 8.43437 7.40925 8.76256 7.73744C9.09075 8.06563 9.53587 8.25 10 8.25C10.4641 8.25 10.9092 8.06563 11.2374 7.73744C11.5656 7.40925 11.75 6.96413 11.75 6.5C11.75 6.03587 11.5656 5.59075 11.2374 5.26256C10.9092 4.93437 10.4641 4.75 10 4.75ZM10 10.875C12.3363 10.875 17 12.0387 17 14.375V17H3V14.375C3 12.0387 7.66375 10.875 10 10.875ZM10 12.5375C7.40125 12.5375 4.6625 13.815 4.6625 14.375V15.3375H15.3375V14.375C15.3375 13.815 12.5988 12.5375 10 12.5375Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1000 B After Width: | Height: | Size: 977 B |
@@ -0,0 +1,73 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { Icon } from '../Icon';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AdvancedOptions: FunctionComponent<Props> = observer(
|
||||
({ appState, application, disabled = false, children }) => {
|
||||
const { server, setServer, enableServerOption, setEnableServerOption } =
|
||||
appState.accountMenu;
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const handleServerOptionChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEnableServerOption(e.target.checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncServerChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setServer(e.target.value);
|
||||
application.setCustomHost(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleShowAdvanced = () => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item font-bold"
|
||||
onClick={toggleShowAdvanced}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Advanced options
|
||||
<Icon type="chevron-down" className="color-grey-1 ml-1" />
|
||||
</div>
|
||||
</button>
|
||||
{showAdvanced ? (
|
||||
<div className="px-3 my-2">
|
||||
{children}
|
||||
<Checkbox
|
||||
name="custom-sync-server"
|
||||
label="Custom sync server"
|
||||
checked={enableServerOption}
|
||||
onChange={handleServerOptionChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<InputWithIcon
|
||||
inputType="text"
|
||||
icon="server"
|
||||
placeholder="https://api.standardnotes.com"
|
||||
value={server}
|
||||
onChange={handleSyncServerChange}
|
||||
disabled={!enableServerOption && !disabled}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
STRING_GENERATING_LOGIN_KEYS,
|
||||
STRING_GENERATING_REGISTER_KEYS,
|
||||
STRING_NON_MATCHING_PASSWORDS
|
||||
STRING_NON_MATCHING_PASSWORDS,
|
||||
} from '@/strings';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import TargetedEvent = JSXInternal.TargetedEvent;
|
||||
@@ -17,13 +17,9 @@ import { AppState } from '@/ui_models/app_state';
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
const Authentication = observer(({
|
||||
application,
|
||||
appState,
|
||||
}: Props) => {
|
||||
};
|
||||
|
||||
const Authentication = observer(({ application, appState }: Props) => {
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -39,12 +35,12 @@ const Authentication = observer(({
|
||||
const {
|
||||
server,
|
||||
notesAndTagsCount,
|
||||
showLogin,
|
||||
showSignIn,
|
||||
showRegister,
|
||||
setShowLogin,
|
||||
setShowSignIn,
|
||||
setShowRegister,
|
||||
setServer,
|
||||
closeAccountMenu
|
||||
closeAccountMenu,
|
||||
} = appState.accountMenu;
|
||||
|
||||
const user = application.getUser();
|
||||
@@ -58,11 +54,11 @@ const Authentication = observer(({
|
||||
|
||||
// Reset password and confirmation fields when hiding the form
|
||||
useEffect(() => {
|
||||
if (!showLogin && !showRegister) {
|
||||
if (!showSignIn && !showRegister) {
|
||||
setPassword('');
|
||||
setPasswordConfirmation('');
|
||||
}
|
||||
}, [showLogin, showRegister]);
|
||||
}, [showSignIn, showRegister]);
|
||||
|
||||
const handleHostInputChange = (event: TargetedEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
@@ -75,7 +71,7 @@ const Authentication = observer(({
|
||||
const passwordConfirmationInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const handleSignInClick = () => {
|
||||
setShowLogin(true);
|
||||
setShowSignIn(true);
|
||||
setIsEmailFocused(true);
|
||||
};
|
||||
|
||||
@@ -90,7 +86,7 @@ const Authentication = observer(({
|
||||
passwordConfirmationInputRef.current?.blur();
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
const signin = async () => {
|
||||
setStatus(STRING_GENERATING_LOGIN_KEYS);
|
||||
setIsAuthenticating(true);
|
||||
|
||||
@@ -105,13 +101,13 @@ const Authentication = observer(({
|
||||
if (!error) {
|
||||
setIsAuthenticating(false);
|
||||
setPassword('');
|
||||
setShowLogin(false);
|
||||
setShowSignIn(false);
|
||||
|
||||
closeAccountMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
setShowLogin(true);
|
||||
setShowSignIn(true);
|
||||
setStatus(undefined);
|
||||
setPassword('');
|
||||
|
||||
@@ -150,10 +146,11 @@ const Authentication = observer(({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthFormSubmit = (event:
|
||||
TargetedEvent<HTMLFormElement> |
|
||||
TargetedMouseEvent<HTMLButtonElement> |
|
||||
TargetedKeyboardEvent<HTMLButtonElement>
|
||||
const handleAuthFormSubmit = (
|
||||
event:
|
||||
| TargetedEvent<HTMLFormElement>
|
||||
| TargetedMouseEvent<HTMLButtonElement>
|
||||
| TargetedKeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -163,8 +160,8 @@ const Authentication = observer(({
|
||||
|
||||
blurAuthFields();
|
||||
|
||||
if (showLogin) {
|
||||
login();
|
||||
if (showSignIn) {
|
||||
signin();
|
||||
} else {
|
||||
register();
|
||||
}
|
||||
@@ -186,19 +183,23 @@ const Authentication = observer(({
|
||||
setEmail(value);
|
||||
};
|
||||
|
||||
const handlePasswordConfirmationChange = (event: TargetedEvent<HTMLInputElement>) => {
|
||||
const handlePasswordConfirmationChange = (
|
||||
event: TargetedEvent<HTMLInputElement>
|
||||
) => {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
setPasswordConfirmation(value);
|
||||
};
|
||||
|
||||
const handleMergeLocalData = async (event: TargetedEvent<HTMLInputElement>) => {
|
||||
const handleMergeLocalData = async (
|
||||
event: TargetedEvent<HTMLInputElement>
|
||||
) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
setShouldMergeLocal(checked);
|
||||
if (!checked) {
|
||||
const confirmResult = await confirmDialog({
|
||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
confirmButtonStyle: 'danger'
|
||||
confirmButtonStyle: 'danger',
|
||||
});
|
||||
setShouldMergeLocal(!confirmResult);
|
||||
}
|
||||
@@ -206,10 +207,12 @@ const Authentication = observer(({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!user && !showLogin && !showRegister && (
|
||||
{!user && !showSignIn && !showRegister && (
|
||||
<div className="sk-panel-section sk-panel-hero">
|
||||
<div className="sk-panel-row">
|
||||
<div className="sk-h1">Sign in or register to enable sync and end-to-end encryption.</div>
|
||||
<div className="sk-h1">
|
||||
Sign in or register to enable sync and end-to-end encryption.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex my-1">
|
||||
<button
|
||||
@@ -226,17 +229,21 @@ const Authentication = observer(({
|
||||
</button>
|
||||
</div>
|
||||
<div className="sk-panel-row sk-p">
|
||||
Standard Notes is free on every platform, and comes
|
||||
standard with sync and encryption.
|
||||
Standard Notes is free on every platform, and comes standard with
|
||||
sync and encryption.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(showLogin || showRegister) && (
|
||||
{(showSignIn || showRegister) && (
|
||||
<div className="sk-panel-section">
|
||||
<div className="sk-panel-section-title">
|
||||
{showLogin ? 'Sign In' : 'Register'}
|
||||
{showSignIn ? 'Sign In' : 'Register'}
|
||||
</div>
|
||||
<form className="sk-panel-form" onSubmit={handleAuthFormSubmit} noValidate>
|
||||
<form
|
||||
className="sk-panel-form"
|
||||
onSubmit={handleAuthFormSubmit}
|
||||
noValidate
|
||||
>
|
||||
<div className="sk-panel-section">
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
@@ -261,26 +268,28 @@ const Authentication = observer(({
|
||||
onKeyDown={handleKeyPressKeyDown}
|
||||
ref={passwordInputRef}
|
||||
/>
|
||||
{showRegister &&
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
name="password_conf"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
onKeyPress={handleKeyPressKeyDown}
|
||||
onKeyDown={handleKeyPressKeyDown}
|
||||
value={passwordConfirmation}
|
||||
onChange={handlePasswordConfirmationChange}
|
||||
ref={passwordConfirmationInputRef}
|
||||
/>}
|
||||
{showRegister && (
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
name="password_conf"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
onKeyPress={handleKeyPressKeyDown}
|
||||
onKeyDown={handleKeyPressKeyDown}
|
||||
value={passwordConfirmation}
|
||||
onChange={handlePasswordConfirmationChange}
|
||||
ref={passwordConfirmationInputRef}
|
||||
/>
|
||||
)}
|
||||
<div className="sk-panel-row" />
|
||||
<button
|
||||
type="button"
|
||||
className="sk-a info font-bold text-left p-0 cursor-pointer hover:underline mr-1 ml-1"
|
||||
onClick={() => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Advanced Options
|
||||
</button>
|
||||
</div>
|
||||
@@ -301,24 +310,28 @@ const Authentication = observer(({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{showLogin && (
|
||||
{showSignIn && (
|
||||
<label className="sk-label padded-row sk-panel-row justify-left">
|
||||
<div className="sk-horizontal-group tight cursor-pointer">
|
||||
<input
|
||||
className="sk-input"
|
||||
type="checkbox"
|
||||
checked={isStrictSignIn}
|
||||
onChange={() => setIsStrictSignIn(prevState => !prevState)}
|
||||
onChange={() =>
|
||||
setIsStrictSignIn((prevState) => !prevState)
|
||||
}
|
||||
/>
|
||||
<p className="sk-p">Use strict sign in</p>
|
||||
<span>
|
||||
<a className="info"
|
||||
href="https://standardnotes.com/help/security" rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
(Learn more)
|
||||
</a>
|
||||
</span>
|
||||
<a
|
||||
className="info"
|
||||
href="https://standardnotes.com/help/security"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
(Learn more)
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
@@ -327,9 +340,12 @@ const Authentication = observer(({
|
||||
)}
|
||||
{!isAuthenticating && (
|
||||
<div className="sk-panel-section form-submit">
|
||||
<button className="sn-button info text-base py-3 text-center" type="submit"
|
||||
disabled={isAuthenticating}>
|
||||
{showLogin ? 'Sign In' : 'Register'}
|
||||
<button
|
||||
className="sn-button info text-base py-3 text-center"
|
||||
type="submit"
|
||||
disabled={isAuthenticating}
|
||||
>
|
||||
{showSignIn ? 'Sign In' : 'Register'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -337,9 +353,9 @@ const Authentication = observer(({
|
||||
<div className="sk-notification neutral">
|
||||
<div className="sk-notification-title">No Password Reset.</div>
|
||||
<div className="sk-notification-text">
|
||||
Because your notes are encrypted using your password,
|
||||
Standard Notes does not have a password reset option.
|
||||
You cannot forget your password.
|
||||
Because your notes are encrypted using your password, Standard
|
||||
Notes does not have a password reset option. You cannot forget
|
||||
your password.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -358,7 +374,7 @@ const Authentication = observer(({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!isEphemeral}
|
||||
onChange={() => setIsEphemeral(prevState => !prevState)}
|
||||
onChange={() => setIsEphemeral((prevState) => !prevState)}
|
||||
/>
|
||||
<p className="sk-p">Stay signed in</p>
|
||||
</div>
|
||||
@@ -371,7 +387,9 @@ const Authentication = observer(({
|
||||
checked={shouldMergeLocal}
|
||||
onChange={handleMergeLocalData}
|
||||
/>
|
||||
<p className="sk-p">Merge local data ({notesAndTagsCount}) notes and tags</p>
|
||||
<p className="sk-p">
|
||||
Merge local data ({notesAndTagsCount}) notes and tags
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
@@ -379,7 +397,8 @@ const Authentication = observer(({
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { STRING_NON_MATCHING_PASSWORDS } from '@/strings';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { StateUpdater, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
email: string;
|
||||
password: string;
|
||||
setPassword: StateUpdater<string>;
|
||||
};
|
||||
|
||||
export const ConfirmPassword: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane, email, password, setPassword }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu;
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
const [isEphemeral, setIsEphemeral] = useState(false);
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setConfirmPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEphemeralChange = () => {
|
||||
setIsEphemeral(!isEphemeral);
|
||||
};
|
||||
|
||||
const handleShouldMergeChange = () => {
|
||||
setShouldMergeLocal(!shouldMergeLocal);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleConfirmFormSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmFormSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!password) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password === confirmPassword) {
|
||||
setIsRegistering(true);
|
||||
application
|
||||
.register(email, password, isEphemeral, shouldMergeLocal)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
application.alertService.alert(err).finally(() => {
|
||||
setPassword('');
|
||||
handleGoBack();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRegistering(false);
|
||||
});
|
||||
} else {
|
||||
application.alertService
|
||||
.alert(STRING_NON_MATCHING_PASSWORDS)
|
||||
.finally(() => {
|
||||
setConfirmPassword('');
|
||||
passwordInputRef?.current.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
setMenuPane(AccountMenuPane.Register);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={handleGoBack}
|
||||
focusable={true}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Confirm password</div>
|
||||
</div>
|
||||
<div className="px-3 mb-3 text-sm">
|
||||
Because your notes are encrypted using your password,{' '}
|
||||
<span className="color-dark-red">
|
||||
Standard Notes does not have a password reset option
|
||||
</span>
|
||||
. If you forget your password, you will permanently lose access to
|
||||
your data.
|
||||
</div>
|
||||
<form onSubmit={handleConfirmFormSubmit} className="px-3 mb-1">
|
||||
<InputWithIcon
|
||||
className="mb-2"
|
||||
icon="password"
|
||||
inputType={showPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm password"
|
||||
value={confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
toggle={{
|
||||
toggleOnIcon: 'eye-off',
|
||||
toggleOffIcon: 'eye',
|
||||
title: 'Show password',
|
||||
toggled: showPassword,
|
||||
onClick: setShowPassword,
|
||||
}}
|
||||
ref={passwordInputRef}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<Button
|
||||
className="btn-w-full mt-1 mb-3"
|
||||
label={
|
||||
isRegistering ? 'Creating account...' : 'Create account & sign in'
|
||||
}
|
||||
type="primary"
|
||||
onClick={handleConfirmFormSubmit}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
onChange={handleEphemeralChange}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
{notesAndTagsCount > 0 ? (
|
||||
<Checkbox
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
onChange={handleShouldMergeChange}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions
|
||||
appState={appState}
|
||||
application={application}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
137
app/assets/javascripts/components/AccountMenu/CreateAccount.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
email: string;
|
||||
setEmail: StateUpdater<string>;
|
||||
password: string;
|
||||
setPassword: StateUpdater<string>;
|
||||
};
|
||||
|
||||
export const CreateAccount: FunctionComponent<Props> = observer(
|
||||
({
|
||||
appState,
|
||||
application,
|
||||
setMenuPane,
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
}) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>();
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) {
|
||||
emailInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEmailChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEmail(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRegisterFormSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterFormSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setEmail(email);
|
||||
setPassword(password);
|
||||
setMenuPane(AccountMenuPane.ConfirmPassword);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setMenuPane(AccountMenuPane.GeneralMenu);
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={handleClose}
|
||||
focusable={true}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Create account</div>
|
||||
</div>
|
||||
<form onSubmit={handleRegisterFormSubmit} className="px-3 mb-1">
|
||||
<InputWithIcon
|
||||
className="mb-2"
|
||||
icon="email"
|
||||
inputType="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={emailInputRef}
|
||||
/>
|
||||
<InputWithIcon
|
||||
className="mb-2"
|
||||
icon="password"
|
||||
inputType={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
toggle={{
|
||||
toggleOnIcon: 'eye-off',
|
||||
toggleOffIcon: 'eye',
|
||||
title: 'Show password',
|
||||
toggled: showPassword,
|
||||
onClick: setShowPassword,
|
||||
}}
|
||||
ref={passwordInputRef}
|
||||
/>
|
||||
<Button
|
||||
className="btn-w-full mt-1"
|
||||
label="Next"
|
||||
type="primary"
|
||||
onClick={handleRegisterFormSubmit}
|
||||
/>
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions application={application} appState={appState} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -6,25 +6,26 @@ import { observer } from 'mobx-react-lite';
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
};
|
||||
|
||||
const Footer = observer(({
|
||||
application,
|
||||
appState,
|
||||
}: Props) => {
|
||||
const Footer = observer(({ application, appState }: Props) => {
|
||||
const {
|
||||
showLogin,
|
||||
showSignIn,
|
||||
showRegister,
|
||||
setShowLogin,
|
||||
setShowSignIn,
|
||||
setShowRegister,
|
||||
setSigningOut
|
||||
setSigningOut,
|
||||
} = appState.accountMenu;
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } = appState;
|
||||
const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } =
|
||||
appState;
|
||||
|
||||
const [appVersion] = useState(() => `v${((window as any).electronAppVersion || application.bridge.appVersion)}`);
|
||||
const [appVersion] = useState(
|
||||
() =>
|
||||
`v${(window as any).electronAppVersion || application.bridge.appVersion}`
|
||||
);
|
||||
|
||||
const disableBetaWarning = () => {
|
||||
disableAppStateBetaWarning();
|
||||
@@ -35,7 +36,7 @@ const Footer = observer(({
|
||||
};
|
||||
|
||||
const hidePasswordForm = () => {
|
||||
setShowLogin(false);
|
||||
setShowSignIn(false);
|
||||
setShowRegister(false);
|
||||
};
|
||||
|
||||
@@ -46,16 +47,20 @@ const Footer = observer(({
|
||||
<span>{appVersion}</span>
|
||||
{showBetaWarning && (
|
||||
<span>
|
||||
<span> (</span>
|
||||
<a className="sk-a" onClick={disableBetaWarning}>Hide beta warning</a>
|
||||
<span>)</span>
|
||||
</span>
|
||||
<span> (</span>
|
||||
<a className="sk-a" onClick={disableBetaWarning}>
|
||||
Hide beta warning
|
||||
</a>
|
||||
<span>)</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(showLogin || showRegister) && (
|
||||
<a className="sk-a right" onClick={hidePasswordForm}>Cancel</a>
|
||||
{(showSignIn || showRegister) && (
|
||||
<a className="sk-a right" onClick={hidePasswordForm}>
|
||||
Cancel
|
||||
</a>
|
||||
)}
|
||||
{!showLogin && !showRegister && (
|
||||
{!showSignIn && !showRegister && (
|
||||
<a className="sk-a right danger capitalize" onClick={signOut}>
|
||||
{user ? 'Sign out' : 'Clear session data'}
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from '../Icon';
|
||||
import { formatLastSyncDate } from '@/preferences/panes/account/Sync';
|
||||
import { SyncQueueStrategy } from '@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
closeMenu: () => void;
|
||||
};
|
||||
|
||||
const iconClassName = 'color-grey-1 mr-2';
|
||||
|
||||
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane, closeMenu }) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
application
|
||||
.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.error) {
|
||||
throw new Error();
|
||||
} else {
|
||||
setLastSyncDate(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
application.alertService.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSyncingInProgress(false);
|
||||
});
|
||||
};
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-3 mt-1 mb-3">
|
||||
<div className="sn-account-menu-headline">Account</div>
|
||||
<div className="flex cursor-pointer" onClick={closeMenu}>
|
||||
<Icon type="close" className="color-grey-1" />
|
||||
</div>
|
||||
</div>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="px-3 mb-2 color-foreground text-sm">
|
||||
<div>You're signed in as:</div>
|
||||
<div className="font-bold">{user.email}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-3 mb-2">
|
||||
{isSyncingInProgress ? (
|
||||
<div className="flex items-center color-info font-semibold">
|
||||
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
||||
Syncing...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center success font-semibold">
|
||||
<Icon type="check-circle" className="mr-2" />
|
||||
Last synced: {lastSyncDate}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex cursor-pointer color-grey-1"
|
||||
onClick={doSynchronization}
|
||||
>
|
||||
<Icon type="sync" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="px-3 mb-1">
|
||||
<div className="mb-3 color-foreground">
|
||||
You’re offline. Sign in to sync your notes and preferences
|
||||
across all your devices and enable end-to-end encryption.
|
||||
</div>
|
||||
<div className="flex items-center color-grey-1">
|
||||
<Icon type="cloud-off" className="mr-2" />
|
||||
<span className="font-semibold">Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
{user ? (
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.preferences.setCurrentPane('account');
|
||||
appState.preferences.openPreferences();
|
||||
}}
|
||||
>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Account settings
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.Register);
|
||||
}}
|
||||
>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Create free account
|
||||
</button>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.SignIn);
|
||||
}}
|
||||
>
|
||||
<Icon type="signIn" className={iconClassName} />
|
||||
Sign in
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.preferences.setCurrentPane('help-feedback');
|
||||
appState.preferences.openPreferences();
|
||||
}}
|
||||
>
|
||||
<Icon type="help" className={iconClassName} />
|
||||
Help & feedback
|
||||
</button>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="signOut" className={iconClassName} />
|
||||
Sign out and clear local data
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
227
app/assets/javascripts/components/AccountMenu/SignIn.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { Icon } from '../Icon';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
};
|
||||
|
||||
export const SignInPane: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const [isEphemeral, setIsEphemeral] = useState(false);
|
||||
const [isStrictSignin, setIsStrictSignin] = useState(false);
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>();
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef?.current) {
|
||||
emailInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetInvalid = () => {
|
||||
if (isInvalid) {
|
||||
setIsInvalid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEmail(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (isInvalid) {
|
||||
setIsInvalid(false);
|
||||
}
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEphemeralChange = () => {
|
||||
setIsEphemeral(!isEphemeral);
|
||||
};
|
||||
|
||||
const handleStrictSigninChange = () => {
|
||||
setIsStrictSignin(!isStrictSignin);
|
||||
};
|
||||
|
||||
const handleShouldMergeChange = () => {
|
||||
setShouldMergeLocal(!shouldMergeLocal);
|
||||
};
|
||||
|
||||
const signIn = () => {
|
||||
setIsSigningIn(true);
|
||||
emailInputRef?.current.blur();
|
||||
passwordInputRef?.current.blur();
|
||||
|
||||
application
|
||||
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
if (err.toString().includes('Invalid email or password')) {
|
||||
setIsInvalid(true);
|
||||
} else {
|
||||
application.alertService.alert(err);
|
||||
}
|
||||
setPassword('');
|
||||
passwordInputRef?.current.blur();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSigningIn(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSignInFormSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInFormSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
signIn();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||
focusable={true}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Sign in</div>
|
||||
</div>
|
||||
<form onSubmit={handleSignInFormSubmit}>
|
||||
<div className="px-3 mb-1">
|
||||
<InputWithIcon
|
||||
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
|
||||
icon="email"
|
||||
inputType="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSigningIn}
|
||||
ref={emailInputRef}
|
||||
/>
|
||||
<InputWithIcon
|
||||
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
|
||||
icon="password"
|
||||
inputType={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSigningIn}
|
||||
toggle={{
|
||||
toggleOnIcon: 'eye-off',
|
||||
toggleOffIcon: 'eye',
|
||||
title: 'Show password',
|
||||
toggled: showPassword,
|
||||
onClick: setShowPassword,
|
||||
}}
|
||||
ref={passwordInputRef}
|
||||
/>
|
||||
{isInvalid ? (
|
||||
<div className="color-dark-red my-2">
|
||||
Invalid email or password.
|
||||
</div>
|
||||
) : null}
|
||||
<Button
|
||||
className="btn-w-full mt-1 mb-3"
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
type="primary"
|
||||
onClick={handleSignInFormSubmit}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleEphemeralChange}
|
||||
/>
|
||||
{notesAndTagsCount > 0 ? (
|
||||
<Checkbox
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleShouldMergeChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions
|
||||
appState={appState}
|
||||
application={application}
|
||||
disabled={isSigningIn}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Checkbox
|
||||
name="use-strict-signin"
|
||||
label="Use strict sign-in"
|
||||
checked={isStrictSignin}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleStrictSigninChange}
|
||||
/>
|
||||
<a
|
||||
href="https://standardnotes.com/help/security"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Learn more"
|
||||
>
|
||||
<Icon type="info" className="color-neutral" />
|
||||
</a>
|
||||
</div>
|
||||
</AdvancedOptions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -2,72 +2,115 @@ import { observer } from 'mobx-react-lite';
|
||||
import { toDirective } from '@/components/utils';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
|
||||
import Authentication from '@/components/AccountMenu/Authentication';
|
||||
import Footer from '@/components/AccountMenu/Footer';
|
||||
import User from '@/components/AccountMenu/User';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { GeneralAccountMenu } from './GeneralAccountMenu';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { SignInPane } from './SignIn';
|
||||
import { CreateAccount } from './CreateAccount';
|
||||
import { ConfirmSignoutContainer } from '../ConfirmSignoutModal';
|
||||
import { ConfirmPassword } from './ConfirmPassword';
|
||||
|
||||
export enum AccountMenuPane {
|
||||
GeneralMenu,
|
||||
SignIn,
|
||||
Register,
|
||||
ConfirmPassword,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const AccountMenu = observer(({ application, appState }: Props) => {
|
||||
const {
|
||||
show: showAccountMenu,
|
||||
showLogin,
|
||||
showRegister,
|
||||
setShowLogin,
|
||||
setShowRegister,
|
||||
closeAccountMenu
|
||||
} = appState.accountMenu;
|
||||
type PaneSelectorProps = Props & {
|
||||
menuPane: AccountMenuPane;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
closeMenu: () => void;
|
||||
};
|
||||
|
||||
const user = application.getUser();
|
||||
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
||||
({ application, appState, menuPane, setMenuPane, closeMenu }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Reset "Login" and "Registration" sections state when hiding account menu,
|
||||
// so the next time account menu is opened these sections are closed
|
||||
if (!showAccountMenu) {
|
||||
setShowLogin(false);
|
||||
setShowRegister(false);
|
||||
}
|
||||
}, [setShowLogin, setShowRegister, showAccountMenu]);
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div id="account-panel" className="sk-panel">
|
||||
<div className="sk-panel-header">
|
||||
<div className="sk-panel-header-title">Account</div>
|
||||
<a className="sk-a info close-button" onClick={closeAccountMenu}>Close</a>
|
||||
</div>
|
||||
<div className="sk-panel-content">
|
||||
<Authentication
|
||||
application={application}
|
||||
switch (menuPane) {
|
||||
case AccountMenuPane.GeneralMenu:
|
||||
return (
|
||||
<GeneralAccountMenu
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
closeMenu={closeMenu}
|
||||
/>
|
||||
);
|
||||
case AccountMenuPane.SignIn:
|
||||
return (
|
||||
<SignInPane
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
/>
|
||||
);
|
||||
case AccountMenuPane.Register:
|
||||
return (
|
||||
<CreateAccount
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
/>
|
||||
);
|
||||
case AccountMenuPane.ConfirmPassword:
|
||||
return (
|
||||
<ConfirmPassword
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
email={email}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const AccountMenu: FunctionComponent<Props> = observer(
|
||||
({ application, appState }) => {
|
||||
const {
|
||||
currentPane,
|
||||
setCurrentPane,
|
||||
shouldAnimateCloseMenu,
|
||||
closeAccountMenu,
|
||||
} = appState.accountMenu;
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div
|
||||
className={`sn-account-menu sn-dropdown ${
|
||||
shouldAnimateCloseMenu
|
||||
? 'slide-up-animation'
|
||||
: 'sn-dropdown--animated'
|
||||
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
|
||||
>
|
||||
<MenuPaneSelector
|
||||
appState={appState}
|
||||
application={application}
|
||||
menuPane={currentPane}
|
||||
setMenuPane={setCurrentPane}
|
||||
closeMenu={closeAccountMenu}
|
||||
/>
|
||||
{!showLogin && !showRegister && user && (
|
||||
<div>
|
||||
<User
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmSignoutContainer
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<Footer
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const AccountMenuDirective = toDirective<Props>(
|
||||
AccountMenu
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const AccountMenuDirective = toDirective<Props>(AccountMenu);
|
||||
|
||||
32
app/assets/javascripts/components/Checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type CheckboxProps = {
|
||||
name: string;
|
||||
checked: boolean;
|
||||
onChange: (e: Event) => void;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const Checkbox: FunctionComponent<CheckboxProps> = ({
|
||||
name,
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<label htmlFor={name} className="flex items-center fit-content mb-2">
|
||||
<input
|
||||
className="mr-2"
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={name}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
@@ -36,11 +36,35 @@ import InfoIcon from '../../icons/ic-info.svg';
|
||||
import CheckIcon from '../../icons/ic-check.svg';
|
||||
import CheckBoldIcon from '../../icons/ic-check-bold.svg';
|
||||
import AccountCircleIcon from '../../icons/ic-account-circle.svg';
|
||||
import CloudOffIcon from '../../icons/ic-cloud-off.svg';
|
||||
import SignInIcon from '../../icons/ic-signin.svg';
|
||||
import SignOutIcon from '../../icons/ic-signout.svg';
|
||||
import CheckCircleIcon from '../../icons/ic-check-circle.svg';
|
||||
import SyncIcon from '../../icons/ic-sync.svg';
|
||||
import ArrowLeftIcon from '../../icons/ic-arrow-left.svg';
|
||||
import ChevronDownIcon from '../../icons/ic-chevron-down.svg';
|
||||
import EmailIcon from '../../icons/ic-email.svg';
|
||||
import ServerIcon from '../../icons/ic-server.svg';
|
||||
import EyeIcon from '../../icons/ic-eye.svg';
|
||||
import EyeOffIcon from '../../icons/ic-eye-off.svg';
|
||||
import LockIcon from '../../icons/ic-lock.svg';
|
||||
|
||||
import { toDirective } from './utils';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
const ICONS = {
|
||||
lock: LockIcon,
|
||||
eye: EyeIcon,
|
||||
'eye-off': EyeOffIcon,
|
||||
server: ServerIcon,
|
||||
email: EmailIcon,
|
||||
'chevron-down': ChevronDownIcon,
|
||||
'arrow-left': ArrowLeftIcon,
|
||||
sync: SyncIcon,
|
||||
'check-circle': CheckCircleIcon,
|
||||
signIn: SignInIcon,
|
||||
signOut: SignOutIcon,
|
||||
'cloud-off': CloudOffIcon,
|
||||
'pencil-off': PencilOffIcon,
|
||||
'plain-text': PlainTextIcon,
|
||||
'rich-text': RichTextIcon,
|
||||
|
||||
@@ -19,6 +19,8 @@ interface Props {
|
||||
title: string;
|
||||
|
||||
focusable: boolean;
|
||||
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +33,8 @@ export const IconButton: FunctionComponent<Props> = ({
|
||||
icon,
|
||||
title,
|
||||
focusable,
|
||||
iconClassName = '',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -42,8 +46,9 @@ export const IconButton: FunctionComponent<Props> = ({
|
||||
title={title}
|
||||
className={`no-border cursor-pointer bg-transparent flex flex-row items-center hover:brightness-130 p-0 ${focusableClass} ${className}`}
|
||||
onClick={click}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon type={icon} />
|
||||
<Icon type={icon} className={iconClassName} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
89
app/assets/javascripts/components/InputWithIcon.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { FunctionComponent, Ref } from 'preact';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { Icon, IconType } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
|
||||
type ToggleProps = {
|
||||
toggleOnIcon: IconType;
|
||||
toggleOffIcon: IconType;
|
||||
title: string;
|
||||
toggled: boolean;
|
||||
onClick: (toggled: boolean) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
icon: IconType;
|
||||
inputType: 'text' | 'email' | 'password';
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
value: string | undefined;
|
||||
onChange: JSXInternal.GenericEventHandler<HTMLInputElement>;
|
||||
onFocus?: JSXInternal.GenericEventHandler<HTMLInputElement>;
|
||||
onKeyDown?: JSXInternal.KeyboardEventHandler<HTMLInputElement>;
|
||||
disabled?: boolean;
|
||||
placeholder: string;
|
||||
toggle?: ToggleProps;
|
||||
};
|
||||
|
||||
const DISABLED_CLASSNAME = 'bg-grey-5 cursor-not-allowed';
|
||||
|
||||
export const InputWithIcon: FunctionComponent<Props> = forwardRef(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
inputType,
|
||||
className,
|
||||
iconClassName,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyDown,
|
||||
disabled,
|
||||
toggle,
|
||||
placeholder,
|
||||
},
|
||||
ref: Ref<HTMLInputElement>
|
||||
) => {
|
||||
const handleToggle = () => {
|
||||
if (toggle) toggle.onClick(!toggle?.toggled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-stretch position-relative bg-default border-1 border-solid border-neutral rounded focus-within:ring-info overflow-hidden ${
|
||||
disabled ? DISABLED_CLASSNAME : ''
|
||||
} ${className}`}
|
||||
>
|
||||
<div className="flex px-2 py-1.5">
|
||||
<Icon type={icon} className={`color-grey-1 ${iconClassName}`} />
|
||||
</div>
|
||||
<input
|
||||
type={inputType}
|
||||
onFocus={onFocus}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
value={value}
|
||||
className={`pr-2 w-full border-0 focus:shadow-none ${
|
||||
disabled ? DISABLED_CLASSNAME : ''
|
||||
}`}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
/>
|
||||
{toggle ? (
|
||||
<div className="flex items-center justify-center px-2">
|
||||
<IconButton
|
||||
className="w-5 h-5 justify-center sk-circle hover:bg-grey-4"
|
||||
icon={toggle.toggled ? toggle.toggleOnIcon : toggle.toggleOffIcon}
|
||||
iconClassName="sn-icon--small"
|
||||
title={toggle.title}
|
||||
onClick={handleToggle}
|
||||
focusable={true}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -14,19 +14,18 @@ type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const OtherSessionsLogoutContainer = observer((props: Props) => {
|
||||
if (!props.appState.accountMenu.otherSessionsLogOut) {
|
||||
export const OtherSessionsSignOutContainer = observer((props: Props) => {
|
||||
if (!props.appState.accountMenu.otherSessionsSignOut) {
|
||||
return null;
|
||||
}
|
||||
return <ConfirmOtherSessionsLogout {...props} />;
|
||||
return <ConfirmOtherSessionsSignOut {...props} />;
|
||||
});
|
||||
|
||||
const ConfirmOtherSessionsLogout = observer(
|
||||
const ConfirmOtherSessionsSignOut = observer(
|
||||
({ application, appState }: Props) => {
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>();
|
||||
function closeDialog() {
|
||||
appState.accountMenu.setOtherSessionsLogout(false);
|
||||
appState.accountMenu.setOtherSessionsSignOut(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -41,9 +40,10 @@ const ConfirmOtherSessionsLogout = observer(
|
||||
</AlertDialogLabel>
|
||||
<AlertDialogDescription className="sk-panel-row">
|
||||
<p className="color-foreground">
|
||||
This action will sign out all other devices signed into your account,
|
||||
and remove your data from those devices when they next regain connection
|
||||
to the internet. You may sign back in on those devices at any time.
|
||||
This action will sign out all other devices signed into
|
||||
your account, and remove your data from those devices when
|
||||
they next regain connection to the internet. You may sign
|
||||
back in on those devices at any time.
|
||||
</p>
|
||||
</AlertDialogDescription>
|
||||
<div className="flex my-1 mt-4">
|
||||
@@ -60,9 +60,9 @@ const ConfirmOtherSessionsLogout = observer(
|
||||
application.revokeAllOtherSessions();
|
||||
closeDialog();
|
||||
application.alertService.alert(
|
||||
"You have successfully revoked your sessions from other devices.",
|
||||
'You have successfully revoked your sessions from other devices.',
|
||||
undefined,
|
||||
"Finish"
|
||||
'Finish'
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -1,48 +1,52 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types';
|
||||
import {
|
||||
PasswordWizardScope,
|
||||
PasswordWizardType,
|
||||
WebDirective,
|
||||
} from './../../types';
|
||||
import template from '%/directives/password-wizard.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||
const DEFAULT_CONTINUE_TITLE = 'Continue';
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
FinishStep = 2
|
||||
FinishStep = 2,
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
currentPassword?: string,
|
||||
newPassword?: string,
|
||||
newPasswordConfirmation?: string,
|
||||
status?: string
|
||||
}
|
||||
currentPassword?: string;
|
||||
newPassword?: string;
|
||||
newPasswordConfirmation?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
lockContinue: boolean
|
||||
formData: FormData,
|
||||
continueTitle: string,
|
||||
step: Steps,
|
||||
title: string,
|
||||
showSpinner: boolean
|
||||
processing: boolean
|
||||
}
|
||||
lockContinue: boolean;
|
||||
formData: FormData;
|
||||
continueTitle: string;
|
||||
step: Steps;
|
||||
title: string;
|
||||
showSpinner: boolean;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type: PasswordWizardType,
|
||||
changePassword: boolean,
|
||||
securityUpdate: boolean
|
||||
}
|
||||
type: PasswordWizardType;
|
||||
changePassword: boolean;
|
||||
securityUpdate: boolean;
|
||||
};
|
||||
|
||||
class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordWizardScope {
|
||||
$element: JQLite
|
||||
application!: WebApplication
|
||||
type!: PasswordWizardType
|
||||
isContinuing = false
|
||||
class PasswordWizardCtrl
|
||||
extends PureViewCtrl<Props, State>
|
||||
implements PasswordWizardScope
|
||||
{
|
||||
$element: JQLite;
|
||||
application!: WebApplication;
|
||||
type!: PasswordWizardType;
|
||||
isContinuing = false;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
constructor($element: JQLite, $timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
this.registerWindowUnloadStopper();
|
||||
@@ -53,13 +57,13 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
this.initProps({
|
||||
type: this.type,
|
||||
changePassword: this.type === PasswordWizardType.ChangePassword,
|
||||
securityUpdate: this.type === PasswordWizardType.AccountUpgrade
|
||||
securityUpdate: this.type === PasswordWizardType.AccountUpgrade,
|
||||
});
|
||||
this.setState({
|
||||
formData: {},
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||
step: Steps.PasswordStep,
|
||||
title: this.props.changePassword ? 'Change Password' : 'Account Update'
|
||||
title: this.props.changePassword ? 'Change Password' : 'Account Update',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +82,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
resetContinueState() {
|
||||
this.setState({
|
||||
showSpinner: false,
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||
});
|
||||
this.isContinuing = false;
|
||||
}
|
||||
@@ -95,7 +99,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
this.isContinuing = true;
|
||||
await this.setState({
|
||||
showSpinner: true,
|
||||
continueTitle: "Generating Keys..."
|
||||
continueTitle: 'Generating Keys...',
|
||||
});
|
||||
const valid = await this.validateCurrentPassword();
|
||||
if (!valid) {
|
||||
@@ -110,8 +114,8 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
this.isContinuing = false;
|
||||
this.setState({
|
||||
showSpinner: false,
|
||||
continueTitle: "Finish",
|
||||
step: Steps.FinishStep
|
||||
continueTitle: 'Finish',
|
||||
step: Steps.FinishStep,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,43 +123,43 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
return this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
...formData
|
||||
}
|
||||
...formData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async validateCurrentPassword() {
|
||||
const currentPassword = this.state.formData.currentPassword;
|
||||
const newPass = this.props.securityUpdate ? currentPassword : this.state.formData.newPassword;
|
||||
const newPass = this.props.securityUpdate
|
||||
? currentPassword
|
||||
: this.state.formData.newPassword;
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
this.application.alertService!.alert(
|
||||
"Please enter your current password."
|
||||
'Please enter your current password.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this.props.changePassword) {
|
||||
if (!newPass || newPass.length === 0) {
|
||||
this.application.alertService!.alert(
|
||||
"Please enter a new password."
|
||||
);
|
||||
this.application.alertService!.alert('Please enter a new password.');
|
||||
return false;
|
||||
}
|
||||
if (newPass !== this.state.formData.newPasswordConfirmation) {
|
||||
this.application.alertService!.alert(
|
||||
"Your new password does not match its confirmation."
|
||||
'Your new password does not match its confirmation.'
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: undefined
|
||||
status: undefined,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!this.application.getUser()?.email) {
|
||||
this.application.alertService!.alert(
|
||||
"We don't have your email stored. Please log out then log back in to fix this issue."
|
||||
"We don't have your email stored. Please sign out then log back in to fix this issue."
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: undefined
|
||||
status: undefined,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -166,7 +170,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
);
|
||||
if (!success) {
|
||||
this.application.alertService!.alert(
|
||||
"The current password you entered is not correct. Please try again."
|
||||
'The current password you entered is not correct. Please try again.'
|
||||
);
|
||||
}
|
||||
return success;
|
||||
@@ -176,10 +180,10 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
await this.application.downloadBackup();
|
||||
await this.setState({
|
||||
lockContinue: true,
|
||||
processing: true
|
||||
processing: true,
|
||||
});
|
||||
await this.setFormDataState({
|
||||
status: "Processing encryption keys…"
|
||||
status: 'Processing encryption keys…',
|
||||
});
|
||||
const newPassword = this.props.securityUpdate
|
||||
? this.state.formData.currentPassword
|
||||
@@ -195,16 +199,16 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
});
|
||||
if (!success) {
|
||||
this.setFormDataState({
|
||||
status: "Unable to process your password. Please try again."
|
||||
status: 'Unable to process your password. Please try again.',
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
status: this.props.changePassword
|
||||
? "Successfully changed password."
|
||||
: "Successfully performed account update."
|
||||
}
|
||||
? 'Successfully changed password.'
|
||||
: 'Successfully performed account update.',
|
||||
},
|
||||
});
|
||||
}
|
||||
return success;
|
||||
@@ -213,7 +217,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
dismiss() {
|
||||
if (this.state.lockContinue) {
|
||||
this.application.alertService!.alert(
|
||||
"Cannot close window until pending tasks are complete."
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
const elem = this.$element;
|
||||
@@ -234,7 +238,7 @@ export class PasswordWizard extends WebDirective {
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
type: '=',
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AccountPreferences, HelpAndFeedback, Listed, General, Security } from './panes';
|
||||
import {
|
||||
AccountPreferences,
|
||||
HelpAndFeedback,
|
||||
Listed,
|
||||
General,
|
||||
Security,
|
||||
} from './panes';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PreferencesMenu } from './PreferencesMenu';
|
||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { MfaProps } from './panes/two-factor-auth/MfaProps';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useEffect, useMemo } from 'preact/hooks';
|
||||
import { Extensions } from './panes/Extensions';
|
||||
|
||||
interface PreferencesProps extends MfaProps {
|
||||
@@ -22,7 +28,9 @@ const PaneSelector: FunctionComponent<
|
||||
> = observer((props) => {
|
||||
switch (props.menu.selectedPaneId) {
|
||||
case 'general':
|
||||
return <General appState={props.appState} application={props.application} />
|
||||
return (
|
||||
<General appState={props.appState} application={props.application} />
|
||||
);
|
||||
case 'account':
|
||||
return (
|
||||
<AccountPreferences
|
||||
@@ -67,20 +75,22 @@ const PreferencesCanvas: FunctionComponent<
|
||||
|
||||
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
|
||||
(props) => {
|
||||
const menu = useMemo(() => new PreferencesMenu(), []);
|
||||
|
||||
useEffect(() => {
|
||||
menu.selectPane(props.appState.preferences.currentPane);
|
||||
const removeEscKeyObserver = props.application.io.addKeyObserver({
|
||||
key: 'Escape',
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault();
|
||||
props.closePreferences();
|
||||
}
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
removeEscKeyObserver();
|
||||
};
|
||||
}, [props]);
|
||||
const menu = new PreferencesMenu();
|
||||
}, [props, menu]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full absolute top-left-0 flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
Sync,
|
||||
SubscriptionWrapper,
|
||||
Credentials,
|
||||
LogOutWrapper,
|
||||
SignOutWrapper,
|
||||
Authentication,
|
||||
} from '@/preferences/panes/account';
|
||||
import { PreferencesPane } from '@/preferences/components';
|
||||
@@ -23,7 +23,7 @@ export const AccountPreferences = observer(
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Authentication application={application} appState={appState} />
|
||||
<LogOutWrapper application={application} appState={appState} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export const AccountPreferences = observer(
|
||||
<Credentials application={application} />
|
||||
<Sync application={application} />
|
||||
<SubscriptionWrapper application={application} />
|
||||
<LogOutWrapper application={application} appState={appState} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,65 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { PreferencesGroup, PreferencesSegment, Subtitle, Text, Title } from "@/preferences/components";
|
||||
import { WebApplication } from "@/ui_models/application";
|
||||
import { AppState } from "@/ui_models/app_state";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { FunctionComponent } from "preact";
|
||||
import { AccountMenuPane } from '@/components/AccountMenu';
|
||||
import { Button } from '@/components/Button';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const Authentication: FunctionComponent<{ application: WebApplication, appState: AppState }> =
|
||||
observer(({ appState }) => {
|
||||
export const Authentication: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ appState }) => {
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setShowLogin(true);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
const clickRegister = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.Register);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
const clickRegister = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setShowRegister(true);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-12">
|
||||
<Title>You're not signed in</Title>
|
||||
<Subtitle className="text-center">Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.</Subtitle>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row w-full">
|
||||
<Button type="primary" onClick={clickSignIn} label="Sign in" className="flex-grow" />
|
||||
<div className="min-w-3" />
|
||||
<Button type="primary" onClick={clickRegister} label="Register" className="flex-grow" />
|
||||
</div>
|
||||
<div className="min-h-3" />
|
||||
<Text className="text-center">Standard Notes is free on every platform, and comes standard with sync and encryption.</Text>
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-12">
|
||||
<Title>You're not signed in</Title>
|
||||
<Subtitle className="text-center">
|
||||
Sign in to sync your notes and preferences across all your devices
|
||||
and enable end-to-end encryption.
|
||||
</Subtitle>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row w-full">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={clickSignIn}
|
||||
label="Sign in"
|
||||
className="flex-grow"
|
||||
/>
|
||||
<div className="min-w-3" />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={clickRegister}
|
||||
label="Register"
|
||||
className="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
<div className="min-h-3" />
|
||||
<Text className="text-center">
|
||||
Standard Notes is free on every platform, and comes standard with
|
||||
sync and encryption.
|
||||
</Text>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
|
||||
import { OtherSessionsLogoutContainer } from '@/components/OtherSessionsLogout';
|
||||
import { OtherSessionsSignOutContainer } from '@/components/OtherSessionsSignOut';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
@@ -13,30 +13,33 @@ import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const LogOutView: FunctionComponent<{
|
||||
const SignOutView: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Log out</Title>
|
||||
<Title>Sign out</Title>
|
||||
<div className="min-h-2" />
|
||||
<Subtitle>Other devices</Subtitle>
|
||||
<Text>Want to log out on all devices except this one?</Text>
|
||||
<Text>Want to sign out on all devices except this one?</Text>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
className="mr-3"
|
||||
type="normal"
|
||||
label="Log out other sessions"
|
||||
label="Sign out other sessions"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setOtherSessionsLogout(true);
|
||||
appState.accountMenu.setOtherSessionsSignOut(true);
|
||||
}}
|
||||
/>
|
||||
<Button type="normal" label="Manage sessions" onClick={() => appState.openSessionsModal()} />
|
||||
<Button
|
||||
type="normal"
|
||||
label="Manage sessions"
|
||||
onClick={() => appState.openSessionsModal()}
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
@@ -45,20 +48,19 @@ const LogOutView: FunctionComponent<{
|
||||
<div className="min-h-3" />
|
||||
<Button
|
||||
type="danger"
|
||||
label="Log out and clear local data"
|
||||
label="Sign out and clear local data"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<OtherSessionsLogoutContainer appState={appState} application={application} />
|
||||
|
||||
<ConfirmSignoutContainer
|
||||
<OtherSessionsSignOutContainer
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
|
||||
<ConfirmSignoutContainer appState={appState} application={application} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -85,19 +87,19 @@ const ClearSessionDataView: FunctionComponent<{
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
<ConfirmSignoutContainer
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
|
||||
</>);
|
||||
<ConfirmSignoutContainer appState={appState} application={application} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const LogOutWrapper: FunctionComponent<{
|
||||
export const SignOutWrapper: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
const isLoggedIn = application.getUser() != undefined;
|
||||
if (!isLoggedIn) return <ClearSessionDataView appState={appState} application={application} />;
|
||||
return <LogOutView appState={appState} application={application} />;
|
||||
if (!isLoggedIn)
|
||||
return (
|
||||
<ClearSessionDataView appState={appState} application={application} />
|
||||
);
|
||||
return <SignOutView appState={appState} application={application} />;
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { SyncQueueStrategy } from '@node_modules/@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
@@ -12,48 +17,54 @@ type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const Sync: FunctionComponent<Props> = observer(({ application }: Props) => {
|
||||
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
export const Sync: FunctionComponent<Props> = observer(
|
||||
({ application }: Props) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
}
|
||||
};
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className='flex flex-row items-center'>
|
||||
<div className='flex-grow flex flex-col'>
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className='font-bold'>on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className='min-w-20 mt-3'
|
||||
type='normal'
|
||||
label='Sync now'
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className="font-bold">on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="normal"
|
||||
label="Sync now"
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ModalDialog,
|
||||
ModalDialogButtons,
|
||||
ModalDialogDescription,
|
||||
ModalDialogLabel
|
||||
ModalDialogLabel,
|
||||
} from '@/components/shared/ModalDialog';
|
||||
import { Button } from '@/components/Button';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
@@ -15,29 +15,31 @@ import { useBeforeUnload } from '@/hooks/useBeforeUnload';
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
GeneratingKeys = 'Generating Keys...',
|
||||
Finish = 'Finish'
|
||||
Finish = 'Finish',
|
||||
}
|
||||
|
||||
enum Steps {
|
||||
InitialStep,
|
||||
FinishStep
|
||||
FinishStep,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onCloseDialog: () => void;
|
||||
application: WebApplication;
|
||||
}
|
||||
};
|
||||
|
||||
export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
onCloseDialog,
|
||||
application
|
||||
application,
|
||||
}) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
|
||||
const [isContinuing, setIsContinuing] = useState(false);
|
||||
const [lockContinue, setLockContinue] = useState(false);
|
||||
const [submitButtonTitle, setSubmitButtonTitle] = useState(SubmitButtonTitles.Default);
|
||||
const [submitButtonTitle, setSubmitButtonTitle] = useState(
|
||||
SubmitButtonTitles.Default
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
|
||||
|
||||
useBeforeUnload();
|
||||
@@ -46,16 +48,12 @@ export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
|
||||
const validateCurrentPassword = async () => {
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
applicationAlertService.alert(
|
||||
'Please enter your current password.'
|
||||
);
|
||||
applicationAlertService.alert('Please enter your current password.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length === 0) {
|
||||
applicationAlertService.alert(
|
||||
'Please enter a new password.'
|
||||
);
|
||||
applicationAlertService.alert('Please enter a new password.');
|
||||
return false;
|
||||
}
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
@@ -67,7 +65,7 @@ export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
|
||||
if (!application.getUser()?.email) {
|
||||
applicationAlertService.alert(
|
||||
'We don\'t have your email stored. Please log out then log back in to fix this issue.'
|
||||
"We don't have your email stored. Please sign out then sign back in to fix this issue."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -172,15 +170,15 @@ export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
<ModalDialogButtons>
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<Button
|
||||
className='min-w-20'
|
||||
type='normal'
|
||||
label='Cancel'
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Cancel"
|
||||
onClick={handleDialogClose}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className='min-w-20'
|
||||
type='primary'
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label={submitButtonTitle}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
||||
export { Sync } from './Sync';
|
||||
export { Credentials } from './Credentials';
|
||||
export { LogOutWrapper } from './LogOutView';
|
||||
export { SignOutWrapper } from './SignOutView';
|
||||
export { Authentication } from './Authentication';
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
} from 'mobx';
|
||||
import { ApplicationEvent, ContentType } from '@standardnotes/snjs';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
|
||||
import { AccountMenuPane } from '@/components/AccountMenu';
|
||||
|
||||
type StructuredItemsCount =
|
||||
{ notes: number, tags: number, deleted: number, archived: number };
|
||||
type StructuredItemsCount = {
|
||||
notes: number;
|
||||
tags: number;
|
||||
deleted: number;
|
||||
archived: number;
|
||||
};
|
||||
|
||||
export class AccountMenuState {
|
||||
show = false;
|
||||
signingOut = false;
|
||||
otherSessionsLogOut = false;
|
||||
otherSessionsSignOut = false;
|
||||
server: string | undefined = undefined;
|
||||
enableServerOption = false;
|
||||
notesAndTags: SNItem[] = [];
|
||||
isEncryptionEnabled = false;
|
||||
encryptionStatusString = '';
|
||||
isBackupEncrypted = false;
|
||||
showLogin = false;
|
||||
showSignIn = false;
|
||||
showRegister = false;
|
||||
shouldAnimateCloseMenu = false;
|
||||
currentPane = AccountMenuPane.GeneralMenu;
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
@@ -25,14 +39,17 @@ export class AccountMenuState {
|
||||
makeObservable(this, {
|
||||
show: observable,
|
||||
signingOut: observable,
|
||||
otherSessionsLogOut: observable,
|
||||
otherSessionsSignOut: observable,
|
||||
server: observable,
|
||||
enableServerOption: observable,
|
||||
notesAndTags: observable,
|
||||
isEncryptionEnabled: observable,
|
||||
encryptionStatusString: observable,
|
||||
isBackupEncrypted: observable,
|
||||
showLogin: observable,
|
||||
showSignIn: observable,
|
||||
showRegister: observable,
|
||||
currentPane: observable,
|
||||
shouldAnimateCloseMenu: observable,
|
||||
|
||||
setShow: action,
|
||||
toggleShow: action,
|
||||
@@ -40,9 +57,11 @@ export class AccountMenuState {
|
||||
setIsEncryptionEnabled: action,
|
||||
setEncryptionStatusString: action,
|
||||
setIsBackupEncrypted: action,
|
||||
setOtherSessionsLogout: action,
|
||||
setOtherSessionsSignOut: action,
|
||||
setCurrentPane: action,
|
||||
setEnableServerOption: action,
|
||||
|
||||
notesAndTagsCount: computed
|
||||
notesAndTagsCount: computed,
|
||||
});
|
||||
|
||||
this.addAppLaunchedEventObserver();
|
||||
@@ -61,14 +80,14 @@ export class AccountMenuState {
|
||||
|
||||
streamNotesAndTags = (): void => {
|
||||
this.appEventListeners.push(
|
||||
this.application.streamItems(
|
||||
[ContentType.Note, ContentType.Tag],
|
||||
() => {
|
||||
runInAction(() => {
|
||||
this.notesAndTags = this.application.getItems([ContentType.Note, ContentType.Tag]);
|
||||
});
|
||||
}
|
||||
)
|
||||
this.application.streamItems([ContentType.Note, ContentType.Tag], () => {
|
||||
runInAction(() => {
|
||||
this.notesAndTags = this.application.getItems([
|
||||
ContentType.Note,
|
||||
ContentType.Tag,
|
||||
]);
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,7 +96,12 @@ export class AccountMenuState {
|
||||
};
|
||||
|
||||
closeAccountMenu = (): void => {
|
||||
this.setShow(false);
|
||||
this.shouldAnimateCloseMenu = true;
|
||||
setTimeout(() => {
|
||||
this.setShow(false);
|
||||
this.shouldAnimateCloseMenu = false;
|
||||
this.setCurrentPane(AccountMenuPane.GeneralMenu);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
setSigningOut = (signingOut: boolean): void => {
|
||||
@@ -88,6 +112,10 @@ export class AccountMenuState {
|
||||
this.server = server;
|
||||
};
|
||||
|
||||
setEnableServerOption = (enableServerOption: boolean): void => {
|
||||
this.enableServerOption = enableServerOption;
|
||||
};
|
||||
|
||||
setIsEncryptionEnabled = (isEncryptionEnabled: boolean): void => {
|
||||
this.isEncryptionEnabled = isEncryptionEnabled;
|
||||
};
|
||||
@@ -100,8 +128,8 @@ export class AccountMenuState {
|
||||
this.isBackupEncrypted = isBackupEncrypted;
|
||||
};
|
||||
|
||||
setShowLogin = (showLogin: boolean): void => {
|
||||
this.showLogin = showLogin;
|
||||
setShowSignIn = (showSignIn: boolean): void => {
|
||||
this.showSignIn = showSignIn;
|
||||
};
|
||||
|
||||
setShowRegister = (showRegister: boolean): void => {
|
||||
@@ -112,16 +140,25 @@ export class AccountMenuState {
|
||||
this.show = !this.show;
|
||||
};
|
||||
|
||||
setOtherSessionsLogout = (otherSessionsLogOut: boolean): void => {
|
||||
this.otherSessionsLogOut = otherSessionsLogOut;
|
||||
}
|
||||
setOtherSessionsSignOut = (otherSessionsSignOut: boolean): void => {
|
||||
this.otherSessionsSignOut = otherSessionsSignOut;
|
||||
};
|
||||
|
||||
setCurrentPane = (pane: AccountMenuPane): void => {
|
||||
this.currentPane = pane;
|
||||
};
|
||||
|
||||
get notesAndTagsCount(): number {
|
||||
return this.notesAndTags.length;
|
||||
}
|
||||
|
||||
get structuredNotesAndTagsCount(): StructuredItemsCount {
|
||||
const count: StructuredItemsCount = { notes: 0, archived: 0, deleted: 0, tags: 0 };
|
||||
const count: StructuredItemsCount = {
|
||||
notes: 0,
|
||||
archived: 0,
|
||||
deleted: 0,
|
||||
tags: 0,
|
||||
};
|
||||
for (const item of this.notesAndTags) {
|
||||
if (item.archived) {
|
||||
count.archived++;
|
||||
@@ -138,7 +175,6 @@ export class AccountMenuState {
|
||||
if (item.content_type === ContentType.Tag) {
|
||||
count.tags++;
|
||||
}
|
||||
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import { PreferenceId } from '@/preferences/PreferencesMenu';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
|
||||
export class PreferencesState {
|
||||
private _open = false;
|
||||
currentPane: PreferenceId = 'general';
|
||||
|
||||
constructor() {
|
||||
makeObservable<PreferencesState, '_open'>(this, {
|
||||
_open: observable,
|
||||
currentPane: observable,
|
||||
openPreferences: action,
|
||||
closePreferences: action,
|
||||
setCurrentPane: action,
|
||||
isOpen: computed,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentPane = (prefId: PreferenceId): void => {
|
||||
this.currentPane = prefId;
|
||||
};
|
||||
|
||||
openPreferences = (): void => {
|
||||
this._open = true;
|
||||
};
|
||||
|
||||
closePreferences = (): void => {
|
||||
this._open = false;
|
||||
this.currentPane = 'general';
|
||||
};
|
||||
|
||||
get isOpen() {
|
||||
|
||||
@@ -69,14 +69,9 @@
|
||||
ng-if='ctrl.newUpdateAvailable == true'
|
||||
)
|
||||
span.info.sk-label New update available.
|
||||
.sk-app-bar-item.no-pointer(
|
||||
ng-if='ctrl.lastSyncDate && !ctrl.isRefreshing'
|
||||
)
|
||||
.sk-label.subtle(ng-if='!ctrl.offline')
|
||||
| Last refreshed {{ctrl.lastSyncDate}}
|
||||
.sk-app-bar-item(
|
||||
ng-click='ctrl.toggleSyncResolutionMenu()',
|
||||
ng-if='(ctrl.state.outOfSync && !ctrl.isRefreshing) || ctrl.showSyncResolution'
|
||||
ng-if='(ctrl.state.outOfSync) || ctrl.showSyncResolution'
|
||||
)
|
||||
.sk-label.warning(ng-if='ctrl.state.outOfSync') Potentially Out of Sync
|
||||
sync-resolution-menu(
|
||||
@@ -85,12 +80,8 @@
|
||||
ng-if='ctrl.showSyncResolution',
|
||||
application='ctrl.application'
|
||||
)
|
||||
.sk-app-bar-item(ng-if='ctrl.lastSyncDate && ctrl.isRefreshing')
|
||||
.sk-spinner.small
|
||||
.sk-app-bar-item(ng-if='ctrl.offline')
|
||||
.sk-label Offline
|
||||
.sk-app-bar-item(ng-click='ctrl.refreshData()' ng-if='!ctrl.offline')
|
||||
.sk-label Refresh
|
||||
.sk-app-bar-item.border(ng-if='ctrl.state.dockShortcuts.length > 0')
|
||||
.sk-app-bar-item.dock-shortcut(ng-repeat='shortcut in ctrl.state.dockShortcuts')
|
||||
.sk-app-bar-item-column(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { RootScopeMessages } from './../../messages';
|
||||
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||
import { WebDirective } from '@/types';
|
||||
import { dateToLocalizedString, preventRefreshing } from '@/utils';
|
||||
import { preventRefreshing } from '@/utils';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
SyncQueueStrategy,
|
||||
ContentType,
|
||||
SNComponent,
|
||||
SNTheme,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
import template from './footer-view.pug';
|
||||
import { AppStateEvent, EventSource } from '@/ui_models/app_state';
|
||||
import {
|
||||
STRING_GENERIC_SYNC_ERROR,
|
||||
STRING_NEW_UPDATE_READY,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_UPGRADE,
|
||||
STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
} from '@/strings';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { alertDialog, confirmDialog } from '@/services/alertService';
|
||||
import { AccountMenuPane } from '@/components/AccountMenu';
|
||||
|
||||
/**
|
||||
* Disable before production release.
|
||||
@@ -69,8 +68,6 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
private queueExtReload = false;
|
||||
private reloadInProgress = false;
|
||||
public hasError = false;
|
||||
public isRefreshing = false;
|
||||
public lastSyncDate?: string;
|
||||
public newUpdateAvailable = false;
|
||||
public dockShortcuts: DockShortcut[] = [];
|
||||
public roomShowState: Partial<Record<string, boolean>> = {};
|
||||
@@ -267,7 +264,6 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
this.appState.accountMenu.setShow(true);
|
||||
}
|
||||
}
|
||||
this.syncUpdated();
|
||||
this.findErrors();
|
||||
this.updateOfflineStatus();
|
||||
break;
|
||||
@@ -463,37 +459,13 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
|
||||
closeAccountMenu() {
|
||||
this.appState.accountMenu.setShow(false);
|
||||
this.appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu);
|
||||
}
|
||||
|
||||
lockApp() {
|
||||
this.application.lock();
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.isRefreshing = true;
|
||||
this.application
|
||||
.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$timeout(() => {
|
||||
this.isRefreshing = false;
|
||||
}, 200);
|
||||
if (response && response.error) {
|
||||
this.application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncUpdated() {
|
||||
this.lastSyncDate = dateToLocalizedString(
|
||||
this.application.getLastSyncDate()!
|
||||
);
|
||||
}
|
||||
|
||||
onNewUpdateAvailable() {
|
||||
this.newUpdateAvailable = true;
|
||||
}
|
||||
@@ -581,7 +553,7 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
if (this.application && this.application.authenticationInProgress()) {
|
||||
return;
|
||||
}
|
||||
this.appState.accountMenu.setShow(false);
|
||||
this.appState.accountMenu.closeAccountMenu();
|
||||
}
|
||||
|
||||
clickPreferences() {
|
||||
|
||||
@@ -16,3 +16,15 @@
|
||||
width: 280px;
|
||||
max-height: calc(85vh - 90px);
|
||||
}
|
||||
|
||||
.sn-account-menu {
|
||||
z-index: $z-index-footer-bar-item-panel;
|
||||
@extend .bottom-100;
|
||||
@extend .left-0;
|
||||
@extend .cursor-auto;
|
||||
}
|
||||
|
||||
.sn-account-menu-headline {
|
||||
@extend .sk-h2;
|
||||
@extend .sk-bold;
|
||||
}
|
||||
|
||||
@@ -218,11 +218,21 @@
|
||||
.mr-3 {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.my-1\.5 {
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.max-w-89 {
|
||||
max-width: 22.25rem;
|
||||
}
|
||||
@@ -329,3 +339,43 @@
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sn-component .btn-w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.hover\:bg-grey-4:hover {
|
||||
background: var(--sn-stylekit-grey-4);
|
||||
}
|
||||
|
||||
.sn-component .spinner-info {
|
||||
border-color: var(--sn-stylekit-info-color);
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-up-animation {
|
||||
animation: slide-up 0.2s ease;
|
||||
}
|
||||
|
||||
.bottom-100 {
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.cursor-auto {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||