* Adjusted theme colors to add more contrast on white theme while making black more OLED friendly

* Links now get an underline on hover
* Cleaned up CSS variable names, added another theme color for more control
* Cleaned up unused CSS, removed scrollbars popping up, tons of other little UI tweaks
* Renamed shared notes to inbox
* Tweaked form display, seperated login and create accouts
* Put login/sign up form on home page
* Created more legitimate marketing for home page
* Tons up updates to note page and note input panel
* Better support for two users editing a note
* MUCH better diff handling, web sockets restore notes with unsaved diffs
* Moved all squire text modifier functions into a mixin class
* It now says saving when closing a note
* Lots of cleanup and better handiling of events on mount and destroy
* Scroll behavior modified to load notes when closer to bottom of page
* Pretty decent shared notes and sharable link support
* Updated help text
* Search now includes tag suggestions and attachment suggestions
* Cleaned up scratch pad a ton, allow for users to create new scratch pads
* Created a 404 Page and a Shared note page
* So many other small improvements. Oh my god, what is wrong with me, not doing commits!?
This commit is contained in:
Max G 2020-06-07 20:57:35 +00:00
parent 8e5e06be9b
commit 6bb856689d
31 changed files with 1605 additions and 1095 deletions

View File

@ -3,6 +3,11 @@ import Vue from 'vue'
const helpers = {}
helpers.timeAgo = (time) => {
if(time.toString().length >= 13){
time = Math.round(time/1000)
}
const time_formats = [
[ 60, 'seconds', 1 ],
[ 120, '1 minute ago', '1 minute from now' ],

View File

@ -17,10 +17,11 @@
:root {
--background_color: #fff;
--body_bg_color: #f5f6f7;
--small_element_bg_color: #fff;
--text_color: #3d3d3d;
--outline_color: rgba(34,36,38,.15);
--border_color: rgba(34,36,38,.20);
--dark_border_color: #DFE1E6;
--border_color: #DFE1E6;
/* Global purple menu styles */
--menu-border: #534c68;
@ -34,7 +35,9 @@
html {
/*scrollbar-width: none;*/
}
a:hover {
text-decoration: underline;
}
div.ui.basic.segment.no-fluf-segment {
margin-top: 0px;
}
@ -59,21 +62,21 @@ div.ui.basic.segment.no-fluf-segment {
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
body {
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--body_bg_color);
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.ui.segment {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color);
}
.button-sub {
display: inline-block;
width: 100%;
font-size: 0.9em;
color: white;
opacity: 0.8;
color: grey;
opacity: 0.9;
padding: 4px 0 0 0;
text-align: center;
}
@ -83,71 +86,43 @@ body {
.ui.form textarea:not([type]),
.ui.form textarea:not([type]):focus {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color);
}
.ui.basic.label, .ui.header, .ui.header div.sub.header {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
background-color: transparent;
border-color: var(--dark_border_color);
}
.ui.icon.input > i.icon {
color: var(--text_color);
}
div.ui.basic.green.label {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
}
.ui.basic.button, .ui.basic.buttons .button {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important;
border: 1px solid;
border-color: var(--border_color) !important;
border-color: var(--dark_border_color) !important;
box-shadow: none;
}
.ui.basic.button:focus, .ui.basic.button:hover {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important;
box-shadow: none;
}
.ui.tabular.menu .item {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important;
}
.ui.tabular.menu .item.active {
background-color: var(--background_color) !important;
background-color: var(--small_element_bg_color) !important;
color: var(--text_color) !important;
border-color: var(--border_color) !important;
border-color: var(--dark_border_color) !important;
}
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
/* Styles for public display pages */
.fun {
color: rgba(0, 0, 0, 0.87);
color: var(--text_color);
}
.fun h1 {
font-size: 2em;
}
.fun h2 {
font-size: 1.9em;
}
.fun h3 {
font-size: 1.7em;
}
.fun p {
/*font-size: 1.5em;*/
}
.fun blockquote {
border-left: 5px solid cornflowerblue;
padding-left: 25px;
margin-left: 5px;
}
/* Styles for public display pages */
a:hover {
text-decoration: underline;
}
/*//
// Purple Global Menu
//*/
@ -255,6 +230,8 @@ a:hover {
word-wrap: break-word;
/*border-bottom: 1px solid #ccc;*/
scrollbar-width: none;
scrollbar-color: transparent transparent;
caret-color: #21BA45;
}
.squire-box::selection,
.squire-box::-moz-selection {
@ -271,19 +248,15 @@ a:hover {
.squire-box a {
cursor: pointer;
}
.note-card-text i:not(.icon),
.squire-box i {
padding: 0.5em 0.99em;
border-radius: 1px;
display: inline-block;
font-style: normal;
background-color: rgba(113, 113, 113, 0.1);
}
.night-mode .note-card-text i:not(.icon),
.night-mode .squire-box i {
background-color: rgba(255, 255, 255, 0.2);
}
.note-card-text pre,
.squire-box pre {
word-wrap: break-word;
}
.note-card-text p,
.squire-box p {
margin-bottom: 0;

View File

@ -6,6 +6,7 @@
display: inline-block;
border: 1px solid;
border-color: var(--border_color);
background-color: var(--small_element_bg_color);
border-radius: 4px;
margin: 0 0 15px;
max-height: 10000px;

View File

@ -40,7 +40,7 @@
let filter = {}
filter[option] = 1
this.$bus.$emit('update_fast_filters', filter)
// this.$bus.$emit('update_fast_filters', filter)
}
}
}
@ -62,7 +62,7 @@
.filter-menu {
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
border: 1px solid;

View File

@ -20,16 +20,16 @@
}
.menu-logo-display {
width: 25px;
margin: 5px 0 0 34px;
margin: 5px 0 0 42px;
display: inline-block;
}
.menu-item {
color: #fff;
padding: 0.8em 10px 0.8em 10px;
padding: 9px 10px;
display: inline-block;
width: 100%;
font-size: 1.15em;
font-size: 1.1em;
box-sizing: border-box;
}
.menu-item i.icon {
@ -76,7 +76,7 @@
left: 0;
right: 0;
z-index: 999;
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
border-bottom: 1px solid;
border-color: var(--border_color);
padding: 5px 1rem 5px;
@ -117,9 +117,9 @@
<i class="green bars icon"></i>
</div>
<router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
<!-- <router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="green home icon"></i>
</router-link>
</router-link> -->
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
<i class="open folder outline icon"></i>
@ -184,6 +184,16 @@
<counter v-if="$store.getters.totals && $store.getters.totals['totalNotes']" class="float-right" number-id="totalNotes" />
</router-link>
<div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
<i class="grey mail outline icon"></i>Inbox
</div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
<i class="grey archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</div>
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
<i class="grey trash alternate outline icon"></i>Trashed
</div>
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
<!-- <div v-on:click="updateFastFilters(1)" class="menu-item menu-button sub"><i class="grey tags icon"></i>Tags</div> -->
@ -218,7 +228,7 @@
<span v-if="$store.getters.getIsNightMode == 0">
<i class="moon outline icon"></i>Black Theme</span>
<span v-if="$store.getters.getIsNightMode == 1">
<i class="moon outline icon"></i>Night Theme</span>
<i class="moon outline icon"></i>Flux Theme</span>
<span v-if="$store.getters.getIsNightMode == 2">
<i class="moon outline icon"></i>Light Theme</span>
</div>
@ -257,7 +267,7 @@
},
data: function(){
return {
version: '2.2.3',
version: '2.3.4',
username: '',
collapsed: false,
mobile: false,
@ -354,27 +364,17 @@
//Reloads note page to initial state
this.$bus.$emit('note_reload')
},
updateFastFilters(index){
updateFastFilters(filterIndex){
//A little hacky, brings user to notes page then filters on click
if(this.$route.name != 'NotesPage'){
if(this.$route.name != 'Note Page'){
this.$router.push('/notes')
setTimeout( () => {
this.updateFastFilters(index)
this.$bus.$emit('update_fast_filters', filterIndex)
}, 500 )
} else {
this.$bus.$emit('update_fast_filters', filterIndex)
}
const options = [
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
]
let filter = {}
filter[options[index]] = 1
this.$bus.$emit('update_fast_filters', filter)
},
reloadPage(){
location.reload(true)

View File

@ -33,29 +33,17 @@
export default {
name: 'LoadingIcon',
props:[ 'message' ],
data () {
return {
items: []
}
},
beforeMount(){
},
mounted(){
},
methods: {
onClickTag(index){
console.log('yup')
},
}
}
</script>
<style type="text/css" scoped>
.loading-container {
text-align: center;
width: 100%;
height: 100px;
min-height: 100px;
margin: 20px 0;
padding: 40px;
border-radius: 7px;
background-color: var(--small_element_bg_color);
}
.loading-container svg {
width: 60px;

View File

@ -1,11 +1,12 @@
<template>
<div v-on:keyup.enter="submit()">
<div v-on:keyup.enter="login()">
<!-- thicc form display -->
<div v-if="!thin" class="ui large form">
<div class="field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
</div>
</div>
<div class="field">
@ -13,27 +14,48 @@
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Sign Up / Login</div>
</div>
<div v-if="thin" class="ui small form">
<div class="fields">
<div class="six wide field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
</div>
<div class="sixteen wide field">
<div class="ui fluid buttons">
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
<i class="power icon"></i>
Login
</div>
<div class="six wide field">
<div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="four wide field">
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui fluid green submit button">Sign Up / Login</div>
<div class="or"></div>
<div v-on:click="register()" class="ui button">
<i class="plug icon"></i>
Sign Up
</div>
</div>
</div>
</div>
<!-- Thin form display -->
<div v-if="thin" class="ui small form">
<div class="fields">
<div class="four wide field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
</div>
</div>
<div class="four wide field">
<div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="four wide field">
<div v-on:click="register()" class="ui fluid green button">
<i class="plug icon"></i>
Sign Up
</div>
</div>
<div class="four wide field">
<div v-on:click="login()" class="ui fluid button">
<i class="power icon"></i>
Login
</div>
</div>
</div>
</div>
@ -65,43 +87,64 @@
}
},
methods: {
submit(){
finalizeLogin(data){
//Both fields are required
if(this.username <= 0){
return false
}
if(this.password <= 0){
return false
//Destroy local data if there is an error
if(data == false){
this.$store.commit('destroyLoginToken')
return
}
let vm = this
//Login user if we have a valid token
if(data && data.token && data.token.length > 0){
const token = data.token
const username = this.username
let data = {
username: this.username,
password: this.password
this.$store.commit('setLoginToken', {token, username})
//Setup socket io after user logs in
this.$io.emit('user_connect', token)
//Redirect user to notes section after login
this.$router.push('/notes')
}
},
register(){
if( this.username.length == 0 || this.password.length == 0 ){
this.$bus.$emit('notification', 'Username and Password Required')
return
}
axios.post('/api/user/login', data)
.then(response => {
if(response.data.success){
const token = response.data.token
const username = response.data.username
const masterKey = response.data.masterKey
axios.post('/api/user/register', {'username': this.username, 'password': this.password})
.then(({data}) => {
this.$store.commit('setLoginToken', {token, username, masterKey})
//Setup socket io after user logs in
this.$io.emit('user_connect', token)
//Redirect user to notes section after login
this.$router.push('/notes')
} else {
// this.password = ''
this.$bus.$emit('notification', 'Incorrect Username or Password')
vm.$store.commit('destroyLoginToken')
if(data == false){
this.$bus.$emit('notification', 'Username already in use')
}
this.finalizeLogin(data)
})
.catch(error => {
this.$bus.$emit('notification', 'Username already in use')
})
},
login(){
if( this.username.length == 0 || this.password.length == 0 ){
this.$bus.$emit('notification', 'Username and Password Required')
return
}
axios.post('/api/user/login', {'username': this.username, 'password': this.password})
.then(({data}) => {
if(data == false){
this.$bus.$emit('notification', 'Incorrect Username or Password')
}
this.finalizeLogin(data)
})
.catch(error => {
this.$bus.$emit('notification', 'Incorrect Username or Password')

File diff suppressed because it is too large Load Diff

View File

@ -323,7 +323,7 @@
height: 40px;
padding: 10px 15px;
cursor: pointer;
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
color: var(--text_color);
}
.suggestion-item.active {

View File

@ -242,11 +242,11 @@
justClosed(){
//Scroll note into view
this.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
// this.$el.scrollIntoView({
// behavior: 'smooth',
// block: 'center',
// inline: 'center'
// })
//After scroll, trigger green outline animation
setTimeout(() => {
@ -353,7 +353,7 @@
display: inline-block;
min-width: 30px;
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
}
.subtext {
display: inline-block;
@ -368,7 +368,7 @@
}
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
/*font-size: 1.0em !important;*/
font-size: 15px !important;
font-size: 16px !important;
}
.small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em;
@ -410,19 +410,17 @@
.note-title-display-card {
position: relative;
/*box-shadow: 0 1px 3px 0 rgba(34,36,38,.15);*/
/*box-shadow: 0 0px 5px 1px rgba(34,36,38,0);*/
/*box-shadow: 0 1px 3px 0 rgba(34,36,38,.15);*/
box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);
transition: box-shadow ease 0.3s;
background-color: var(--small_element_bg_color);
/*The subtle shadow*/
/*box-shadow: 0px 1px 2px 1px rgba(210, 211, 211, 0.46);*/
transition: box-shadow ease 0.5s, transform linear 0.1s;
margin: 5px;
/*padding: 0.7em 1em;*/
border-radius: .28571429rem;
border: 1px solid transparent;
/*border-color: var(--border_color);*/
border-color: var(--border_color);
/*width: calc(33.333% - 10px);*/
width: calc(25% - 10px);
max-width: 300px;
min-width: 190px;
min-height: 130px;
/*transition: box-shadow 0.3s;*/
@ -436,13 +434,15 @@
text-align: left;
}
.note-title-display-card:hover {
box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);
/*box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);*/
/*transform: translateY(-2px);*/
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.note-title-display-card.title-view {
width: 100%;
min-height: 10px;
min-height: 20px;
max-width: none;
box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
}
.single-line-text {
@ -527,6 +527,7 @@
.one-column .note-title-display-card {
width: 100%;
max-width: none;
/*margin: 0px -5px 10px -5px;*/
}
.overflow-hidden {
overflow: hidden;
@ -561,11 +562,15 @@
}
/* Tweak mobile display to show only one column */
@media only screen and (min-width: 1500px) {
.note-title-display-card {
width: calc(20% - 10px);
}
}
@media only screen and (max-width: 740px) {
.note-title-display-card {
width: calc(100% + 10px);
margin: 0px -5px 10px -5px;
max-width: none;
}
}

View File

@ -71,14 +71,6 @@
</div>
</div>
<div class="ui very compact grid" v-if="tagSuggestions.length > 0">
<div class="sixteen wide column">
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagClick(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
</div>
</div>
</div>
</div>
@ -141,11 +133,7 @@
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
.then( response => {
//Open Quick Note
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
this.$bus.$emit('notification', 'Saved To Scratch Pad')
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
},
@ -173,20 +161,20 @@
clearTimeout(this.tagSearchDebounce)
if(this.searchTerm.length == 0){
this.tagSuggestions = []
return
}
// if(this.searchTerm.length == 0){
// this.tagSuggestions = []
// return
// }
this.tagSearchDebounce = setTimeout(() => {
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
// this.tagSearchDebounce = setTimeout(() => {
// this.tagSuggestions = []
// axios.post('/api/tag/suggest', postData)
// .then( response => {
this.tagSuggestions = response.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
}, 800)
// this.tagSuggestions = response.data
// })
// .catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
// }, 800)
},
onKeyDown(event){

View File

@ -5,7 +5,30 @@
<template>
<div>
<div class="ui grid" v-if="this.shareUsername == null">
<div class="ui grid" v-if="shareUsername == null">
<div v-if="!isNoteShared" class="sixteen wide column">
<div class="ui button" v-on:click="makeShared()">Enable Shared</div>
<p>Shared notes are different and junk.</p>
</div>
<div v-if="isNoteShared" class="sixteen wide column">
<p>Generating a shared URL will expose the password of this note.</p>
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
<div v-if="sharedUrl.length > 0">
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
<div class="ui input">
<input type="text" v-model="sharedUrl">
</div>
</div>
</div>
</div>
<div class="ui grid" v-if="shareUsername == null">
<div class="row">
<div class="eight wide column">
@ -38,7 +61,7 @@
</div>
<div class="ui grid" v-if="this.shareUsername != null">
<div class="ui grid" v-if="shareUsername != null">
<div class="sixteen wide column">
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
</div>
@ -56,10 +79,12 @@
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
data () {
return {
isNoteShared: false,
sharedWithUsers: [],
shareUserInput: '',
debounce: null,
enableSubmitShare: false,
sharedUrl: '',
}
},
beforeMount(){
@ -67,6 +92,8 @@
},
mounted(){
// this.isNoteShared = this.noteShared
if(this.shareUsername == null){
this.loadShareList()
}
@ -74,12 +101,39 @@
},
methods: {
loadShareList(){
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
axios.post('/api/note/getshareinfo', {'noteId':this.noteId, 'rawTextId':this.rawTextId })
.then( ({data}) => {
this.sharedWithUsers = data
this.isNoteShared = (data.shareStatus == 2)
this.sharedWithUsers = data.shareUsers
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') })
},
makeShared(){
axios.post('/api/note/enableshare', {'noteId':this.noteId })
.then( ({data}) => {
this.isNoteShared = true
})
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
},
removeShared(){
axios.post('/api/note/disableshare', {'noteId':this.noteId })
.then( ({data}) => {
this.isNoteShared = false
})
.catch(error => { this.$bus.$emit('notification', 'Failed to remove share status') })
},
getSharedUrl(){
axios.post('/api/note/getsharekey', {'noteId':this.noteId })
.then( ({data}) => {
const encodedKey = encodeURIComponent(data)
this.sharedUrl = `${window.location.protocol}//${window.location.hostname}/#/public/note/${this.noteId}/${encodedKey}`
})
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
},
onRevokeAccess(sharedNoteId){
const postData = {

View File

@ -10,14 +10,14 @@
height: 100%;
color: var(--text_color);
background-color: var(--background_color);
background-color: var(--small_element_bg_color);
}
.slide-content {
box-sizing: border-box;
/*padding: 1em 1.5em;*/
height: calc(100% - 43px);
border-right: 1px solid var(--menu-border);
/*background-color: var(--background_color);*/
/*background-color: var(--small_element_bg_color);*/
overflow-x: scroll;
}
.slide-shadow {

View File

@ -0,0 +1,344 @@
const SquireButtonFunctions = {
data(){
return {
//active button states
activeBold: false,
activeItalics: false,
activeUnderline: false,
activeTitle: false,
activeList: false,
activeToDo: false,
activeColor: null,
}
},
methods: {
//
// Inside squire init function
//
pathChangeEvent(e){
//Reset all button states
this.activeBold = false
this.activeTitle = false
this.activeItalics = false
this.activeList = false
this.activeToDo = false
this.activeColor = null
this.activeUnderline = false
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
this.activeUnderline = true
}
if(e.path.indexOf('>B>') > -1 || e.path.search(/B$/) > -1){
this.activeBold = true
}
if(e.path.indexOf('>I') > -1){
this.activeItalics = true
}
if(e.path.indexOf('fontSize') > -1){
this.activeTitle = true
}
if(e.path.indexOf('OL>LI') > -1){
this.activeList = true
}
if(e.path.indexOf('UL>LI') > -1){
this.activeToDo = true
}
const colorIndex = e.path.indexOf('color=')
if(colorIndex > -1){
//Get all digigs after color index, then limit to 3
let colors = e.path.substring(colorIndex).match(/\d+/g).slice(0,3)
this.activeColor=`rgb(${colors.join(',')})`
}
},
//
//Inside Squire Init
//
removeFormatting(){
this.selectLineIfNoSelect()
this.editor.removeAllFormatting()
},
//If nothing is selected, select the entire line
selectLineIfNoSelect(){
//Select entire line if range is not set
let selection = this.editor.getSelection()
if(selection.startOffset == selection.endOffset && selection.startContainer == selection.endContainer){
let squireRange = this.editor.createRange(
selection.startContainer, 0,
selection.endContainer, selection.commonAncestorContainer.textContent.length)
this.editor.setSelection(squireRange)
}
},
modifyFont(inSize){
this.selectLineIfNoSelect()
let fontInfo = this.editor.getFontInfo()
//Toggle font size between large and normal
if(fontInfo.size){
this.editor.setFontSize(null)
} else {
this.editor.setFontSize(inSize)
}
},
modifyColor(color){
this.selectLineIfNoSelect()
//Set color of font
this.editor.setTextColour(color)
},
toggleList(type){
//Undo list if its already a lits
if(this.editor.hasFormat(type)){
this.editor.removeList()
return
}
if(type == 'ol'){
this.editor.makeOrderedList()
}
if(type == 'ul'){
this.editor.makeUnorderedList()
}
},
toggleUnderline(){
this.selectLineIfNoSelect()
if( this.editor.hasFormat('u') ){
this.editor.removeUnderline()
} else {
this.editor.underline()
}
},
toggleBold(){
this.selectLineIfNoSelect()
if( this.editor.hasFormat('b') ){
this.editor.removeBold()
} else {
this.editor.bold()
}
},
toggleItalic(){
this.selectLineIfNoSelect()
if( this.editor.hasFormat('i') ){
this.editor.removeItalic()
} else {
this.editor.italic()
}
},
undoCustom(){
//The same as pressing CTRL + Z
// this.editor.focus()
// document.execCommand("undo", false, null)
this.editor.undo()
},
uncheckAllListItems(){
//
// Uncheck All List Items
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
},
deleteCompletedListItems(){
//
// Delete Completed List Items
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(!checkedItem){
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
},
sortList(){
//
// Sort list, checked at the bottom, unchecked at the top
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
}
} else {
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
},
calculateMath(){
//
// Find math in note and calculate the outcome
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
// simple function that trys to evaluate javascript
const shittyMath = (string) => {
//Remove all chars but math chars
const cleanString = String(string).replace(/[a-zA-Z\s]*/g,'')
try {
return Function('"use strict"; return (' + cleanString + ')')();
} catch (error) {
console.log('Math Error: ', string)
return null
}
}
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
const line = node.innerText.trim()
// = sign exists and its the last character in the string
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
//Pull out everything before the formula and try to evaluate it
const formula = line.split('=').shift()
const output = shittyMath(formula)
//If its a number and didn't throw an error, update the line
if(!isNaN(output) && output != null){
//Since there is HTML in the line, splice in the number after the = sign
let equalLocation = node.innerHTML.indexOf('=')
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Slam in that new HTML with the output
node.innerHTML = newLine
}
}
})
},
setText(inText){
this.editor.setHTML(inText)
// this.noteText = this.editor._getHTML()
// this.diffNoteText = this.editor._getHTML()
},
getText(){
return this.editor.getHTML()
},
},
}
export default SquireButtonFunctions

View File

@ -11,7 +11,7 @@
<!-- Content copied from note -->
<!-- https://www.solidscribe.com/#/notes/open/552 -->
<p><b>Every Note is Encrypted</b><br></p><p>Only you can read your notes. Even if every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p><p><br></p><p><b>Some Data is not encrypted</b><br></p><p>Everything isn't encrypted, to keep up ease of use. Files, Tags and Attachments are not encrypted.<br></p><p><br></p><p><b>Searching is somewhat limited</b><br></p><p>Since every note is encrypted, searching is limited. To maintain security, only single words can be searched. Your search index is private and Encrypted.<br></p><p><br></p><p><b>Quick Note</b><br></p><p>The Quick note feature was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p><p>All data pushed to the quick note can still be edited like a normal note.<br></p><p><br></p><p><b>Dark Theme</b><br></p><p>Dark theme was designed to minimize the amount of blue. Less blue entering your eyes is supposed to help you fall asleep.<br></p><p>Most things turn sepia and a filter is applied to images to make them more sepia.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p><p><br></p><p><b>Password Protected Notes</b><br></p><p>Note protected with a password are encrypted. This means the data is scrambled and unreadable unless the correct password is used to decrypt them.<br></p><p>If a password is forgotten, it can never be recovered. Passwords are not saved for encrypted notes. If you lose the password to a protected note, that note text is lost. <br></p><p>Only the text of the note is protected. Tags, Files attached to the note, and the title of the note are still visible without a password. You can not search text in a password protected note. But you can search by the title.<br></p><p><br></p><p><b>Links in notes</b><br></p><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p><br></p><p><b>Files in notes</b><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have the text pulled out so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time.<br></p><p><br></p><p><b>Deleting notes</b><br></p><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p><p><br></p><p><b>Daily Backups</b><br></p><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p>
<p><b>Only Note Text is Encrypted</b><br></p><p>Only you can read your notes. Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. Even if every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p><p><br></p><p><b>Some Data is not encrypted</b><br></p><p>Everything isn't encrypted, to keep up ease of use. Files, Tags and Attachments are not encrypted.<br></p><p><br></p><p><b>Searching is somewhat limited</b><br></p><p>Since every note is encrypted, searching is limited. To maintain security, only single words can be searched. Your search index is private and Encrypted.<br></p><p><br></p><p><b>The Scrat</b><b></b><b>ch Pad</b><b></b><br></p><p>The Scratch Pad was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p><ul><li>All data pushed to the quick note can still be edited like a normal note.<br></li><li>Creating a new quick note will not erase your old <br></li></ul><p><br></p><p><b>Flux Theme</b><br></p><p>Flux theme limits the amount of blue emitted by your screen. Most things turn sepia and a filter is applied to images to make them more sepia. Less blue light at night is supposed to be helpful for falling asleep.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p><p><br></p><p><b>Keyboard Shor</b><b></b><b>tcuts</b><br></p><p>Number List - CTRL + SHIFT + 9<br></p><p>Todo List - CTRL + SHIFT + 8<br></p><p>Underline CTRL + u<br></p><p>Bold - CTRL + b<br></p><p>Quote - CRTL + i<br></p><p>Indent - CTL + ]<br></p><p>Outdent - CRTL + [<br></p><p>Undo - CTRL + z<br></p><p>Redo - CTRL + y<br></p><p><br></p><p><b>Links in notes</b><br></p><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. You can edit the text of scarped links and any time and search for it later. <br></p><p><br></p><p><b>Files in notes</b><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have text scanned, so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time.<br></p><p><br></p><p><b>Deleting notes</b><br></p><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p><p><br></p><p><b>Daily Backups</b><br></p><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p>
<!-- content copied from note -->
</div>

View File

@ -78,6 +78,10 @@
.home-main img {
max-height: 400px !important;
}
.white-link {
text-decoration: underline;
color: white;
}
</style>
@ -120,7 +124,7 @@
</h2>
<h3 class="subtext">
An easy, free, secure Note App<i class="i cursor icon blinking"></i>
A free, secure Note App<i class="i cursor icon blinking"></i>
</h3>
</div>
@ -146,7 +150,7 @@
<div class="ui text container">
<h2>
<i class="plug icon"></i>
Sign Up Now - Only a Username and Password are required.</h2>
Sign Up Now - Only a Username and Password required</h2>
<login-form :thin="true" />
</div>
</div>
@ -155,7 +159,7 @@
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide right aligned column">
<h2>Solid Scribe is an online note application that focuses on ease of use and security</h2>
<h2>Solid Scribe is a browser based note application that focuses on ease of use while keeping your data private</h2>
<h3>Tools to organize and collaborate on notes while maintaining security and respecting your privacy.</h3>
</div>
<div class="six wide column">
@ -163,24 +167,25 @@
</div>
</div>
<div class="middle aligned centered row">
<div class="middle aligned centered green row">
<div class="six wide right aligned column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
</div>
<div class="six wide column">
<h2>Tools to organize thousands of notes</h2>
<h3>Tag, Pin, Color, Archive, Attach Images and Search notes or links in notes</h3>
<h2>All Note text is encrypted</h2>
<h3>Only you can read your notes. <a class="white-link" target="_blank" href="https://www.forbes.com/sites/zakdoffman/2019/01/30/facebook-has-just-been-caught-spying-on-users-private-messages-and-data-again/#1e27e00a31ce"> Employees can not snoop your account</a>. <a class="white-link" target="_blank" href="https://mashable.com/article/google-reading-your-emails-response/">No one can read your data for advertising</a>. Note text is completely unreadable without your password.</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered green row">
<div class="middle aligned centered row">
<div class="six wide column">
<h2>Privacy through Encryption</h2>
<h3>All notes are encrypted. No one can read your notes, even if they steal the data from the database.</h3>
<h2>Organize your notes</h2>
<h3>Tag, Pin, Color, Archive, Attach Images, Share Encrypted Notes and Search</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
</div>
</div>
@ -189,7 +194,7 @@
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
</div>
<div class="six wide column">
<h2>Extremely accessible</h2>
<h2>Extremely accessible - Nothing to install</h2>
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
</div>
</div>
@ -198,7 +203,7 @@
<div class="middle aligned centered row">
<div class="six wide right aligned column">
<h2>Secure Search</h2>
<h3>Keyword search using an encrypted search index helps you find what you need without compromising security</h3>
<h3>Keyword search using an encrypted search index helps you find what you need without compromising security.</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
@ -210,16 +215,16 @@
<img loading="lazy" width="100%" src="/api/static/assets/marketing/plan.svg" alt="Scheme for planetary destruction">
</div>
<div class="six wide column">
<h2>Embrace the Void</h2>
<h3>Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss</h3>
<h2>Create Lists with Check Boxes</h2>
<h3>Todo lists are supported. With options to removed checked items, sort by completed and un-check all.</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<div class="six wide right aligned column">
<h2>Space for Growth</h2>
<h3>Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol</h3>
<h2>Powerful Text Editing</h2>
<h3>A plethora of editing tools are provided for coloring, underlining, bolding, attaching images and more.</h3>
</div>
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/growth.svg" alt="Endless progress at the cost of sanity and health">
@ -231,13 +236,13 @@
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
</div>
<div class="six wide column">
<h2>Become your Data</h2>
<h3>We exist as electrical impulses, no different from data on a computer</h3>
<h2>Secure Data Sharing</h2>
<h3>Share notes with friends without compromising privacy. The data remains encrypted with a shared password for you and people you invite to view it.</h3>
</div>
</div>
<!-- set -->
<div class="middle aligned centered row">
<!-- <div class="middle aligned centered row">
<div class="six wide right aligned column">
<h2>Ice Cream</h2>
<h3>Get excited without all the screaming</h3>
@ -265,7 +270,7 @@
<div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
</div>
</div>
</div> -->
<!-- final slide -->
<div class="middle aligned centered green row">
@ -282,20 +287,18 @@
<br>
<br>
<br>
OR
<br>
<br>
<br>
<span class="ui button" v-on:click="showRealInformation">View real information about this site</span>
<span class="ui button" v-on:click="showRealInformation">About</span>
</div>
</div>
<div v-if="realInformation" class="middle aligned centered row" ref="real">
<div class="six wide column">
<h2 class="ui center aligned">
What is this really?
Why Does this App exist?
</h2>
<h3>Its just a little web app for taking notes. This page is mocking the "over the top" marketing sites use to sell their products.</h3>
<p>
This App exists because I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
</p>

View File

@ -10,7 +10,7 @@
<div class="ui text container">
<div class="ui segment" v-on:keyup.enter="submit">
<div class="ui segment">
<h4 class="ui header">
<i class="plug icon"></i>
@ -44,53 +44,10 @@
data () {
return {
enabled: false,
username: '',
password: ''
}
},
methods: {
submit(){
//Both fields are required
if(this.username <= 0){
return false
}
if(this.password <= 0){
return false
}
let vm = this
let data = {
username: this.username,
password: this.password
}
axios.post('/api/user/login', data)
.then(response => {
if(response.data.success){
const token = response.data.token
const username = response.data.username
const masterKey = response.data.masterKey
this.$store.commit('setLoginToken', {token, username, masterKey})
//Setup socket io after user logs in
this.$io.emit('user_connect', token)
//Redirect user to notes section after login
this.$router.push('/notes')
} else {
// this.password = ''
this.$bus.$emit('notification', 'Incorrect Username or Password')
vm.$store.commit('destroyLoginToken')
}
})
.catch(error => {
this.$bus.$emit('notification', 'Incorrect Username or Password')
})
}
}
}
</script>

View File

@ -0,0 +1,30 @@
<template>
<div class="ui basic segment">
<div class="ui grid">
<div class="sixteen wide column">
<div class="ui text container">
<h2 class="ui dividing header">
Page Not Found
</h2>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'NotFoundPage',
props:[ 'message' ],
data () {
return {
items: []
}
},
methods: {
}
}
</script>

View File

@ -7,27 +7,19 @@
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<div class="ui stackable grid">
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
<search-input />
</div>
<div class="ten wide column"
:class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button shrinking"
v-on:click="updateFastFilters(3)"
v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)"
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
style="position: relative;">
<i class="green mail icon"></i>Shared Notes
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
{{ $store.getters.totals['unreadNotes'] }}
</span>
</div>
<div class="ui basic button shrinking" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
<i class="green archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</div>
<div class="ui basic icon button shrinking" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
<i class="trash alternate outline icon"></i>
<i class="green mail icon"></i>Inbox
+{{ $store.getters.totals['youGotMailCount'] }}
</div>
<tag-display
@ -42,12 +34,6 @@
</div>
<div class="six wide column">
<search-input
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-if="$store.getters.totals && $store.getters.totals['totalNotes']" />
</div>
<div class="eight wide column" v-if="showClear">
<!-- <fast-filters /> -->
<span class="ui fluid green button" @click="reset">
@ -75,7 +61,7 @@
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2 >Trash
<h2>Trash
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<i class="poo storm icon"></i>
@ -88,6 +74,25 @@
<h2>Shared Notes</h2>
</div>
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
@ -123,18 +128,6 @@
</div>
</div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h4><i class="folder open outline icon"></i> Found in Files ({{ foundAttachments.length }})</h4>
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
</div>
@ -170,7 +163,7 @@
data () {
return {
initComponent: true,
commonTags: [],
tagSuggestions:[],
searchTerm: '',
searchResultsCount: 0,
searchTags: [],
@ -191,13 +184,6 @@
//Clear button is not visible
showClear: false,
initialPostData: null,
currentPostData: null,
containsNormalNotes: 0,
containsPinnednotes: 0,
containsTextResults: 0,
// containsTagResults: 0,
// containsAttachmentResults: 0,
//Currently open notes in app
activeNoteId1: null,
@ -214,8 +200,8 @@
sectionData: {
'pinned': ['thumbtack', 'Pinned'],
'archived': ['archive', 'Archived'],
'shared': ['envelope outline', 'Received Notes'],
'sent': ['paper plane outline', 'Shared Notes'],
'shared': ['envelope outline', 'Inbox'],
'sent': ['paper plane outline', 'Sent Notes'],
'notes': ['file','Notes'],
'highlights': ['paragraph', 'Found In Text'],
'trashed': ['poop', 'Trashed Notes'],
@ -275,13 +261,12 @@
this.$store.dispatch('fetchAndUpdateUserTotals')
//Close note event
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
this.$bus.$on('close_active_note', ({noteId, modified}) => {
this.closeNote()
if(modified){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(parseInt(noteId))
}
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(parseInt(noteId), modified)
})
this.$bus.$on('note_deleted', (noteId) => {
@ -295,15 +280,12 @@
return
}
})
})
})
})
this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter
//Fast filters always return all the results and tags
this.search(true, this.batchSize, false).then( () => {
// return
})
this.$bus.$on('update_fast_filters', filterIndex => {
this.updateFastFilters(filterIndex)
})
//Event to update search from other areas
@ -312,8 +294,18 @@
this.search(true, this.batchSize)
.then( () => {
console.log('Search attachments disabled for now')
// this.searchAttachments()
this.searchAttachments()
const postData = {
'tagText':this.searchTerm.trim()
}
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
this.tagSuggestions = response.data
})
// return
})
@ -365,6 +357,9 @@
//Loads initial batch and tags
this.reset()
// this.search(true, this.firstLoadBatchSize, false)
// .then( r => this.search(false, this.batchSize, true))
},
methods: {
toggleTitleView(){
@ -390,7 +385,7 @@
if(this.activeNoteId1 == null){
this.activeNoteId1 = id
this.activeNote1Position = 0 //Middel of page
this.$router.push('/notes/open/'+this.activeNoteId1)
this.$router.push('/notes/open/'+this.activeNoteId1).catch(e => { console.log(e) })
return
}
},
@ -417,24 +412,18 @@
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
//Distance to bottom of page
const bottomOfWindow =
Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
+ window.innerHeight
//Detect distance scrolled down the page
const scrolledDown = window.pageYOffset + window.innerHeight
//Get height of div to properly detect scroll distance down
const height = document.getElementById('app').scrollHeight
//height of page
const offsetHeight = this.$refs.content.clientHeight
//Determine percentage down the page
const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
//If greater than 80 of the way down the page, load the next batch
if(percentageDown >= 65 && this.scrollLoadEnabled){
//Load if less than 500px from the bottom
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
this.search(false, this.batchSize, true)
}
}, 50)
}, 30)
return
@ -491,7 +480,10 @@
noteId = parseInt(noteId)
//Find local note, if it exists; continue
let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = this.$refs['note-'+noteId][0].note
}
//Lookup one note using passed in ID
const postData = {
@ -508,60 +500,62 @@
//Pull note data out of note set
let newNote = results.data.notes[0]
let foundNote = false
if(newNote === undefined){
return
}
//Find Just updated note and modify all its attributes
Object.keys(this.noteSections).forEach(key => {
if(note && newNote){
this.noteSections[key].forEach( (note,index) => {
//Don't move notes that were not changed
if(note.updated == newNote.updated){
return
}
if(note.id == noteId){
foundNote = true
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//Don't move notes that were not changed
if(note.updated == newNote.updated){
// return
}
//Push new note to front if its modified
if(focuseAndAnimate){
//Compare note tags, if they changed, reload tags
if(newNote.tag_count != note.tag_count){
}
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//Remove note from location and push to front
this.noteSections[key].splice(index, 1)
this.noteSections[key].unshift(note)
this.$nextTick( () => {
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && focuseAndAnimate){
this.$refs['note-'+noteId][0].justClosed()
// Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (searchNote, index) => {
if(searchNote.id == noteId){
//Remove note from location and push to front
this.noteSections[key].splice(index, 1)
this.noteSections[key].unshift(note)
return
}
})
})
return
}
})
})
this.$nextTick( () => {
//Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed()
})
}
}
//New notes don't exist in list, push them to the front
if(!foundNote){
if(note == null){
this.noteSections.notes.unshift(newNote)
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed()
}
}
//Trigger section rebuild
this.rebuildNoteCategorise()
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
.catch(error => {
console.log(error)
this.$bus.$emit('notification', 'Failed to Update Note')
})
},
searchAttachments(){
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
@ -570,13 +564,13 @@
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
},
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){
console.log('Loading in progress, cancel operation')
return resolve()
console.log('Loading already in progress')
return resolve(false)
}
//Reset a lot of stuff if we are not merging batches
@ -585,7 +579,6 @@
this.noteSections[key] = []
})
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
// this.commonTags = [] //Don't reset tags, if search returns tags, they will be set
}
this.searchResultsCount = 0
@ -628,11 +621,6 @@
//Enable or disable scroll loading
this.scrollLoadEnabled = response.data.notes.length > 0
//Mush the two new sets of data together (set will be empty is reset is on)
// if(response.data.tags.length > 0){
// this.commonTags = response.data.tags
// }
if(response.data.total > 0){
this.searchResultsCount = response.data.total
}
@ -742,33 +730,22 @@
this.scrollLoadEnabled = true
this.searchTerm = ''
this.searchTags = []
this.tagSuggestions = []
this.fastFilters = {}
this.updateFastFilters(5)
this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters')
//Load initial batch, then tags, then other batch
this.search(true, this.firstLoadBatchSize)
.then( () => {
//Load a larger batch once first batch has loaded
return this.search(false, this.batchSize, true)
})
this.$bus.$emit('reset_fast_filters')
this.updateFastFilters(5) //This loads notes
},
updateFastFilters(index){
//clear out tags
this.searchTags = []
this.tagSuggestions = []
this.loadingInProgress = false
this.searchTerm = ''
this.$bus.$emit('reset_fast_filters')
//A little hacky, brings user to notes page then filters on click
if(this.$route.name != 'Note Page'){
this.$router.push('/notes')
setTimeout( () => {
this.updateFastFilters(index)
}, 500 )
}
this.$bus.$emit('reset_fast_filters') //Clear out search
const options = [
'withLinks', // 'Only Show Notes with Links'
@ -782,7 +759,10 @@
let filter = {}
filter[options[index]] = 1
this.$bus.$emit('update_fast_filters', filter)
this.fastFilters = filter
//Fetch First batch of notes with new filter
this.search(true, this.firstLoadBatchSize, false)
.then( r => this.search(false, this.batchSize, true))
}
}
}

View File

@ -12,11 +12,11 @@
</h2>
</div>
<div class="sixteen wide middle aligned column">
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0">
<div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
<i class="sync alternate reload icon"></i>
New Quick Note
New Scratch Pad
</div>
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
<i class="close icon"></i>

View File

@ -1,9 +1,49 @@
<template>
<div class="ui basic segment">
<div class="ui container">
<div class="fun" :style="{'color':color}" v-if="noteText" v-html="noteText"></div>
<div class="ui grid">
<div class="sixteen wide column"></div>
<div class="sixteen wide column" v-if="text.length > 0 || title.length > 0">
<div class="ui text container squire-box" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<h1 v-if="title">{{title}}</h1>
<div v-if="text" v-html="text"></div>
</div>
</div>
<div class="ui basic segment"></div>
<div class="sixteen wide column" v-if="!$store.getters.getLoggedIn">
<div class="ui text container">
<h2 class="ui header">
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
<div class="content">
Solid Scribe is an easy, free, secure Note App
<div class="sub header">
Encrypted notes, only readable by you. Unless you share them.
</div>
</div>
</h2>
<div class="ui grid">
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/login">
<i class="plug icon"></i>Sign Up
</router-link>
</div>
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/">
<i class="comment outline icon"></i>
Learn More
</router-link>
</div>
</div>
</div>
</div>
<div class="ui sixteen wide center aligned column">
<h4>{{ failText }}</h4>
</div>
</div>
</template>
@ -15,36 +55,51 @@
name: 'SharePage',
data(){
return {
noteText: null,
color: '#000'
title: '',
text: '',
failText: '',
styleObject:{},
}
},
beforeMount(){
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
//You can put something here for live updates
// this.$io.on
this.openNote()
},
methods:{
openNote(noteId){
axios.post('/api/public/note', {'noteId': noteId})
.then( response => {
fail(){
this.failText = 'Failed to open Shared Note'
this.$bus.$emit('notification', 'Failed to Open Shared Note')
},
openNote(){
let colors = JSON.parse(response.data.color)
const noteId = this.$route.params.id
const sharedKey = this.$route.params.token
if(colors && colors.noteBackground){
document.body.style.background = colors.noteBackground
axios.post('/api/public/opensharednote', {noteId, sharedKey})
.then( ({data}) => {
if(data.success){
this.title = data.title
this.text = data.text
this.styleObject = data.styleObject
} else {
this.fail()
}
if(colors && colors.noteText){
this.color = colors.noteText
}
this.noteText = response.data.text
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Open Public Note') })
.catch(error => { this.fail() })
}
}
}
</script>
</script>
<style type="text/css" scoped>
.small-logo {
width: 30px;
height: auto;
}
</style>

View File

@ -10,6 +10,7 @@ const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/Shar
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
Vue.use(Router)
@ -52,7 +53,7 @@ export default new Router({
component: HelpPage
},
{
path: '/share/:id',
path: '/public/note/:id/:token',
name: 'Share',
meta: {title:'Shared'},
component: SharePage
@ -81,5 +82,11 @@ export default new Router({
meta: {title:'Attachments by Type'},
component: AttachmentsPage
},
{
path: '*',
name: 'Page Not Found',
meta: {title:'404 Page Not Found'},
component: NotFoundPage
},
]
})

View File

@ -45,34 +45,36 @@ export default new Vuex.Store({
const themes = {
'white':{
'background_color': '#fff',
'body_bg_color': '#f5f6f7',
'small_element_bg_color': '#fff',
'text_color': '#3d3d3d',
'outline_color': 'rgba(34,36,38,0.15)',
'border_color': 'rgba(34,36,38,0.20)',
'dark_border_color': '#DFE1E6',
'border_color': '#DFE1E6',
'menu-accent': '#cecece',
'menu-text': '#5e6268',
},
'black':{
'background_color': '#000',
'body_bg_color': '#000',
'small_element_bg_color': '#000',
'text_color': '#FFF',
'outline_color': '#FFF',
'border_color': 'rgba(255, 255, 255, 0.70)',
'dark_border_color': '#ACACAC', //Lighter color to accent elemnts user can interact with
'border_color': '#555',
'menu-accent': '#626262',
'menu-text': '#d9d9d9',
},
'night':{
'background_color': '#000',
'body_bg_color': '#000',
'small_element_bg_color': '#000',
'text_color': '#a98457',
'outline_color': '#a98457',
'border_color': 'rgba(255, 255, 255, 0.31)',
'dark_border_color': '#a98457',
'border_color': '#555',
'menu-accent': '#626262',
'menu-text': '#d9d9d9',
'menu-text': '#a69682',
},
}
//Catch values not in set
const totalThemes = Object.keys(themes).length
state.nightMode++
if(state.nightMode > totalThemes-1){

View File

@ -40,11 +40,10 @@ var io = require('socket.io')(http, {
//Set socket IO as a global in the app
global.SocketIo = io
let noteDiffs = {}
io.on('connection', function(socket){
// console.log('New user ', socket.id)
//When a user connects, add them to their own room
// This allows the server to emit events to that specific user
// access socket.io in the controller with SocketIo global
@ -58,14 +57,43 @@ io.on('connection', function(socket){
})
})
socket.on('join_room', roomId => {
// console.log('Join room ', roomId)
socket.join(roomId)
socket.on('join_room', rawTextId => {
// Join user to rawtextid room when they enter
socket.join(rawTextId)
const usersInRoom = io.sockets.adapter.rooms[roomId]
//If there are past diffs for this note, send them to the user
if(noteDiffs[rawTextId] != undefined){
//Sort all note diffs by when they were created.
noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
//Emit all sorted diffs to user
socket.emit('past_diffs', noteDiffs[rawTextId])
} else {
socket.emit('past_diffs', null)
}
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
if(usersInRoom){
// console.log('Users in room', usersInRoom.length)
io.to(roomId).emit('update_user_count', usersInRoom.length)
//Update users in room count
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
//Debugging text
console.log('Note diff object')
console.log(noteDiffs)
let noteDiffKeys = Object.keys(noteDiffs)
let totalDiffs = 0
noteDiffKeys.forEach(diffSetKey => {
if(noteDiffs[diffSetKey]){
totalDiffs += noteDiffs[diffSetKey].length
}
})
console.log('Total notes in limbo -> ', noteDiffKeys.length)
console.log('Total Diffs for all notes -> ', totalDiffs)
}
})
@ -83,13 +111,41 @@ io.on('connection', function(socket){
socket.on('note_diff', data => {
//Log each diff for note
const noteId = data.id
delete data.id
if(noteDiffs[noteId] == undefined){ noteDiffs[noteId] = [] }
data.time = +new Date
noteDiffs[noteId].push(data)
//Remove duplicate diffs if they exist
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
let pastDiff = noteDiffs[noteId][i]
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
let currentDiff = noteDiffs[noteId][j]
if(i == j){
continue
}
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
console.log('Removing Duplicate')
noteDiffs[noteId].splice(i,1)
}
}
}
//Each user joins a room when they open the app.
io.in(data.id).clients((error, clients) => {
io.in(noteId).clients((error, clients) => {
if (error) throw error;
//Go through each client in note room and send them the diff
clients.forEach(socketId => {
if(socketId != socket.id){
io.to(socketId).emit('incoming_diff', data.diff)
io.to(socketId).emit('incoming_diff', data)
}
})
@ -97,6 +153,38 @@ io.on('connection', function(socket){
})
socket.on('truncate_diffs_at_save', checkpoint => {
let diffSet = noteDiffs[checkpoint.rawTextId]
if(diffSet && diffSet.length > 0){
//Make sure all diffs are sorted before cleaning
noteDiffs[checkpoint.rawTextId].sort((a,b) => { return a.time - b.time })
// Remove all diffs until it reaches the current hash
let sliceTo = 0
for (var i = 0; i < diffSet.length; i++) {
if(diffSet[i].hash == checkpoint){
sliceTo = i
break
}
}
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
if(noteDiffs[checkpoint.rawTextId].length == 0){
delete noteDiffs[checkpoint.rawTextId]
}
//Debugging
else {
console.log('Diffset after save')
console.log(noteDiffs[checkpoint.rawTextId])
}
}
})
socket.on('disconnect', function(){
// console.log('user disconnected');
});
@ -139,7 +227,7 @@ app.use(function(req, res, next){
const printResults = false
let UserTest = require('@models/User')
let NoteTest = require('@models/Note')
UserTest.keyPairTest('genMan2', '1', printResults)
UserTest.keyPairTest('genMan12', '1', printResults)
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
.then( message => {
if(printResults) console.log(message)

View File

@ -280,9 +280,7 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => {
//Once everything is done being scraped, emit new attachment events
if(io){
io.to(userId).emit('update_counts')
}
SocketIo.to(userId).emit('update_counts')
solrAttachmentText += freshlyScrapedText
resolve(solrAttachmentText)

View File

@ -72,7 +72,7 @@ Note.test = (userId, masterKey, printResults) => {
if(printResults) console.log('Test: Normal Note Search Index - Pass')
} else { console.log('Test: Search Index - Fail') }
return ShareNote.migrateNoteToShared(userId, testNoteId, shareUserId, masterKey)
return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey)
})
.then(({success, shareUserId, sharedUserNoteId}) => {
@ -134,13 +134,13 @@ Note.test = (userId, masterKey, printResults) => {
if(printResults) console.log('Test: Delete Shared Note - Pass')
return ShareNote.migrateNoteToShared(userId, testNoteId2, shareUserId, masterKey)
return ShareNote.addUserToSharedNote(userId, testNoteId2, shareUserId, masterKey)
})
.then(({success, shareUserId, sharedUserNoteId}) => {
if(printResults) console.log('Test: Created Another New Shared Note - pass')
return ShareNote.removeUserFromShared(userId, testNoteId2, sharedUserNoteId, masterKey)
return ShareNote.removeUserFromSharedNote(userId, testNoteId2, sharedUserNoteId, masterKey)
})
.then(() => {
@ -162,75 +162,6 @@ Note.test = (userId, masterKey, printResults) => {
return resolve('Test: Complete ---')
})
})
}
//User doesn't have an encrypted note set. Encrypt all notes
Note.encryptEveryNote = (userId, masterKey) => {
return new Promise((resolve, reject) => {
//Select all the user notes
db.promise().query(`
SELECT * FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE salt IS NULL AND user_id = ? AND shared = 0`, [userId])
.then((rows, fields) => {
let foundNotes = rows[0]
console.log('Encrypting user notes ',rows[0].length)
// return resolve(true)
let allTheUpdates = []
let timeoutAdder = 0
foundNotes.forEach(note => {
timeoutAdder += 100
const newUpdate = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Encrypting Note ', note.id)
const created = Math.round((+new Date)/1000)
const salt = cs.createSmallSalt()
const noteText = note.text
const noteTitle = note.title
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
const noteSnippet = cs.encrypt(masterKey, salt, snippet)
const textObject = JSON.stringify([noteTitle, noteText])
const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise()
.query('UPDATE note_raw_text SET title = ?, text = ?, snippet = ?, salt = ? WHERE id = ?',
[null, encryptedText, noteSnippet, salt, note.note_raw_text_id])
.then(() => {
resolve(true)
})
}, timeoutAdder)
})
allTheUpdates.push(newUpdate)
})
Promise.all(allTheUpdates).then(done => {
console.log('Indexing first 100')
return Note.reindex(userId, masterKey)
}).then(results => {
console.log('Done')
resolve(true)
})
})
})
}
@ -490,7 +421,8 @@ Note.reindex = (userId, masterKey, removeId = null) => {
Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => {
return new Promise((resolve, reject) => {
const now = Math.round((+new Date)/1000)
// const now = Math.round((+new Date)/1000)
const now = +new Date
let noteSnippet = ''
@ -691,10 +623,6 @@ Note.delete = (userId, noteId, masterKey = null) => {
// delete tags
return db.promise()
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
})
.then((rows, fields) => {
@ -706,6 +634,8 @@ Note.delete = (userId, noteId, masterKey = null) => {
}
})
SocketIo.to(userId).emit('update_counts')
if(masterKey){
//Remove note ID from index
Note.reindex(userId, masterKey, [noteId])
@ -803,6 +733,7 @@ Note.get = (userId, noteId, masterKey) => {
note.archived,
note.trashed,
note.color,
note.shared,
note.encrypted_share_password_key,
count(distinct attachment.id) as attachment_count,
note.note_raw_text_id as rawTextId,
@ -845,8 +776,8 @@ Note.get = (userId, noteId, masterKey) => {
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
//Return note data
delete noteData.salt //remove salt from return data
delete noteData.encrypted_share_password_key
// delete noteData.salt //remove salt from return data
// delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut
resolve(noteData)
@ -859,17 +790,48 @@ Note.get = (userId, noteId, masterKey) => {
}
//Public note share action -> may not be used
Note.getShared = (noteId) => {
Note.getShared = (noteId, sharedKey) => {
return new Promise((resolve, reject) => {
db.promise()
.query('SELECT text, color FROM note WHERE id = ? AND shared = 1 LIMIT 1', [noteId])
.query(`
SELECT
note_raw_text.text,
note_raw_text.salt,
note_raw_text.updated as updated,
note.id,
note.user_id,
note.created,
note.pinned,
note.archived,
note.trashed,
note.color,
note.shared,
note.encrypted_share_password_key,
note.note_raw_text_id as rawTextId
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE note.id = ? LIMIT 1`, [noteId])
.then((rows, fields) => {
//Return note data
resolve(rows[0][0])
let noteData = rows[0][0]
const decipheredText = cs.decrypt(sharedKey, noteData.salt, noteData.text)
if(decipheredText == null){
throw new Error('Unable to decropt note text')
}
const success = true
const noteObject = JSON.parse(decipheredText)
const title = noteObject[0]
const text = noteObject[1]
const styleObject = JSON.parse(noteData.color)
return resolve({success, title, text, styleObject})
})
.catch(console.log)
.catch(error => {
resolve({'success':false})
})
})
}

View File

@ -12,19 +12,19 @@ let ShareNote = module.exports = {}
const crypto = require('crypto')
const cs = require('@helpers/CryptoString')
ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
ShareNote.addUserToSharedNote = (userId, noteId, shareUserId, masterKey) => {
return new Promise((resolve, reject) => {
const Note = require('@models/Note')
const User = require('@models/User')
//generate new random salts and password
const sharedNoteMasterKey = cs.createSmallSalt()
let sharedNoteMasterKey = null
let encryptedSharedKey = null //new key for note encrypted with shared users pubic key
//Current note object
let note = null
let noteObject = null
let publicKey = null
db.promise().query('SELECT id FROM user WHERE id = ?', [shareUserId])
@ -34,20 +34,16 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
throw new Error('User Does Not Exist')
}
return Note.get(userId, noteId, masterKey)
return ShareNote.migrateToShared(userId, noteId, masterKey)
})
.then( noteObject => {
.then(({note, sharedNoteKey})=> {
if(!noteObject){
throw new Error('Note Not Found')
}
note = noteObject
sharedNoteMasterKey = sharedNoteKey
noteObject = note
return db.promise()
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, note.rawTextId])
})
.then((rows, fields) => {
@ -55,46 +51,6 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
throw new Error('User Already has this note shared with them')
}
//All check pass, proceed with sharing note
return User.getPublicKey(userId)
})
.then( userPublicKey => {
//Get users public key
publicKey = userPublicKey
//
// Modify note to have a shared password, encrypt text with this password
//
const sharedNoteSalt = cs.createSmallSalt()
//Encrypt note text with new password
const textObject = JSON.stringify([note.title, note.text])
const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject)
//Update note raw text with new data
return db.promise()
.query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)",
[encryptedText, sharedNoteSalt, note.rawTextId])
})
.then((rows, fields) => {
//New Encrypted snippet, using new shared password
const sharedNoteSnippetSalt = cs.createSmallSalt()
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet)
//Encrypt shared password for this user
const encryptedSharedKey = crypto.publicEncrypt(publicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
//Update note snippet for current user with public key encoded snippet
return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?',
[encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId])
})
.then((rows, fields) => {
return User.getPublicKey(shareUserId)
})
@ -102,7 +58,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
//New Encrypted snippet, using new shared password
const newSnippetSalt = cs.createSmallSalt()
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
const snippet = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, newSnippetSalt, snippet)
//Encrypt shared password for this user
@ -111,7 +67,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
//Insert new note for shared user
return db.promise().query(`
INSERT INTO note (user_id, note_raw_text_id, created, color, share_user_id, snippet, snippet_salt, encrypted_share_password_key) VALUES (?,?,?,?,?,?,?,?);
`, [shareUserId, note.rawTextId, note.created, note.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey])
`, [shareUserId, noteObject.rawTextId, noteObject.created, noteObject.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey])
})
.then((rows, fields) => {
@ -119,7 +75,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
const sharedUserNoteId = rows[0]['insertId']
//Emit update count event to user shared with - so they see the note in real time
SocketIo.to(sharedUserNoteId).emit('update_counts')
SocketIo.to(shareUserId).emit('update_counts')
let success = true
return resolve({success, shareUserId, sharedUserNoteId})
@ -133,7 +89,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
})
}
ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) => {
ShareNote.removeUserFromSharedNote = (userId, noteId, shareNoteUserId, masterKey) => {
return new Promise((resolve, reject) => {
let rawTextId = null
@ -160,32 +116,9 @@ ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) =>
//Convert back to normal note if there is only one person with this note
if(rows[0][0]['count'] == 1){
Note.get(userId, noteId, masterKey)
.then(noteObject => {
const salt = cs.createSmallSalt()
const snippetSalt = cs.createSmallSalt()
const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
const textObject = JSON.stringify([noteObject.title, noteObject.text])
const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise()
.query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`,
[snippet, snippetSalt, null, noteId, userId])
.then((r,f) => {
db.promise()
.query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?',
[encryptedText, salt, noteObject.rawTextId])
.then(() => {
return resolve(true)
})
})
return ShareNote.migrateToNormal(userId, noteId, masterKey)
.then(results => {
resolve(true)
})
} else {
@ -196,9 +129,163 @@ ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) =>
})
}
// Get users who see a shared note
ShareNote.getUsers = (userId, rawTextId) => {
//Encrypt note with private shared key
ShareNote.migrateToShared = (userId, noteId, masterKey) => {
const User = require('@models/User')
return new Promise((resolve, reject) => {
//generate new random password
const sharedNoteMasterKey = cs.createSmallSalt()
let userPublicKey = null
let userPrivateKey = null
let note = null
User.generateKeypair(userId, masterKey)
.then( ({publicKey, privateKey}) => {
//Get users public key
userPublicKey = publicKey
userPrivateKey = privateKey
return Note.get(userId, noteId, masterKey)
})
.then(noteObject => {
note = noteObject
if(note.shared == 2){
//Note is already shared, decrypt sharedKey
let sharedNoteKey = null
const encryptedShareKey = note.encrypted_share_password_key
if(encryptedShareKey != null){
sharedNoteKey = crypto.privateDecrypt(userPrivateKey,
Buffer.from(encryptedShareKey, 'base64') )
}
return resolve({note, sharedNoteKey})
} else {
//
// Update raw_text to have a shared password, encrypt text with this password
//
const sharedNoteSalt = cs.createSmallSalt()
//Encrypt note text with new password
const textObject = JSON.stringify([note.title, note.text])
const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject)
//Update note raw text with new data
db.promise()
.query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)",
[encryptedText, sharedNoteSalt, note.rawTextId])
.then((rows, fields) => {
//
// Update snippet using new shared password
// + Save shared password (encrypted with public key)
//
const sharedNoteSnippetSalt = cs.createSmallSalt()
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet)
//Encrypt shared password for this user
const encryptedSharedKey = crypto.publicEncrypt(userPublicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
//Update note snippet for current user with public key encoded snippet
return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?',
[encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId])
})
.then((rows, fields) => {
return resolve({note, 'sharedNoteKey':sharedNoteMasterKey})
})
}
})
})
}
//Remove private shared key, encrypt note with users master password
ShareNote.migrateToNormal = (userId, noteId, masterKey) => {
return new Promise((resolve, reject) => {
Note.get(userId, noteId, masterKey)
.then(noteObject => {
const salt = cs.createSmallSalt()
const snippetSalt = cs.createSmallSalt()
const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)])
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
const textObject = JSON.stringify([noteObject.title, noteObject.text])
const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise()
.query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`,
[snippet, snippetSalt, null, noteId, userId])
.then((r,f) => {
db.promise()
.query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?',
[encryptedText, salt, noteObject.rawTextId])
.then(() => {
return resolve(true)
})
})
})
})
}
ShareNote.decryptSharedKey = (userId, noteId, masterKey) => {
return new Promise((resolve, reject) => {
let userPrivateKey = null
const User = require('@models/User')
User.generateKeypair(userId, masterKey)
.then( ({publicKey, privateKey}) => {
userPrivateKey = privateKey
return Note.get(userId, noteId, masterKey)
})
.then(noteObject => {
//Shared notes use encrypted key - decrypt key then return it.
const encryptedShareKey = noteObject.encrypted_share_password_key
if(encryptedShareKey != null && userPrivateKey != null){
const currentNoteKey = crypto.privateDecrypt(userPrivateKey,
Buffer.from(encryptedShareKey, 'base64') )
return resolve(currentNoteKey)
} else {
return resolve(null)
}
})
})
}
// Get users who see a shared note
ShareNote.getShareInfo = (userId, noteId, rawTextId) => {
return new Promise((resolve, reject) => {
let shareUsers = []
let shareStatus = 0
db.promise()
.query(`
SELECT username, note.id as noteId
@ -211,47 +298,14 @@ ShareNote.getUsers = (userId, rawTextId) => {
.then((rows, fields) => {
//Return a list of user names
return resolve (rows[0])
shareUsers = rows[0]
return db.promise().query('SELECT shared FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
})
})
}
.then((rows, fields) => {
// Remove a user from a shared note
ShareNote.removeUser = (userId, noteId) => {
return new Promise((resolve, reject) => {
const Note = require('@models/Note')
let rawTextId = null
let removeUserId = null
//note.id = noteId, share_user_id = userId
db.promise()
.query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId])
.then( (rows, fields) => {
rawTextId = rows[0][0]['note_raw_text_id']
removeUserId = rows[0][0]['user_id']
//Delete note entry for other user - remove users access
if(removeUserId && Number.isInteger(removeUserId)){
//Delete this users access to the note
return Note.delete(removeUserId, noteId, masterKey)
} else {
return new Promise((resolve, reject) => { resolve(true) })
}
shareStatus = rows[0][0]['shared']
return resolve({ shareStatus, shareUsers })
})
.then(stuff => {
resolve(true)
})
.catch(error => {
console.log(error)
resolve(false)
})
})
}

View File

@ -19,15 +19,6 @@ User.login = (username, password) => {
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
.then((rows, fields) => {
// Create New Account
//
if(rows[0].length == 0){
User.create(lowerName, password)
.then( ({token, userId}) => {
return resolve({ token, userId })
})
}
// Login User
//
if(rows[0].length == 1){
@ -60,6 +51,8 @@ User.login = (username, password) => {
reject('Password does not match database')
}
})
} else {
return reject('Incorrect Username or Password')
}
})
.catch(console.log)
@ -69,7 +62,7 @@ User.login = (username, password) => {
//Create user account
//Issues login token
User.create = (username, password) => {
User.register = (username, password) => {
//For some reason, username won't get into the promise. But password will @TODO figure this out
const lowerName = username.toLowerCase().trim()
@ -152,8 +145,8 @@ User.getCounts = (userId) => {
SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
SUM(trashed = 1) AS trashedNotes,
SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes,
SUM( (share_user_id != ? && opened IS null && trashed = 0) || (share_user_id != ? && note_raw_text.updated > opened && trashed = 0) ) AS unreadNotes
SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount,
SUM(share_user_id != ? && trashed = 0) AS sharedToNotes
FROM note
LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE user_id = ?`, [userId, userId, userId, userId])
@ -406,12 +399,12 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
const randomUsername = Math.random().toString(36).substring(2, 15);
const randomPassword = '1'
User.login(testUserName, password)
User.register(testUserName, password)
.then( ({ token, userId }) => {
testUserId = userId
if(printResults) console.log('Test: Create/Login User '+testUserName+' - Pass')
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
return User.getMasterKey(testUserId, password)
})
@ -439,6 +432,12 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
//Convert it back to string
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
return User.login(testUserName, password)
})
.then( ({token, userId}) => {
if(printResults) console.log('Test: Login New User - Pass')
resolve({testUserId, masterKey})
})
})

View File

@ -23,8 +23,13 @@ router.use(function setUserId (req, res, next) {
//
router.post('/get', function (req, res) {
Note.get(userId, req.body.noteId, masterKey)
.then( data => {
res.send(data)
.then( noteObject => {
delete noteObject.snippet_salt
delete noteObject.salt
delete noteObject.encrypted_share_password_key
res.send(noteObject)
})
})
@ -91,8 +96,8 @@ router.post('/settrashed', function (req, res) {
//
// Share Note Actions
//
router.post('/getshareusers', function (req, res) {
ShareNote.getUsers(userId, req.body.rawTextId)
router.post('/getshareinfo', function (req, res) {
ShareNote.getShareInfo(userId, req.body.noteId, req.body.rawTextId)
.then(results => res.send(results))
})
@ -100,7 +105,7 @@ router.post('/shareadduser', function (req, res) {
// ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username, masterKey)
User.getByUserName(req.body.username)
.then( user => {
return ShareNote.migrateNoteToShared(userId, req.body.noteId, user.id, masterKey)
return ShareNote.addUserToSharedNote(userId, req.body.noteId, user.id, masterKey)
})
.then( ({success, shareUserId}) => {
@ -110,10 +115,28 @@ router.post('/shareadduser', function (req, res) {
router.post('/shareremoveuser', function (req, res) {
// (userId, noteId, shareNoteUserId, shareUserId, masterKey)
ShareNote.removeUserFromShared(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
ShareNote.removeUserFromSharedNote(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
.then(results => res.send(results))
})
router.post('/enableshare', function (req, res) {
//Create Shared Encryption Key for Note
ShareNote.migrateToShared(userId, req.body.noteId, masterKey)
.then(results => res.send(true))
})
router.post('/getsharekey', function (req, res) {
//Get Shared Key for a note
ShareNote.decryptSharedKey(userId, req.body.noteId, masterKey)
.then(results => res.send(results))
})
router.post('/disableshare', function (req, res) {
//Removed shared encryption key from note
ShareNote.migrateToNormal(userId, req.body.noteId, masterKey)
.then(results => res.send(true))
})
//
// Testing Action

View File

@ -1,12 +1,15 @@
var express = require('express')
var router = express.Router()
let Notes = require('@models/Note')
let Note = require('@models/Note')
router.post('/note', function (req, res) {
Notes.getShared(req.body.noteId)
.then( data => res.send(data) )
//
// Public Note action
//
router.post('/opensharednote', function (req, res) {
Note.getShared(req.body.noteId, req.body.sharedKey)
.then(results => res.send(results))
})

View File

@ -18,34 +18,31 @@ router.get('/about', function (req, res) {
User.getUsername(req.headers.userId)
.then( data => res.send(data) )
})
// define the login route
// Login User
router.post('/login', function (req, res) {
//Pull out variables we want
const username = req.body.username
const password = req.body.password
let returnData = {
success: false,
token: '',
username: ''
}
User.login(username, password)
.then( ({token, userId}) => {
returnData['username'] = username
returnData['token'] = token
returnData['success'] = true
User.login(req.body.username, req.body.password)
.then( returnData => {
res.send(returnData)
return
})
.catch(e => {
console.log(e)
res.send(returnData)
res.send(false)
})
})
// Login User
router.post('/register', function (req, res) {
User.register(req.body.username, req.body.password)
.then( returnData => {
res.send(returnData)
})
.catch(e => {
res.send(false)
})
})
// fetch counts of users notes
router.post('/totals', function (req, res) {