Browse Source

Use SQL, Allow comments using TAP

HelloZeroNet 9 years ago
parent
commit
20d2fb09a3

+ 122 - 95
content.json

@@ -1,96 +1,123 @@
-{
-  "address": "1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 
-  "background-color": "white", 
-  "description": "Blogging platform Demo", 
-  "domain": "Blog.ZeroNetwork.bit", 
-  "files": {
-    "css/all.css": {
-      "sha1": "f0ad30ad5eccdf4c3740a3c8aa62e83b9c7544e4", 
-      "sha512": "2a9a13b6a0e461e0df2caa948b3a85383e060a0210719fd8ac53dec65a8e10be", 
-      "size": 106106
-    }, 
-    "data.json": {
-      "sha1": "66c833f1543b2293c0351a2d029ad7819aed6718", 
-      "sha512": "b91a4125f818d4f8fc49ded8e7c27a9c29fd643324ebcffa8b03afff3dc48697", 
-      "size": 18746
-    }, 
-    "data.json-default": {
-      "sha1": "c5e086beb6c4fb2243a0833e33fee134053a7ef0", 
-      "sha512": "53b13a1d181295af27bdeca328adfe7a45d4784044ce632bc647d5bbf7661831", 
-      "size": 367
-    }, 
-    "img/loading.gif": {
-      "sha1": "a669dbd8a577f6957656fc88ba4960b93fcd23d9", 
-      "sha512": "8a42b98962faea74618113166886be488c09dad10ca47fe97005edc5fb40cc00", 
-      "size": 723
-    }, 
-    "img/post/autoupdate.png": {
-      "sha1": "44614d2597b632e848410675416604fdce0d7987", 
-      "sha512": "d2b4dc8e0da2861ea051c0c13490a4eccf8933d77383a5b43de447c49d816e71", 
-      "size": 24460
-    }, 
-    "img/post/domain.png": {
-      "sha1": "baf4993774204f24e29a63fd5664fc390279d743", 
-      "sha512": "ce87e0831f4d1e95a95d7120ca4d33f8273c6fce9f5bbedf7209396ea0b57b6a", 
-      "size": 11881
-    }, 
-    "img/post/memory.png": {
-      "sha1": "a144e004c2c2a05ff3869a5dcdb2dfff5d0f0294", 
-      "sha512": "dd56515085b4a79b5809716f76f267ec3a204be3ee0d215591a77bf0f390fa4e", 
-      "size": 12775
-    }, 
-    "img/post/multiuser.png": {
-      "sha1": "3e3c9a48b0a2dafa3532dc8ba1f5c715f7b81d0f", 
-      "sha512": "88e3f795f9b86583640867897de6efc14e1aa42f93e848ed1645213e6cc210c6", 
-      "size": 29480
-    }, 
-    "img/post/progressbar.png": {
-      "sha1": "daed129cfe8d6816c04121d8c83d32440d2a535f", 
-      "sha512": "23d592ae386ce14158cec34d32a3556771725e331c14d5a4905c59e0fe980ebf", 
-      "size": 13294
-    }, 
-    "img/post/zeroname.png": {
-      "sha1": "636e537d7b273fb87ba8f6ed85017f0bf043cdb9", 
-      "sha512": "bab45a1bb2087b64e4f69f756b2ffa5ad39b7fdc48c83609cdde44028a7a155d", 
-      "size": 36031
-    }, 
-    "img/post/zerotalk-mark.png": {
-      "sha1": "c84f3abd0b2b938748acd3de4b2907c9014384f8", 
-      "sha512": "a335b2fedeb8d291ca68d3091f567c180628e80f41de4331a5feb19601d078af", 
-      "size": 44862
-    }, 
-    "img/post/zerotalk-upvote.png": {
-      "sha1": "6cd2925417dc3862b76b32a67c706159f026f64e", 
-      "sha512": "b1ffd7f948b4f99248dde7efe256c2efdfd997f7e876fb9734f986ef2b561732", 
-      "size": 41092
-    }, 
-    "img/post/zerotalk.png": {
-      "sha1": "e895a3d1be6302c079e22c5415a45423196ac676", 
-      "sha512": "54d10497a1ffca9a4780092fd1bd158c15f639856d654d2eb33a42f9d8e33cd8", 
-      "size": 26606
-    }, 
-    "index.html": {
-      "sha1": "a953bec1321c6b1b396cce732366338206767c94", 
-      "sha512": "0aee2bdc996cb6c2b2f52d4d590361869b6d5ecc755a1bb4611f61ab8237d1fc", 
-      "size": 3042
-    }, 
-    "js/all.js": {
-      "sha1": "12e90b8ed71361baea465bf659bb78fb9485157b", 
-      "sha512": "0a48f1a5a6d198a98251e32165e2098d1055d01eb358c5a273fee0df8d541fdd", 
-      "size": 177349
-    }
-  }, 
-  "ignore": "(js|css)/(?!all.(js|css))", 
-  "modified": 1427758561.089, 
-  "sign": [
-    53953400820955190667635972349061991029644657484611237957871861698091410857920, 
-    107066614956441522514077176017113827700277673122983235786944239766041598409619
-  ], 
-  "signers_sign": "G7W/oNvczE5nPTFYVOqv8+GOpQd23LS/Dc1Q6xQ1NRDDHlYzmoSE63UQ7Za05kD0rwIYXbuUSr8z8p6RhZmnUs8=", 
-  "signs": {
-    "1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "G/TBxlRoCOVv+T1Q/WmizzKiCtquKdV2ch1OvEjtPrZWKMbPyiUAmlGb09wU79+lcYi/v8EI7Ha21UPj+/6Jb34="
-  }, 
-  "signs_required": 1, 
-  "title": "ZeroBlog", 
-  "zeronet_version": "0.2.8"
+{
+  "address": "1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 
+  "background-color": "white", 
+  "description": "Blogging platform Demo", 
+  "domain": "Blog.ZeroNetwork.bit", 
+  "files": {
+    "css/all.css": {
+      "sha512": "df1181197a5a9ceb896048b9f84d93ee2342426aeacecf759ce4f051a06ddb27", 
+      "size": 111169
+    }, 
+    "data-default/data.json": {
+      "sha512": "3f5c5a220bde41b464ab116cce0bd670dd0b4ff5fe4a73d1dffc4719140038f2", 
+      "size": 196
+    }, 
+    "data-default/users/content-default.json": {
+      "sha512": "0603ce08f7abb92b3840ad0cf40e95ea0b3ed3511b31524d4d70e88adba83daa", 
+      "size": 679
+    }, 
+    "data/data.json": {
+      "sha512": "f3df0c6e47b8475b46e4fcddf2f05dca760fde4cc3506d27461d861ed12f0e51", 
+      "size": 30813
+    }, 
+    "data/img/autoupdate.png": {
+      "sha512": "d2b4dc8e0da2861ea051c0c13490a4eccf8933d77383a5b43de447c49d816e71", 
+      "size": 24460
+    }, 
+    "data/img/direct_domains.png": {
+      "sha512": "5f14b30c1852735ab329b22496b1e2ea751cb04704789443ad73a70587c59719", 
+      "size": 16185
+    }, 
+    "data/img/domain.png": {
+      "sha512": "ce87e0831f4d1e95a95d7120ca4d33f8273c6fce9f5bbedf7209396ea0b57b6a", 
+      "size": 11881
+    }, 
+    "data/img/memory.png": {
+      "sha512": "dd56515085b4a79b5809716f76f267ec3a204be3ee0d215591a77bf0f390fa4e", 
+      "size": 12775
+    }, 
+    "data/img/multiuser.png": {
+      "sha512": "88e3f795f9b86583640867897de6efc14e1aa42f93e848ed1645213e6cc210c6", 
+      "size": 29480
+    }, 
+    "data/img/progressbar.png": {
+      "sha512": "23d592ae386ce14158cec34d32a3556771725e331c14d5a4905c59e0fe980ebf", 
+      "size": 13294
+    }, 
+    "data/img/slides.png": {
+      "sha512": "1933db3b90ab93465befa1bd0843babe38173975e306286e08151be9992f767e", 
+      "size": 14439
+    }, 
+    "data/img/slots_memory.png": {
+      "sha512": "82a250e6da909d7f66341e5b5c443353958f86728cd3f06e988b6441e6847c29", 
+      "size": 9488
+    }, 
+    "data/img/trayicon.png": {
+      "sha512": "e7ae65bf280f13fb7175c1293dad7d18f1fcb186ebc9e1e33850cdaccb897b8f", 
+      "size": 19040
+    }, 
+    "data/img/zeroblog-comments.png": {
+      "sha512": "efe4e815a260e555303e5c49e550a689d27a8361f64667bd4a91dbcccb83d2b4", 
+      "size": 24001
+    }, 
+    "data/img/zeroid.png": {
+      "sha512": "b46d541a9e51ba2ddc8a49955b7debbc3b45fd13467d3c20ef104e9d938d052b", 
+      "size": 18875
+    }, 
+    "data/img/zeroname.png": {
+      "sha512": "bab45a1bb2087b64e4f69f756b2ffa5ad39b7fdc48c83609cdde44028a7a155d", 
+      "size": 36031
+    }, 
+    "data/img/zerotalk-mark.png": {
+      "sha512": "a335b2fedeb8d291ca68d3091f567c180628e80f41de4331a5feb19601d078af", 
+      "size": 44862
+    }, 
+    "data/img/zerotalk-upvote.png": {
+      "sha512": "b1ffd7f948b4f99248dde7efe256c2efdfd997f7e876fb9734f986ef2b561732", 
+      "size": 41092
+    }, 
+    "data/img/zerotalk.png": {
+      "sha512": "54d10497a1ffca9a4780092fd1bd158c15f639856d654d2eb33a42f9d8e33cd8", 
+      "size": 26606
+    }, 
+    "dbschema.json": {
+      "sha512": "7b756e8e475d4d6b345a24e2ae14254f5c6f4aa67391a94491a026550fe00df8", 
+      "size": 1529
+    }, 
+    "img/loading.gif": {
+      "sha512": "8a42b98962faea74618113166886be488c09dad10ca47fe97005edc5fb40cc00", 
+      "size": 723
+    }, 
+    "img/post/slides.png": {
+      "sha512": "1933db3b90ab93465befa1bd0843babe38173975e306286e08151be9992f767e", 
+      "size": 14439
+    }, 
+    "index.html": {
+      "sha512": "a5491a826284e1a4189f27a34724aa3648fc31ac483018273668affbb51e71e1", 
+      "size": 4551
+    }, 
+    "js/all.js": {
+      "sha512": "8e08c6796ecdff7b37374b8a40ff4f809af139241a3cde18c519e2c19ba8b22c", 
+      "size": 203681
+    }
+  }, 
+  "ignore": "((js|css)/(?!all.(js|css))|data/.*db|data/users/.*/.*)", 
+  "includes": {
+    "data/users/content.json": {
+      "signers": [], 
+      "signers_required": 1
+    }
+  }, 
+  "modified": 1432553186.297, 
+  "sign": [
+    105555054054113025747542657031928354966312963790903427393098566938883997052983, 
+    81423101399547255552344950697489499996433371779546841212447229873461700442154
+  ], 
+  "signers_sign": "G7W/oNvczE5nPTFYVOqv8+GOpQd23LS/Dc1Q6xQ1NRDDHlYzmoSE63UQ7Za05kD0rwIYXbuUSr8z8p6RhZmnUs8=", 
+  "signs": {
+    "1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "G0ik/beLriP3pMcPAqx8NbJNZ2RRN6dBd3q6yDqG8ufZsVLMZECrLuc5cVjx8JpG4RIP8HfNXbxzwmYvLE1CZ2g="
+  }, 
+  "signs_required": 1, 
+  "title": "ZeroBlog", 
+  "zeronet_version": "0.3.0"
 }

+ 47 - 0
css/Comments.css

@@ -0,0 +1,47 @@
+.comments { margin-bottom: 60px }
+.comment { background-color: white; padding: 25px 0px; margin: 1px; border-top: 1px solid #EEE }
+.comment .user_name { font-size: 14px; font-weight: bold }
+.comment .added { color: #AAA }
+.comment .reply { color: #CCC; opacity: 0; transition: opacity 0.3s }
+.comment:hover .reply { opacity: 1 }
+.comment .reply .icon { opacity: 0.3 }
+.comment .reply:hover { border-bottom: none; color: #666 }
+.comment .reply:hover .icon { opacity: 1 }
+.comment .info { font-size: 12px; color: #AAA; margin-bottom: 7px }
+.comment .info .score { margin-left: 5px }
+.comment .body { line-height: 1.5em }
+.comment .body p { margin-top: 0.5em; margin-bottom: 0.5em }
+.comment .body.editor { margin-top: 0.5em !important; margin-bottom: 0.5em !important }
+.comment .body h1, .comment .body h2, .comment .body h3 { font-size: 110% }
+.comment .body blockquote { padding: 1px 15px; border-left: 2px solid #E7E7E7; margin: 0px; margin-top: 30px }
+.comment .body blockquote:first-child { margin-top: 0px }
+.comment .body blockquote p { margin: 0px; color: #999; font-size: 90% }
+.comment .body blockquote a { color: #333 }
+
+.comment-new { margin-bottom: 5px; border-top: 0px }
+.comment-new .button-submit { 
+	margin: 0px; font-weight: normal; padding: 5px 15px; display: inline-block;
+	background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; font-size: 15px; line-height: 30px
+}
+.comment-new h2 { margin-bottom: 25px }
+
+/* Input */
+.comment-new textarea { 
+	line-height: 1.5em; width: 100%; padding: 10px; font-family: 'Roboto', sans-serif; font-size: 16px;
+	transition: border 0.3s; border: 2px solid #eee; box-sizing: border-box; overflow-y: auto
+}
+input.text:focus, textarea:focus { border-color: #5FC0EA; outline: none; background-color: white }
+
+.comment-nocert textarea { opacity: 0.5; pointer-events: none }
+.comment-nocert .info { opacity: 0.1; pointer-events: none }
+.comment-nocert .button-submit-comment { opacity: 0.1; pointer-events: none }
+.comment-nocert .button.button-certselect { display: inherit }
+.button.button-certselect { 
+	position: absolute; left: 50%; white-space: nowrap; transform: translateX(-50%); z-index: 99;
+	margin-top: 13px; background-color: #007AFF; color: white; border-bottom-color: #3543F9; display: none
+}
+.button.button-certselect:hover { background-color: #3396FF; color: white; border-bottom-color: #5D68FF; }
+.button.button-certselect:active { position: absolute; transform: translateX(-50%) translateY(1px); top: auto; }
+
+.user-size { font-size: 11px; margin-top: 6px; box-sizing: border-box; text-transform: uppercase; display: inline-block; color: #AAA }
+.user-size-used { position: absolute; color: #B10DC9; overflow: hidden; width: 40px; white-space: nowrap } 

+ 5 - 1
css/ZeroBlog.css

@@ -94,13 +94,16 @@ a:hover { color: #3498db }
 
 .posts, .post-full { display: none; position: relative; }
 .page-main .posts { display: block }
-.page-post .post-full { display: block }
+.page-post.loaded .post-full { display: block; border-bottom: none }
 
 
 .post { margin-bottom: 50px; padding-bottom: 50px; border-bottom: 1px solid #eee; min-width: 500px }
 .post .title a { text-decoration: none; color: inherit; display: inline-block; border-bottom: none; font-weight: inherit }
 .posts .title a:visited { color: #666969 }
 .post .details { color: #BBB; margin-top: 5px; margin-bottom: 20px }
+.post .details .comments-num { border: none; color: #BBB; font-weight: normal; }
+.post .details .comments-num .num { border-bottom: 1px solid #eee; color: #000; }
+.post .details .comments-num:hover .num { border-bottom: 1px solid #D6A1DE; }
 .post .body { font-size: 21.5px; line-height: 1.6; font-family: Tinos; margin-top: 20px }
 
 .post .body h1 { text-align: center; margin-top: 50px }
@@ -110,6 +113,7 @@ a:hover { color: #3498db }
 .post .body li { margin-top: 0.5em; margin-bottom: 0.5em }
 .post .body hr:first-of-type { display: none }
 
+.post .body a img { margin-bottom: -8px }
 .post .body img { max-width: 100% }
 
 code { 

+ 68 - 1
css/all.css

@@ -1,5 +1,57 @@
 
 
+/* ---- data/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8/css/Comments.css ---- */
+
+
+.comments { margin-bottom: 60px }
+.comment { background-color: white; padding: 25px 0px; margin: 1px; border-top: 1px solid #EEE }
+.comment .user_name { font-size: 14px; font-weight: bold }
+.comment .added { color: #AAA }
+.comment .reply { color: #CCC; opacity: 0; -webkit-transition: opacity 0.3s ; -moz-transition: opacity 0.3s ; -o-transition: opacity 0.3s ; -ms-transition: opacity 0.3s ; transition: opacity 0.3s  }
+.comment:hover .reply { opacity: 1 }
+.comment .reply .icon { opacity: 0.3 }
+.comment .reply:hover { border-bottom: none; color: #666 }
+.comment .reply:hover .icon { opacity: 1 }
+.comment .info { font-size: 12px; color: #AAA; margin-bottom: 7px }
+.comment .info .score { margin-left: 5px }
+.comment .body { line-height: 1.5em }
+.comment .body p { margin-top: 0.5em; margin-bottom: 0.5em }
+.comment .body.editor { margin-top: 0.5em !important; margin-bottom: 0.5em !important }
+.comment .body h1, .comment .body h2, .comment .body h3 { font-size: 110% }
+.comment .body blockquote { padding: 1px 15px; border-left: 2px solid #E7E7E7; margin: 0px; margin-top: 30px }
+.comment .body blockquote:first-child { margin-top: 0px }
+.comment .body blockquote p { margin: 0px; color: #999; font-size: 90% }
+.comment .body blockquote a { color: #333 }
+
+.comment-new { margin-bottom: 5px; border-top: 0px }
+.comment-new .button-submit { 
+	margin: 0px; font-weight: normal; padding: 5px 15px; display: inline-block;
+	background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; font-size: 15px; line-height: 30px
+}
+.comment-new h2 { margin-bottom: 25px }
+
+/* Input */
+.comment-new textarea { 
+	line-height: 1.5em; width: 100%; padding: 10px; font-family: 'Roboto', sans-serif; font-size: 16px;
+	-webkit-transition: border 0.3s; -moz-transition: border 0.3s; -o-transition: border 0.3s; -ms-transition: border 0.3s; transition: border 0.3s ; border: 2px solid #eee; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; overflow-y: auto
+}
+input.text:focus, textarea:focus { border-color: #5FC0EA; outline: none; background-color: white }
+
+.comment-nocert textarea { opacity: 0.5; pointer-events: none }
+.comment-nocert .info { opacity: 0.1; pointer-events: none }
+.comment-nocert .button-submit-comment { opacity: 0.1; pointer-events: none }
+.comment-nocert .button.button-certselect { display: inherit }
+.button.button-certselect { 
+	position: absolute; left: 50%; white-space: nowrap; -webkit-transform: translateX(-50%); -moz-transform: translateX(-50%); -o-transform: translateX(-50%); -ms-transform: translateX(-50%); transform: translateX(-50%) ; z-index: 99;
+	margin-top: 13px; background-color: #007AFF; color: white; border-bottom-color: #3543F9; display: none
+}
+.button.button-certselect:hover { background-color: #3396FF; color: white; border-bottom-color: #5D68FF; }
+.button.button-certselect:active { position: absolute; -webkit-transform: translateX(-50%) translateY(1px); -moz-transform: translateX(-50%) translateY(1px); -o-transform: translateX(-50%) translateY(1px); -ms-transform: translateX(-50%) translateY(1px); transform: translateX(-50%) translateY(1px) ; top: auto; }
+
+.user-size { font-size: 11px; margin-top: 6px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; text-transform: uppercase; display: inline-block; color: #AAA }
+.user-size-used { position: absolute; color: #B10DC9; overflow: hidden; width: 40px; white-space: nowrap } 
+
+
 /* ---- data/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8/css/ZeroBlog.css ---- */
 
 
@@ -99,13 +151,16 @@ a:hover { color: #3498db }
 
 .posts, .post-full { display: none; position: relative; }
 .page-main .posts { display: block }
-.page-post .post-full { display: block }
+.page-post.loaded .post-full { display: block; border-bottom: none }
 
 
 .post { margin-bottom: 50px; padding-bottom: 50px; border-bottom: 1px solid #eee; min-width: 500px }
 .post .title a { text-decoration: none; color: inherit; display: inline-block; border-bottom: none; font-weight: inherit }
 .posts .title a:visited { color: #666969 }
 .post .details { color: #BBB; margin-top: 5px; margin-bottom: 20px }
+.post .details .comments-num { border: none; color: #BBB; font-weight: normal; }
+.post .details .comments-num .num { border-bottom: 1px solid #eee; color: #000; }
+.post .details .comments-num:hover .num { border-bottom: 1px solid #D6A1DE; }
 .post .body { font-size: 21.5px; line-height: 1.6; font-family: Tinos; margin-top: 20px }
 
 .post .body h1 { text-align: center; margin-top: 50px }
@@ -115,6 +170,7 @@ a:hover { color: #3498db }
 .post .body li { margin-top: 0.5em; margin-bottom: 0.5em }
 .post .body hr:first-of-type { display: none }
 
+.post .body a img { margin-bottom: -8px }
 .post .body img { max-width: 100% }
 
 code { 
@@ -301,3 +357,14 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
 .hljs-chunk {
   color: #aaa;
 }
+
+
+
+/* ---- data/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8/css/icons.css ---- */
+
+
+.icon-profile { font-size: 6px; top: 0em; -webkit-border-radius: 0.7em 0.7em 0 0; -moz-border-radius: 0.7em 0.7em 0 0; -o-border-radius: 0.7em 0.7em 0 0; -ms-border-radius: 0.7em 0.7em 0 0; border-radius: 0.7em 0.7em 0 0 ; background: #FFFFFF; width: 1.5em; height: 0.7em; position: relative; display: inline-block; margin-right: 4px }
+.icon-profile:before { position: absolute; content: ""; top: -1em; left: 0.38em; width: 0.8em; height: 0.85em; -webkit-border-radius: 50%; -moz-border-radius: 50%; -o-border-radius: 50%; -ms-border-radius: 50%; border-radius: 50% ; background: #FFFFFF; } 
+
+.icon-comment { width: 16px; height: 10px; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; background: #B10DC9; margin-top: 0px; display: inline-block; position: relative; top: -2px; }
+.icon-comment:after { left: 9px; border: 2px solid transparent; border-top-color: #B10DC9; border-left-color: #B10DC9; background: transparent; content: ""; display: block; margin-top: 10px; width: 0px; margin-left: 7px; }

+ 5 - 0
css/icons.css

@@ -0,0 +1,5 @@
+.icon-profile { font-size: 6px; top: 0em; border-radius: 0.7em 0.7em 0 0; background: #FFFFFF; width: 1.5em; height: 0.7em; position: relative; display: inline-block; margin-right: 4px }
+.icon-profile:before { position: absolute; content: ""; top: -1em; left: 0.38em; width: 0.8em; height: 0.85em; border-radius: 50%; background: #FFFFFF; } 
+
+.icon-comment { width: 16px; height: 10px; border-radius: 2px; background: #B10DC9; margin-top: 0px; display: inline-block; position: relative; top: -2px; }
+.icon-comment:after { left: 9px; border: 2px solid transparent; border-top-color: #B10DC9; border-left-color: #B10DC9; background: transparent; content: ""; display: block; margin-top: 10px; width: 0px; margin-left: 7px; }

+ 10 - 0
data-default/data.json

@@ -0,0 +1,10 @@
+{
+	"title": "MyZeroBlog",
+	"description": "My ZeroBlog.",
+	"links": "- [Source code](https://github.com/HelloZeroNet)",
+	"next_post_id": 1,
+	"demo": false,
+	"modified": 1432515193,
+	"post": [
+	]
+}

+ 25 - 0
data-default/users/content-default.json

@@ -0,0 +1,25 @@
+{
+  "files": {}, 
+  "ignore": ".*", 
+  "modified": 1432466966.003, 
+  "signs": {
+    "1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8": "HChU28lG4MCnAiui6wDAaVCD4QUrgSy4zZ67+MMHidcUJRkLGnO3j4Eb1N0AWQ86nhSBwoOQf08Rha7gRyTDlAk="
+  }, 
+  "user_contents": {
+    "cert_signers": {
+      "zeroid.bit": [ "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz" ]
+    }, 
+    "permission_rules": {
+      ".*": {
+        "files_allowed": "data.json", 
+        "max_size": 10000
+      }, 
+      "bitid/.*@zeroid.bit": { "max_size": 40000 }, 
+      "bitmsg/.*@zeroid.bit": { "max_size": 15000 }
+    }, 
+    "permissions": {
+      "banexample@zeroid.bit": false, 
+      "nofish@zeroid.bit": { "max_size": 20000 }
+    }
+  }
+}

File diff suppressed because it is too large
+ 0 - 27
data.json


+ 0 - 17
data.json-default

@@ -1,17 +0,0 @@
-{
-	"title": "MyZeroBlog",
-	"description": "Demo for decentralized, self publishing blogging platform.",
-	"links": "- [Source code](https://github.com/HelloZeroNet)",
-	"next_id": 2,
-	"demo": false,
-	"modified": 1422102473,
-	"posts": [
-		{
-			"id": 1,
-			"title": "Hello ZeroBlog!",
-			"posted": 1422065620.441,
-			"edited": false,
-			"body": "Your first post"
-		}
-	]
-}

+ 54 - 0
dbschema.json

@@ -0,0 +1,54 @@
+{
+	"db_name": "ZeroID",
+	"db_file": "data/zeroblog.db",
+	"version": 2,
+	"maps": {
+		"users/.+/data.json": {
+			"to_table": [ 
+				"comment",
+				{"node": "comment_vote", "table": "comment_vote", "key_col": "comment_uri", "val_col": "vote"} 
+			]
+		},
+		"users/.+/content.json": { 
+			"to_keyvalue": [ "cert_user_id" ]
+		},
+		"data.json": {
+			"to_table": [ "post" ],
+			"to_keyvalue": [ "title", "description", "links", "next_post_id", "demo", "modified" ]
+		}
+
+	},
+	"tables": {
+		"comment": {
+			"cols": [
+				["comment_id", "INTEGER"], 
+				["post_id", "INTEGER"],
+				["body", "TEXT"],
+				["date_added", "INTEGER"],
+				["json_id", "INTEGER REFERENCES json (json_id)"]
+			],
+			"indexes": ["CREATE UNIQUE INDEX comment_key ON comment(json_id, comment_id)", "CREATE INDEX comment_post_id ON comment(post_id)"],
+			"schema_changed": 1426195823
+		},
+		"comment_vote": {
+			"cols": [
+				["comment_uri", "TEXT"],
+				["vote", "INTEGER"],
+				["json_id", "INTEGER REFERENCES json (json_id)"]
+			],
+			"indexes": ["CREATE INDEX comment_vote_comment_uri ON comment_vote(comment_uri)", "CREATE INDEX comment_vote_json_id ON comment_vote(json_id)"],
+			"schema_changed": 1426195822
+		},
+		"post": {
+			"cols": [
+				["post_id", "INTEGER"],
+				["title", "TEXT"],
+				["body", "TEXT"],
+				["date_published", "INTEGER"],
+				["json_id", "INTEGER REFERENCES json (json_id)"]
+			],
+			"indexes": ["CREATE UNIQUE INDEX post_uri ON post(json_id, post_id)", "CREATE INDEX post_id ON post(post_id)"],
+			"schema_changed": 1426195823
+		}
+	}
+}

BIN
img/post/slides.png


+ 50 - 8
index.html

@@ -57,29 +57,71 @@
 
 <!-- right -->
 <div class="right">
- <!-- posts -->
+
+
+ <!-- Post listing -->
  <div class="posts">
   <a href="#New+Post" class="button button-outline new">Add new post</a>
   
   <!-- Template: post -->
   <div class="post template" data-object="Post:23" data-deletable="True">
-   <h1 class="title"><a href="?Post:23:Title" data-editable="title" data-editable-mode="simple">Title</a></h1>
-   <div class="details" data-editable="posted" data-editable-mode="timestamp">21 hours ago &middot; 2 min read</div>
+   <h1 class="title"><a href="?Post:23:Title" data-editable="title" data-editable-mode="simple" class="editable">Title</a></h1>
+   <div class="details">
+    <span class="published" data-editable="date_published" data-editable-mode="timestamp">21 hours ago &middot; 2 min read</span>
+    <a href="?Post:23:title" class="comments-num">&middot; <div class='icon-comment'></div> <span class="num">3 comments</span></a>
+   </div>
    <div class="body" data-editable="body">Body</div>
    <a class="more" href="#"><span class='readmore'>Read more</span> →</a>
   </div>
   <!-- EOF Template: post -->
  
  </div>
- <!-- EOF posts -->
+ <!-- EOF Post listing -->
 
- <!-- post-full -->
+ <!-- Single Post show -->
  <div class="post post-full" data-object="Post:23" data-deletable="True">
-  <h1 class="title"><a href="?Post:23:Title" data-editable="title" data-editable-mode="simple">Title</a></h1>
-  <div class="details" data-editable="posted" data-editable-mode="timestamp">21 hours ago &middot; 2 min read</div>
+  <h1 class="title"><a href="?Post:23:Title" data-editable="title" data-editable-mode="simple" class="editable">Title</a></h1>
+  <div class="details"> <span class="published" data-editable="date_published" data-editable-mode="timestamp">21 hours ago &middot; 2 min read</span> </div>
   <div class="body" data-editable="body"></div>
+
+  <h2 id="Comments"><span class="comments-num">0</span> Comments:</h2>
+  <!-- New comment -->
+  <div class="comment comment-new">
+   <div class="info">
+    <a class="user_name certselect" href="#Change+user" title='Change user'>Please sign in</a>
+    &#9473; 
+    <span class="added">new comment</span>
+   </div>
+   <div class="comment-body">
+    <a class="button button-submit button-certselect certselect" href="#Change+user"><div class='icon-profile'></div>Sign in as...</a>
+   	<textarea class="comment-textarea"></textarea>
+   	<a href="#Submit+comment" class="button button-submit button-submit-comment">Submit comment</a>
+   	<div style='float: right; margin-top: -6px'>
+     <div class="user-size user-size-used"></div>
+     <div class="user-size"></div>
+    </div>
+   	<div style="clear: both"></div>
+   </div>
+  </div>
+  <!-- EOF New comment -->
+
+  <div class="comments">
+   <!-- Template: Comment -->
+   <div class="comment template">
+    <div class="info">
+     <span class="user_name">user_name</span>
+     <!--<span class="cert_domain"></span>-->
+     &#9473; 
+     <span class="added">1 day ago</span>
+    </div>
+    <div class="comment-body">Body</div>
+   </div>
+   <!-- EOF Template: Comment -->
+  </div>
  </div>
- <!-- EOF post-full -->
+ <!-- EOF Single Post sho -->
+
+
 
 </div>
 <!-- EOF right -->

+ 147 - 0
js/Comments.coffee

@@ -0,0 +1,147 @@
+class Comments extends Class
+	pagePost: (post_id, cb=false) ->
+		@post_id = post_id
+		@rules = {}
+		$(".button-submit-comment").on "click", =>
+			@submitComment()
+			return false
+		@loadComments("noanim", cb)
+		@autoExpand $(".comment-textarea") 
+
+		$(".certselect").on "click", =>
+			if Page.server_info.rev < 160
+				Page.cmd "wrapperNotification", ["error", "Comments requires at least ZeroNet 0.3.0 Please upgade!"]
+			else
+				Page.cmd "certSelect", [["zeroid.bit"]]
+			return false
+
+
+	loadComments: (type="show", cb=false) ->
+		query = "SELECT comment.*, json_content.json_id AS content_json_id, keyvalue.value AS cert_user_id, json.directory,
+			(SELECT COUNT(*) FROM comment_vote WHERE comment_vote.comment_uri = comment.comment_id || '@' || json.directory)+1 AS votes
+			FROM comment 
+			LEFT JOIN json USING (json_id) 
+			LEFT JOIN json AS json_content ON (json_content.directory = json.directory AND json_content.file_name='content.json')
+			LEFT JOIN keyvalue ON (keyvalue.json_id = json_content.json_id AND key = 'cert_user_id')
+			WHERE post_id = #{@post_id} ORDER BY date_added DESC"
+
+		Page.cmd "dbQuery", query, (comments) =>
+			$(".comments-num").text(comments.length)
+			for comment in comments
+				user_address = comment.directory.replace("users/", "")
+				comment_address = "#{comment.comment_id}_#{user_address}"
+				elem = $("#comment_"+comment_address)
+				if elem.length == 0 # Create if not exits
+					elem = $(".comment.template").clone().removeClass("template").attr("id", "comment_"+comment_address).data("post_id", @post_id)
+					if type != "noanim"
+						elem.cssSlideDown()
+				@applyCommentData(elem, comment)
+				elem.appendTo(".comments")
+
+
+	applyCommentData: (elem, comment) ->
+		[user_name, cert_domain] = comment.cert_user_id.split("@")
+		user_address = comment.directory.replace("users/", "")
+		$(".comment-body", elem).html Text.toMarked(comment.body, {"sanitize": true})
+		$(".user_name", elem).text(user_name).css("color": Text.toColor(comment.cert_user_id)).attr("title", "#{user_name}@#{cert_domain}: #{user_address}")
+		$(".added", elem).text(Time.since(comment.date_added)).attr("title", Time.date(comment.date_added, "long"))
+		#$(".cert_domain", elem).html("@#{cert_domain}").css("display", "none")
+
+
+	submitComment: ->
+		if not Page.site_info.cert_user_id # Not registered
+			Page.cmd "wrapperNotification", ["info", "Please, select your account."]
+			return false
+
+		body = $(".comment-new .comment-textarea").val()
+		if not body
+			$(".comment-new .comment-textarea").focus()
+			return false
+
+		$(".comment-new .button-submit").addClass("loading")
+		inner_path = "data/users/#{Page.site_info.auth_address}/data.json"
+		Page.cmd "fileGet", {"inner_path": inner_path, "required": false}, (data) =>
+			if data
+				data = JSON.parse(data)
+			else # Default data
+				data = {"next_comment_id": 1, "comment": [], "comment_vote": {} }
+
+			data.comment.push {
+				"comment_id": data.next_comment_id, 
+				"body": body,
+				"post_id": @post_id,
+				"date_added": Time.timestamp()
+			}
+			data.next_comment_id += 1
+			json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
+			Page.writePublish inner_path, btoa(json_raw), (res) =>
+				$(".comment-new .button-submit").removeClass("loading")
+				@loadComments()
+				@checkCert("updaterules")
+				@log "Writepublish result", res
+				if res != false
+					$(".comment-new .comment-textarea").val("")
+
+
+	checkCert: (type) ->
+		last_cert_user_id = $(".comment-new .user_name").text()
+		if Page.site_info.cert_user_id
+			$(".comment-new").removeClass("comment-nocert")
+			$(".comment-new .user_name").text(Page.site_info.cert_user_id)
+		else
+			$(".comment-new").addClass("comment-nocert")
+			$(".comment-new .user_name").text("Please sign in")
+
+		if $(".comment-new .user_name").text() != last_cert_user_id or type == "updaterules" # User changed
+			# Update used/allowed space
+			if Page.site_info.cert_user_id
+				Page.cmd "fileRules", "data/users/#{Page.site_info.auth_address}/content.json", (rules) =>
+					@rules = rules
+					if rules.max_size
+						@setCurrentSize(rules.current_size)
+					else
+						@setCurrentSize(0)
+			else
+				@setCurrentSize(0)
+
+
+	setCurrentSize: (current_size) ->
+		if current_size
+			current_size_kb = current_size/1000
+			$(".user-size").text("used: #{current_size_kb.toFixed(1)}k/#{Math.round(@rules.max_size/1000)}k")
+			$(".user-size-used").css("width", Math.round(70*current_size/@rules.max_size))
+		else
+			$(".user-size").text("")
+
+
+	autoExpand: (elem) ->
+		editor = elem[0]
+		# Autoexpand
+		if elem.height() > 0 then elem.height(1)
+
+		elem.on "input", =>
+			if editor.scrollHeight > elem.height()
+				old_height = elem.height()
+				elem.height(1)
+				new_height = editor.scrollHeight
+				new_height += parseFloat elem.css("borderTopWidth")
+				new_height += parseFloat elem.css("borderBottomWidth")
+				new_height -= parseFloat elem.css("paddingTop")
+				new_height -= parseFloat elem.css("paddingBottom")
+
+				min_height = parseFloat(elem.css("lineHeight"))*2 # 2 line minimum
+				if new_height < min_height then new_height = min_height+4
+
+				elem.height(new_height-4)
+			# Update used space
+			if @rules.max_size
+				if elem.val().length > 0
+					current_size = @rules.current_size + elem.val().length + 90
+				else
+					current_size = @rules.current_size
+				@setCurrentSize(current_size)
+		if elem.height() > 0 then elem.trigger "input"
+		else elem.height("48px")
+
+
+window.Comments = new Comments()

+ 3 - 3
js/InlineEditor.coffee

@@ -41,8 +41,8 @@ class InlineEditor
 
 		$(".editable-edit").css("display", "none") # Hide all edit button until its not finished
 
-		$(".editbar").css("display", "inline-block").addClassLater("visible", 10) 
-		$(".publishbar").css("opacity", 0) # Hide publishbar
+		$(".editbar").cssLater("display", "inline-block", "now").addClassLater("visible", 10) 
+		$(".publishbar").cssLater("opacity", 0, "now") # Hide publishbar
 		$(".editbar .object").text @getObject(@elem).data("object")+"."+@elem.data("editable")
 		$(".editbar .button").removeClass("loading")
 
@@ -94,7 +94,7 @@ class InlineEditor
 
 	deletePost: =>
 		object_type = @getObject(@elem).data("object").split(":")[0]
-		window.zero_blog.cmd "wrapperConfirm", ["Are you sure you sure to delete this #{object_type}?", "Delete"], (confirmed) => 
+		Page.cmd "wrapperConfirm", ["Are you sure you sure to delete this #{object_type}?", "Delete"], (confirmed) => 
 			@stopEdit()
 			@saveContent @getObject(@elem), null
 		return false

+ 157 - 166
js/ZeroBlog.coffee

@@ -1,19 +1,11 @@
 class ZeroBlog extends ZeroFrame
 	init: ->
-		# Set avatar
-		address = document.location.href.replace("/media", "").match(/\/([A-Za-z0-9\._-]+)\//)[1]
-		@log "Address:", address
-
-		imagedata = new Identicon(address, 70).toString();
-		$("body").append("<style>.avatar { background-image: url(data:image/png;base64,#{imagedata}) }</style>")
-
 		@data = null
 		@site_info = null
 		@server_info = null
 
 		@event_page_load = $.Deferred()
 		@event_site_info = $.Deferred()
-		@loadData()
 
 		# Editable items on own site
 		$.when(@event_page_load, @event_site_info).done =>
@@ -28,18 +20,29 @@ class ZeroBlog extends ZeroFrame
 					$(".editbar .icon-help").toggleClass("active")
 					return false
 
-		@log "inited!"
+		$.when(@event_site_info).done =>
+			@log "event site info"
+			# Set avatar
+			imagedata = new Identicon(@site_info.address, 70).toString();
+			$("body").append("<style>.avatar { background-image: url(data:image/png;base64,#{imagedata}) }</style>")
 
+		@log "inited!"
 
-	loadData: ->
-		$.get "data.json", (data) =>
-			@data = data
-			$(".left h1 a").html(data.title)
-			$(".left h2").html(marked(data.description))
-			$(".left .links").html(marked(data.links))
 
-			# Show page based on url
-			@routeUrl(window.location.search.substring(1))
+	loadData: (query="new") ->
+		# Get blog parameters
+		if query == "old" # Old type query for pre 0.3.0
+			query = "SELECT key, value FROM json LEFT JOIN keyvalue USING (json_id) WHERE path = 'data.json'"
+		else
+			query = "SELECT key, value FROM json LEFT JOIN keyvalue USING (json_id) WHERE directory = '' AND file_name = 'data.json'"
+		@cmd "dbQuery", [query], (res) =>
+			@data = {}
+			if res
+				for row in res
+					@data[row.key] = row.value
+				$(".left h1 a").html(@data.title).data("content", @data.title)
+				$(".left h2").html(Text.toMarked(@data.description)).data("content", @data.description)
+				$(".left .links").html(Text.toMarked(@data.links)).data("content", @data.links)
 
 
 	routeUrl: (url) ->
@@ -57,73 +60,77 @@ class ZeroBlog extends ZeroFrame
 
 	pagePost: (post_id) ->
 		s = (+ new Date)
-		found = false
-		for post in @data.posts
-			if post.id == post_id
-				found = true
-				break
-
-		if found
-			@applyPostdata($(".post-full"), post, true)
-		else
-			$(".post-full").html("<h1>Not found</h1>")
-		@pageLoaded()
-		@log "Post loaded in", ((+ new Date)-s),"ms"
+		@cmd "dbQuery", ["SELECT * FROM post WHERE post_id = #{post_id} LIMIT 1"], (res) =>
+			if res.length
+				@applyPostdata($(".post-full"), res[0], true)
+				Comments.pagePost(post_id)
+			else
+				$(".post-full").html("<h1>Not found</h1>")
+			@pageLoaded()
 
 
 
-	pageMain: ->
-		s = (+ new Date)
-		for post in @data.posts
-			elem = $(".post.template").clone().removeClass("template")
-			@applyPostdata(elem, post)
-			elem.appendTo(".posts")
-		@pageLoaded()
-		@log "Posts loaded in", ((+ new Date)-s),"ms"
-
-		$(".posts .new").on "click", => # Create new blog post
-			# Add to data
-			@data.posts.unshift
-				id: @data.next_id
-				title: "New blog post"
-				posted: (+ new Date)/1000
-				edited: false
-				body: "Blog post body"
-			@data.next_id += 1
-
-			# Create html elements
-			elem = $(".post.template").clone().removeClass("template")
-			@applyPostdata(elem, @data.posts[0])
-			elem.hide()
-			elem.prependTo(".posts").slideDown()
-			@addInlineEditors(elem)
-
-			@writeData()
-			return false
 
+	pageMain: ->
+		@cmd "dbQuery", ["SELECT post.*, COUNT(comment_id) AS comments FROM post LEFT JOIN comment USING (post_id) GROUP BY post_id ORDER BY date_published"], (res) =>
+			s = (+ new Date)
+			for post in res
+				elem = $("#post_#{post.post_id}")
+				if elem.length == 0 # Not exits yet
+					elem = $(".post.template").clone().removeClass("template").attr("id", "post_#{post.post_id}")
+					elem.prependTo(".posts")
+				@applyPostdata(elem, post)
+			@pageLoaded()
+			@log "Posts loaded in", ((+ new Date)-s),"ms"
+
+			$(".posts .new").on "click", => # Create new blog post
+				@cmd "fileGet", ["data/data.json"], (res) =>
+					data = JSON.parse(res)
+					# Add to data
+					data.post.unshift
+						post_id: data.next_post_id
+						title: "New blog post"
+						date_published: (+ new Date)/1000
+						body: "Blog post body"
+					data.next_post_id += 1
+
+					# Create html elements
+					elem = $(".post.template").clone().removeClass("template")
+					@applyPostdata(elem, data.post[0])
+					elem.hide()
+					elem.prependTo(".posts").slideDown()
+					@addInlineEditors(elem)
+
+					@writeData(data)
+				return false
 
 
 	# - EOF Pages -
 
 
 	# All page content loaded
-	pageLoaded: ->
+	pageLoaded: =>
 		$("body").addClass("loaded") # Back/forward button keep position support
 		$('pre code').each (i, block) -> # Higlight code blocks
 			hljs.highlightBlock(block)
 		@event_page_load.resolve()
+		@cmd "innerLoaded", true
 
 
-	# Add inline editor markers
 	addInlineEditors: (parent) ->
-		elems = $("[data-editable]:visible", parent)
+		@logStart "Adding inline editors"
+		elems = $("[data-editable]:visible", parent) 
 		for elem in elems
-			new InlineEditor($(elem), @getContent, @saveContent, @getObject)
+			elem = $(elem)
+			if not elem.data("editor") and not elem.hasClass("editor")
+				editor = new InlineEditor(elem, @getContent, @saveContent, @getObject)
+				elem.data("editor", editor)
+		@logEnd "Adding inline editors"
 
 
 	# Check if publishing is necessary
 	checkPublishbar: ->
-		if not @data.modified or @data.modified > @site_info.content.modified
+		if not @site_modified or @site_modified > @site_info.content.modified
 			$(".publishbar").addClass("visible")
 		else
 			$(".publishbar").removeClass("visible")
@@ -131,9 +138,6 @@ class ZeroBlog extends ZeroFrame
 
 	# Sign and Publish site
 	publish: =>
-		if not @server_info.ip_external # No port open
-			@cmd "wrapperNotification", ["error", "To publish the site please open port <b>#{@server_info.fileserver_port}</b> on your router"]
-			return false
 		@cmd "wrapperPrompt", ["Enter your private key:", "password"], (privatekey) => # Prompt the private key
 			$(".publishbar .button").addClass("loading")
 			@cmd "sitePublish", [privatekey], (res) =>
@@ -146,31 +150,38 @@ class ZeroBlog extends ZeroFrame
 	# Apply from data to post html element
 	applyPostdata: (elem, post, full=false) ->
 		title_hash = post.title.replace(/[#?& ]/g, "+").replace(/[+]+/g, "+")
-		elem.data("object", "Post:"+post.id)
-		$(".title a", elem).html(post.title).attr("href", "?Post:#{post.id}:#{title_hash}")
-		details = @formatSince(post.posted)
-
+		elem.data("object", "Post:"+post.post_id)
+		$(".title .editable", elem).html(post.title).attr("href", "?Post:#{post.post_id}:#{title_hash}").data("content", post.title)
+		date_published = Time.since(post.date_published)
+		# Published date
 		if post.body.match /^---/m # Has more over fold
-			details += " &middot; #{@readtime(post.body)}" # If has break add readtime
-			$(".more", elem).css("display", "inline-block").attr("href", "?Post:#{post.id}:#{title_hash}")
-		$(".details", elem).html(details)
+			date_published += " &middot; #{Time.readtime(post.body)}" # If has break add readtime
+			$(".more", elem).css("display", "inline-block").attr("href", "?Post:#{post.post_id}:#{title_hash}")
+		$(".details .published", elem).html(date_published).data("content", post.date_published)
+		# Comments num
+		if post.comments > 0
+			$(".details .comments-num", elem).css("display", "inline").attr("href", "?Post:#{post.post_id}:#{title_hash}#Comments")
+			$(".details .comments-num .num", elem).text("#{post.comments} comments")
+		else
+			$(".details .comments-num", elem).css("display", "none")
 
 		if full 
 			body = post.body
 		else # On main page only show post until the first --- hr separator
 			body = post.body.replace(/^([\s\S]*?)\n---\n[\s\S]*$/, "$1")
 
-		$(".body", elem).html(marked(body))
+		$(".body", elem).html(Text.toMarked(body)).data("content", post.body)
 
 
 	# Wrapper websocket connection ready
 	onOpenWebsocket: (e) =>
+		@loadData()
+		@routeUrl(window.location.search.substring(1))
 		@cmd "siteInfo", {}, @setSiteinfo
 		@cmd "serverInfo", {}, (ret) => # Get server info
 			@server_info = ret
-			version = @server_info.version.split(".")
-			if version[0] == "0" and version[1] == "1" and parseInt(version[2]) < 6
-				@cmd "wrapperNotification", ["error", "ZeroBlog requires ZeroNet 0.1.6, please update!"]
+			if @server_info.rev < 160
+				@loadData("old")
 
 
 	# Returns the elem parent object
@@ -182,71 +193,68 @@ class ZeroBlog extends ZeroFrame
 	getContent: (elem, raw=false) =>
 		[type, id] = @getObject(elem).data("object").split(":")
 		id = parseInt(id)
-		if type == "Post"
-			post = (post for post in @data.posts when post.id == id)[0]
-			content = post[elem.data("editable")]
-
-			if elem.data("editable-mode") == "timestamp" # Time hash
-				content = @formatDate(content, "full")
-		else if type == "Site"
-			content = @data[elem.data("editable")]
-		else
-			content = "Unknown"
-
+		content = elem.data("content")
+		if elem.data("editable-mode") == "timestamp" # Convert to time
+			content = Time.date(content, "full")
 
 		if elem.data("editable-mode") == "simple" or raw # No markdown
 			return content
 		else
-			return marked(content)
+			return Text.toMarked(content)
 
 
 	# Save content to data.json
 	saveContent: (elem, content, cb=false) =>
 		if elem.data("deletable") and content == null then return @deleteObject(elem) # Its a delete request
-
+		elem.data("content", content)
 		[type, id] = @getObject(elem).data("object").split(":")
 		id = parseInt(id)
-
-		if type == "Post"
-			post = (post for post in @data.posts when post.id == id)[0]
-
-			if elem.data("editable-mode") == "timestamp" # Time parse to timestamp
-				content = @timestamp(content)
-
-			post[elem.data("editable")] = content
-		else if type == "Site"
-			@data[elem.data("editable")] = content
-
-		@writeData (res) =>
-			if cb
-				if res == true # OK
-					if elem.data("editable-mode") == "simple" # No markdown
-						cb(content)
-					else if elem.data("editable-mode") == "timestamp" # Format timestamp
-						cb(@formatSince(content))
-					else
-						cb(marked(content))
-				else # Error
-					cb(false)
+		@cmd "fileGet", ["data/data.json"], (res) =>
+			data = JSON.parse(res)
+			if type == "Post"
+				post = (post for post in data.post when post.post_id == id)[0]
+
+				if elem.data("editable-mode") == "timestamp" # Time parse to timestamp
+					content = Time.timestamp(content)
+
+				post[elem.data("editable")] = content
+			else if type == "Site"
+				data[elem.data("editable")] = content
+
+			@writeData data, (res) =>
+				if cb
+					if res == true # OK
+						if elem.data("editable-mode") == "simple" # No markdown
+							cb(content)
+						else if elem.data("editable-mode") == "timestamp" # Format timestamp
+							cb(Time.since(content))
+						else
+							cb(Text.toMarked(content))
+					else # Error
+						cb(false)
 
 
 	deleteObject: (elem) ->
 		[type, id] = elem.data("object").split(":")
 		id = parseInt(id)
 
-		if type == "Post"
-			post = (post for post in @data.posts when post.id == id)[0]
-			if not post then return false # No post found for this id
-			@data.posts.splice(@data.posts.indexOf(post), 1) # Remove from data
+		@cmd "fileGet", ["data/data.json"], (res) =>
+			data = JSON.parse(res)
+			if type == "Post"
+				post = (post for post in data.post when post.post_id == id)[0]
+				if not post then return false # No post found for this id
+				data.post.splice(data.post.indexOf(post), 1) # Remove from data
 
-			@writeData (res) ->
-				if res == true then window.open("?Home", "_top") # Go to home
+				@writeData data, (res) =>
+					if res == true then window.open("?Home", "_top") # Go to home
 
 
-	writeData: (cb=null) ->
-		@data.modified = @timestamp()
-		json_raw = unescape(encodeURIComponent(JSON.stringify(@data, undefined, '\t'))) # Encode to json, encode utf8
-		@cmd "fileWrite", ["data.json", btoa(json_raw)], (res) => # Convert to to base64 and send
+	writeData: (data, cb=null) ->
+		if not data
+			return @log "Data missing"
+		@data["modified"] = data.modified = Time.timestamp()
+		json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t'))) # Encode to json, encode utf8
+		@cmd "fileWrite", ["data/data.json", btoa(json_raw)], (res) => # Convert to to base64 and send
 			if res == "ok"
 				if cb then cb(true)
 			else
@@ -256,59 +264,30 @@ class ZeroBlog extends ZeroFrame
 
 		# Updating title in content.json
 		$.get "content.json", ((content) =>
-			content = content.replace /"title": ".*?"/, "\"title\": \"#{@data.title}\"" # Load as raw html to prevent js bignumber problems
+			content = content.replace /"title": ".*?"/, "\"title\": \"#{data.title}\"" # Load as raw html to prevent js bignumber problems
 			@cmd "fileWrite", ["content.json", btoa(content)], (res) =>
 				if res != "ok"
 					@cmd "wrapperNotification", ["error", "Content.json write error: #{res}"]
 		), "html"
 
 
-	# - Date -
-
-	formatSince: (time) ->
-		now = +(new Date)/1000
-		secs = now - time
-		if secs < 60
-			back = "Just now"
-		else if secs < 60*60
-			back = "#{Math.round(secs/60)} minutes ago"
-		else if secs < 60*60*24
-			back = "#{Math.round(secs/60/60)} hours ago"
-		else if secs < 60*60*24*3
-			back = "#{Math.round(secs/60/60/24)} days ago"
-		else
-			back = "on "+@formatDate(time)
-		back = back.replace(/1 ([a-z]+)s/, "1 $1") # 1 days ago fix
-		return back
-
-
-	# Get elistamated read time for post
-	readtime: (text) ->
-		chars = text.length
-		if chars > 1500
-			return parseInt(chars/1500)+" min read"
-		else
-			return "less than 1 min read"
-
-
-	formatDate: (timestamp, format="short") ->
-		parts = (new Date(timestamp*1000)).toString().split(" ")
-		if format == "short"
-			display = parts.slice(1, 4)
-		else
-			display = parts.slice(1, 5)
-		return display.join(" ").replace(/( [0-9]{4})/, ",$1")
+	writePublish: (inner_path, data, cb) ->
+		@cmd "fileWrite", [inner_path, data], (res) =>
+			if res != "ok" # fileWrite failed
+				@cmd "wrapperNotification", ["error", "File write error: #{res}"]
+				cb(false)
+				return false
 
+			@cmd "sitePublish", {"inner_path": inner_path}, (res) =>
+				if res == "ok"
+					cb(true)
+				else
+					cb(res)
 
-	timestamp: (date="") ->
-		if date == "now" or date == ""
-			return parseInt(+(new Date)/1000)
-		else
-			return parseInt(Date.parse(date)/1000)
 
 
-	# Route incoming requests
-	route: (cmd, message) ->
+	# Parse incoming requests
+	onRequest: (cmd, message) ->
 		if cmd == "setSiteInfo" # Site updated
 			@actionSetSiteInfo(message)
 		else
@@ -317,7 +296,6 @@ class ZeroBlog extends ZeroFrame
 
 	# Siteinfo changed
 	actionSetSiteInfo: (message) =>
-		@log "setSiteinfo", message
 		@setSiteinfo(message.params)
 		@checkPublishbar()
 
@@ -325,6 +303,19 @@ class ZeroBlog extends ZeroFrame
 	setSiteinfo: (site_info) =>
 		@site_info = site_info
 		@event_site_info.resolve(site_info)
+		if $("body").hasClass("page-post") then Comments.checkCert() # Update if username changed
+		# User commented
+		if site_info.event?[0] == "file_done" and site_info.event[1].match /.*users.*data.json$/
+			if $("body").hasClass("page-post") 
+				Comments.loadComments() # Post page, reload comments
+			if $("body").hasClass("page-main")
+				RateLimit 500, =>
+					@pageMain()
+		else if site_info.event?[0] == "file_done" and site_info.event[1] == "data/data.json"
+			@loadData()
+			@pageMain()
+		else
+
 
 
-window.zero_blog = new ZeroBlog()
+window.Page = new ZeroBlog()

File diff suppressed because it is too large
+ 978 - 197
js/all.js


+ 21 - 0
js/lib/jquery.cssanim.coffee

@@ -0,0 +1,21 @@
+jQuery.fn.cssSlideDown = ->
+	elem = @
+	elem.css({"opacity": 0, "margin-bottom": 0, "margin-top": 0, "padding-bottom": 0, "padding-top": 0, "display": "none", "transform": "scale(0.8)"}) 
+	setTimeout (->
+		elem.css("display", "")
+		height = elem.outerHeight()
+		elem.css({"height": 0, "display": ""}).cssLater("transition", "all 0.3s ease-out", 20)
+		elem.cssLater({"height": height, "opacity": 1, "margin-bottom": "", "margin-top": "", "padding-bottom": "", "padding-top": "", "transform": "scale(1)"}, null, 40)
+		elem.cssLater("transition", "", 1000, "noclear")
+	), 10
+	return @
+
+
+jQuery.fn.fancySlideDown = ->
+	elem = @
+	elem.css({"opacity": 0, "transform":"scale(0.9)"}).slideDown().animate({"opacity": 1, "scale": 1}, {"duration": 600, "queue": false, "easing": "easeOutBack"})
+
+
+jQuery.fn.fancySlideUp = ->
+	elem = @
+	elem.delay(600).slideUp(600).animate({"opacity": 0, "scale": 0.9}, {"duration": 600, "queue": false, "easing": "easeOutQuad"})

+ 19 - 13
js/lib/jquery.csslater.coffee

@@ -1,3 +1,5 @@
+timers = {}
+
 jQuery.fn.readdClass = (class_name) ->
 	elem = @
 	elem.removeClass class_name
@@ -14,37 +16,41 @@ jQuery.fn.removeLater = (time = 500) ->
 	return @
 
 jQuery.fn.hideLater = (time = 500) ->
-	elem = @
-	setTimeout ( ->
-		elem.css("display", "none")
-	), time
+	@.cssLater("display", "none", time)
 	return @
 
-jQuery.fn.addClassLater = (class_name, time = 5) ->
+jQuery.fn.addClassLater = (class_name, time=5, mode="clear") ->
 	elem = @
-	setTimeout ( ->
+	if timers[class_name] and mode == "clear" then clearInterval(timers[class_name])
+	timers[class_name] = setTimeout ( ->
 		elem.addClass(class_name)
 	), time
 	return @
 
-jQuery.fn.removeClassLater = (class_name, time = 500) ->
+jQuery.fn.removeClassLater = (class_name, time=500, mode="clear") ->
 	elem = @
-	setTimeout ( ->
+	if timers[class_name] and mode == "clear" then clearInterval(timers[class_name])
+	timers[class_name] = setTimeout ( ->
 		elem.removeClass(class_name)
 	), time
 	return @
 
-jQuery.fn.cssLater = (name, val, time = 500) ->
+jQuery.fn.cssLater = (name, val, time=500, mode="clear") ->
 	elem = @
-	setTimeout ( ->
+	if timers[name] and mode == "clear" then clearInterval(timers[name])
+	if time == "now"
 		elem.css name, val
-	), time
+	else
+		timers[name] = setTimeout ( ->
+			elem.css name, val
+		), time
 	return @
 
 
-jQuery.fn.toggleClassLater = (name, val, time = 10) ->
+jQuery.fn.toggleClassLater = (name, val, time=10, mode="clear") ->
 	elem = @
-	setTimeout ( ->
+	if timers[name] and mode == "clear" then clearInterval(timers[name])
+	timers[name] = setTimeout ( ->
 		elem.toggleClass name, val
 	), time
 	return @

+ 23 - 0
js/utils/Class.coffee

@@ -0,0 +1,23 @@
+class Class
+	trace: true
+
+	log: (args...) ->
+		return unless @trace
+		return if typeof console is 'undefined'
+		args.unshift("[#{@.constructor.name}]")
+		console.log(args...)
+		@
+		
+	logStart: (name, args...) ->
+		return unless @trace
+		@logtimers or= {}
+		@logtimers[name] = +(new Date)
+		@log "#{name}", args..., "(started)" if args.length > 0
+		@
+		
+	logEnd: (name, args...) ->
+		ms = +(new Date)-@logtimers[name]
+		@log "#{name}", args..., "(Done in #{ms}ms)"
+		@ 
+
+window.Class = Class

+ 159 - 0
js/utils/InlineEditor.coffee

@@ -0,0 +1,159 @@
+class InlineEditor
+	constructor: (@elem, @getContent, @saveContent, @getObject) ->
+		@edit_button = $("<a href='#Edit' class='editable-edit icon-edit'></a>")
+		@edit_button.on "click", @startEdit
+		@elem.addClass("editable").before(@edit_button)
+		@editor = null
+		@elem.on "mouseenter", (e) =>
+			@edit_button.css("opacity", "0.4")
+			# Keep in display
+			scrolltop = $(window).scrollTop()
+			top = @edit_button.offset().top-parseInt(@edit_button.css("margin-top"))
+			if scrolltop > top
+				@edit_button.css("margin-top", scrolltop-top+e.clientY-20)
+			else
+				@edit_button.css("margin-top", "")
+		@elem.on "mouseleave", =>
+			@edit_button.css("opacity", "")
+
+		if @elem.is(":hover") then @elem.trigger "mouseenter"
+
+
+	startEdit: =>
+		@content_before = @elem.html() # Save current to restore on cancel
+
+		@editor = $("<textarea class='editor'></textarea>")
+		@editor.css("outline", "10000px solid rgba(255,255,255,0)").cssLater("transition", "outline 0.3s", 5).cssLater("outline", "10000px solid rgba(255,255,255,0.9)", 10) # Animate other elements fadeout
+		@editor.val @getContent(@elem, "raw")
+		@elem.after(@editor)
+
+		@elem.html [1..50].join("fill the width") # To make sure we span the editor as far as we can
+		@copyStyle(@elem, @editor) # Copy elem style to editor
+		@elem.html @content_before # Restore content
+
+		
+		@autoExpand(@editor) # Set editor to autoexpand
+		@elem.css("display", "none") # Hide elem
+
+		if $(window).scrollTop() == 0 # Focus textfield if scroll on top
+			@editor[0].selectionEnd = 0
+			@editor.focus()
+
+		$(".editable-edit").css("display", "none") # Hide all edit button until its not finished
+
+		$(".editbar").css("display", "inline-block").addClassLater("visible", 10) 
+		$(".publishbar").css("opacity", 0) # Hide publishbar
+		$(".editbar .object").text @getObject(@elem).data("object")+"."+@elem.data("editable")
+		$(".editbar .button").removeClass("loading")
+
+		$(".editbar .save").off("click").on "click", @saveEdit
+		$(".editbar .delete").off("click").on "click", @deleteObject
+		$(".editbar .cancel").off("click").on "click", @cancelEdit
+
+		# Deletable button show/hide
+		if @getObject(@elem).data("deletable")
+			$(".editbar .delete").css("display", "").html("Delete "+@getObject(@elem).data("object").split(":")[0])
+		else
+			$(".editbar .delete").css("display", "none")
+
+		window.onbeforeunload = ->
+			return 'Your unsaved blog changes will be lost!'
+		
+		return false
+
+
+	stopEdit: =>
+		@editor.remove()
+		@editor = null
+		@elem.css("display", "")
+
+		$(".editable-edit").css("display", "") # Show edit buttons
+
+		$(".editbar").cssLater("display", "none", 1000).removeClass("visible") # Hide editbar
+		$(".publishbar").css("opacity", 1) # Show publishbar
+
+		window.onbeforeunload = null
+
+
+	saveEdit: =>
+		content = @editor.val()
+		$(".editbar .save").addClass("loading")
+		@saveContent @elem, content, (content_html) =>
+			if content_html # File write ok
+				$(".editbar .save").removeClass("loading")
+				@stopEdit()
+				if typeof content_html == "string" # Returned the new content
+					@elem.html content_html
+
+				$('pre code').each (i, block) -> # Higlight code blocks
+					hljs.highlightBlock(block)
+			else
+				$(".editbar .save").removeClass("loading")
+
+		return false
+
+
+	deleteObject: =>
+		object_type = @getObject(@elem).data("object").split(":")[0]
+		Page.cmd "wrapperConfirm", ["Are you sure you sure to delete this #{object_type}?", "Delete"], (confirmed) => 
+			$(".editbar .delete").addClass("loading")
+			Page.saveContent @getObject(@elem), null, =>
+				@stopEdit()
+		return false
+
+
+	cancelEdit: =>
+		@stopEdit()
+		@elem.html @content_before
+
+		$('pre code').each (i, block) -> # Higlight code blocks
+			hljs.highlightBlock(block)
+
+		return false
+
+
+	copyStyle: (elem_from, elem_to) ->
+		elem_to.addClass(elem_from[0].className)
+		from_style = getComputedStyle(elem_from[0])
+
+		elem_to.css
+			fontFamily: 	from_style.fontFamily
+			fontSize: 		from_style.fontSize
+			fontWeight: 	from_style.fontWeight
+			marginTop: 		from_style.marginTop
+			marginRight: 	from_style.marginRight
+			marginBottom: 	from_style.marginBottom
+			marginLeft: 	from_style.marginLeft
+			paddingTop: 	from_style.paddingTop
+			paddingRight: 	from_style.paddingRight
+			paddingBottom: 	from_style.paddingBottom
+			paddingLeft: 	from_style.paddingLeft
+			lineHeight: 	from_style.lineHeight
+			textAlign: 		from_style.textAlign
+			color: 			from_style.color
+			letterSpacing: 	from_style.letterSpacing
+
+		if elem_from.innerWidth() < 1000 # inline elems fix
+			elem_to.css "minWidth", elem_from.innerWidth()
+
+
+	autoExpand: (elem) ->
+		editor = elem[0]
+		# Autoexpand
+		elem.height(1)
+		elem.on "input", ->
+			if editor.scrollHeight > elem.height()
+				elem.height(1).height(editor.scrollHeight + parseFloat(elem.css("borderTopWidth")) + parseFloat(elem.css("borderBottomWidth")))
+		elem.trigger "input"
+
+		# Tab key support
+		elem.on 'keydown', (e) ->
+			if e.which == 9
+				e.preventDefault()
+				s = this.selectionStart
+				val = elem.val()
+				elem.val(val.substring(0,this.selectionStart) + "\t" + val.substring(this.selectionEnd))
+				this.selectionEnd = s+1; 
+
+ 
+window.InlineEditor = InlineEditor

+ 14 - 0
js/utils/RateLimit.coffee

@@ -0,0 +1,14 @@
+limits = {}
+call_after_interval = {}
+window.RateLimit = (interval, fn) ->
+	if not limits[fn]
+		call_after_interval[fn] = false
+		fn() # First call is not delayed
+		limits[fn] = setTimeout (->
+			if call_after_interval[fn]
+				fn()
+			delete limits[fn]
+			delete call_after_interval[fn]
+		), interval
+	else # Called within iterval, delay the call
+		call_after_interval[fn] = true

+ 48 - 0
js/utils/Text.coffee

@@ -0,0 +1,48 @@
+class Renderer extends marked.Renderer
+	image: (href, title, text) ->
+		return ("<code>![#{text}](#{href})</code>")
+
+class Text
+	toColor: (text) ->
+		hash = 0
+		for i in [0..text.length-1]
+			hash = text.charCodeAt(i) + ((hash << 5) - hash)
+		color = '#'
+		return "hsl(" + (hash % 360) + ",30%,50%)";
+		for i in [0..2]
+			value = (hash >> (i * 8)) & 0xFF
+			color += ('00' + value.toString(16)).substr(-2)
+		return color
+
+
+	toMarked: (text, options={}) ->
+		options["gfm"] = true
+		options["breaks"] = true
+		if options.sanitize
+			options["renderer"] = renderer # Dont allow images
+		text = marked(text, options)
+		return @fixHtmlLinks text
+
+
+	# Convert zeronet html links to relaitve
+	fixHtmlLinks: (text) ->
+		if window.is_proxy
+			return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="http://zero')
+		else
+			return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="')
+
+
+	# Convert a single link to relative
+	fixLink: (link) ->
+		if window.is_proxy
+			return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero')
+		else
+			return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '')
+
+
+	toUrl: (text) =>
+		return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "")
+
+window.is_proxy = (window.location.pathname == "/")
+window.renderer = new Renderer()
+window.Text = new Text()

+ 44 - 0
js/utils/Time.coffee

@@ -0,0 +1,44 @@
+class Time
+	since: (time) ->
+		now = +(new Date)/1000
+		secs = now - time
+		if secs < 60
+			back = "Just now"
+		else if secs < 60*60
+			back = "#{Math.round(secs/60)} minutes ago"
+		else if secs < 60*60*24
+			back = "#{Math.round(secs/60/60)} hours ago"
+		else if secs < 60*60*24*3
+			back = "#{Math.round(secs/60/60/24)} days ago"
+		else
+			back = "on "+@date(time)
+		back = back.replace(/1 ([a-z]+)s/, "1 $1") # 1 days ago fix
+		return back
+
+
+	date: (timestamp, format="short") ->
+		parts = (new Date(timestamp*1000)).toString().split(" ")
+		if format == "short"
+			display = parts.slice(1, 4)
+		else
+			display = parts.slice(1, 5)
+		return display.join(" ").replace(/( [0-9]{4})/, ",$1")
+
+
+	timestamp: (date="") ->
+		if date == "now" or date == ""
+			return parseInt(+(new Date)/1000)
+		else
+			return parseInt(Date.parse(date)/1000)
+
+
+	# Get elistamated read time for post
+	readtime: (text) ->
+		chars = text.length
+		if chars > 1500
+			return parseInt(chars/1500)+" min read"
+		else
+			return "less than 1 min read"
+
+
+window.Time = new Time

+ 3 - 7
js/lib/ZeroFrame.coffee → js/utils/ZeroFrame.coffee

@@ -1,4 +1,4 @@
-class ZeroFrame
+class ZeroFrame extends Class
 	constructor: (url) ->
 		@url = url
 		@waiting_cb = {}
@@ -34,7 +34,7 @@ class ZeroFrame
 		else if cmd == "wrapperClosedWebsocket"
 			@onCloseWebsocket()
 		else
-			@route cmd, message
+			@onRequest cmd, message
 
 
 	route: (cmd, message) =>
@@ -55,11 +55,7 @@ class ZeroFrame
 		@target.postMessage(message, "*")
 		if cb
 			@waiting_cb[message.id] = cb
-
-
-	log: (args...) ->
-		console.log "[ZeroFrame]", args...
-
+			
 
 	onOpenWebsocket: =>
 		@log "Websocket open"

Some files were not shown because too many files changed in this diff