Gigantic Update
* Migrated manual tests to jest and started working on better coverage * Added a bookmarklet and push key generation tool allowing URL pushing from bookmarklets * Updated web scraping with tons of bug fixes * Updated attachments page to handle new push links * Aggressive note change checking, if patches get out of sync, server overwrites bad updates.
This commit is contained in:
@@ -117,11 +117,16 @@
|
||||
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="ui small compact basic button" v-on:click="openNote">
|
||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
<div class="ui small compact basic button" v-on:click="openEditAttachments"
|
||||
<div v-if="!item.note_id" class="ui small compact basic disabled button">
|
||||
<i class="angle double up icon"></i>
|
||||
Pushed from Web
|
||||
</div>
|
||||
|
||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
|
||||
:class="{ 'disabled':this.searchParams.noteId }">
|
||||
<i class="folder open outline icon"></i>
|
||||
Note Files
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
|
||||
<!-- thicc form display -->
|
||||
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register()">
|
||||
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||
@@ -15,6 +15,11 @@
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="require2FA">
|
||||
<div class="ui input">
|
||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||
@@ -24,17 +29,10 @@
|
||||
<div class="ui fluid buttons">
|
||||
|
||||
|
||||
<div v-on:click="register()" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
|
||||
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
|
||||
<i class="plug icon"></i>
|
||||
Sign Up
|
||||
</div>
|
||||
|
||||
<div class="or"></div>
|
||||
|
||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui button">
|
||||
<i class="power icon"></i>
|
||||
Login
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,12 +47,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Thin form display -->
|
||||
<div v-if="thin" class="ui small form" v-on:keyup.enter="login()">
|
||||
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
|
||||
|
||||
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<div v-on:click="register()" class="ui green button">
|
||||
<div v-on:click="register" class="ui green button">
|
||||
<i class="plug icon"></i>
|
||||
Sign Up Now!
|
||||
</div>
|
||||
@@ -87,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-on:click="login()" class="ui fluid button">
|
||||
<div v-on:click="login" class="ui fluid button">
|
||||
<i class="power icon"></i>
|
||||
Login
|
||||
</div>
|
||||
@@ -128,6 +126,7 @@
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
authToken: '',
|
||||
require2FA: false,
|
||||
}
|
||||
@@ -160,13 +159,21 @@
|
||||
},
|
||||
register(){
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
let error = false
|
||||
|
||||
if(this.$route.name == 'LoginPage'){
|
||||
this.$bus.$emit('notification', 'Both a Username and Password are Required')
|
||||
return
|
||||
}
|
||||
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
|
||||
|
||||
this.$bus.$emit('notification', 'All fields are required.')
|
||||
error = true
|
||||
}
|
||||
|
||||
if( this.password !== this.password2 ){
|
||||
|
||||
this.$bus.$emit('notification', 'Passwords must be identical.')
|
||||
error = true
|
||||
}
|
||||
|
||||
if(error){
|
||||
//Login section
|
||||
this.$router.push('/login')
|
||||
return
|
||||
|
@@ -172,6 +172,10 @@
|
||||
|
||||
<span class="status-menu" v-on:click=" hash=0; save()">
|
||||
|
||||
<span v-if="idleNote" data-position="left center" data-tooltip="Idle: Awaiting Changes">
|
||||
<i class="vertically flipped grey wifi icon"></i>
|
||||
</span>
|
||||
|
||||
<span v-if="diffsApplied > 0">
|
||||
<i class="blue wave square icon"></i>
|
||||
+{{ diffsApplied }}
|
||||
@@ -347,6 +351,10 @@
|
||||
:class="{ 'fade-me-out':sizeDown }"
|
||||
v-on:click="closeButtonAction()"></div> -->
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -404,13 +412,11 @@
|
||||
pinned: 0,
|
||||
archived: 0,
|
||||
attachmentCount: 0,
|
||||
attachments: [],
|
||||
styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges
|
||||
|
||||
sizeDown: false, //Used to animate close state
|
||||
|
||||
//Settings vars
|
||||
lastVisibilityState: null,
|
||||
|
||||
//All the squire settings
|
||||
editor: null,
|
||||
usersOnNote: 0,
|
||||
@@ -426,6 +432,9 @@
|
||||
//Diff text/sync text variables
|
||||
diffTextTimeout: null,
|
||||
diffsApplied: null,
|
||||
idleNote: true, // If note is idle, get updates from server
|
||||
idleNoteTimeout: null,
|
||||
reloadNoteDebounce: null,
|
||||
|
||||
//Used to restore caret position
|
||||
lastRange: null,
|
||||
@@ -486,9 +495,12 @@
|
||||
|
||||
this.$bus.$off('new_file_upload')
|
||||
|
||||
this.destroyAttachmentStyles()
|
||||
|
||||
this.destroyWebSockets()
|
||||
|
||||
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
|
||||
window.removeEventListener('blur', this.windowBlurEvent)
|
||||
window.removeEventListener('focus', this.windowFocusEvent)
|
||||
|
||||
//Obliterate squire instance
|
||||
this.editor.destroy()
|
||||
@@ -504,7 +516,9 @@
|
||||
this.forceShowLoading = true
|
||||
}, 500)
|
||||
|
||||
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
|
||||
window.addEventListener('blur', this.windowBlurEvent)
|
||||
window.addEventListener('focus', this.windowFocusEvent)
|
||||
// this.logNoteInteraction()
|
||||
|
||||
//Init squire as early as possible
|
||||
if(this.editor && this.editor.destroy){
|
||||
@@ -685,13 +699,16 @@
|
||||
this.editor.addEventListener('keyup', event => {
|
||||
this.onKeyup(event)
|
||||
this.diffText(event)
|
||||
this.logNoteInteraction()
|
||||
})
|
||||
|
||||
this.editor.addEventListener('focus', e => {
|
||||
this.logNoteInteraction()
|
||||
// this.diffText(e)
|
||||
})
|
||||
|
||||
this.editor.addEventListener('blur', e => {
|
||||
this.idleNote = true
|
||||
this.diffText(e)
|
||||
})
|
||||
|
||||
@@ -811,6 +828,12 @@
|
||||
this.pinned = response.data.pinned
|
||||
}
|
||||
this.archived = response.data.archived
|
||||
|
||||
// Fetch attachmets if the count changed
|
||||
if(response.data.attachment_count > 0){
|
||||
this.getAttachments()
|
||||
}
|
||||
|
||||
this.attachmentCount = response.data.attachment_count
|
||||
|
||||
return true
|
||||
@@ -967,6 +990,9 @@
|
||||
// save editor HTML after change for future comparisons
|
||||
rawNoteText = editorElement.innerHTML
|
||||
|
||||
// update hash on patch
|
||||
this.lastNoteHash = this.hashString( rawNoteText )
|
||||
|
||||
this.$nextTick(() => {
|
||||
return resolve(true)
|
||||
})
|
||||
@@ -975,12 +1001,13 @@
|
||||
onKeyup(event){
|
||||
|
||||
this.statusText = 'modified'
|
||||
this.idleNote = false
|
||||
|
||||
//Save after x seconds
|
||||
clearTimeout(this.editDebounce)
|
||||
this.editDebounce = setTimeout(() => {
|
||||
this.save()
|
||||
}, 5 * 1000)
|
||||
}, 4 * 1000)
|
||||
|
||||
//Save after x keystrokes
|
||||
this.keyPressesCounter = (this.keyPressesCounter + 1)
|
||||
@@ -1013,6 +1040,7 @@
|
||||
}
|
||||
|
||||
//tell websockets to truncate history at this save
|
||||
this.lastNoteHash = currentHash //Update last saved note hash
|
||||
this.$io.emit('truncate_diffs_at_save', {'rawTextId':this.rawTextId, 'hash':currentHash })
|
||||
|
||||
const postData = {
|
||||
@@ -1033,44 +1061,54 @@
|
||||
this.modified = true
|
||||
this.diffsApplied = 0
|
||||
|
||||
//Update last saved note hash
|
||||
this.lastNoteHash = currentHash
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') })
|
||||
})
|
||||
},
|
||||
checkForUpdatedNote(){
|
||||
loadNoteNextFromServer(){
|
||||
|
||||
const now = +new Date
|
||||
//Only check every 3 seconds
|
||||
const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (2 * 1000)
|
||||
clearTimeout(this.reloadNoteDebounce)
|
||||
this.reloadNoteDebounce = setTimeout(() => {
|
||||
|
||||
// flash note text to show the update
|
||||
// this.setText('')
|
||||
|
||||
//If user leaves page then returns to page, reload the first batch
|
||||
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible' && checkForUpdateTimeout){
|
||||
|
||||
//Focus Regained on Note, check for update
|
||||
axios.post('/api/note/get', { 'noteId': this.noteid })
|
||||
.then(response => {
|
||||
|
||||
const serverTextHash = this.hashString( response.data.text )
|
||||
this.setupLoadedNoteData(response)
|
||||
|
||||
if(this.lastNoteHash != serverTextHash){
|
||||
// console.log('note was changed UPDATE THAT BITCH!!!!')
|
||||
this.setupLoadedNoteData(response)
|
||||
|
||||
//Manually set squire text to show
|
||||
this.setText(this.noteText)
|
||||
}
|
||||
//Manually set squire text to show
|
||||
this.setText(this.noteText)
|
||||
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}, 200)
|
||||
|
||||
//Keep track of visibility change and last interaction time
|
||||
this.lastVisibilityState = document.visibilityState
|
||||
this.lastInteractionTimestamp = +new Date
|
||||
},
|
||||
windowFocusEvent(){
|
||||
|
||||
//Only check if its been greater than a few seconds
|
||||
const now = +new Date
|
||||
const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (3 * 1000)
|
||||
|
||||
//If user leaves page then returns to page, reload the first batch
|
||||
if(checkForUpdateTimeout){
|
||||
|
||||
this.loadNoteNextFromServer()
|
||||
this.lastInteractionTimestamp = now
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
windowBlurEvent(){
|
||||
|
||||
this.idleNote = true
|
||||
this.lastInteractionTimestamp = +new Date
|
||||
|
||||
},
|
||||
hashString(inText){
|
||||
|
||||
@@ -1127,6 +1165,9 @@
|
||||
this.$io.removeListener('past_diffs')
|
||||
this.$io.removeListener('update_user_count')
|
||||
this.$io.removeListener('incoming_diff')
|
||||
this.$io.removeListener('update_note_attachments')
|
||||
|
||||
clearTimeout(this.idleNoteTimeout)
|
||||
},
|
||||
initWebsocketEvents(){
|
||||
|
||||
@@ -1159,6 +1200,31 @@
|
||||
})
|
||||
})
|
||||
|
||||
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||
|
||||
const sameIdCheck = (this.idleNote && this.noteid == noteId)
|
||||
const differentHashCheck = (hash != this.lastNoteHash)
|
||||
|
||||
// if hashes do not match, reload text from server
|
||||
if(sameIdCheck && differentHashCheck){
|
||||
this.loadNoteNextFromServer()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
this.$io.on('update_note_attachments', () => {
|
||||
this.getAttachments()
|
||||
})
|
||||
|
||||
},
|
||||
logNoteInteraction(){
|
||||
|
||||
this.idleNote = false
|
||||
clearTimeout(this.idleNoteTimeout)
|
||||
this.idleNoteTimeout = setTimeout(() => {
|
||||
this.idleNote = true
|
||||
}, 5000)
|
||||
|
||||
},
|
||||
saveCaretPosition(event){
|
||||
|
||||
@@ -1202,6 +1268,88 @@
|
||||
element.style.height = (element.scrollHeight) +'px'
|
||||
}
|
||||
},
|
||||
destroyAttachmentStyles(){
|
||||
// Remove attachment preview styles
|
||||
var head = document.head
|
||||
var styleElement = document.getElementById('attachmentGeneratedStyles'+this.noteid)
|
||||
if(styleElement){
|
||||
head.removeChild(styleElement)
|
||||
}
|
||||
},
|
||||
getAttachments(){
|
||||
|
||||
axios.post('/api/attachment/search', {'noteId':this.noteid})
|
||||
.then( results => {
|
||||
|
||||
|
||||
// generate new style group
|
||||
var style = document.createElement('style')
|
||||
style.id = 'attachmentGeneratedStyles'+this.noteid
|
||||
style.type = 'text/css'
|
||||
|
||||
// iterate attachments and build unique style for each
|
||||
let attachmentStyles = []
|
||||
results.data.forEach(attachment => {
|
||||
|
||||
// thumbnail location
|
||||
const bgurl = `/api/static/thumb_${attachment.file_location}`
|
||||
let padding = '2px 0 0'
|
||||
|
||||
// increase padding if there is a valid file
|
||||
if(attachment.file_location){
|
||||
padding = '13px 0 13px 155px'
|
||||
}
|
||||
|
||||
// unescaped characters will break content attribute
|
||||
const strippedText = attachment.text
|
||||
.replace(/'/g, "\\'") //Escape ' s
|
||||
.replace(/\n/g, '\\A') //Escape new lines
|
||||
|
||||
// strip down URL, *= matches anywhere is string
|
||||
const substringsToRemove = ['https://','http://','www.']
|
||||
var pattern = new RegExp(substringsToRemove.join('|'), 'g');
|
||||
const strippedurl = attachment.url
|
||||
.replace(pattern, '') // remove url protocol
|
||||
.replace(/&.*/, '') // remove anything after &
|
||||
|
||||
const cleanStyle = `
|
||||
.squire-box a[href*="${strippedurl}" i]::before {
|
||||
content: '${strippedText}';
|
||||
display: inline-block;
|
||||
padding: ${padding};
|
||||
pointer-events: none;
|
||||
font-size: 1.3em !important;
|
||||
width: 100%;
|
||||
background-position: left center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 140px auto;
|
||||
background-image: url(${bgurl});
|
||||
border-bottom: solid 1px var(--main-accent);
|
||||
margin-bottom: -10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.squire-box a[href*="${strippedurl}" i] {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
`
|
||||
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
|
||||
.replace(/\s+/g, ' ') // remove double spaces
|
||||
|
||||
attachmentStyles.push(cleanStyle)
|
||||
})
|
||||
|
||||
// Destroy just before creating new to prevent page jumping
|
||||
this.destroyAttachmentStyles()
|
||||
|
||||
// push new styles into <head>
|
||||
style.innerHTML = attachmentStyles.join(' ')
|
||||
document.head.appendChild(style)
|
||||
|
||||
})
|
||||
.catch(error => { console.log(error);this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="button-fix">
|
||||
<div class="ui right floated basic shrinking icon button" v-on:click="showPasteInputArea">
|
||||
<i class="paste icon"></i>
|
||||
<i class="green paste icon"></i>
|
||||
Paste
|
||||
</div>
|
||||
<div class="shade" v-if="showPasteArea" @click.prevent="close">
|
||||
|
@@ -8,6 +8,13 @@
|
||||
<div class="content">
|
||||
Files
|
||||
<div class="sub header">Uploaded Files and Websites from notes.</div>
|
||||
<div class="sub header">
|
||||
<i class="green angle double up icon icon"></i>
|
||||
<router-link
|
||||
to="/bookmarklet">
|
||||
Push any website to solid scribe
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
@@ -125,6 +132,11 @@
|
||||
//Load more attachments on scroll
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.on('update_note_attachments', () => {
|
||||
this.reset()
|
||||
this.searchAttachments()
|
||||
})
|
||||
|
||||
//Mount notes on load if note ID is set
|
||||
this.searchAttachments()
|
||||
},
|
||||
@@ -132,6 +144,8 @@
|
||||
|
||||
//Remove scroll event on destroy
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.removeListener('update_note_attachments')
|
||||
},
|
||||
watch:{
|
||||
$route (to, from){
|
||||
|
66
client/src/pages/BookmarkletPage.vue
Normal file
66
client/src/pages/BookmarkletPage.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="text-container squire-box">
|
||||
|
||||
<h2 class="ui header">
|
||||
<i class="green angle double up icon icon"></i>
|
||||
<div class="content">
|
||||
Push URL to Solid Scribe - Bookmarklet
|
||||
<div class="sub header">Push any website to your file list.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<p>A bookmarklet is a small piece of code that can be run from a bookmark.</p>
|
||||
<p>Use the bookmarklet below to push URLs of website to solid scribe for later</p>
|
||||
<p>The bookmarklet works in a secure way and won't leak any data.</p>
|
||||
<p>To install the bookmarklet, all you need to do is drag it to your bookmarks bar.</p>
|
||||
|
||||
<h2>
|
||||
Drag the link below to your bookmarks.
|
||||
</h2>
|
||||
<h3>
|
||||
<a :href="`${(bookmarkletscript)}`" class="ui huge text">Push to SolidScribe</a>
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
loading: true,
|
||||
bookmarkletscript:'',
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
// Perform Login check
|
||||
this.$parent.loginGateway()
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
this.getBookmarklet()
|
||||
},
|
||||
beforeDestroy(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
getBookmarklet(){
|
||||
|
||||
this.loading = true
|
||||
axios.post('/api/attachment/getbookmarklet')
|
||||
.then( results => {
|
||||
|
||||
this.bookmarkletscript = results.data
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to get bookmarklet') })
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -13,6 +13,7 @@ const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/Note
|
||||
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
|
||||
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
|
||||
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
|
||||
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
|
||||
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
|
||||
|
||||
Vue.use(Router)
|
||||
@@ -67,6 +68,12 @@ export default new Router({
|
||||
meta: {title:'Terms'},
|
||||
component: TermsPage
|
||||
},
|
||||
{
|
||||
path: '/bookmarklet',
|
||||
name: 'Bookmarklet',
|
||||
meta: {title:'Bookmarklet'},
|
||||
component: BookmarkletPage
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
|
Reference in New Issue
Block a user