diff --git a/app/assets/icons/ic-restore.svg b/app/assets/icons/ic-restore.svg new file mode 100644 index 000000000..7b59194bc --- /dev/null +++ b/app/assets/icons/ic-restore.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 8702fa60d..6fb7dbeda 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -7,6 +7,8 @@ import ArchiveIcon from '../../icons/ic-archive.svg'; import UnarchiveIcon from '../../icons/ic-unarchive.svg'; import HashtagIcon from '../../icons/ic-hashtag.svg'; import ChevronRightIcon from '../../icons/ic-chevron-right.svg'; +import RestoreIcon from '../../icons/ic-restore.svg'; +import CloseIcon from '../../icons/ic-close.svg'; import { toDirective } from './utils'; export enum IconType { @@ -19,6 +21,8 @@ export enum IconType { Unarchive = 'unarchive', Hashtag = 'hashtag', ChevronRight = 'chevron-right', + Restore = 'restore', + Close = 'close', } const ICONS = { @@ -31,6 +35,8 @@ const ICONS = { [IconType.Unarchive]: UnarchiveIcon, [IconType.Hashtag]: HashtagIcon, [IconType.ChevronRight]: ChevronRightIcon, + [IconType.Restore]: RestoreIcon, + [IconType.Close]: CloseIcon, }; type Props = { diff --git a/app/assets/javascripts/components/NotesOptions.tsx b/app/assets/javascripts/components/NotesOptions.tsx index 41b5bfb0e..2e804d94e 100644 --- a/app/assets/javascripts/components/NotesOptions.tsx +++ b/app/assets/javascripts/components/NotesOptions.tsx @@ -44,8 +44,8 @@ export const NotesOptions = observer( useEffect(() => { const openTrashAlert = async () => { if (shouldTrashNotes && blurLocked) { - await appState.notes.setTrashSelectedNotes(!trashed, trashButtonRef); setShouldTrashNotes(false); + await appState.notes.setTrashSelectedNotes(!trashed, trashButtonRef); setLockCloseOnBlur(false); } }; @@ -190,9 +190,23 @@ export const NotesOptions = observer( setLockCloseOnBlur(true); }} > - + {trashed ? 'Restore' : 'Move to Trash'} + {appState.selectedTag?.isTrashTag && ( + + )} ); } diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts index 1772e7975..abea399ab 100644 --- a/app/assets/javascripts/directives/views/accountMenu.ts +++ b/app/assets/javascripts/directives/views/accountMenu.ts @@ -4,7 +4,6 @@ import template from '%/directives/account-menu.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { STRING_ACCOUNT_MENU_UNCHECK_MERGE, - STRING_SIGN_OUT_CONFIRMATION, STRING_E2E_ENABLED, STRING_LOCAL_ENC_ENABLED, STRING_ENC_NOT_ENABLED, @@ -18,7 +17,7 @@ import { STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_UNSUPPORTED_BACKUP_FILE_VERSION, - Strings, + StringUtils, } from '@/strings'; import { PasswordWizardType } from '@/types'; import { @@ -110,7 +109,7 @@ class AccountMenuCtrl extends PureViewCtrl { storage.get(StorageKey.DisableErrorReporting) === false, showSessions: false, errorReportingId: errorReportingId(), - keyStorageInfo: Strings.keyStorageInfo(this.application), + keyStorageInfo: StringUtils.keyStorageInfo(this.application), importData: null, syncInProgress: false, protectionsDisabledUntil: this.getProtectionsDisabledUntil(), diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index b3de2183b..1c112f341 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -109,6 +109,13 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade'; export const Strings = { + protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.', + openAccountMenu: 'Open Account Menu', + trashNotesTitle: 'Move to Trash', + trashNotesText: 'Are you sure you want to move these notes to the trash?', +}; + +export const StringUtils = { keyStorageInfo(application: SNApplication): string | null { if (!isDesktopApplication()) { return null; @@ -125,8 +132,26 @@ export const Strings = { : 'password manager'; return `Your keys are currently stored in your operating system's ${keychainName}. Adding a passcode prevents even your operating system from reading them.`; }, - protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.', - openAccountMenu: 'Open Account Menu', - trashNotesTitle: 'Move To Trash', - trashNotesText: 'Are you sure you want to move these notes to the trash?' -}; + deleteNotes(permanently: boolean, notesCount = 1, title?: string): string { + if (notesCount === 1) { + return permanently + ? `Are you sure you want to permanently delete ${title}?` + : `Are you sure you want to move ${title} to the trash?`; + } else { + return permanently + ? `Are you sure you want to permanently delete these notes?` + : `Are you sure you want to move these notes to the trash?`; + } + }, + archiveLockedNotesAttempt(archive: boolean, notesCount = 1): string { + const archiveString = archive ? 'archive' : 'unarchive'; + return notesCount === 1 + ? `This note has editing disabled. If you'd like to ${archiveString} it, enable editing, and try again.` + : `One or more of these notes have editing disabled. If you'd like to ${archiveString} them, make sure editing is enabled on all of them, and try again.`; + }, + deleteLockedNotesAttempt(notesCount = 1): string { + return notesCount === 1 + ? "This note has editing disabled. If you'd like to delete it, enable editing, and try again." + : "One or more of these notes have editing disabled. If you'd like to delete them, make sure editing is enabled on all of them, and try again."; + }, +}; \ No newline at end of file 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 585a0aca3..e3324b86c 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -1,6 +1,6 @@ import { confirmDialog } from '@/services/alertService'; import { KeyboardModifier } from '@/services/ioService'; -import { Strings } from '@/strings'; +import { Strings, StringUtils } from '@/strings'; import { UuidString, SNNote, @@ -36,6 +36,7 @@ export class NotesState { selectedNotesCount: computed, + deleteNotesPermanently: action, selectNote: action, setArchiveSelectedNotes: action, setContextMenuOpen: action, @@ -158,14 +159,17 @@ export class NotesState { trashed: boolean, trashButtonRef: RefObject ): Promise { - if ( - !trashed || - (await confirmDialog({ - title: Strings.trashNotesTitle, - text: Strings.trashNotesText, - confirmButtonStyle: 'danger', - })) - ) { + if (trashed) { + const notesDeleted = await this.deleteNotes(false); + if (notesDeleted) { + runInAction(() => { + this.selectedNotes = {}; + this.contextMenuOpen = false; + }); + } else { + trashButtonRef.current?.focus(); + } + } else { this.application.changeItems( Object.keys(this.selectedNotes), (mutator) => { @@ -177,11 +181,62 @@ export class NotesState { this.selectedNotes = {}; this.contextMenuOpen = false; }); - } else { - trashButtonRef.current?.focus(); } } + async deleteNotesPermanently(): Promise { + await this.deleteNotes(true); + } + + async deleteNotes(permanently: boolean): Promise { + if (Object.values(this.selectedNotes).some((note) => note.locked)) { + const text = StringUtils.deleteLockedNotesAttempt( + this.selectedNotesCount + ); + this.application.alertService.alert(text); + return false; + } + + const title = Strings.trashNotesTitle; + let noteTitle = undefined; + if (this.selectedNotesCount === 1) { + const selectedNote = Object.values(this.selectedNotes)[0]; + noteTitle = selectedNote.safeTitle().length + ? `'${selectedNote.title}'` + : 'this note'; + } + const text = StringUtils.deleteNotes( + permanently, + this.selectedNotesCount, + noteTitle + ); + + if ( + await confirmDialog({ + title, + text, + confirmButtonStyle: 'danger', + }) + ) { + if (permanently) { + for (const note of Object.values(this.selectedNotes)) { + await this.application.deleteItem(note); + } + } else { + this.application.changeItems( + Object.keys(this.selectedNotes), + (mutator) => { + mutator.trashed = true; + }, + false + ); + } + return true; + } + + return false; + } + setPinSelectedNotes(pinned: boolean): void { this.application.changeItems( Object.keys(this.selectedNotes), @@ -192,7 +247,13 @@ export class NotesState { ); } - setArchiveSelectedNotes(archived: boolean): void { + async setArchiveSelectedNotes(archived: boolean): Promise { + if (Object.values(this.selectedNotes).some((note) => note.locked)) { + this.application.alertService.alert( + StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount) + ); + return; + } this.application.changeItems( Object.keys(this.selectedNotes), (mutator) => { @@ -201,6 +262,7 @@ export class NotesState { ); runInAction(() => { this.selectedNotes = {}; + this.contextMenuOpen = false; }); } diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index cf0d5e751..273a96e10 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -117,7 +117,7 @@ stylekit-class="'warning'" ) menu-row( - action='self.selectedMenuItem(); self.deleteNotePermanantely()' + action='self.selectedMenuItem(); self.deleteNotePermanently()' desc="'Delete this note permanently from all your devices'", label="'Delete Permanently'", ng-show='!self.note.trashed && self.note.errorDecrypting', @@ -132,7 +132,7 @@ stylekit-class="'info'" ) menu-row( - action='self.selectedMenuItem(true); self.deleteNotePermanantely()' + action='self.selectedMenuItem(true); self.deleteNotePermanently()' desc="'Delete this note permanently from all your devices'", label="'Delete Permanently'", stylekit-class="'danger'" diff --git a/app/assets/javascripts/views/editor/editor_view.ts b/app/assets/javascripts/views/editor/editor_view.ts index c01f4a101..2092e990e 100644 --- a/app/assets/javascripts/views/editor/editor_view.ts +++ b/app/assets/javascripts/views/editor/editor_view.ts @@ -712,7 +712,7 @@ class EditorViewCtrl extends PureViewCtrl { ); } - deleteNotePermanantely() { + deleteNotePermanently() { this.deleteNote(true); } diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index df6d0af1b..3c55ae8f8 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -66,6 +66,10 @@ color: var(--sn-stylekit-foreground-color); } +.color-danger { + color: var(--sn-stylekit-danger-color); +} + .ring-info { box-shadow: 0 0 0 2px var(--sn-stylekit-info-color); }