diff --git a/.eslintrc b/.eslintrc
index 74fa349e3d5..3841621ad15 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -19,7 +19,10 @@
"camelcase": "warn",
"sort-imports": "off",
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
- "react-hooks/exhaustive-deps": "error" // Checks effect dependencies
+ "react-hooks/exhaustive-deps": "error", // Checks effect dependencies
+ "eol-last": "error",
+ "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
+ "no-trailing-spaces": "error"
},
"env": {
"browser": true
diff --git a/.gitignore b/.gitignore
index 4656f67cc2b..01c40b0e4ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
# OS & IDE
.DS_Store
+.idea
# Ignore bundler config.
/.bundle
diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx
index 4a383edda04..bf13b5509a9 100644
--- a/app/assets/javascripts/components/Button.tsx
+++ b/app/assets/javascripts/components/Button.tsx
@@ -1,6 +1,6 @@
import { FunctionComponent } from 'preact';
-const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content cursor-pointer`;
+const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`;
const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \
focus:bg-contrast hover:bg-contrast`;
@@ -12,15 +12,19 @@ export const Button: FunctionComponent<{
type: 'normal' | 'primary';
label: string;
onClick: () => void;
-}> = ({ type, label, className = '', onClick }) => {
+ disabled?: boolean;
+}> = ({ type, label, className = '', onClick, disabled = false }) => {
const buttonClass = type === 'primary' ? primaryClass : normalClass;
+ const cursorClass = disabled ? 'cursor-default' : 'cursor-pointer';
+
return (
diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx
index e1472fcbc14..239955ce52a 100644
--- a/app/assets/javascripts/preferences/PreferencesView.tsx
+++ b/app/assets/javascripts/preferences/PreferencesView.tsx
@@ -1,19 +1,21 @@
import { RoundIconButton } from '@/components/RoundIconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
-import { HelpAndFeedback, Security } from './panes';
+import { AccountPreferences, HelpAndFeedback, Security } from './panes';
import { observer } from 'mobx-react-lite';
import { PreferencesMenu } from './preferences-menu';
import { PreferencesMenuView } from './PreferencesMenuView';
+import { WebApplication } from '@/ui_models/application';
const PaneSelector: FunctionComponent<{
prefs: PreferencesMenu;
-}> = observer(({ prefs: menu }) => {
+ application: WebApplication;
+}> = observer(({ prefs: menu, application }) => {
switch (menu.selectedPaneId) {
case 'general':
return null;
case 'account':
- return null;
+ return ;
case 'appearance':
return null;
case 'security':
@@ -33,15 +35,19 @@ const PaneSelector: FunctionComponent<{
const PreferencesCanvas: FunctionComponent<{
preferences: PreferencesMenu;
-}> = observer(({ preferences: prefs }) => (
+ application: WebApplication;
+}> = observer(({ preferences: prefs, application }) => (
));
-const PreferencesView: FunctionComponent<{ close: () => void }> = observer(
- ({ close }) => {
+const PreferencesView: FunctionComponent<{
+ close: () => void;
+ application: WebApplication;
+}> = observer(
+ ({ close, application }) => {
const prefs = new PreferencesMenu();
return (
@@ -58,7 +64,7 @@ const PreferencesView: FunctionComponent<{ close: () => void }> = observer(
icon="close"
/>
-
+
);
}
@@ -66,12 +72,16 @@ const PreferencesView: FunctionComponent<{ close: () => void }> = observer(
export interface PreferencesWrapperProps {
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
+ application: WebApplication;
}
export const PreferencesViewWrapper: FunctionComponent =
- observer(({ appState }) => {
+ observer(({ appState, application }) => {
if (!appState.preferences.isOpen) return null;
return (
- appState.preferences.closePreferences()} />
+ appState.preferences.closePreferences()}
+ />
);
});
diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx
new file mode 100644
index 00000000000..6b034d7c6d3
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx
@@ -0,0 +1,12 @@
+import { Sync } from '@/preferences/panes/account';
+import { PreferencesPane } from '@/preferences/components';
+import { observer } from 'mobx-react-lite';
+import { WebApplication } from '@/ui_models/application';
+
+export const AccountPreferences = observer(({application}: {application: WebApplication}) => {
+ return (
+
+
+
+ );
+});
diff --git a/app/assets/javascripts/preferences/panes/account/Sync.tsx b/app/assets/javascripts/preferences/panes/account/Sync.tsx
new file mode 100644
index 00000000000..7190bfa0e69
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/Sync.tsx
@@ -0,0 +1,60 @@
+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';
+import { useState } from '@node_modules/preact/hooks';
+import { dateToLocalizedString } from '@/utils';
+import { observer } from '@node_modules/mobx-react-lite';
+import { WebApplication } from '@/ui_models/application';
+
+type Props = {
+ application: WebApplication;
+};
+
+const Sync = observer(({ application }: Props) => {
+ const formatLastSyncDate = (lastUpdatedDate: Date) => {
+ return dateToLocalizedString(lastUpdatedDate);
+ };
+
+ const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
+ const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
+
+ 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));
+ }
+ };
+
+ return (
+
+
+
+
+
Sync
+
+ Last synced on {lastSyncDate}
+
+
+
+
+
+
+ );
+});
+
+export default Sync;
diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts
new file mode 100644
index 00000000000..2225b588171
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/index.ts
@@ -0,0 +1 @@
+export { default as Sync } from './Sync';
diff --git a/app/assets/javascripts/preferences/panes/index.ts b/app/assets/javascripts/preferences/panes/index.ts
index f176e8116c0..741508ad065 100644
--- a/app/assets/javascripts/preferences/panes/index.ts
+++ b/app/assets/javascripts/preferences/panes/index.ts
@@ -1,2 +1,3 @@
export * from './HelpFeedback';
export * from './Security';
+export * from './AccountPreferences';
diff --git a/app/assets/javascripts/views/application/application-view.pug b/app/assets/javascripts/views/application/application-view.pug
index cff5275ee42..9bd688c897d 100644
--- a/app/assets/javascripts/views/application/application-view.pug
+++ b/app/assets/javascripts/views/application/application-view.pug
@@ -28,6 +28,7 @@
)
preferences(
app-state='self.appState'
+ application='self.application'
)
challenge-modal(
ng-repeat="challenge in self.challenges track by challenge.id"
diff --git a/app/assets/stylesheets/_ui.scss b/app/assets/stylesheets/_ui.scss
index e1f7a1dee56..6fa1b0b9af8 100644
--- a/app/assets/stylesheets/_ui.scss
+++ b/app/assets/stylesheets/_ui.scss
@@ -199,6 +199,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
}
}
+.cursor-default {
+ cursor: default;
+}
+
.fill-current {
fill: currentColor;
}
diff --git a/yarn.lock b/yarn.lock
index e54b8deb922..30920454c8c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2016,6 +2016,11 @@
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.1.1.tgz#834701c2e14d31eb204bff90457fa05e9183464a"
integrity sha512-E9zDYZ1gJkVZBEzd7a1L2haQ4GYeH1lUrY87UmDH1AMYUHW+c0SqZ71af1fBNqGzrx3EZSXk+Qzr7RyOa6N1Mw==
+"@standardnotes/features@1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.0.0.tgz#906af029b6e58241689ca37436982c37a888a418"
+ integrity sha512-PEQyP/p/TQLVcNYcbu9jEIWNRqBrFFG1Qyy8QIcvNUt5o4lpLZGEY1T+PJUsPSisnuKKNpQrgVLc9LjhUKpuYw==
+
"@standardnotes/sncrypto-common@^1.2.7", "@standardnotes/sncrypto-common@^1.2.9":
version "1.2.9"
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.2.9.tgz#5212a959e4ec563584e42480bfd39ef129c3cbdf"
@@ -2029,12 +2034,13 @@
"@standardnotes/sncrypto-common" "^1.2.7"
libsodium-wrappers "^0.7.8"
-"@standardnotes/snjs@2.7.21":
- version "2.7.21"
- resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.21.tgz#db451e5facaf5fa41fa509eb1f304723929c3541"
- integrity sha512-GhkGk1LJmD494COZkSOgyHaUnGnLWNLlSuCZMTwbw3dgkN5PjobbRhfDvEZaLqjwok+h9nkiQt3hugQ3h6Cy5w==
+"@standardnotes/snjs@2.7.23":
+ version "2.7.23"
+ resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.23.tgz#fedc9c025301dbe20ed2d598fb378e36f90ff64e"
+ integrity sha512-eoEwKlV2PZcJXFbCt6bgovu9nldVoT7DPoterTBo/NZ4odRILOwxLA1SAgL5H5FYPb9NHkwaaCt9uTdIqdNYhA==
dependencies:
"@standardnotes/auth" "3.1.1"
+ "@standardnotes/features" "1.0.0"
"@standardnotes/sncrypto-common" "^1.2.9"
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":