diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 8ccfb3d56..e7d242798 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -47,7 +47,9 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { const onFormSubmit = async (event: Event) => { event.preventDefault(); - await appState.noteTags.createAndAddNewTag(); + if (autocompleteSearchQuery !== '') { + await appState.noteTags.createAndAddNewTag(); + } }; const onKeyDown = (event: KeyboardEvent) => { @@ -115,8 +117,9 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { ref={dropdownRef} className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} sn-dropdown flex flex-col py-2 absolute`} style={{ maxHeight: dropdownMaxHeight, maxWidth: tagsContainerMaxWidth }} + onBlur={closeOnBlur} > -
+
{autocompleteTagResults.map((tagResult) => ( { }); const ConfirmSignoutModal = observer(({ application, appState }: Props) => { - const [deleteLocalBackups, setDeleteLocalBackups] = useState( - application.hasAccount() - ); + const [deleteLocalBackups, setDeleteLocalBackups] = useState(false); const cancelRef = useRef(); function close() { diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index fa21ac7eb..abd4fa354 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -23,7 +23,7 @@ const NotesContextMenu = observer(({ appState }: Props) => { return appState.notes.contextMenuOpen ? (
{appState.tags.tags.map((tag) => (
-
+
{refreshing ? ( <>
diff --git a/app/assets/javascripts/services/autolock_service.ts b/app/assets/javascripts/services/autolock_service.ts index 895674dcd..4cfe8f9f1 100644 --- a/app/assets/javascripts/services/autolock_service.ts +++ b/app/assets/javascripts/services/autolock_service.ts @@ -1,10 +1,8 @@ import { ApplicationService } from '@standardnotes/snjs'; -import { WebApplication } from '@/ui_models/application'; import { isDesktopApplication } from '@/utils'; -import { AppStateEvent } from '@/ui_models/app_state'; const MILLISECONDS_PER_SECOND = 1000; -const FOCUS_POLL_INTERVAL = 1 * MILLISECONDS_PER_SECOND; +const POLL_INTERVAL = 50; const LOCK_INTERVAL_NONE = 0; const LOCK_INTERVAL_IMMEDIATE = 1; const LOCK_INTERVAL_ONE_MINUTE = 60 * MILLISECONDS_PER_SECOND; @@ -15,37 +13,21 @@ const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey"; export class AutolockService extends ApplicationService { - private unsubState?: () => void; - private pollFocusInterval: any + private pollInterval: any private lastFocusState?: 'hidden' | 'visible' private lockAfterDate?: Date - private lockTimeout?: any onAppLaunch() { - this.observeVisibility(); + if (!isDesktopApplication()) { + this.beginPolling(); + } return super.onAppLaunch(); } - observeVisibility() { - this.unsubState = (this.application as WebApplication).getAppState().addObserver( - async (eventName) => { - if (eventName === AppStateEvent.WindowDidBlur) { - this.documentVisibilityChanged(false); - } else if (eventName === AppStateEvent.WindowDidFocus) { - this.documentVisibilityChanged(true); - } - } - ); - if (!isDesktopApplication()) { - this.beginWebFocusPolling(); - } - } - deinit() { - this.unsubState?.(); this.cancelAutoLockTimer(); - if (this.pollFocusInterval) { - clearInterval(this.pollFocusInterval); + if (this.pollInterval) { + clearInterval(this.pollInterval); } } @@ -85,11 +67,15 @@ export class AutolockService extends ApplicationService { * Verify document is in focus every so often as visibilitychange event is * not triggered on a typical window blur event but rather on tab changes. */ - beginWebFocusPolling() { - this.pollFocusInterval = setInterval(() => { - if (document.hidden) { - /** Native event listeners will have fired */ - return; + beginPolling() { + this.pollInterval = setInterval(async () => { + const locked = await this.application.isLocked(); + if ( + !locked && + this.lockAfterDate && + new Date() > this.lockAfterDate + ) { + this.lockApplication(); } const hasFocus = document.hasFocus(); if (hasFocus && this.lastFocusState === 'hidden') { @@ -99,7 +85,7 @@ export class AutolockService extends ApplicationService { } /* Save this to compare against next time around */ this.lastFocusState = hasFocus ? 'visible' : 'hidden'; - }, FOCUS_POLL_INTERVAL); + }, POLL_INTERVAL); } getAutoLockIntervalOptions() { @@ -129,14 +115,6 @@ export class AutolockService extends ApplicationService { async documentVisibilityChanged(visible: boolean) { if (visible) { - const locked = await this.application.isLocked(); - if ( - !locked && - this.lockAfterDate && - new Date() > this.lockAfterDate - ) { - this.lockApplication(); - } this.cancelAutoLockTimer(); } else { this.beginAutoLockTimer(); @@ -148,29 +126,15 @@ export class AutolockService extends ApplicationService { if (interval === LOCK_INTERVAL_NONE) { return; } - /** - * Use a timeout if possible, but if the computer is put to sleep, timeouts won't - * work. Need to set a date as backup. this.lockAfterDate does not need to be - * persisted, as living in memory is sufficient. If memory is cleared, then the - * application will lock anyway. - */ const addToNow = (seconds: number) => { const date = new Date(); date.setSeconds(date.getSeconds() + seconds); return date; }; this.lockAfterDate = addToNow(interval / MILLISECONDS_PER_SECOND); - clearTimeout(this.lockTimeout); - this.lockTimeout = setTimeout(() => { - this.cancelAutoLockTimer(); - this.lockApplication(); - this.lockAfterDate = undefined; - }, interval); } cancelAutoLockTimer() { - clearTimeout(this.lockTimeout); this.lockAfterDate = undefined; - this.lockTimeout = undefined; } } diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index c57d88fb3..1c8191bd7 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -142,7 +142,7 @@ export class AppState { this.noAccountWarning.reset(); } this.actionsMenu.reset(); - this.unsubApp(); + this.unsubApp?.(); this.unsubApp = undefined; this.observers.length = 0; this.appEventObserverRemovers.forEach((remover) => remover()); diff --git a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts index 80b5273f8..f263fcb7d 100644 --- a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts +++ b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts @@ -198,9 +198,15 @@ export class NoteTagsState { async removeTagFromActiveNote(tag: SNTag): Promise { const { activeNote } = this; if (activeNote) { - await this.application.changeItem(tag.uuid, (mutator) => { - mutator.removeItemAsRelationship(activeNote); - }); + const descendantTags = this.application.getTagDescendants(tag); + const tagsToRemove = [...descendantTags, tag]; + await Promise.all( + tagsToRemove.map(async (tag) => { + await this.application.changeItem(tag.uuid, (mutator) => { + mutator.removeItemAsRelationship(activeNote); + }); + }) + ); this.application.sync(); this.reloadTags(); } diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts index 46f2ab65e..daa9881aa 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -346,13 +346,18 @@ export class NotesState { async removeTagFromSelectedNotes(tag: SNTag): Promise { const selectedNotes = Object.values(this.selectedNotes); - await this.application.changeItem(tag.uuid, (mutator) => { - for (const note of selectedNotes) { - mutator.removeItemAsRelationship(note); - } - }); + const descendantTags = this.application.getTagDescendants(tag); + const tagsToRemove = [...descendantTags, tag]; + await Promise.all( + tagsToRemove.map(async (tag) => { + await this.application.changeItem(tag.uuid, (mutator) => { + for (const note of selectedNotes) { + mutator.removeItemAsRelationship(note); + } + }); + }) + ); this.application.sync(); - } isTagInSelectedNotes(tag: SNTag): boolean { diff --git a/app/assets/javascripts/views/abstract/pure_view_ctrl.ts b/app/assets/javascripts/views/abstract/pure_view_ctrl.ts index 6f2a24297..71d7bae6f 100644 --- a/app/assets/javascripts/views/abstract/pure_view_ctrl.ts +++ b/app/assets/javascripts/views/abstract/pure_view_ctrl.ts @@ -40,8 +40,8 @@ export class PureViewCtrl

{ } deinit(): void { - this.unsubApp(); - this.unsubState(); + this.unsubApp?.(); + this.unsubState?.(); for (const disposer of this.reactionDisposers) { disposer(); } diff --git a/app/assets/javascripts/views/tags/tags_view.ts b/app/assets/javascripts/views/tags/tags_view.ts index d7ce1bbc8..2c63341cb 100644 --- a/app/assets/javascripts/views/tags/tags_view.ts +++ b/app/assets/javascripts/views/tags/tags_view.ts @@ -289,10 +289,10 @@ class TagsViewCtrl extends PureViewCtrl { async saveTag($event: Event, tag: SNTag) { ($event.target! as HTMLInputElement).blur(); - if (!this.titles[tag.uuid]?.length) { - return this.undoCreateTag(tag); - } if (this.getState().templateTag) { + if (!this.titles[tag.uuid]?.length) { + return this.undoCreateTag(tag); + } return this.saveNewTag(); } else { return this.saveTagRename(tag); @@ -314,6 +314,9 @@ class TagsViewCtrl extends PureViewCtrl { if (newTitle.length === 0) { this.titles[tag.uuid] = this.editingOriginalName; this.editingOriginalName = undefined; + await this.setState({ + editingTag: undefined + }); return; } const existingTag = this.application.findTagByTitle(newTitle); @@ -345,6 +348,7 @@ class TagsViewCtrl extends PureViewCtrl { this.application.alertService!.alert( "A tag with this name already exists." ); + this.undoCreateTag(newTag); return; } const insertedTag = await this.application.insertItem(newTag); diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index a312819ce..7120ea12d 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -109,6 +109,11 @@ padding-bottom: 0.375rem; } +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + .outline-none { outline: none; } @@ -254,6 +259,10 @@ height: 4.5rem; } +.h-90vh { + height: 90vh; +} + .max-h-120 { max-height: 30rem; } @@ -266,8 +275,8 @@ position: fixed; } -.overflow-y-scroll { - overflow-y: scroll; +.overflow-y-auto { + overflow-y: auto; } .overflow-auto { @@ -351,7 +360,6 @@ .sn-dropdown { @extend .bg-default; - // @extend .min-w-80; @extend .rounded; @extend .box-shadow; diff --git a/app/assets/stylesheets/_tags.scss b/app/assets/stylesheets/_tags.scss index 95060e799..300d5af52 100644 --- a/app/assets/stylesheets/_tags.scss +++ b/app/assets/stylesheets/_tags.scss @@ -72,6 +72,8 @@ } > .title { + @extend .focus\:outline-none; + @extend .focus\:shadow-none; width: 80%; background-color: transparent; font-weight: 600; diff --git a/package.json b/package.json index bcc502563..8fae7c363 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.9.0-beta01", + "version": "3.8.1", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -71,7 +71,7 @@ "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.2.10", - "@standardnotes/snjs": "2.6.0", + "@standardnotes/snjs": "2.6.3", "mobx": "^6.1.6", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12" diff --git a/yarn.lock b/yarn.lock index de7bfddca..a14d4d07a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1936,10 +1936,10 @@ "@standardnotes/sncrypto-common" "^1.2.7" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.6.0.tgz#8ebdfcb0918c308198b38a63d7aa946387b83ac4" - integrity sha512-Gb/kAdMtjVlSiQH7pkDzFxKtIrrY43i2hSejO2c+zCviZspiDZPpXLpEhMJ295ow2tluhOf8zfBUda3LMC6oDw== +"@standardnotes/snjs@2.6.3": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.6.3.tgz#7677899c050b0616d994423fd4ec9caf03394f35" + integrity sha512-5pWh+BPVPpd6JlP3avo2puGk9EWUaH0+6Y1fN9rMR8oLZ2oc8Dffiy5S4TLNm8zL4q504oMlm1/ALkwwZpKLEQ== dependencies: "@standardnotes/auth" "^2.0.0" "@standardnotes/sncrypto-common" "^1.2.9"