* Delete Crunch Menu Component

* Disabled Quick Note
* Note crunches over when menu is open
* Added a cool loader
* Remomoved locked notes
* Added full note encryption
* Added encrypted search index
* Added encrypted shared notes
* Made search bar have a clear and search button
* Tags only loade when clicking on the tags menu
* Tweaked home page to be a little more sane
* built out some gigantic test cases
* simplified a lot of things to make entire app easier to maintain
This commit is contained in:
Max G
2020-05-10 21:15:59 +00:00
parent 1005913c0b
commit 2861042485
16 changed files with 797 additions and 603 deletions

View File

@@ -2,9 +2,9 @@
<!-- change class to .master-note-edit to have it popup on the screen -->
<div
id="InputNotes"
class="master-note-edit"
class="master-note-edit full-focus"
@keyup.esc="close()"
:class="[{ 'full-focus':(fullFocusEditor) }, 'position-'+position ]"
:class="[ 'position-'+position ]"
>
<!-- Main Menu -->
@@ -100,6 +100,8 @@
<div class="edit-button" v-on:click="openEditAttachment" data-tooltip="Files" data-position="bottom center" data-inverted>
<i class="folder icon"></i>
</div>
<span>{{ statusText }}</span>
</div>
@@ -110,19 +112,20 @@
<div class="bottom-edit-menu"></div>
<div class="input-container-wrapper" :class="{ 'size-down':(sizeDown == true)}" >
<!-- Loading indicator -->
<div v-if="loading" class="loading-note">
<div class="loading-text">
Decrypting Note &
{{loadingMessage}}
</div>
</div>
<div class="input-container-wrapper" :class="{ 'side-menu-open':sideMenuOpen, 'size-down':(sizeDown == true)}" >
<!-- Squire box grows -->
<div class="note-wrapper" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<!-- Loading indicator -->
<transition name="fade">
<div v-if="loading || forceShowLoading" class="loading-note" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<div class="loading-text">
<loading-icon :message="loadingMessage" />
</div>
</div>
</transition>
<!-- Title input area -->
<textarea
ref="titleTextarea"
@@ -134,45 +137,8 @@
v-on:blur="save" type="text" v-model="noteTitle" placeholder="Title" class="stealth-input">
</textarea>
<!-- Squire Box - only appears if decrypted -->
<div v-show="isDecrypted" id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div>
<!-- Decrypt note prompt -->
<div v-if="isEncrypted && !isDecrypted" class="ui basic padded segment">
<div class="ui raised segment">
<h3 class="ui center aligned icon header">
<i class="green lock alternate icon"></i>
<span v-if="!lockedOut">
This note is encrypted and requires a password to be opened.
</span>
<!-- note is locked for 5 minutes -->
<span v-if="lockedOut">
To many unlock attempts. Note is locked for 5 minutes.
</span>
</h3>
<!-- Decrypt note -->
<div class="ui form" v-if="!lockedOut">
<h5 class="ui horizontal divider header" v-if="passwordHint && passwordHint.length > 0">
Hint: {{ passwordHint }}
</h5>
<div class="field">
<input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password" v-on:keyup.enter="decryptNote" autofocus ref="decryptNotePrompt">
</div>
<div class="field">
<div v-on:click="decryptNote" class="ui green fluid button" v-if="password.length >= 3">
Unlock Note
</div>
<div class="ui disabled fluid button" v-if="password.length < 3">
Unlock Note
</div>
</div>
</div>
</div>
</div>
<!-- Squire Box -->
<div id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div>
</div>
@@ -287,11 +253,13 @@
'share-note-component': () => import('@/components/ShareNoteComponent.vue'),
'color-tooltip':require('@/components/TextColorTooltipComponent.vue').default,
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
},
data(){
return {
loading: true,
forceShowLoading: true,
loadingMessage: 'Loading Note',
currentNoteId: 0,
modified: false,
@@ -315,13 +283,8 @@
styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges
sizeDown: false, //Used to animate close state
colorPickerLocation: null,
fullFocusEditor: true, //Initialized editor instance
//Settings vars
showAllSettings: true,
lastVisibilityState: null,
//All the squire settings
@@ -329,23 +292,12 @@
// pastFocusedNode: null,
usersOnNote: 0,
sideMenuOpen: false,
tags: false,
colors: false,
images: false,
options: false,
colorpicker: false,
//Encryption options
passwordHint: '',
password: '', //Field Variables, only for form
passwordConfirm: '', //Only a form variable
hashedPass: '', //sha-256 password hash, sends to server for decryption
isEncrypted: false,
isDecrypted: false,
passwordprotect: false,
decryptAttempts: 0,
lockedOut: false,
autoLockTimeout: null,
}
},
watch: {
@@ -372,9 +324,9 @@
}
//Reset all note menus on URL change
this.sideMenuOpen = false
this.colors = false
this.tags = false
this.passwordprotect = false
this.options = false
this.images = false
@@ -382,7 +334,7 @@
if(newVal.openMenu){
//Only modify menu boolean if its defined
if(typeof this[newVal.openMenu] == 'boolean'){
this.sideMenuOpen = true
this[newVal.openMenu] = true
}
}
@@ -399,22 +351,23 @@
},
beforeDestroy(){
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
clearTimeout(this.autoLockTimeout)
// this.$io.emit('leave_room', this.rawTextId)
this.$bus.$off('new_file_upload')
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
this.editor.destroy()
if(this.editor){
this.editor.destroy()
}
},
mounted: function() {
setTimeout(()=>{
this.forceShowLoading = false
}, 500)
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
this.$nextTick(() => {
@@ -429,6 +382,9 @@
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
this.setText(this.noteText)
this.lastNoteHash = this.hashString(this.getText())
console.log('hash on load', this.lastNoteHash)
//focus on open, not on mobile, thats annoying
if(!this.$store.getters.getIsUserOnMobile){
// this.editor.focus()
@@ -826,12 +782,12 @@
//Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){
axios.post('/api/note/get', { 'noteId': this.noteid, 'password':this.hashedPass })
axios.post('/api/note/get', { 'noteId': this.noteid })
.then(response => {
//Block notes you don't have access to from opening
if(response.data === false){
this.$bus.$emit('notification', 'Invalid Note')
this.$bus.$emit('notification', 'Error opening Note')
this.close(true)
return
}
@@ -840,7 +796,6 @@
this.currentNoteId = this.noteid
this.rawTextId = response.data.rawTextId
this.shareUsername = response.data.shareUsername
this.passwordHint = response.data.password_hint
this.created = response.data.created
this.updated = response.data.updated
@@ -852,7 +807,6 @@
this.noteText = response.data.text
this.diffNoteText = response.data.text
this.lastNoteHash = this.hashString(response.data.text)
//Set up note colors
if(response.data.color){
this.styleObject = JSON.parse(response.data.color)
@@ -866,29 +820,12 @@
this.loading = false
this.isDecrypted = response.data.decrypted
this.isEncrypted = response.data.encrypted == 1
this.decryptAttempts = response.data.decrypt_attempts_count
this.lockedOut = response.data.lockedOut
//If password is required, display a prompt and focus on it
if(this.password.length == 0 && this.isEncrypted && !this.isDecrypted){
this.$nextTick(() => {
if(this.$refs.decryptNotePrompt){
// this.editor.moveCursorToEnd()
this.$refs.decryptNotePrompt.focus()
}
})
}
this.$nextTick(() => {
//Adjust note title size after load
this.titleResize()
this.setupWebSockets()
this.initSquire()
this.startAutolockTimer()
})
})
@@ -1064,7 +1001,7 @@
},
onKeyup(){
this.statusText = 'Save'
this.statusText = ''
this.diffText()
@@ -1088,23 +1025,16 @@
// return resolve(true)
//Encrypted notes that are not decrypted should not be saved
if(this.isEncrypted && !this.isDecrypted){
return resolve(true)
}
//Don't save note if its hash doesn't change
const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){
const currentHash = this.hashString( currentNoteText )
if( this.lastNoteHash == currentHash){
this.statusText = 'Saved'
return resolve(true)
}
//If user accidentally clears note, it won't delete it
if(currentNoteText == ''){
this.statusText = 'Empty'
console.log('Prevented from saving empty note.')
return resolve(true)
}
@@ -1115,10 +1045,11 @@
'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned,
'archived': this.archived,
'password': this.hashedPass,
'hint': this.passwordHint,
'hash': currentHash,
}
console.log('Save Hash', currentHash)
this.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => {
this.statusText = 'Saved'
@@ -1126,8 +1057,8 @@
this.modified = true
//Update last saved note hash
this.lastNoteHash = this.hashString( currentNoteText )
this.startAutolockTimer()
// this.lastNoteHash = this.hashString( currentNoteText )
this.lastNoteHash = currentHash
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') })
@@ -1135,7 +1066,9 @@
},
checkForUpdatedNote(){
// return
//Ignore visibility changes, handle this with socket IO
//Just keep it always up to date if user is on note
return
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
@@ -1169,18 +1102,15 @@
//Track visibility state
this.lastVisibilityState = document.visibilityState
},
hashString(text){
hashString(inText){
text = this.noteTitle + text
let text = this.noteTitle + inText
var hash = 0;
let hash = 0;
if (text == null || text.length == 0) {
return hash;
}
//Simplified for speed
// return text.length
for (let i = 0; i < text.length; i++) {
let char = text.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
@@ -1217,6 +1147,11 @@
},
setupWebSockets(){
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
console.log('Current hash', this.lastNoteHash)
console.log('Incoming Hash', hash)
})
return
//Tell server to push this note into a room
@@ -1231,62 +1166,6 @@
this.patchText(incomingDiffData)
})
},
decryptNote(){
const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64')
//Remove plaintext password
this.hashedPass = hashed
this.password = ''
this.passwordConfirm = ''
this.loadNote()
},
lockNote(){
this.save().then(results => {
this.isDecrypted = false
this.password = ''
this.hashedPass = ''
this.passwordprotect = false
this.setText('')
})
},
enableEncryption(){
if(this.noteText == ''){
this.noteText = 'Text Typed here is encrypted.'
}
const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64')
//Remove plaintext password
this.hashedPass = hashed
this.lastNoteHash = 0
this.password = ''
this.passwordConfirm = ''
this.passwordprotect = false
this.save()
.then(results => {
this.$bus.$emit('notification', 'Password Protection Enabled')
this.loadNote()
})
},
disableEncryption(){
this.lastNoteHash = 0
this.isEncrypted = false
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
this.passwordprotect = false
//Reload Note
this.save()
.then(results => {
this.loadNote()
this.$bus.$emit('notification', 'Password Protection Removed')
})
},
titleResize(){
//Resize the title field
let element = this.$refs.titleTextarea
@@ -1295,15 +1174,6 @@
element.style.height = 'auto';
element.style.height = (element.scrollHeight + padding) +'px';
},
startAutolockTimer(){
//Start autolock timer on encrypted notes that are encrypted and in a decrypted state
if(this.isEncrypted && this.isDecrypted){
clearTimeout(this.autoLockTimeout)
this.autoLockTimeout = setTimeout(() => {
this.lockNote()
}, (60 * 1000 * 20) ) //Autolock after 20 min
}
},
}
}
</script>
@@ -1343,6 +1213,7 @@
background-color: var(--background_color);
border: 1px solid var(--menu-accent);;
margin: 45px 0 45px 0;
position: relative;
}
/*
@@ -1438,18 +1309,18 @@
}
.loading-note {
position: absolute;
top: 20%;
left: 20%;
right: 20%;
bottom: 20%;
background: transparent;
color: #5e6268;;
font-size: 1.3em;
top: 0;
width: 100%;
height: 100%;
min-height: 300px;
background: var(--background_color);
/*opacity: 0.;*/
z-index: 1;
}
.loading-text {
margin: 0;
position: absolute;
top: 50%;
top: 200px;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
@@ -1464,6 +1335,10 @@
left: 15%;
right: 15%;
}
.side-menu-open {
left: calc(50% + 10px) !important;
right: calc(0% + 10px) !important;
}
@media only screen and (max-width: 740px) {
.input-container-wrapper {
left: 0;
@@ -1580,6 +1455,24 @@
right: 150%;
}
}
/* Fade out transition animation */
.fade-enter {
/*opacity: 0;*/
}
.fade-enter-active {
/*transition: opacity 0.7s;*/
}
.fade-leave {
/* opacity: 0; */
}
.fade-leave-active {
transition: opacity 0.7s;
opacity: 0;
}
/* animations END */
</style>