diff --git a/customize.dist/fsStore.js b/customize.dist/fsStore.js new file mode 100644 index 000000000..c72163c36 --- /dev/null +++ b/customize.dist/fsStore.js @@ -0,0 +1,180 @@ +define([ + '/api/config?cb=' + Math.random().toString().slice(2), + '/customize/messages.js?app=fs', + '/bower_components/chainpad-listmap/chainpad-listmap.js', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/file/fileObject.js' +], function (Config, Messages, Listmap, Crypto, TextPatcher, FO) { + /* + This module uses localStorage, which is synchronous, but exposes an + asyncronous API. This is so that we can substitute other storage + methods. + + To override these methods, create another file at: + /customize/storage.js + */ + + var Store = {}; + var storeObj; + var ready = false; + var filesOp; + + var safeSet = function (key, val) { + storeObj[key] = val; + }; + + // Store uses nodebacks... + Store.set = function (key, val, cb) { + safeSet(key, val); + cb(); + }; + + // implement in alternative store + Store.setBatch = function (map, cb) { + Object.keys(map).forEach(function (key) { + safeSet(key, val); + }); + cb(void 0, map); + }; + + var safeGet = window.safeGet = function (key) { + return storeObj[key]; + }; + + Store.get = function (key, cb) { + cb(void 0, safeGet(key)); + }; + + // implement in alternative store + Store.getBatch = function (keys, cb) { + var res = {}; + keys.forEach(function (key) { + res[key] = safeGet(key); + }); + cb(void 0, res); + }; + + var safeRemove = function (key) { + delete storeObj[key]; + }; + + Store.remove = function (key, cb) { + safeRemove(key); + cb(); + }; + + // implement in alternative store + Store.removeBatch = function (keys, cb) { + keys.forEach(function (key) { + safeRemove(key); + }); + cb(); + }; + + Store.keys = function (cb) { + cb(void 0, Object.keys(storeObj)); + }; + + Store.addPad = function (href, data) { + filesOp.addPad(href, data); + }; + + Store.forgetPad = function (href, cb) { + filesOp.forgetPad(href); + cb(); + }; + + var changeHandlers = Store.changeHandlers = []; + + Store.change = function (f) { + if (typeof(f) !== 'function') { + throw new Error('[Store.change] callback must be a function'); + } + changeHandlers.push(f); + + if (changeHandlers.length === 1) { + // start listening for changes +/* TODO: listen for changes in the proxy + window.addEventListener('storage', function (e) { + changeHandlers.forEach(function (f) { + f({ + key: e.key, + oldValue: e.oldValue, + newValue: e.newValue, + }); + }); + }); +*/ + } + }; + + var onReady = function (f, proxy, storageKey) { + filesOp = FO.init(proxy, { + storageKey: storageKey + }); + storeObj = proxy; + ready = true; + if (typeof(f) === 'function') { + f(void 0, Store); + } + }; + + var init = function (f, Cryptpad) { + if (!Cryptpad) { return; } + var hash = localStorage.FS_hash; + var secret = Cryptpad.getSecrets(hash); + var listmapConfig = { + data: {}, + websocketURL: Cryptpad.getWebsocketURL(), + channel: secret.channel, + readOnly: false, + validateKey: secret.keys.validateKey || undefined, + crypto: Crypto.createEncryptor(secret.keys), + }; + + var rt = window.rt = Listmap.create(listmapConfig); + rt.proxy.on('create', function (info) { + var realtime = info.realtime; + localStorage.FS_hash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + window.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + + }).on('ready', function () { + if (JSON.stringify(rt.proxy) === '{}') { + var oldStore = Cryptpad.getStore(true); + oldStore.get(Cryptpad.storageKey, function (err, s) { + rt.proxy.filesData = s; + onReady(f, rt.proxy, Cryptpad.storageKey); + }); + return; + } + onReady(f, rt.proxy, Cryptpad.storageKey); + }) + .on('disconnect', function () { + //setEditable(false); + Cryptpad.alert(Messages.common_connectionLost); + }); + + }; + + Store.ready = function (f, Cryptpad) { + if (Cryptpad.parsePadUrl(window.location.href).type === "file") { + if (typeof(f) === 'function') { + f(void 0, Cryptpad.getStore(true)); + } + return; + } + if (ready) { + if (typeof(f) === 'function') { + f(void 0, Store); + } + } else { + init(f, Cryptpad); + } + }; + + return Store; +}); diff --git a/customize.dist/store.js b/customize.dist/store.js index 5b82921ed..5c4138fdf 100644 --- a/customize.dist/store.js +++ b/customize.dist/store.js @@ -71,6 +71,12 @@ define(function () { } }; + Store.addPad = function () {}; + + Store.forgetPad = function (href, cb) { + + }; + var changeHandlers = Store.changeHandlers = []; Store.change = function (f) { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 68ebd95dc..3fc6b6b05 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -7,10 +7,11 @@ define([ '/bower_components/spin.js/spin.min.js', '/common/clipboard.js', + '/customize/fsStore.js', '/customize/user.js', '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Messages, Store, Crypto, Alertify, Spinner, Clipboard, User) { +], function (Config, Messages, Store, Crypto, Alertify, Spinner, Clipboard, FS, User) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata about pads to your local storage for future use and improved usability. @@ -23,6 +24,7 @@ define([ User: User, }; var store; + var fsStore; var userProxy; var userStore; @@ -34,7 +36,8 @@ define([ var getStore = common.getStore = function (legacy) { if (!legacy && userStore) { return userStore; } - if (store) { return store; } + if (legacy) { return store; } + if (!legacy && fsStore) { return fsStore; } throw new Error("Store is not ready!"); }; @@ -94,7 +97,6 @@ define([ store = Store; }); - var isArray = function (o) { return Object.prototype.toString.call(o) === '[object Array]'; }; var fixHTML = common.fixHTML = function (html) { @@ -163,13 +165,13 @@ define([ }; var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys; - var getSecrets = common.getSecrets = function () { + var getSecrets = common.getSecrets = function (secretHash) { var secret = {}; - if (!/#/.test(window.location.href)) { + if (!secretHash && !/#/.test(window.location.href)) { secret.keys = Crypto.createEditCryptor(); secret.key = Crypto.createEditCryptor().editKeyStr; } else { - var hash = window.location.hash.slice(1); + var hash = secretHash || window.location.hash.slice(1); if (hash.length === 0) { secret.keys = Crypto.createEditCryptor(); secret.key = Crypto.createEditCryptor().editKeyStr; @@ -380,40 +382,31 @@ define([ var forgetPad = common.forgetPad = function (href, cb, legacy) { var parsed = parsePadUrl(href); - getRecentPads(function (err, recentPads) { - setRecentPads(recentPads.filter(function (pad) { - var p = parsePadUrl(pad.href); - // find duplicates - if (parsed.hash === p.hash && parsed.type === p.type) { - console.log("Found a duplicate"); - return; - } - return true; - }), function (err, data) { - if (err) { - cb(err); - return; - } + var callback = function (err, data) { + if (err) { + cb(err); + return; + } - getStore(legacy).keys(function (err, keys) { - if (err) { - cb(err); - return; - } - var toRemove = keys.filter(function (k) { - return k.indexOf(parsed.hash) === 0; - }); + getStore(legacy).keys(function (err, keys) { + var toRemove = keys.filter(function (k) { + return k.indexOf(parsed.hash) === 0; + }); - if (!toRemove.length) { - cb(); - return; - } - getStore(legacy).removeBatch(toRemove, function (err, data) { - cb(err, data); - }); + if (!toRemove.length) { + cb(); + return; + } + getStore(legacy).removeBatch(toRemove, function (err, data) { + cb(err, data); }); - }, legacy); - }, legacy); + }); + }; + + if (typeof(getStore(legacy).forgetPad) === "function") { + // TODO implement forgetPad in store.js + getStore(legacy).forgetPad(href, callback); + } }; // STORAGE @@ -462,7 +455,12 @@ define([ }); if (!contains) { - renamed.push(makePad(href, name)); + var data = makePad(href, name); + renamed.push(data); + if (typeof(getStore(legacy).addPad) === "function") { + //TODO implement addPad in store.js + getStore().addPad(href); + } } setRecentPads(renamed, function (err, data) { @@ -535,8 +533,8 @@ define([ f(void 0, env); }; - Store.ready(function (err, store) { - common.store = env.store = store; + FS.ready(function (err, store) { + fsStore = common.store = env.store = store; $(function() { // Race condition : if document.body is undefined when alertify.js is loaded, Alertify @@ -597,7 +595,7 @@ define([ cb(); }); */ - }); + }, common); }; /* diff --git a/www/file/file.css b/www/file/file.css index b5efa3b0f..03744a7f9 100644 --- a/www/file/file.css +++ b/www/file/file.css @@ -54,10 +54,12 @@ li { border: 1px dotted #bbb; background: #666; color: #eee; + margin: -1px; } /* TREE */ + #tree { border: 2px solid blue; box-sizing: border-box; @@ -194,6 +196,10 @@ li { flex: 1; } +#content li * { + pointer-events: none; +} + #content li:hover:not(.header) .name { text-decoration: underline; } diff --git a/www/file/fileObject.js b/www/file/fileObject.js index 0db53d9ab..2f470d060 100644 --- a/www/file/fileObject.js +++ b/www/file/fileObject.js @@ -12,6 +12,7 @@ define([ var NEW_FOLDER_NAME = Messages.fm_newFolder; var init = module.init = function (files, config) { + FILES_DATA = config.storageKey; var DEBUG = config.DEBUG || false; var logging = console.log; var log = config.log || logging; @@ -192,6 +193,16 @@ define([ return ret; }; + var getFilesDataFiles = function () { + var ret = []; + for (var el in files[FILES_DATA]) { + if (el.href && ret.indexOf(el.href) === -1) { + ret.push(el.href); + } + } + return ret; + }; + var removeFileFromRoot = function (root, href) { if (isFile(root)) { return; } for (var e in root) { @@ -536,18 +547,39 @@ define([ pushToTrash(key, href, [UNSORTED]); }; - var addPad = exp.addPad = function (href, data) { - if (!getFileData(href)) { - files[FILES_DATA].push(data); - } + var addUnsortedPad = exp.addPad = function (href) { var unsortedFiles = getUnsortedFiles().slice(); var rootFiles = getRootFiles().slice(); - //var trashFiles = getTrashFiles().slice(); - if (unsortedFiles.indexOf(href) === -1 && rootFiles.indexOf(href) === -1) { + var trashFiles = getTrashFiles().slice(); + if (unsortedFiles.indexOf(href) === -1 && rootFiles.indexOf(href) === -1 && trashFiles.indexOf(href) === -1) { files[UNSORTED].push(href); } }; + var checkNewPads = exp.checkNewPads = function () { + var fd = files[FILES_DATA]; + var rootFiles = getRootFiles().slice(); + var unsortedFiles = getUnsortedFiles().slice(); + var trashFiles = getTrashFiles().slice(); + fd.forEach(function (el, idx) { + if (!el.href) { return; } + if (rootFiles.indexOf(el.href) === -1 + && unsortedFiles.indexOf(el.href) === -1 + && trashFiles.indexOf(el.href) === -1) { + debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el); + files[UNSORTED].push(el.href); + } + }); + }; + + var checkRemovedPads = exp.checkRemovedPads = function () { + var fd = files[FILES_DATA]; + var rootFiles = getRootFiles().slice(); + var unsortedFiles = getUnsortedFiles().slice(); + var trashFiles = getTrashFiles().slice(); + + }; + var fixFiles = exp.fixFiles = function () { // Explore the tree and check that everything is correct: // * 'root', 'trash' and 'filesData' exist and are objects diff --git a/www/file/main.js b/www/file/main.js index cedfc8ad0..90df70b9d 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -21,17 +21,13 @@ define([ //var hash = Cryptpad.getAttribute('FS_hash', cb); var hash = localStorage.FS_hash; - if (hash) { - window.location.hash = hash; - } - - var secret = Cryptpad.getSecrets(); + var secret = Cryptpad.getSecrets(hash); var ROOT = "root"; var ROOT_NAME = Messages.fm_rootName; var UNSORTED = "unsorted"; var UNSORTED_NAME = Messages.fm_unsortedName; - var FILES_DATA = "filesData"; + var FILES_DATA = Cryptpad.storageKey; var FILES_DATA_NAME = Messages.fm_filesDataName; var TRASH = "trash"; var TRASH_NAME = Messages.fm_trashName; @@ -44,6 +40,7 @@ define([ var NEW_FOLDER_NAME = Messages.fm_newFolder; var config = {}; + config.storageKey = FILES_DATA; var DEBUG = config.DEBUG = true; var debug = config.debug = DEBUG ? console.log : function() {return;}; var logError = config.logError = console.error; @@ -55,7 +52,7 @@ define([ } }; - var filesObject = module.files = { + var filesObject = { root: { "Directory 1": { "Dir A": { @@ -213,7 +210,7 @@ define([ localStorage[LOCALSTORAGE_LAST] = JSON.stringify(path); }; - var initLSOpened = function () { + var initLocalStorage = function () { try { var store = JSON.parse(localStorage[LOCALSTORAGE_OPENED]); if (!$.isArray(store)) { @@ -562,14 +559,13 @@ define([ var addDragAndDropHandlers = function ($element, path, isFolder, droppable) { // "dragenter" is fired for an element and all its children // "dragleave" may be fired when entering a child + // --> we use pointer-events: none in CSS, but we still need a counter to avoid some issues // --> We store the number of enter/leave and the element entered and we remove the // highlighting only when we have left everything var counter = 0; - var dragenterList = []; $element.on('dragstart', function (e) { e.stopPropagation(); counter = 0; - dragenterList = []; onDrag(e.originalEvent, path); }); @@ -585,18 +581,15 @@ define([ $element.on('dragenter', function (e) { e.preventDefault(); e.stopPropagation(); - if (dragenterList.indexOf(e.target) !== -1) { return; } - dragenterList.push(e.target); counter++; $element.addClass('droppable'); }); $element.on('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); - var idx = dragenterList.indexOf(e.target); - dragenterList.splice(idx, 1); counter--; if (counter <= 0) { + counter = 0; $element.removeClass('droppable'); } }); @@ -1283,7 +1276,6 @@ define([ var realtime = module.realtime = info.realtime; var editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); - window.location.hash = editHash; //Cryptpad.setAttribute("FS_hash", editHash, cb, store); localStorage.FS_hash = editHash; @@ -1303,16 +1295,17 @@ define([ }); });*/ }).on('ready', function () { + module.files = rt.proxy; if (JSON.stringify(rt.proxy) === '{}') { - var store = Cryptpad.getStore(); + var store = Cryptpad.getStore(true); store.get(Cryptpad.storageKey, function (err, s) { - rt.proxy.filesData = s; - initLSOpened(); + rt.proxy[FILES_DATA] = s; + initLocalStorage(); init(rt.proxy); }); return; } - initLSOpened(); + initLocalStorage(); init(rt.proxy); }) .on('disconnect', function () {