import api from './api.js'; import App from './App.js'; import nanoid from './nanoid.min.js'; import {initial_load_bin_handler, hash_change_handler} from './handlers/index.js'; import {bin, note} from './models.js'; const produce = immer.produce; immer.setAutoFreeze(false); // needed for high-frequency updated values, like onkeyup->note.temp_text; only once 'save' is called will it produce a new immutable state tree var root = document.body; /* Ruthlessly taken from [https://gist.github.com/kitze/fb65f527803a93fb2803ce79a792fff8]: */ const handleActions = (actionsMap, defaultState) => (state=defaultState, {type, payload}) => produce(state, draft => { const action = actionsMap[type]; action && action(draft, payload); }); // get bin id from URL, and load notes from that bin; if a bin id isn't specified in the URL, create a new one and update the URL let bin_id = window.location.hash.substring(1); // extract the leading '#' if(bin_id === ''){ bin_id = nanoid(); window.history.replaceState(null,'', '#'+bin_id); } // the actual loading from server is done later, after store and dispatch are defined /* Integrates a database row from the server into the local store: */ const integrate = function(s, model_object){ const id = model_object.id; s.db[id] = s.db[id] || {ref_count: 0, model: null}; s.db[id].model = model_object; }; const i = integrate; /* integrates a list of model objects, as if 'integrate' were called on each element. */ const integrateAll = function(s, model_objects){ model_objects.forEach(mo=>{ integrate(s, mo); }); }; /* Register a reference-by-id: */ const ref = function(s, o, key, new_model_id){ const old_model_id = o[key]; s.db[new_model_id].ref_count++; if(old_model_id && s.db[old_model_id]){ s.db[old_model_id].ref_count--; if(s.db[old_model_id].ref_count === 0){ delete s.db[old_model_id]; } } // this needs to come after the above 'if' statement: o[key] = new_model_id; }; /* Register references in-bulk: */ /* o[key] refers to an array-of-ids */ const refAll = function(s, o, key, new_model_ids){ const old_model_ids = o[key]; // ref the new refs: o[key] = new_model_ids; new_model_ids.forEach(new_model_id=>{ s.db[new_model_id].ref_count++; }); // de-ref the existing refs, deleting no-longer-referred-to models: old_model_ids.forEach(old_model_id=>{ s.db[old_model_id].ref_count--; // if this is the last reference, delete it from the store: if(s.db[old_model_id].ref_count === 0){ delete s.db[old_model_id]; } }); }; /* TODO: routine to de-ref and ref in bulk when the id is in an object which is in an array (as opposed to refAll, where the id itself is in the array) This routine would need to be called for state.notes, which contains refs of the form: `state.notes[n].note_id` */ // TODO: doesn't update anything that references the deleted record; results in dangling pointers: const disintegrate = function(s, id){ delete s.db[id]; }; function addToBinListIfLoggedIn(state){ const s = state; // if user is logged in: if(s.login.is_logged_in){ // if bin is not already in the list: if(s.login.bin_ids.filter(bin_id=>bin_id===s.bin_id).length===0){ i(s, {id: s.bin_id, name: s.db[s.bin_id].model.name}); // causes problems because ref_count isn't properly updated: //s.login.bin_ids.push(s.bin_id); ref(s, s.login.bin_ids, s.login.bin_ids.length, s.bin_id); } } } const reducer = handleActions({ 'new-bin': (s, bin) => { i(s,bin); ref(s,s,'bin_id',bin.id); s.notes=[]; s.temp_bin_name=bin.id; }, 'bin-requested': (s, bin_id) => { i(s,{id:bin_id}); ref(s,s,'bin_id',bin_id); s.notes = []; s.error_message=''; }, 'bin-loaded': (s, bin) => { i(s,bin); ref(s,s,'bin_id',bin.id); s.temp_bin_name=bin.name; }, 'bin-unauthorized': (s) => { s.error_message = 'Not authorized to load bin. Please sign-in.'; }, 'update-search-term': (s, search_term) => { s.search_term=search_term; }, 'update-search-results': (s, _notes) => { integrateAll(s,s.notes,'note_id',_notes); s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin_id, note_id:n.id})); }, 'add-note': (s, {id, date}) => { i(s, {id: id, text: '', modified: date}); s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin_id, is_focused:true, is_new: true, note_id: id}); }, 'notes-loaded': (s, _notes) => { integrateAll(s,_notes); s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin_id, note_id:n.id})); }, 'notes-unauthorized': (s) => { s.error_message = 'Not authorized to load notes in bin. Please sign-in.'; }, 'immediately-cancel-note': (s, note_id) => { disintegrate(s,note_id); s.notes.splice(s.notes.findIndex(n=>n.note_id===note_id), 1); }, 'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note_id===id); note_s.is_new=false; s.db[note_s.note_id].model.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below 'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note_id===id); note_s.is_editing=is_editing; note_s.temp_text=s.db[note_s.note_id].model.text; }, 'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note_id===id); s.db[note_s.note_id].model.text=text; note_s.temp_text=text; note_s.is_editing=false; }, 'update-sorting': (s, sorting) => { s.sorting=sorting; }, 'update-username': (s, username) => { s.login.username=username; }, 'update-password': (s, password) => { s.login.password=password; }, 'login-requested': (s) => { s.login.showing=false; }, 'login-succeeded': (s, {user, session_id}) => { i(s, user); s.login.is_logged_in=true; s.login.showing=true; s.login.password=''; ref(s, s.login, 'user_id', user.id); s.login.session_id=session_id; }, 'login-failed': (s) => { s.login.showing=true; s.login.password=''; }, 'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; refAll(s, s.login, 'bin_ids', []); }, 'user-bin-list-loaded': (s, bins) => { integrateAll(s, bins); refAll(s, s.login, 'bin_ids', bins.map(b=>b.id)); }, 'update-bin-name-editing': (s, is_editing) => { s.is_editing_bin_name=is_editing; }, 'update-bin-name': (s, name) => { s.temp_bin_name=name; }, 'commit-bin-name': (s) => { s.db[s.bin_id].model.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); } }, { bin_id: bin_id, is_editing_bin_name: false, temp_bin_name: bin_id, notes: [ //{is_editing: false, temp_text: '', bin_id: '', note_id: ''}, ], search_term: '', sorting: 'new->old', login: { showing: true, username: '', // value of textbox password: '', // value of textbox is_logged_in: false, user_id: '' /*{ id: '', username: '' }*/, session_id: '', bin_ids: [] }, db: { [bin_id]: { ref_count: 1, model: {id: bin_id, name: bin_id, user_id: ''} } }, error_message: '' //search_result_notes: [] }); // create Redux store, with Redux DevTools enabled: const store = Redux.createStore(reducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); const dispatch = function(action_type, payload){ store.dispatch({type:action_type, payload}); }; store.subscribe(()=>{ const state = store.getState(); m.render(root, m(App, {state, dispatch})); }); api.setStore(store); window.addEventListener("hashchange", (e)=>hash_change_handler(store.getState(), dispatch, e), false); let state = store.getState(); initial_load_bin_handler(state, dispatch, bin_id); // we don't want Mithril auto-redraw system in place, since Redux will manually re-render when necessary with store.subscribe(): // m.mount(root, App); m.render(root, m(App, {state, dispatch}))