├── LICENSE ├── README.md ├── content.json ├── css ├── Activity.css ├── Button.css ├── Comment.css ├── Editable.css ├── Head.css ├── Hub.css ├── Maxheight.css ├── Menu.css ├── Overlay.css ├── Post.css ├── Uploadable.css ├── User.css ├── ZeroMe.css ├── all.css ├── dark.css ├── fonts.css ├── icons.css └── mobile.css ├── dbschema.json ├── img ├── loading-circle.gif ├── loading.gif ├── logo.png ├── logo.svg ├── nav-icon.png ├── nav-icon.svg └── unkown.png ├── index.html ├── js-external └── pngencoder.js ├── js ├── ActivityList.coffee ├── AnonUser.coffee ├── ContentCreateProfile.coffee ├── ContentFeed.coffee ├── ContentProfile.coffee ├── ContentUsers.coffee ├── Head.coffee ├── Post.coffee ├── PostCreate.coffee ├── PostList.coffee ├── PostMeta.coffee ├── Trigger.coffee ├── User.coffee ├── UserList.coffee ├── ZeroMe.coffee ├── all.js ├── lib │ ├── Class.coffee │ ├── Dollar.coffee │ ├── Promise.coffee │ ├── Property.coffee │ ├── Prototypes.coffee │ ├── RateLimitCb.coffee │ ├── anime.min.js │ ├── clone.js │ ├── maquette.js │ └── marked.min.js └── utils │ ├── Animation.coffee │ ├── Autosize.coffee │ ├── Debug.coffee │ ├── Editable.coffee │ ├── ImagePreview.coffee │ ├── ItemList.coffee │ ├── Maxheight.coffee │ ├── Menu.coffee │ ├── Overlay.coffee │ ├── Scrollwatcher.coffee │ ├── Text.coffee │ ├── Time.coffee │ ├── Translate.coffee │ ├── Uploadable.coffee │ └── ZeroFrame.coffee ├── languages ├── da.json ├── fa.json ├── fr.json ├── hu.json ├── it.json ├── nl.json ├── pt-br.json ├── sk.json ├── tr.json ├── zh-tw.json └── zh.json └── zerome.ico /README.md: -------------------------------------------------------------------------------- 1 | # ZeroMe 2 | 3 | Social network for ZeroNet 4 | 5 | Zite Address: [Me.ZeroNetwork.bit](http://127.0.0.1:43110/Me.ZeroNetwork.bit) or [1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH](http://127.0.0.1:43110/1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH) 6 | 7 | ![Screenshot](http://funkyimg.com/i/2BKNE.png) 8 | -------------------------------------------------------------------------------- /content.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH", 3 | "background-color": "#F2F4F6", 4 | "background-color-dark": "#26242E", 5 | "cloneable": true, 6 | "description": "Social site", 7 | "domain": "Me.ZeroNetwork.bit", 8 | "favicon": "zerome.ico", 9 | "files": { 10 | "css/all.css": { 11 | "sha512": "9c2813dfa7d1e97e109b593eaca83b34bbbf78345aac3af4c7750213ec18645d", 12 | "size": 140729 13 | }, 14 | "dbschema.json": { 15 | "sha512": "7e9fb8aff94c46b9874b9c10f2e056549664a671468f73094cb77cd85a4dd7d2", 16 | "size": 3550 17 | }, 18 | "img/loading-circle.gif": { 19 | "sha512": "339baf1bccb9b80ae29c2493e73b40cf0588c96fc095954b475dea0538aaa929", 20 | "size": 2346 21 | }, 22 | "img/loading.gif": { 23 | "sha512": "8a42b98962faea74618113166886be488c09dad10ca47fe97005edc5fb40cc00", 24 | "size": 723 25 | }, 26 | "img/logo.png": { 27 | "sha512": "d78789ff8f7095d45ad484943b3d4e214289aea942cd5bec03dcde3f20a4c7a7", 28 | "size": 3771 29 | }, 30 | "img/logo.svg": { 31 | "sha512": "a9764ac630d7206ad4de38fc13648df17e0b866a9d5dc14494071f70fcd7f374", 32 | "size": 491 33 | }, 34 | "img/nav-icon.png": { 35 | "sha512": "08b21939aa2c2a8761e6a41a19ae4dfa2efa0ed7309c0b6c976cfbc1ef1dc42e", 36 | "size": 937 37 | }, 38 | "img/nav-icon.svg": { 39 | "sha512": "27ca47ed3acf67db6ce75190cb396c4b4501bb498e897dbf395e2de803807ade", 40 | "size": 638 41 | }, 42 | "img/unkown.png": { 43 | "sha512": "a972f05819097c3a38801b9004bf5011a2dcca257b915617d8ea44c202f2cdbd", 44 | "size": 595 45 | }, 46 | "index.html": { 47 | "sha512": "5177583475bc5ef2ee543b5e1d945341ba0c24d855d3765e5fe4f12dc8588177", 48 | "size": 719 49 | }, 50 | "js-external/pngencoder.js": { 51 | "sha512": "ecaadaad552d2610336995b48360253cd6d847b6aa590fc3d96e7245a5d5fe11", 52 | "size": 53602 53 | }, 54 | "js/all.js": { 55 | "sha512": "8f2fd6284c70144be8989ff37cf7156dcb46a7afa9ff714bafe35afe8e91ce0c", 56 | "size": 269337 57 | }, 58 | "languages/da.json": { 59 | "sha512": "5dc810fae4c6bfbcf2251ab28ff003a64155c4830cf79979f84868e41f48a64a", 60 | "size": 3114 61 | }, 62 | "languages/fr.json": { 63 | "sha512": "193c033c87333fb0b5ab844433f5a299a27d6306e06b99d2df439f9f0d0ba7f5", 64 | "size": 3447 65 | }, 66 | "languages/hu.json": { 67 | "sha512": "bc0f53e096ff1c701c63b0db227e5c51373c1903785ac448f1d9c44e81a1701f", 68 | "size": 3294 69 | }, 70 | "languages/it.json": { 71 | "sha512": "ba67a787f4b3aef92008043499c26e6267ba975902700e16cff9491c6fe4bdf0", 72 | "size": 3382 73 | }, 74 | "languages/nl.json": { 75 | "sha512": "b4f0863769a636b4fdb58091e7ffe5e84e315995b59e7c0b70e2b87b6927a8f9", 76 | "size": 3540 77 | }, 78 | "languages/pt-br.json": { 79 | "sha512": "dfd554eca364f1fb86ecedc98866638dee8c2118bedbf9b32fee797ec6b60fe4", 80 | "size": 3491 81 | }, 82 | "languages/sk.json": { 83 | "sha512": "779e69b2a6750847438458971431b0406047784314c1c30a66f6b03091d378c1", 84 | "size": 3518 85 | }, 86 | "languages/tr.json": { 87 | "sha512": "d9df6b218bcf359766ee3416a0cf4b4a3e9181601a7106957f674b22a79aa07a", 88 | "size": 3428 89 | }, 90 | "languages/zh-tw.json": { 91 | "sha512": "79a98d4b5673789214143de370216c3bb51f3f863031d66fcf7f36c57ee15933", 92 | "size": 3403 93 | }, 94 | "languages/zh.json": { 95 | "sha512": "fdcc53981de208ad50359d4abd427e0b1de5d6291c683b99e95b8bbefc2142f2", 96 | "size": 3424 97 | }, 98 | "zerome.ico": { 99 | "sha512": "94fb8e604e75fdb1aabc04839ff18b384190f216361c60d57dc32b7a8f1f58b3", 100 | "size": 1150 101 | } 102 | }, 103 | "ignore": "(merged-.*|(js|css)/(?!all.(js|css)))", 104 | "inner_path": "content.json", 105 | "modified": 1513204798, 106 | "postmessage_nonce_security": true, 107 | "settings": { 108 | "default_hubs": { 109 | "1MoonP8t4rk9QamBUPh5Aspkwa1Xhf5ux2": { 110 | "description": "Hub for ZeroMe users. Runner: Nofish", 111 | "title": "Moon hub" 112 | } 113 | } 114 | }, 115 | "signers_sign": "G/UCyELE5shc5f/FSJSe2KvxGlZiS5fzvn7Ezhha0gN/QOFeznJtZWS61J20FfkGfHdp1HcpWv//anioez1iOW0=", 116 | "signs": { 117 | "1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH": "G/4+dyjTSYsqdEBCthX7vsLK8Iev7JGeQlsYwyvd28pmeNnmVrtJXVJbBqlvPOjmH4Nw29dj/aOgVNAxhLaeSpw=" 118 | }, 119 | "signs_required": 1, 120 | "title": "ZeroMe", 121 | "translate": ["js/all.js"], 122 | "viewport": "width=device-width, initial-scale=0.8", 123 | "zeronet_version": "0.6.0" 124 | } -------------------------------------------------------------------------------- /css/Activity.css: -------------------------------------------------------------------------------- 1 | .activity-list { margin-bottom: 30px } 2 | .activity-list .items a { color: #555; font-weight: bold } 3 | 4 | .activity-list .items { position: relative; margin-left: -6px; margin-bottom: -10px } 5 | .activity-list .bg-line { 6 | height: calc(100% - 40px); width: 2px; position: absolute; background-color: #c7c7c8; 7 | margin-left: 12px; margin-top: 5px; box-sizing: border-box; z-index: 0; 8 | } 9 | 10 | .activity-list .circle { 11 | width: 8px; height: 8px; border: 2px solid #c5c5c5; position: absolute; pointer-events: none; 12 | margin-left: -28px; border-radius: 15px; background-color: #f6f7f8 13 | } 14 | 15 | .activity { padding-left: 35px; padding-bottom: 19px; font-family: Roboto, Helvetica, Arial; font-size: 15px; line-height: 1.5em; color: #888 } 16 | .activity .body { top: -5px; position: relative; max-height: 67px; overflow: hidden; -webkit-line-clamp: 3; -webkit-box-orient: vertical; display: -webkit-box; } 17 | .activity:last-child { background-color: #F6F7F8 } 18 | .activity.latest .circle { border-color: #666 } -------------------------------------------------------------------------------- /css/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | margin-top: 4px; border: 1px solid hsla(236,100%,79%,1); color: #5d68ff; border-radius: 33px; display: inline-block; 3 | font-size: 19px; font-weight: lighter; text-align: center; transition: all 0.3s; padding: 8px 30px; background-position: -200px center; 4 | } 5 | .button:hover { background-color: #5d68ff; color: #F6F7F8; text-decoration: none; border-color: #5d68ff; transition: none } 6 | .button:hover .icon { background-color: #FFF; transition: none } 7 | .button:focus { transition: all 0.3s } 8 | .button:active { transform: translateY(1px); transition: all 0.3s, transform none; box-shadow: inset 0px 5px 7px -3px rgba(212, 212, 212, 0.41); outline: none; transition: none } 9 | 10 | .button.loading { 11 | color: rgba(0,0,0,0) !important; background: rgba(128, 128, 128, 0.5) url(../img/loading.gif) no-repeat center center !important; border-color: rgba(0,0,0,0) !important; 12 | transition: all 0.5s ease-out; pointer-events: none; transition-delay: 0.5s 13 | } 14 | 15 | /* Follow */ 16 | .button-follow { width: 32px; line-height: 32px; padding: 0px; border: 1px solid #aaa; color: #999; padding-left: 1px; padding-bottom: 1px; } 17 | .button-follow:hover { background-color: rgba(255,255,255,0.3) !important; border-color: #2ecc71 !important; color: #2ecc71 } 18 | .button-follow-big { padding-left: 25px; float: none; border: 1px solid #2ecc71; color: #2ecc71; min-width: 100px; } 19 | .button-follow-big .icon-follow { margin-right: 10px; display: inline-block; transition: transform 0.3s ease-in-out } 20 | .button-follow-big:hover { border-color: #2ecc71 !important; color: #2ecc71; background-color: white; text-decoration: underline; } 21 | 22 | /* Submit */ 23 | .button-submit { 24 | padding: 12px 30px; border-radius: 3px; margin-top: 11px; background-color: #5d68ff; /*box-shadow: 0px 1px 4px rgba(93, 104, 255, 0.41);*/ 25 | border: none; border-bottom: 2px solid #4952c7; font-weight: bold; color: #ffffff; font-size: 12px; text-transform: uppercase; margin-left: 10px; 26 | } 27 | .button-submit:hover, .button-submit:focus { color: white; background-color: #6d78ff } 28 | 29 | .button-small { padding: 7px 20px; margin-left: 10px } 30 | .button-outline { background-color: white; border: 1px solid #EEE; border-bottom: 2px solid #EEE; color: #AAA; } 31 | .button-outline:hover { background-color: white; border: 1px solid #CCC; border-bottom: 2px solid #CCC; color: #777 } 32 | -------------------------------------------------------------------------------- /css/Comment.css: -------------------------------------------------------------------------------- 1 | .comment-list { 2 | background-color: #fafafa; padding-left: 80px; margin-left: -80px; padding-right: 20px; margin-right: -20px; margin-bottom: -17px; padding-bottom: 10px; 3 | border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; padding-top: 19px; margin-top: 10px; border-top: 1px solid #E3E3E3; font-size: 85%; 4 | } 5 | .comment-list .body { padding-top: 0px } 6 | .comment-list .body p { padding: 0px } 7 | 8 | .comment-create { position: relative; } 9 | .comment-create textarea { width: 100%; margin-bottom: 11px; box-sizing: border-box; padding-right: 90px; } 10 | .comment-create .button-submit { right: 13px; position: absolute; bottom: 23px; } 11 | .comment { padding-top: 10px } 12 | .comment-list .comment .user { padding-bottom: 0px; white-space: nowrap; line-height: 15px; } 13 | .comment .body { padding-top: 4px; padding-bottom: 10px; } 14 | .comment .body.maxheight:before { background: linear-gradient(rgba(0,0,0,0), #fafafa 70%) } 15 | .comment .icon-reply { opacity: 0; transition: all 0.3s } 16 | .comment:hover .icon-reply { opacity: 0.5 } 17 | .comment .icon-reply:hover { opacity: 1; transition: none } 18 | .comment .user .name { line-height: 16px; } 19 | 20 | .comment h1, .comment h2, .comment h3, .comment h4, .comment h5, .comment h6 { font-size: inherit; font-weight: bold } 21 | -------------------------------------------------------------------------------- /css/Editable.css: -------------------------------------------------------------------------------- 1 | .editable .icon-edit { margin-left: -24px; padding: 7px; border-radius: 30px; margin-top: -5px; position: absolute; opacity: 0; transition: all 0.3s } 2 | .editable:hover .icon-edit { opacity: 0.7 } 3 | .editable .icon-edit:hover { opacity: 1; transition: none } 4 | .editable .editablebuttons { text-align: right; padding-bottom: 10px } 5 | .editable .editablebuttons .link:focus { outline: none } 6 | .editable .empty { opacity: 0.6; } 7 | .editable.editing.overlay { 8 | padding: 10px; margin-left: -10px; background-color: rgba(255,255,255,0.9); 9 | box-shadow: 0px 0px 20px #EEE; margin-bottom: -62px; z-index: 999; position: relative; 10 | } 11 | -------------------------------------------------------------------------------- /css/Head.css: -------------------------------------------------------------------------------- 1 | .head-container { background-color: white; box-shadow: 0px -7px 32px rgba(0,0,0,0.15); } 2 | 3 | .head .logo { 4 | height: 50px; padding: 4px 6px; box-sizing: border-box; display: inline-block; 5 | color: white; font-size: 30px; font-weight: lighter; text-decoration: none 6 | } 7 | 8 | .head .right { float: right; } 9 | .head .user { display: inline-block; vertical-align: top; margin-right: 20px; text-align: right; padding-top: 7px; } 10 | .head .user .name { color: #5d68ff; font-weight: normal; } 11 | .head .user .address { display: block } 12 | .head .settings { 13 | display: inline-block; height: 50px; width: 50px; text-align: center; vertical-align: middle; transition: all 0.3s; 14 | border-left: 1px solid rgba(0, 0, 0, 0.05); line-height: 50px; font-size: 20px; color: #AAA; font-weight: normal; text-decoration: none; 15 | } 16 | .head .settings:hover { color: #5d68ff; background-color: #FAFAFA; transition: none } 17 | .head .settings:active { background-color: #F5F5F5; transition: none } 18 | .head .menu { box-shadow: 0px 4px 8px rgba(0,0,0,0.1) } 19 | 20 | 21 | @media screen and (max-width: 1100px) { 22 | .head .right { margin-right: 80px; } 23 | } -------------------------------------------------------------------------------- /css/Hub.css: -------------------------------------------------------------------------------- 1 | .hub.card { padding: 21px; text-align: left; font-size: 18px; width: 80%; display: block; margin-left: auto; margin-right: auto; } 2 | .hub .intro { font-weight: lighter; font-size: 16px; margin-top: 7px; } 3 | .hub .avatars { float: right; } 4 | .hub .button-join { float: right; margin-left: 20px; } 5 | .hub .avatar { margin-left: 5px } 6 | .hubselect { padding-top: 30px } -------------------------------------------------------------------------------- /css/Maxheight.css: -------------------------------------------------------------------------------- 1 | .maxheight { max-height: 550px; overflow: hidden; transition: all 1s ease-in-out } 2 | .maxheight-limited:before { 3 | display: block; width: 100%; content: "Read more"; position: relative; background: linear-gradient(rgba(0,0,0,0), white 70%); 4 | margin-top: -125px; height: 125px; line-height: 56px; vertical-align: bottom; padding-top: 70px; box-sizing: border-box; 5 | cursor: pointer; color: #5d68ff; text-align: center; top: 550px; 6 | } -------------------------------------------------------------------------------- /css/Menu.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | background-color: white; padding: 10px 0px; position: absolute; top: 0px; max-height: 0px; overflow: hidden; transform: translate(-100%, -30px); pointer-events: none; 3 | box-shadow: 0px 2px 8px rgba(0,0,0,0.1); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; z-index: 99; 4 | display: inline-block; z-index: 99999999999999999; margin-top: 50px; 5 | } 6 | .menu-right { left: 100% } 7 | .menu.visible { opacity: 1; max-height: 350px; transform: translate(-100%, 0px); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; pointer-events: all } 8 | 9 | .menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; padding-left: 32px; white-space: nowrap; } 10 | .menu-item-separator { margin-top: 3px; margin-bottom: 3px; border-top: 1px solid #eee } 11 | 12 | .menu-item:hover { background-color: #F6F6F6; transition: none; color: inherit; cursor: pointer; color: black; text-decoration: none } 13 | .menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; transition: none } 14 | .menu-item.selected:before { 15 | content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1); line-height: 15px; 16 | font-weight: bold; position: absolute; margin-left: -14px; font-size: 12px; margin-top: 2px; 17 | } -------------------------------------------------------------------------------- /css/Overlay.css: -------------------------------------------------------------------------------- 1 | #Overlay { 2 | position: absolute; z-index: 9999; width: 100%; background-color: rgba(255, 255, 255, 0); 3 | height: 100%; transition: background-color 0.6s; pointer-events: none; cursor: zoom-in; 4 | } 5 | #Overlay.visible { background-color: rgba(255, 255, 255, 0.9); pointer-events: all; } 6 | #Overlay .img { position: absolute; background-size: contain; transition: all 0.3s; background-repeat: no-repeat; } -------------------------------------------------------------------------------- /css/Post.css: -------------------------------------------------------------------------------- 1 | .post { 2 | background-color: white; padding: 16px 20px; padding-left: 80px; border-radius: 4px; backface-visibility: hidden; transform-style: preserve-3d; transform: translateZ(0); 3 | border: 1px solid #EEF0F1; border-bottom: 2px solid #ECEDEE; margin-bottom: 12px; transition: all 0.3s; 4 | } 5 | .post.selected { box-shadow: 0px 0px 40px rgba(0,0,0,0.1); } 6 | 7 | .post .user .settings { float: right; color: #666; transition: all 0.3s; padding: 5px 10px; margin-right: -10px; margin-top: -5px; font-size: 19px; } 8 | .post .user .settings:hover { text-decoration: none; color: #333 } 9 | .post .user .settings:active { background-color: #F5F5F5; transition: none } 10 | 11 | .post .user { padding-bottom: 8px; height: 21px; line-height: 20px; } 12 | .post .user .address, .post .added, .post .sep { font-size: 14px; color: #AAA;} 13 | .post .body { padding-top: 2px; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; line-height: 1.5em; color: #333; word-break: break-word; overflow: hidden } 14 | .post .reply-name { font-size: 14px; font-family: Roboto, Helvetica, Arial; color: #000 } 15 | .post .actions { height: 30px; margin-left: -5px; } 16 | .post .actions .icon { margin-right: 1px } 17 | .post .actions .link { color: #AAA; font-size: 12px; height: 30px; vertical-align: middle; line-height: 30px; display: inline-block; padding-right: 10px } 18 | .post .actions .link.active { color: #5d68ff; } 19 | .post .actions .like { width: 35px; margin-right: 0px; transition: width 0.3s, margin-right 0.3s; white-space: nowrap; position: relative; } 20 | .post .actions .like.like-zero { width: 20px; margin-right: 5px; } 21 | 22 | .post code { background-color: #FBFBFB; padding: 3px 4px; border: solid 1px #e1e4e5; font-family: Consolas, Monaco, monospace; font-size: 13px } 23 | .post pre code { 24 | border: none; padding: 0px; background-color: transparent; border: none; overflow-x: auto; display: inline-block; max-width: 100%; padding-right: 20px; box-sizing: border-box; 25 | border: solid 1px #e1e4e5; padding: 10px; background-color: #FBFBFB; font-family: Consolas, Monaco, monospace; border-bottom: 2px solid #e1e4e5; line-height: 1em; 26 | } 27 | 28 | .post blockquote { padding-left: 2em; margin-left: 3px; border-left: 3px solid #c7c7c8; padding-top: 0.8em; font-style: italic; padding-bottom: 0.8em; } 29 | 30 | .post ul { margin: 0px; padding-bottom: 0.6em; } 31 | 32 | .post table { border-collapse: collapse; margin-bottom: 10px; } 33 | .post td, .post th { padding: 5px 10px; border: 1px solid #EEE; border-collapse: collapse; } 34 | 35 | .post-create-container { margin-bottom: 12px } 36 | .post-create { transition: all 0.6s } 37 | .post-create .postfield { 38 | font-family: Roboto; font-size: 16px; border: 1px solid white; padding: 15px 15px; 39 | font-size: 16px; width: 100%; height: 52px; box-sizing: border-box; padding-right: 40px; 40 | } 41 | .post-create .user { margin-bottom: 0px; height: auto; padding-bottom: 0px; } 42 | .post-create .postbuttons { height: 55px; box-sizing: border-box; transform: scale(1); overflow: hidden; transition: all 0.3s; text-align: right } 43 | .post-create.editing .postfield { border: 1px solid #c6caff } 44 | .post-create.editing { box-shadow: 0px 1px 13px 1px #eaeaea } 45 | .post-create .select-user-container { width: 100%; text-align: center; margin-bottom: -100px; height: 100px; z-index: 1; position: relative; } 46 | .post-create .attach { font-size: 12px; text-transform: uppercase; border: 0px; padding: 8px 20px; font-weight: bold; } 47 | .post-create .icon-image { position: absolute; margin-left: 490px; margin-top: 17px; color: #CECECE; transition: all 0.3s } 48 | .post-create .icon-image:hover { color: #5d68ff; transition: none } 49 | .post-create .image { 50 | width: 100%; height: 0px; background-repeat: no-repeat; transform: scale(1); background-position: top center; 51 | background-size: cover; opacity: 1; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1); overflow: hidden; 52 | } 53 | .post-create .image .close { float: right; color: white; font-weight: bold; text-shadow: 0px 0px 11px black; padding: 10px; } 54 | .post-create .image.empty { opacity: 0; transform: scale(1.5) } 55 | 56 | .post-create:not(.editing) .postbuttons { opacity: 0; height: 0px; } 57 | .post-create:not(.editing) .image { opacity: 0; height: 0px; } 58 | 59 | .post-list-empty { text-align: center; padding-top: 100px } 60 | .post-list-type { text-align: right; } 61 | .post-list-type a { 62 | margin-right: 10px; padding-bottom: 5px; display: inline-block; border-bottom: 2px solid rgba(0,0,0,0); 63 | color: #999; margin-bottom: 7px; margin-top: 10px; margin-left: 10px; transition: all 0.3s 64 | } 65 | .post-list-type .active { border-bottom: 2px solid #606aff; color: #606aff; } 66 | .post-list-type a:hover { border-bottom: 2px solid #606aff; color: #606aff; transition: none; text-decoration: none } 67 | 68 | .post .img { background-size: cover; text-align: center; position: relative; margin-bottom: 10px; } 69 | .post .img .show { 70 | top: 50%; display: inline-block; color: white; background-color: #606aff; overflow: hidden; 71 | width: 140px; position: absolute; margin-left: -70px; box-sizing: border-box; margin-top: -20px; height: 40px; padding: 12px 20px; 72 | text-transform: uppercase; font-size: 12px; border-radius: 25px; transition: all 0.3s; 73 | } 74 | .post .img .show .title { white-space: pre; line-height: 30px; height: 16px; transform: translateY(-36px); transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) } 75 | .post .img .details { 76 | bottom: 0px; position: absolute; font-size: 14px; right: 10px; padding: 5px 10px; margin-bottom: 12px; text-decoration: none; 77 | background-color: rgba(0, 0, 0, 0.4); border-radius: 30px; color: white; opacity: 0; transition: all 0.3s 78 | } 79 | .post .img .details .size { display: inline-block; margin-right: 9px; max-width: 80px; overflow: hidden; white-space: nowrap; vertical-align: -4px; transition: all 0.3s ease-out; } 80 | .post .img .fullsize { width: 100%; height: 100%; display: block; background-size: contain; opacity: 0; transition: all 0.6s; position: absolute; background-repeat: no-repeat; background-color: white; } 81 | .post .img.loading .show { background-color: #2ecc71; animation: bounce .3s infinite alternate ease-out; pointer-events: none; } 82 | .post .img.loading .show .title { transform: translateY(-6px); } 83 | .post .img.downloaded .fullsize { opacity: 1; cursor: zoom-in } 84 | .post .img.downloaded .show { opacity: 0; pointer-events: none; visibility: hidden; } 85 | .post .img.hasinfo .details { opacity: 1 } 86 | .post .img.downloaded .details { opacity: 0 } 87 | .post .img.downloaded .details .size { max-width: 0px; } 88 | .post .img.downloaded:hover .details { opacity: 1 } 89 | .post .img .menu { text-align: right; margin-top: -94px; z-index: 999; font-size: 15px; line-height: 20px; } 90 | .post .img .image-settings { color: white; padding: 2px 7px; text-decoration: none } 91 | .post .img .details:hover { background-color: rgba(0,0,0,0.7); transition: none } 92 | .post .img .details:active { background-color: rgba(0,0,0,0.5); transition: none } 93 | .post .img .oldversion { background-color: rgba(0, 0, 0, 0.5); padding: 10px 10px; top: 49%; position: relative; color: white; border-radius: 25px; } 94 | -------------------------------------------------------------------------------- /css/Uploadable.css: -------------------------------------------------------------------------------- 1 | .uploadable .icon-upload { opacity: 0; transition: all 0.3s } 2 | .uploadable .icon-upload:hover { opacity: 0.8; transition: all 0.1s } -------------------------------------------------------------------------------- /css/User.css: -------------------------------------------------------------------------------- 1 | .users .user { padding-left: 70px; padding-bottom: 20px } 2 | .users .user .nameline { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 26px } 3 | .users .user .intro { font-weight: 100; font-size: 13px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; min-height: 18px; word-wrap: break-word; } 4 | .users .user .added { font-size: 11px; color: #999; margin-left: 6px; vertical-align: 3px; } 5 | .users .user .intro-full { margin-left: -57px; padding-top: 18px; font-weight: 100; line-height: 1.5em; overflow: hidden; word-wrap: break-word; } 6 | .users .button-follow { float: right; margin-left: 5px; } 7 | .users .user.followed .button-follow, .users .user.followed .button-follow:hover { 8 | background-color: #2ecc71 !important; border-color: #2ecc71 !important; color: #FFF; transform: rotate(45deg) 9 | } 10 | .users .user.followed .button-follow-big { 11 | background-color: #2ecc71 !important; border-color: #2ecc71 !important; color: #FFF 12 | } 13 | .users .user.followed .button-follow-big .icon-follow { transform: rotate(45deg); } 14 | 15 | .user .name { font-weight: bold; color: #5d68ff; overflow: hidden; max-width: 200px; display: inline-block; white-space: nowrap; vertical-align: text-bottom; text-overflow: ellipsis } 16 | .user .address, .user .cert_user_id { font-size: 13px; color: #AAA; } 17 | .user .avatar { position: absolute; margin-left: -67px } 18 | 19 | .user.card { padding: 15px; padding-left: 75px; overflow: hidden } 20 | .user.card.profile { margin-bottom: 30px; } 21 | .user.card .avatar { margin-top: -4px; position: absolute; margin-left: -60px; } 22 | .user.card .follow-container { margin-left: -57px; text-align: center; margin-top: 30px; margin-bottom: 20px; } 23 | 24 | .users.gray .button-follow { border: 1px solid #aaa; color: #999; } 25 | .users.gray .button-follow:hover, .users.gray .button-follow:active { color: #2ecc71; } 26 | .users.gray .name { color: #333; } 27 | 28 | .user .uploadable .icon-upload { position: absolute; margin-left: -48px; z-index: 999; margin-top: 7px } 29 | .user.notseeding .button-follow { border-color: #E0E0E0; } 30 | .user.card.notseeding .button-follow { border-color: #EFEFEF; } 31 | 32 | .user .help { margin-left: -57px; font-size: 14px; color: #AAA; } 33 | 34 | .checkbox-skin { background-color: #CCC; width: 35px; height: 18px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; } 35 | .checkbox-skin:before { 36 | content: ""; position: relative; width: 14px; background-color: white; height: 14px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px; 37 | transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); 38 | } 39 | .checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px; } 40 | .checkbox.checked .checkbox-skin:before { margin-left: 19px; } 41 | .checkbox.checked .checkbox-skin { background-color: #2ECC71; } 42 | 43 | .user-mute { text-align: center; display: block; margin-top: -22px; color: #999; font-size: 14px; margin-bottom: 30px; } 44 | .user-notfound { text-align: center; font-size: 22px; margin-top: 100px; font-weight: lighter; } 45 | -------------------------------------------------------------------------------- /css/ZeroMe.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F6F7F8; font-family: Roboto, Helvetica, Arial; margin: 0px; padding: 0px; 3 | backface-visibility: hidden; height: 100%; position: absolute; width: 100%; overflow-x: hidden; height: 35000px 4 | } 5 | body.loaded { height: 100%; overflow: auto } 6 | 7 | p, h1, h2, h3, h4 { margin: 0px; padding-bottom: 0.6em; } 8 | 9 | input.text, textarea { border: 1px solid #EEE; padding: 15px 15px; transition: all 0.3s; width: 100%; box-sizing: border-box; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; line-height: 1.4em; } 10 | input.text:disabled, textarea:disabled { background-color: #FAFAFA; color: #9A9A9A } 11 | input.big { font-size: 20px; font-weight: 100; font-family: Roboto, Helvetica, Arial } 12 | input.search { border-radius: 50px; padding-left: 30px; margin-bottom: 30px } 13 | input.text:focus, textarea:focus { outline: none; border: 1px solid #c6caff } 14 | textarea.autosize { overflow: hidden; transition: border 0.3s, background-color 0.3s, color 0.3s } 15 | 16 | a { text-decoration: none; color: #5d68ff } 17 | a:hover { text-decoration: underline; } 18 | a:active { text-decoration: none } 19 | a.link:active { background-color: rgba(0,0,0,0.05); outline: 4px solid rgba(0,0,0,0.05); transition: none } 20 | 21 | h1 { font-size: 34px; line-height: 34px } 22 | h1, h2, h3 { font-weight: lighter } 23 | h2 a.link { font-size: 13px; margin-left: 10px; font-weight: normal; margin-top: 8px; } 24 | h2.sep { border-top: 1px solid #EEE; padding-top: 20px } 25 | h5 { font-weight: normal; color: rgba(0, 0, 0, 0.5) } 26 | 27 | .center { width: 960px; margin-left: auto; margin-right: auto; } 28 | 29 | /* Content */ 30 | #Content { margin-top: 30px; margin-bottom: 50px } 31 | 32 | .content-signup { text-align: center } 33 | .content-signup .button-certselect { margin: 20px; display: inline-block; } 34 | 35 | /* Cols */ 36 | .col-left, .col-center, .col-right { width: 66%; display: inline-block; vertical-align: top; box-sizing: border-box } 37 | .col-center { padding-left: 5px; } 38 | .col-left, .col-right { width: 33%; padding-left: 20px; margin-top: 90px; transition: all 0.3s } 39 | .col-left { padding-left: 0px; padding-right: 20px; margin-top: 0px; transition: all 0.3s } 40 | .col-left.faded { opacity: 0.3; filter: grayscale(1); } 41 | 42 | /* Card */ 43 | .cards { margin-right: -20px } 44 | .card { 45 | border-radius: 4px; box-shadow: 0px 1px 11px #EAEAEA; background-color: white; width: 33%; width: calc(33% - 10px); 46 | box-sizing: border-box; margin-right: 10px; margin-bottom: 10px; min-width: 300px; display: inline-block; 47 | } 48 | 49 | /* Avatar */ 50 | .avatar { width: 50px; height: 50px; background: #EEE; border-radius: 100px; display: inline-block; background-size: cover; background-position: center center; } 51 | .avatar.empty { vertical-align: top; font-size: 11px; line-height: 51px; text-align: center; text-decoration: none; color: #666; font-weight: bold; } 52 | 53 | /* More */ 54 | .more { width: 100%; display: block; clear: both; text-align: center; padding: 20px; box-sizing: border-box; box-shadow: inset 0px 30px 25px -40px #5d68ff } 55 | .more.small { font-size: 14px; box-shadow: none; padding: 10px } 56 | 57 | /* Animate */ 58 | .animate { transition: all 0.3s ease-out !important; } 59 | .animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } 60 | .animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } 61 | .animate-inback { transition: all 0.6s cubic-bezier(0.6, -0.28, 0.735, 0.045) !important; } 62 | .animate-in { transition: all 0.6s cubic-bezier(0.6, 0.04, 0.98, 0.335) !important; } 63 | .animate-out { transition: all 0.6s ease-out !important; } 64 | 65 | @keyframes flash-in { 66 | 0% { transform: scale(1.5); opacity: 0 } 67 | 80% { transform: scale(1); opacity: 1 } 68 | 100% { transform: scale(1); opacity: 0 } 69 | } 70 | 71 | @keyframes flash-in-big { 72 | 0% { transform: scale(1.2); opacity: 0 } 73 | 80% { transform: scale(1); opacity: 1 } 74 | 100% { transform: scale(1); opacity: 0 } 75 | } 76 | 77 | @keyframes flash-out { 78 | 0% { transform: scale(1); opacity: 1 } 79 | 100% { transform: scale(1.5); opacity: 0 } 80 | } 81 | @keyframes flash-out-big { 82 | 0% { transform: scale(1); opacity: 1 } 83 | 100% { transform: scale(1.2); opacity: 0 } 84 | } 85 | 86 | @keyframes bounce { 87 | 0% { transform: translateY(0); opacity: 1 } 88 | 100% { transform: translateY(-3px); opacity: 0.7 } 89 | } -------------------------------------------------------------------------------- /css/dark.css: -------------------------------------------------------------------------------- 1 | .theme-dark { background-color: #26242E; color: white; } 2 | .theme-dark .head-container { background-color: #2b293e; } 3 | 4 | .theme-dark #Overlay.visible { background-color: rgba(53, 52, 60, 0.89); } 5 | 6 | .theme-dark a { color: #58dbec } 7 | .theme-dark .menu-item { color: #000 } 8 | .theme-dark .post .user .settings:hover { color: #FFF } 9 | 10 | .theme-dark .head .user .name { color: #58dbec } 11 | .theme-dark .user .name { color: #58dbec } 12 | .theme-dark .post-list-type a { color: #adadad; } 13 | .theme-dark .post-list-type a:hover { color: #58dbec; border-bottom-color: #58dbec } 14 | .theme-dark .post-list-type .active { color: #58dbec; border-bottom-color: #58dbec } 15 | 16 | .theme-dark .post-create .postfield { border-color: #4e5a5c; } 17 | .theme-dark .post-create.editing { box-shadow: 0px 1px 13px 1px #111213 } 18 | 19 | .theme-dark .button-submit { color: #FFF } 20 | .theme-dark .button-outline { color: #5e5a6d; } 21 | 22 | .theme-dark .post .body { color: #dcd9e5; } 23 | .theme-dark .post { background-color: #302c3f; border: none; border-radius: 0px; } 24 | .theme-dark .comment-list { background-color: #2c2939; border-top-color: #423d53 } 25 | .theme-dark .post .img .fullsize { background-color: #33313c } 26 | .theme-dark .post .reply-name { color: white } 27 | .theme-dark .post .user .address, .theme-dark .post .added, .theme-dark .post .sep { color: #bdb5da } 28 | .theme-dark .post .body a { color: #58dbec } 29 | .theme-dark .post blockquote { border-left: 3px solid #47dbec; } 30 | 31 | .theme-dark .activity-list .items a { color: #a398c4 } 32 | .theme-dark .activity { color: #8e899c } 33 | .theme-dark .activity-list .bg-line { background-color: #38334b } 34 | .theme-dark .activity-list .circle { border-color: #4b465a; background-color: #26242e } 35 | .theme-dark .activity:last-child { background-color: #26242e; } 36 | 37 | .theme-dark input.text, .theme-dark textarea { background-color: #26242e; border-color: #4e5a5c; color: white; } 38 | .theme-dark .card { box-shadow: 0px 1px 11px #26242e; background-color: #302c3f; } 39 | 40 | .theme-dark .button-follow:hover { background-color: rgba(74, 160, 177, 0.1) !important; border-color: #55cedf !important; color: #55cedf !important } 41 | .theme-dark .users.gray .button-follow { border: 1px solid #3b8297 } 42 | .theme-dark .user.notseeding .button-follow { border-color: #393251 } 43 | .theme-dark h2.sep { border-top: 1px solid #495355; } 44 | 45 | .theme-dark .post code { background-color: #232323; border-color: #0A0A0A; color: #d3d3d3; } 46 | .theme-dark .icon-heart.active { color: #58dbec; filter: sepia(1) hue-rotate(504deg) brightness(0.75) saturate(5); } 47 | .theme-dark .post .actions .link.active { color: #58dbec; } 48 | 49 | .theme-dark .button-follow-big:hover { background-color: #3b374a; } 50 | .theme-dark .maxheight-limited:before { background: linear-gradient(rgba(38, 36, 46, 0), #302c3f 70%); } 51 | -------------------------------------------------------------------------------- /css/icons.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block; vertical-align: text-bottom; background-repeat: no-repeat; height: 30px; 3 | vertical-align: middle; line-height: 30px; color: #AAA; font-size: 12px; transition: background-color 0.3s; 4 | } 5 | .icon.icon-button:hover { background-color: #F3F3F3; outline: 0px solid #F3F3F3; transition: none; } 6 | .icon.loading { pointer-events: none; animation: bounce .3s infinite alternate ease-out; animation-delay: 1s; } 7 | /*.icon:focus { animation: clicked 1s ease-in-out forwards; } 8 | 9 | @keyframes clicked { 10 | 0% { outline: 1px solid #F3F3F3; } 11 | 100% { outline: 15px solid rgba(250, 108, 141, 0) } 12 | }*/ 13 | 14 | .icon-profile { font-size: 7px; top: 1px; border-radius: 0.7em 0.7em 0 0; background: #FFF; width: 1.5em; height: 0.7em; position: relative; display: inline-block; margin-right: 7px } 15 | .icon-profile:before { position: absolute; content: ""; top: -1em; left: 0.38em; width: 0.8em; height: 0.85em; border-radius: 50%; background: #FFF; } 16 | 17 | /*.icon-comment { width: 16px; height: 10px; border-radius: 2px; background: #B10DC9; margin-top: 0px; display: inline-block; position: relative; top: -2px; } 18 | .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; } 19 | */ 20 | .icon-comment { 21 | padding-left: 30px; padding-right: 10px; background-position: 5px 7px; 22 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAASBAMAAABGPIgdAAAAG1BMVEUAAACurq6urq6urq6urq6urq6urq6urq6urq4asLPtAAAACHRSTlMAxOurFsllVoVPYlAAAAAySURBVAjXY0ABEh1g0Ahid0ABmA2Rpg+7oyEJZq9ERyvcPQwcHSZwh3KoIxzN7MCACQDJKxqoZp3x4wAAAABJRU5ErkJggg=='); 23 | } 24 | .icon-comment:empty { padding-right: 0px } 25 | 26 | 27 | .icon-edit { 28 | width: 16px; height: 16px; background-repeat: no-repeat; background-position: 6px center; 29 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAOVBMVEUAAAC9w8e9w8e9w8e9w8e/xMi9w8e9w8e+w8e9w8e9w8e9w8e9w8e9w8e9w8e+w8e/xMi9w8e9w8fvY4+KAAAAEnRSTlMASPv3WQbwOTCkt4/psX4YDMWr+RRCAAAAUUlEQVQY06XLORKAMAxDUTs7kA3d/7AYGju0UfffjIgoHkxm0vB5bZyxKHx9eX0FJw0Y4bcXKQ4/CTtS5yqp5GFFOjGpVGl00k1pNDIb3Nv9AHC7BOZC4ZjvAAAAAElFTkSuQmCC+d0ckOwyAMRVGHUOO0gUyd+P8f7WApz4Iki9wFmyOEATrXLZcFp5LrGogPOxKp6zfFf9fZ1/I/cY7YZSS3U6S3XFZJmGBwL+FuJX/F1K0wUUlZyZGlXgXESthTEs4B8fh7xoVUDPGYJnsfkCRarKAgz8cAKbpD6pqDPz3XB8K6HdUEeN9NAAAAAElFTkSuQmCC); 30 | } 31 | .icon-reply { 32 | padding-left: 25px; margin-left: 5px; padding-right: 10px; background-position: 5px 6px; height: 19px; line-height: 19px; background-position: 5px 0px; 33 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAIVBMVEUAAABmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYs5FxxAAAAC3RSTlMAgBFwYExAMHgoCDJmUTYAAAA3SURBVAjXY8APGGEMQZgAjCEoKBwEEQCCAoiIh6AQVM1kMaguJhGYOSJQjexiUMbiAChDCclCAOHqBBdHpwQTAAAAAElFTkSuQmCC); 34 | } 35 | .icon-reply:empty { padding-right: 0px } 36 | 37 | .icon-share { 38 | padding-left: 32px; padding-right: 10px; background-position: 7px 5px; 39 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUBAMAAAB/pwA+AAAAKlBMVEUAAACurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq7nAmlHAAAADXRSTlMA48a1j0sVCfTorDQhpSwkfQAAAFdJREFUCNdjIAkkJ5y8CwSiDAzsuurcvUDmRQYGrruXGWJBwgwMvnfvOgCV8d4FEncvMJDGZLl79wqQCQRXGdbeNWBgkAUyb2woulTAwBAIEp7DZkaSawHVjTFYqPfYUwAAAABJRU5ErkJggg==') 40 | } 41 | .icon-share:empty { padding-right: 0px } 42 | 43 | .icon-heart { 44 | padding-left: 32px; padding-right: 2px; background-position: 7px 5px; 45 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASBAMAAACp/uMjAAAAMFBMVEUAAACqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqobnPPZAAAAD3RSTlMADcS4pt/IwK9qXE9ELRXt9n51AAAAaElEQVQI12PABRhtGRguC4CZQl8f8MUrggX1/yeK/f8kABL8//+L////imBBEAAJM/+HAgMGHhjzAAPDfgjrN1AbF4S5AGTafBDrJ9gKFhDTAWJz////P6COYAaZBAX13+FuY1JAcywAx1BERa6oCoIAAAAASUVORK5CYII=') 46 | } 47 | .icon-heart.active { color: #5d68ff; filter: sepia(1) hue-rotate(192deg) brightness(0.55) saturate(5) } 48 | 49 | .icon-up { font-weight: normal !important; font-size: 15px; font-family: Tahoma; vertical-align: -4px; padding-right: 5px; display: inline; height: 1px; } 50 | .icon-upload { 51 | width: 26px; height: 26px; background-repeat: no-repeat; 52 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAAmklEQVR42u3VwQ2AIAwFUEZgBEZxNDZwFEdwBDaigiHKwdBCvyYm/KQnKi8KgjEdISKbaitlzRspSKA7AY49IHisgeAwAaLHOpBxbADpxxSIHAMgMiwNroSL597KpVqqkqZ+xo3sPlEQP+yEJvRXiGuMMe4cknvY+Tkon1ktrIxZNYT69N9D7Gk7dkP7C5IstjbnZuEWG4Fk4wCMv9vjGFiLRgAAAABJRU5ErkJggg==') 53 | } 54 | 55 | .icon-image { 56 | width: 22px; height: 18px; border-radius: 0px; overflow: hidden; margin: 4px 2px; display: inline-block; vertical-align: middle; position: relative; 57 | font-style: normal; color: #ddd; text-align: left; text-indent: -9999px; direction: ltr; box-sizing: border-box; border: 1px solid; 58 | } 59 | .icon-image:before { 60 | content: ''; pointer-events: none; position: absolute; width: 10px; height: 18px; left: -2px; top: 7px; 61 | transform: rotate(45deg); box-shadow: inset 0 0 0 32px, 10px -6px 0 0; 62 | } 63 | .icon-image:after { 64 | content: ''; pointer-events: none; position: absolute; width: 3px; height: 3px; 65 | border-radius: 50%; box-shadow: inset 0 0 0 32px; top: 3px; right: 5px; 66 | } 67 | 68 | .icon-mute { 69 | width: 17px; height: 14px; background-repeat: no-repeat; padding-right: 5px; vertical-align: -2px; opacity: 0.2; 70 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAOCAMAAAD+MweGAAAARVBMVEUAAAABAAIAAAACAAIAAAIAAAABAAECAAICAAIAAAAAAAABAAIBAAIBAAIBAAIBAAMBAAMBAAEAAAIAAAAAAAAAAAABAAJgeLYdAAAAFnRSTlMA7jGWeQe9pY02IvTk2tLMxbhsORoUV1Pd4wAAAFBJREFUCNd1z+kKgDAMA+B0cx7zvvL+j6pDQcJcoH8+2kJQSmhqhXMhBeJIld34SAW4e1aSr5jvrAX4iTN6/MmUXYW49fo5bQ9JJMesoi3yXMOvBYnUTBisAAAAAElFTkSuQmCC') 71 | } 72 | 73 | @keyframes bounce { 74 | 0% { transform: translateY(0); } 75 | 100% { transform: translateY(-3px); } 76 | } -------------------------------------------------------------------------------- /css/mobile.css: -------------------------------------------------------------------------------- 1 | /* ZeroMe Mobile */ 2 | #Trigger { 3 | display: none; 4 | } 5 | 6 | @media screen and (max-width: 750px) { 7 | html, body, p, th, td, li { 8 | font-size: medium !important; 9 | } 10 | 11 | .head-container { 12 | position: fixed; 13 | width: 100%; 14 | height: 50px; 15 | z-index: 30; 16 | } 17 | .head { 18 | width: 100%; 19 | } 20 | .head .logo { 21 | margin-left: 50px; 22 | position: absolute; 23 | z-index: 1; 24 | } 25 | .right { 26 | position: fixed; 27 | right: 0; 28 | top: 0; 29 | } 30 | #Content { 31 | width: 100%; 32 | margin-top: 60px; 33 | margin-bottom: 5px; 34 | } 35 | .col-center { 36 | width: 100%; 37 | overflow: auto; 38 | padding-left: 5px; 39 | padding-right: 5px; 40 | display: block; 41 | padding-top: 60px; 42 | margin-top: -60px; 43 | } 44 | .col-left, .col-right { 45 | position: fixed; 46 | top: 60px; 47 | left: -305px; 48 | height: 100%; 49 | background-color: #f6f7f8; 50 | margin-left: 0px; 51 | padding-top: 60px; 52 | padding-right: 15px; 53 | padding-left: 15px; 54 | margin-top: -60px; 55 | width: 300px; 56 | overflow-x: hidden; 57 | overflow-y: auto; 58 | z-index: 1; 59 | transition: 0.3s all cubic-bezier(0.77, 0, 0.175, 1); 60 | } 61 | .theme-dark .col-left, .theme-dark .col-right { 62 | background-color: #26242e; 63 | } 64 | .trigger-on .col-left, .trigger-on .col-right { 65 | left: 0px; 66 | box-shadow: 0px 0px 30px #999; 67 | } 68 | 69 | #Trigger { 70 | display: block; 71 | position: fixed; 72 | left: 5px; 73 | top: 3px; 74 | width: 40px; 75 | height: 40px; 76 | z-index: 100; 77 | border: 1px solid rgba(187, 187, 187, 0.32); 78 | border-radius: 5px; 79 | } 80 | #Trigger .icon { 81 | background-image: url(../img/nav-icon.png); 82 | display: block; 83 | border: 12px solid transparent; 84 | height: 16px; 85 | outline: none; 86 | border-radius: 5px; 87 | } 88 | .col-left.faded { 89 | opacity: 1; 90 | filter: none; 91 | } 92 | .user.card { 93 | box-shadow: none; 94 | margin-top: 10px; 95 | margin-left: -15px; 96 | } 97 | .activity-list .items { 98 | margin-bottom: 0px; 99 | } 100 | .more.small { 101 | padding: 10px; 102 | margin-bottom: 5px; 103 | border-radius: 5px; 104 | } 105 | .post-create .icon-image { 106 | margin-left: unset; 107 | right: 40px; 108 | } 109 | #Overlay .img { 110 | width: 100% !important; 111 | margin-left: 0px !important; 112 | margin-top: -40px !important; 113 | left: 0px !important; 114 | } 115 | .post .img { 116 | width: 100% !important; 117 | max-height: 300px !important; 118 | min-height: 100px; 119 | background-size: cover !important; 120 | } 121 | .post .img .fullsize { 122 | background-size: cover !important; 123 | } 124 | .post .img .details { 125 | z-index: 50; 126 | } 127 | .post blockquote { 128 | padding-left: 10px; 129 | padding-top: 10px; 130 | padding-bottom: 10px; 131 | } 132 | } 133 | 134 | @media screen and (max-width: 500px) { 135 | .post .sep, .user .address { 136 | display: none; 137 | } 138 | .post .added { 139 | margin-left: 10px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /dbschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "ZeroMe", 3 | "db_file": "merged-ZeroMe/ZeroMe.db", 4 | "version": 3, 5 | "maps": { 6 | ".+/data/userdb/.+/content.json": { 7 | "to_json_table": [ "cert_auth_type", "cert_user_id" ], 8 | "to_table": ["user"] 9 | }, 10 | ".+/data/userdb/users.*json": { 11 | "to_table": ["user"] 12 | }, 13 | ".+/data/users/.+/content.json": { 14 | "to_json_table": [ "cert_auth_type", "cert_user_id" ], 15 | "file_name": "data.json" 16 | }, 17 | ".+/data/users/.+/data.json": { 18 | "to_table": [ 19 | "post", 20 | "comment", 21 | "follow", 22 | {"node": "post_like", "table": "post_like", "key_col": "post_uri", "val_col": "date_added"} 23 | ], 24 | "to_json_table": [ "hub", "user_name", "avatar", "intro" ] 25 | } 26 | }, 27 | "tables": { 28 | "json": { 29 | "cols": [ 30 | ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], 31 | ["site", "TEXT"], 32 | ["directory", "TEXT"], 33 | ["file_name", "TEXT"], 34 | ["cert_auth_type", "TEXT"], 35 | ["cert_user_id", "TEXT"], 36 | ["hub", "TEXT"], 37 | ["user_name", "TEXT"], 38 | ["intro", "TEXT"], 39 | ["avatar", "TEXT"] 40 | ], 41 | "indexes": ["CREATE UNIQUE INDEX path ON json(directory, site, file_name)"], 42 | "schema_changed": 4 43 | }, 44 | "post": { 45 | "cols": [ 46 | ["post_id", "INTEGER"], 47 | ["body", "TEXT"], 48 | ["meta", "TEXT"], 49 | ["date_added", "INTEGER"], 50 | ["json_id", "INTEGER REFERENCES json (json_id)"] 51 | ], 52 | "indexes": ["CREATE UNIQUE INDEX post_key ON post(json_id, post_id)", "CREATE INDEX post_id ON post(post_id)", "CREATE INDEX added ON post(date_added)"], 53 | "schema_changed": 4 54 | }, 55 | "post_like": { 56 | "cols": [ 57 | ["post_uri", "TEXT"], 58 | ["date_added", "INTEGER"], 59 | ["json_id", "INTEGER REFERENCES json (json_id)"] 60 | ], 61 | "indexes": ["CREATE UNIQUE INDEX post_like_key ON post_like(json_id, date_added)", "CREATE INDEX post_uri ON post_like(post_uri)"], 62 | "schema_changed": 1 63 | }, 64 | "comment": { 65 | "cols": [ 66 | ["comment_id", "INTEGER"], 67 | ["post_uri", "TEXT"], 68 | ["body", "TEXT"], 69 | ["date_added", "INTEGER"], 70 | ["json_id", "INTEGER REFERENCES json (json_id)"] 71 | ], 72 | "indexes": ["CREATE UNIQUE INDEX comment_key ON comment(json_id, comment_id)", "CREATE INDEX comment_post_uri ON comment(post_uri)", "CREATE INDEX comment_date_added ON comment(date_added)"], 73 | "schema_changed": 2 74 | }, 75 | "follow": { 76 | "cols": [ 77 | ["follow_id", "INTEGER"], 78 | ["user_name", "TEXT"], 79 | ["auth_address", "TEXT"], 80 | ["hub", "TEXT"], 81 | ["date_added", "INTEGER"], 82 | ["json_id", "INTEGER REFERENCES json (json_id)"] 83 | ], 84 | "indexes": ["CREATE UNIQUE INDEX follow_key ON follow(json_id, follow_id)", "CREATE INDEX follow_date_added ON follow(date_added)"], 85 | "schema_changed": 2 86 | }, 87 | "user": { 88 | "cols": [ 89 | ["auth_address", "TEXT"], 90 | ["cert_user_id", "TEXT"], 91 | ["hub", "TEXT"], 92 | ["user_name", "TEXT"], 93 | ["avatar", "TEXT"], 94 | ["intro", "TEXT"], 95 | ["date_added", "INTEGER"], 96 | ["json_id", "INTEGER REFERENCES json (json_id)"] 97 | ], 98 | "indexes": ["CREATE INDEX json_id ON user(json_id)", "CREATE INDEX date_added ON user(date_added)"], 99 | "schema_changed": 3 100 | } 101 | }, 102 | "feeds": { 103 | "Posts": "SELECT 'post' AS type, post.date_added AS date_added, 'In ' || json.user_name || \"'s post\" AS title, post.body AS body, '?Post/' || json.site || '/' || REPLACE(json.directory, 'data/users/', '') || '/' || post_id AS url FROM post LEFT JOIN json USING (json_id)", 104 | "Comments": "SELECT 'comment' AS type, comment.date_added AS date_added, 'a post' AS title, '@' || user_name || ': ' || comment.body AS body, '?Post/' || json.site || '/' || REPLACE(post_uri, '_', '/') AS url FROM comment LEFT JOIN json USING (json_id)" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /img/loading-circle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroMe/07e8090cf87f261d33a363d1b6a7a0eb1b93bc80/img/loading-circle.gif -------------------------------------------------------------------------------- /img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroMe/07e8090cf87f261d33a363d1b6a7a0eb1b93bc80/img/loading.gif -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroMe/07e8090cf87f261d33a363d1b6a7a0eb1b93bc80/img/logo.png -------------------------------------------------------------------------------- /img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /img/nav-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroMe/07e8090cf87f261d33a363d1b6a7a0eb1b93bc80/img/nav-icon.png -------------------------------------------------------------------------------- /img/nav-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/unkown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroMe/07e8090cf87f261d33a363d1b6a7a0eb1b93bc80/img/unkown.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ZeroMe! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /js/ActivityList.coffee: -------------------------------------------------------------------------------- 1 | class ActivityList extends Class 2 | constructor: -> 3 | @activities = null 4 | @directories = [] 5 | @need_update = true 6 | @limit = 10 7 | @found = 0 8 | @loading = true 9 | @update_timer = null 10 | 11 | queryActivities: (cb) -> 12 | if @directories == "all" 13 | where = "WHERE date_added > #{Time.timestamp()-60*60*24*2} AND date_added < #{Time.timestamp()+120} " 14 | else 15 | where = "WHERE json.directory IN #{Text.sqlIn(@directories)} AND date_added < #{Time.timestamp()+120} " 16 | 17 | query = """ 18 | SELECT 19 | 'comment' AS type, json.*, 20 | json.site || "/" || post_uri AS subject, body, date_added, 21 | NULL AS subject_auth_address, NULL AS subject_hub, NULL AS subject_user_name 22 | FROM 23 | json 24 | LEFT JOIN comment USING (json_id) 25 | #{where} 26 | 27 | UNION ALL 28 | 29 | SELECT 30 | 'post_like' AS type, json.*, 31 | json.site || "/" || post_uri AS subject, '' AS body, date_added, 32 | NULL AS subject_auth_address, NULL AS subject_hub, NULL AS subject_user_name 33 | FROM 34 | json 35 | LEFT JOIN post_like USING (json_id) 36 | #{where} 37 | """ 38 | 39 | if @directories != "all" # Dont show follows in all users activity feed 40 | query += """ 41 | UNION ALL 42 | 43 | SELECT 44 | 'follow' AS type, json.*, 45 | follow.hub || "/" || follow.auth_address AS subject, '' AS body, date_added, 46 | follow.auth_address AS subject_auth_address, follow.hub AS subject_hub, follow.user_name AS subject_user_name 47 | FROM 48 | json 49 | LEFT JOIN follow USING (json_id) 50 | #{where} 51 | """ 52 | 53 | query += """ 54 | 55 | ORDER BY date_added DESC 56 | LIMIT #{@limit+1} 57 | """ 58 | @logStart("Update") 59 | 60 | Page.cmd "dbQuery", [query, {directories: @directories}], (rows) => 61 | # Resolve subject's name 62 | directories = [] 63 | rows = (row for row in rows when row.subject) # Remove deleted users activities 64 | for row in rows 65 | row.auth_address = row.directory.replace("data/users/", "") 66 | subject_address = row.subject.replace(/_.*/, "").replace(/.*\//, "") # Only keep user's address 67 | row.post_id = row.subject.replace(/.*_/, "").replace(/.*\//, "") 68 | row.subject_address = subject_address 69 | directory = "data/users/#{subject_address}" 70 | if directory not in directories 71 | directories.push directory 72 | 73 | Page.cmd "dbQuery", ["SELECT * FROM json WHERE ?", {directory: directories}], (subject_rows) => 74 | # Add subject node to rows 75 | subject_db = {} 76 | for subject_row in subject_rows 77 | subject_row.auth_address = subject_row.directory.replace("data/users/", "") 78 | subject_db[subject_row.auth_address] = subject_row 79 | for row in rows 80 | row.subject = subject_db[row.subject_address] 81 | row.subject ?= {} 82 | row.subject.auth_address ?= row.subject_auth_address 83 | row.subject.hub ?= row.subject_hub 84 | row.subject.user_name ?= row.subject_user_name 85 | 86 | # Merge same activities from same user to one line 87 | last_row = null 88 | row_group = [] 89 | row_groups = [] 90 | for row in rows 91 | if not last_row or (row.auth_address == last_row?.auth_address and row.type == last_row?.type and row.type in ["post_like", "follow"]) 92 | row_group.push row 93 | else 94 | row_groups.push row_group 95 | row_group = [row] 96 | last_row = row 97 | if row_group.length 98 | row_groups.push row_group 99 | 100 | @found = rows.length 101 | @logEnd("Update") 102 | cb(row_groups) 103 | 104 | handleMoreClick: => 105 | @limit += 20 106 | @update(0) 107 | return false 108 | 109 | renderActivity: (activity_group) -> 110 | back = [] 111 | now = Time.timestamp() 112 | activity = activity_group[0] 113 | if not activity.subject.user_name 114 | return back 115 | activity_user_link = "?Profile/#{activity.hub}/#{activity.auth_address}/#{activity.cert_user_id}" 116 | subject_user_link = "?Profile/#{activity.subject.hub}/#{activity.subject.auth_address}/#{activity.subject.cert_user_id or ''}" 117 | subject_post_link = "?Post/#{activity.subject.hub}/#{activity.subject.auth_address}/#{activity.post_id}" 118 | if activity.type == "post_like" 119 | body = [ 120 | h("a.link", {href: activity_user_link, onclick: @Page.handleLinkClick}, activity.user_name), " liked ", 121 | h("a.link", {href: subject_user_link, onclick: @Page.handleLinkClick}, activity.subject.user_name), "'s ", 122 | h("a.link", {href: subject_post_link, onclick: @Page.handleLinkClick}, _("post", "like post")) 123 | ] 124 | # Add more target 125 | if activity_group.length > 1 126 | for activity_more in activity_group[1..10] 127 | subject_user_link = "?Profile/#{activity_more.subject.hub}/#{activity_more.subject.auth_address}/#{activity_more.subject.cert_user_id or ''}" 128 | subject_post_link = "?Post/#{activity_more.subject.hub}/#{activity_more.subject.auth_address}/#{activity_more.post_id}" 129 | body.push ", " 130 | body.push h("a.link", {href: subject_user_link, onclick: @Page.handleLinkClick}, activity_more.subject.user_name) 131 | body.push "'s " 132 | body.push h("a.link", {href: subject_post_link, onclick: @Page.handleLinkClick}, _("post", "like post")) 133 | else if activity.type == "comment" 134 | body = [ 135 | h("a.link", {href: activity_user_link, onclick: @Page.handleLinkClick}, activity.user_name), " commented on ", 136 | h("a.link", {href: subject_user_link, onclick: @Page.handleLinkClick}, activity.subject.user_name), "'s ", 137 | h("a.link", {href: subject_post_link, onclick: @Page.handleLinkClick}, _("post", "comment post")), ": #{activity.body[0..100]}" 138 | ] 139 | else if activity.type == "follow" 140 | body = [ 141 | h("a.link", {href: activity_user_link, onclick: @Page.handleLinkClick}, activity.user_name), " started following ", 142 | h("a.link", {href: subject_user_link, onclick: @Page.handleLinkClick}, activity.subject.user_name) 143 | ] 144 | # Add more target 145 | if activity_group.length > 1 146 | for activity_more in activity_group[1..10] 147 | subject_user_link = "?Profile/#{activity_more.subject.hub}/#{activity_more.subject.auth_address}/#{activity_more.subject.cert_user_id or ''}" 148 | body.push ", " 149 | body.push h("a.link", {href: subject_user_link, onclick: @Page.handleLinkClick}, activity_more.subject.user_name) 150 | else 151 | body = activity.body 152 | 153 | # opacity = Math.max(0.5, 1 - (now - activity.date_added) / 10000) 154 | if activity.body 155 | title = Time.since(activity.date_added) + " - " + if activity.body.length > 500 then activity.body[0..500] + "..." else activity.body 156 | else 157 | title = Time.since(activity.date_added) 158 | back.push h("div.activity", {key: "#{activity.cert_user_id}_#{activity.date_added}_#{activity_group.length}", title: title, classes: {latest: now - activity.date_added < 600}, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 159 | h("div.circle"), 160 | h("div.body", body) 161 | ]) 162 | return back 163 | 164 | render: => 165 | if @need_update 166 | @need_update = false 167 | @queryActivities (res) => 168 | @activities = res 169 | Page.projector.scheduleRender() 170 | 171 | if @activities == null # Not loaded yet 172 | return null 173 | 174 | h("div.activity-list", [ 175 | if @activities.length > 0 176 | h("h2", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "Activity feed") 177 | h("div.items", [ 178 | h("div.bg-line"), 179 | @activities[0..@limit-1].map(@renderActivity) 180 | ]), 181 | if @found > @limit 182 | h("a.more.small", {href: "#More", onclick: @handleMoreClick, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "Show more...") 183 | # if @loading 184 | # h("span.more.small", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "Loading...", ) 185 | 186 | ]) 187 | 188 | update: (delay=600) => 189 | clearInterval @update_timer 190 | if not @need_update 191 | @update_timer = setTimeout ( => 192 | @need_update = true 193 | Page.projector.scheduleRender() 194 | ), delay 195 | 196 | window.ActivityList = ActivityList 197 | -------------------------------------------------------------------------------- /js/AnonUser.coffee: -------------------------------------------------------------------------------- 1 | class AnonUser extends Class 2 | constructor: -> 3 | @auth_address = null 4 | @hub = null 5 | @followed_users = {} 6 | @likes = {} 7 | 8 | updateInfo: (cb=null) => 9 | Page.on_local_storage.then => 10 | @followed_users = Page.local_storage.followed_users 11 | cb?(true) 12 | 13 | like: (site, post_uri, cb=null) -> 14 | Page.cmd "wrapperNotification", ["info", "You need a profile for this feature"] 15 | cb(true) 16 | 17 | dislike: (site, post_uri, cb=null) -> 18 | Page.cmd "wrapperNotification", ["info", "You need a profile for this feature"] 19 | cb(true) 20 | 21 | followUser: (hub, auth_address, user_name, cb=null) -> 22 | @followed_users[hub+"/"+auth_address] = true 23 | @save cb 24 | Page.needSite hub # Download followed user's site if necessary 25 | Page.content.update() 26 | 27 | unfollowUser: (hub, auth_address, cb=null) -> 28 | delete @followed_users[hub+"/"+auth_address] 29 | @save cb 30 | Page.content.update() 31 | 32 | comment: (site, post_uri, body, cb=null) -> 33 | Page.cmd "wrapperNotification", ["info", "You need a profile for this feature"] 34 | cb?(false) 35 | 36 | save: (cb=null) => 37 | Page.saveLocalStorage cb 38 | 39 | 40 | window.AnonUser = AnonUser -------------------------------------------------------------------------------- /js/ContentCreateProfile.coffee: -------------------------------------------------------------------------------- 1 | class ContentCreateProfile extends Class 2 | constructor: -> 3 | @loaded = true 4 | @hubs = [] 5 | @default_hubs = [] 6 | @need_update = true 7 | @creation_status = [] 8 | @downloading = {} 9 | 10 | handleDownloadClick: (e) => 11 | hub = e.target.attributes.address.value 12 | @downloading[hub] = true 13 | Page.needSite hub, => 14 | @update() 15 | return false 16 | 17 | 18 | handleJoinClick: (e) => 19 | hub_address = e.target.attributes.address.value 20 | if Page.user?.hub 21 | hub_name = (hub.content.title for hub in @hubs when hub.address == Page.user.hub)?[0] 22 | hub_name ?= Page.user.hub 23 | Page.cmd "wrapperConfirm", ["You already have profile on hub #{hub_name},
are you sure you want to create a new one?", "Create new profile"], => 24 | @joinHub(hub_address) 25 | else 26 | @joinHub(hub_address) 27 | 28 | joinHub: (hub) => 29 | user = new User({hub: hub, auth_address: Page.site_info.auth_address}) 30 | @creation_status.push "Checking user on selected hub..." 31 | Page.cmd "fileGet", {"inner_path": user.getPath()+"/content.json", "required": false}, (found) => 32 | if found 33 | Page.cmd "wrapperNotification", ["error", "User #{Page.site_info.cert_user_id} already exists on this hub"] 34 | @creation_status = [] 35 | return 36 | 37 | # Create new profile 38 | user_name = Page.site_info.cert_user_id.replace(/@.*/, "") 39 | data = user.getDefaultData() 40 | data.avatar = "generate" 41 | data.user_name = user_name.charAt(0).toUpperCase()+user_name.slice(1) 42 | data.hub = hub 43 | @creation_status.push "Creating new profile..." 44 | user.save data, hub, => 45 | @creation_status = [] 46 | Page.checkUser() 47 | Page.setUrl("?Home") 48 | 49 | return false 50 | 51 | 52 | updateHubs: => 53 | Page.cmd "mergerSiteList", true, (sites) => 54 | # Get userlist 55 | Page.cmd "dbQuery", "SELECT * FROM json", (users) => 56 | site_users = {} 57 | for user in users 58 | site_users[user.hub] ?= [] 59 | site_users[user.hub].push(user) 60 | hubs = [] 61 | for address, site of sites 62 | if address == Page.userdb 63 | continue 64 | site["users"] = site_users[site.address] or [] 65 | hubs.push(site) 66 | @hubs = hubs 67 | Page.projector.scheduleRender() 68 | 69 | @default_hubs = [] 70 | for address, content of Page.site_info.content.settings.default_hubs 71 | if not sites[address] and not @downloading[address] 72 | @default_hubs.push { 73 | users: [], 74 | address: address, 75 | content: content, 76 | type: "available" 77 | } 78 | 79 | 80 | renderHub: (hub) => 81 | rendered = 0 82 | h("div.hub.card", {key: hub.address+hub.type, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 83 | if hub.type == "available" 84 | h("a.button.button-join", {href: "#Download:#{hub.address}", address: hub.address, onclick: @handleDownloadClick}, "Download") 85 | else 86 | h("a.button.button-join", {href: "#Join:#{hub.address}", address: hub.address, onclick: @handleJoinClick}, "Join!") 87 | h("div.avatars", [ 88 | hub.users.map (user) => 89 | if user.avatar not in ["jpg", "png"] or rendered >= 4 90 | return "" 91 | avatar = "merged-ZeroMe/#{hub.address}/#{user.directory}/avatar.#{user.avatar}" 92 | rendered += 1 93 | h("a.avatar", {key: user.user_name, title: user.user_name, style: "background-image: url('#{avatar}')"}) 94 | if hub.users.length - rendered > 0 95 | h("a.avatar.empty", "+#{hub.users.length - rendered}") 96 | ]) 97 | h("div.name", hub.content.title), 98 | h("div.intro", hub.content.description) 99 | ]) 100 | 101 | 102 | renderSeededHubs: => 103 | h("div.hubs.hubs-seeded", @hubs.map(@renderHub)) 104 | 105 | renderDefaultHubs: => 106 | h("div.hubs.hubs-default", @default_hubs.map(@renderHub)) 107 | 108 | 109 | handleSelectUserClick: -> 110 | Page.cmd "certSelect", {"accepted_domains": ["zeroid.bit"], "accept_any": true} 111 | return false 112 | 113 | 114 | render: => 115 | if @loaded and not Page.on_loaded.resolved then Page.on_loaded.resolve() 116 | if @need_update 117 | @updateHubs() 118 | @need_update = false 119 | 120 | h("div#Content.center.content-signup", [ 121 | h("h1", "Create new profile"), 122 | h("a.button.button-submit.button-certselect.certselect", {href: "#Select+user", onclick: @handleSelectUserClick}, [ 123 | h("div.icon.icon-profile"), 124 | if Page.site_info?.cert_user_id 125 | "As: #{Page.site_info.cert_user_id}" 126 | else 127 | "Select ID..." 128 | ]) 129 | if @creation_status.length > 0 130 | h("div.creation-status", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 131 | @creation_status.map (creation_status) => 132 | h("h3", {key: creation_status, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, creation_status) 133 | ]) 134 | else if Page.site_info.cert_user_id 135 | h("div.hubs", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 136 | if @hubs.length 137 | h("div.hubselect.seeded", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 138 | h("h2", "Seeded HUBs") 139 | @renderSeededHubs() 140 | ]) 141 | if @default_hubs.length 142 | h("div.hubselect.default", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 143 | h("h2", "Available HUBs") 144 | @renderDefaultHubs() 145 | ]) 146 | h("h5", "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)") 147 | ]) 148 | ]) 149 | 150 | update: => 151 | @need_update = true 152 | Page.projector.scheduleRender() 153 | 154 | 155 | 156 | window.ContentCreateProfile = ContentCreateProfile -------------------------------------------------------------------------------- /js/ContentFeed.coffee: -------------------------------------------------------------------------------- 1 | class ContentFeed extends Class 2 | constructor: -> 3 | @post_create = new PostCreate() 4 | @post_list = new PostList() 5 | @activity_list = new ActivityList() 6 | @new_user_list = new UserList("new") 7 | @suggested_user_list = new UserList("suggested") 8 | @need_update = true 9 | @type = "followed" 10 | @update() 11 | 12 | handleListTypeClick: (e) => 13 | @type = e.currentTarget.attributes.type.value 14 | @post_list.limit = 10 15 | @activity_list.limit = 10 16 | @update() 17 | return false 18 | 19 | render: => 20 | if @post_list.loaded and not Page.on_loaded.resolved then Page.on_loaded.resolve() 21 | 22 | if @need_update 23 | @log "Updating", @type 24 | @need_update = false 25 | 26 | @new_user_list.need_update = true 27 | @suggested_user_list.need_update = true 28 | 29 | # Post list 30 | if @type == "followed" 31 | @post_list.directories = ("data/users/#{key.split('/')[1]}" for key, followed of Page.user.followed_users) 32 | if Page.user.hub # Also show my posts 33 | @post_list.directories.push("data/users/"+Page.user.auth_address) 34 | @post_list.filter_post_ids = null 35 | else if @type == "liked" 36 | @post_list.directories = ("data/users/#{like.split('_')[0]}" for like, _ of Page.user.likes) 37 | @post_list.filter_post_ids = (like.split('_')[1] for like, _ of Page.user.likes) 38 | else 39 | @post_list.directories = "all" 40 | @post_list.filter_post_ids = null 41 | @post_list.need_update = true 42 | 43 | # Activity list 44 | if @type == "followed" 45 | @activity_list.directories = ("data/users/#{key.split('/')[1]}" for key, followed of Page.user.followed_users) 46 | else 47 | @activity_list.directories = "all" 48 | @activity_list.update() 49 | 50 | 51 | h("div#Content.center", [ 52 | h("div.col-center", [ 53 | @post_create.render(), 54 | h("div.post-list-type", 55 | h("a.link", {href: "#Everyone", onclick: @handleListTypeClick, type: "everyone", classes: {active: @type == "everyone"}}, "Everyone") 56 | h("a.link", {href: "#Liked", onclick: @handleListTypeClick, type: "liked", classes: {active: @type == "liked"}}, "Liked") 57 | h("a.link", {href: "#Followed+users", onclick: @handleListTypeClick, type: "followed", classes: {active: @type == "followed"}}, "Followed users") 58 | ), 59 | @post_list.render() 60 | ]), 61 | h("div.col-right.noscrollfix", [ 62 | @activity_list.render(), 63 | if @new_user_list.users.length > 0 64 | h("h2.sep.new", [ 65 | "New users", 66 | h("a.link", {href: "?Users", onclick: Page.handleLinkClick}, "Browse all \u203A") 67 | ]) 68 | @new_user_list.render(".gray"), 69 | 70 | if @suggested_user_list.users.length > 0 71 | h("h2.sep.suggested", [ 72 | "Suggested users" 73 | ]) 74 | @suggested_user_list.render(".gray"), 75 | ]) 76 | ]) 77 | 78 | update: => 79 | @need_update = true 80 | Page.projector.scheduleRender() 81 | 82 | window.ContentFeed = ContentFeed 83 | -------------------------------------------------------------------------------- /js/ContentProfile.coffee: -------------------------------------------------------------------------------- 1 | class ContentProfile extends Class 2 | constructor: -> 3 | @post_list = null 4 | @activity_list = null 5 | @user_list = null 6 | @auth_address = null 7 | @user = new User() 8 | @activity_list = new ActivityList() 9 | @owned = false 10 | @need_update = true 11 | @filter_post_id = null 12 | @loaded = false 13 | @help_distribute = false 14 | 15 | renderNotSeeded: => 16 | return h("div#Content.center.#{@auth_address}", [ 17 | h("div.col-left", [ 18 | h("div.users", [ 19 | h("div.user.card.profile", [ 20 | @user.renderAvatar() 21 | h("a.name.link", 22 | {href: @user.getLink(), style: "color: #{Text.toColor(@user.row.auth_address)}", onclick: Page.handleLinkClick}, 23 | @user.row.user_name 24 | ), 25 | h("div.cert_user_id", @user.row.cert_user_id) 26 | h("div.intro-full", 27 | @user.row.intro 28 | ), 29 | h("div.follow-container", [ 30 | h("a.button.button-follow-big", {href: "#", onclick: @user.handleFollowClick, classes: {loading: @user.submitting_follow}}, 31 | h("span.icon-follow", "+"), 32 | if @user.isFollowed() then "Unfollow" else "Follow" 33 | ) 34 | ]) 35 | ]) 36 | ]) 37 | ]), 38 | h("div.col-center", {style: "padding-top: 30px; text-align: center"}, [ 39 | h("h1", "Download profile site"), 40 | h("h2", "User's profile site not loaded to your client yet."), 41 | h("a.button.submit", {href: "#Add+site", onclick: @user.handleDownloadClick}, "Download user's site") 42 | ]) 43 | ]) 44 | 45 | setUser: (@hub, @auth_address) => 46 | @loaded = false 47 | @log "setUser", @hub, @auth_address 48 | if not @post_list or @post_list.directories[0] != "data/users/"+@auth_address 49 | # Changed user, create clean status objects 50 | # @post_create = new PostCreate() 51 | @post_list = new PostList() 52 | @activity_list = new ActivityList() 53 | @user_list = new UserList() 54 | @user = new User() 55 | @post_list.directories = ["data/users/"+@auth_address] 56 | @user_list.followed_by = @user 57 | @user_list.limit = 50 58 | @need_update = true 59 | @ 60 | 61 | findUser: (user_name, cb) => 62 | query = """ 63 | SELECT 64 | json.cert_user_id, 65 | REPLACE(REPLACE(json.directory, 'data/userdb/', ''), 'data/users/', '') AS auth_address, 66 | CASE WHEN user.hub IS NOT NULL THEN user.hub ELSE json.site END AS hub, 67 | user.* 68 | FROM 69 | json 70 | LEFT JOIN user USING (json_id) 71 | WHERE user.user_name = :user_name OR json.user_name = :user_name 72 | ORDER BY date_added DESC LIMIT 1 73 | """ 74 | Page.cmd "dbQuery", [query, {user_name: user_name}], (res) => 75 | user = new User() 76 | user.setRow(res[0]) 77 | cb(user) 78 | 79 | filter: (post_id) => 80 | @log "Filter", post_id 81 | @filter_post_id = post_id 82 | @need_update = true 83 | 84 | handleIntroSave: (intro, cb) => 85 | @user.row.intro = intro 86 | @user.getData @user.hub, (data) => 87 | data.intro = intro 88 | @user.save data, @user.hub, (res) => 89 | cb(res) 90 | @update() 91 | 92 | handleUserNameSave: (user_name, cb) => 93 | @user.row.user_name = user_name 94 | @user.getData @user.hub, (data) => 95 | data.user_name = user_name 96 | @user.save data, @user.hub, (res) => 97 | cb(res) 98 | @update() 99 | 100 | handleAvatarUpload: (image_base64uri) => 101 | # Cleanup previous avatars 102 | Page.cmd "fileDelete", @user.getPath()+"/avatar.jpg" 103 | Page.cmd "fileDelete", @user.getPath()+"/avatar.png" 104 | 105 | if not image_base64uri 106 | # Delete image 107 | @user.getData @user.hub, (data) => 108 | data.avatar = "generate" 109 | @user.save data, @user.hub, (res) => 110 | Page.cmd "wrapperReload" # Reload the page 111 | return false 112 | 113 | # Handle upload 114 | image_base64 = image_base64uri?.replace(/.*?,/, "") 115 | ext = image_base64uri.match("image/([a-z]+)")[1] 116 | if ext == "jpeg" then ext = "jpg" 117 | 118 | 119 | Page.cmd "fileWrite", [@user.getPath()+"/avatar."+ext, image_base64], (res) => 120 | @user.getData @user.hub, (data) => 121 | data.avatar = ext 122 | @user.save data, @user.hub, (res) => 123 | Page.cmd "wrapperReload" # Reload the page 124 | 125 | handleOptionalHelpClick: => 126 | if Page.server_info.rev < 1700 127 | Page.cmd "wrapperNotification", ["info", "You need ZeroNet version 0.5.0 use this feature"] 128 | return false 129 | 130 | @user.hasHelp (optional_helping) => 131 | @optional_helping = optional_helping 132 | if @optional_helping 133 | Page.cmd "OptionalHelpRemove", ["data/users/#{@user.auth_address}", @user.hub] 134 | @optional_helping = false 135 | else 136 | Page.cmd "OptionalHelp", ["data/users/#{@user.auth_address}", "#{@user.row.user_name}'s new files", @user.hub] 137 | @optional_helping = true 138 | Page.content_profile.update() 139 | Page.projector.scheduleRender() 140 | return true 141 | 142 | render: => 143 | if @need_update 144 | @log "Updating" 145 | @need_update = false 146 | 147 | # Update components 148 | @post_list.filter_post_ids = if @filter_post_id then [@filter_post_id] else null 149 | @post_list?.need_update = true 150 | @user_list?.need_update = true 151 | @activity_list?.need_update = true 152 | @activity_list.directories = ["data/users/#{@auth_address}"] 153 | 154 | # Update profile details 155 | @user.auth_address = @auth_address 156 | @user.hub = @hub 157 | @user.get @hub, @auth_address, (res) => 158 | if res 159 | @owned = @user.auth_address == Page.user?.auth_address 160 | if @owned and not @editable_intro 161 | @editable_intro = new Editable("div", @handleIntroSave) 162 | @editable_intro.render_function = Text.renderMarked 163 | @editable_user_name = new Editable("span", @handleUserNameSave) 164 | @uploadable_avatar = new Uploadable(@handleAvatarUpload) 165 | @uploadable_avatar.try_png = true 166 | @uploadable_avatar.preverse_ratio = false 167 | @post_create = new PostCreate() 168 | Page.projector.scheduleRender() 169 | @loaded = true 170 | else 171 | Page.queryUserdb @auth_address, (row) => 172 | @log "UserDb row", row 173 | @user.setRow(row) 174 | Page.projector.scheduleRender() 175 | @loaded = true 176 | 177 | 178 | if not Page.merged_sites[@hub] 179 | # Not seeded user, get details from userdb 180 | Page.queryUserdb @auth_address, (row) => 181 | @user.setRow(row) 182 | Page.projector.scheduleRender() 183 | @loaded = true 184 | 185 | @user.hasHelp (res) => 186 | @optional_helping = res 187 | 188 | if not @user?.row?.cert_user_id 189 | if @loaded 190 | return h("div#Content.center.#{@auth_address}", [h("div.user-notfound", "User not found or muted")]) 191 | else 192 | return h("div#Content.center.#{@auth_address}", []) 193 | 194 | if not Page.merged_sites[@hub] 195 | return @renderNotSeeded() 196 | 197 | if @post_list.loaded and not Page.on_loaded.resolved then Page.on_loaded.resolve() 198 | 199 | h("div#Content.center.#{@auth_address}", [ 200 | h("div.col-left", {classes: {faded: @filter_post_id}}, [ 201 | h("div.users", [ 202 | h("div.user.card.profile", {classes: {followed: @user.isFollowed()}}, [ 203 | if @owned then @uploadable_avatar.render(@user.renderAvatar) else @user.renderAvatar() 204 | h("span.name.link", 205 | {style: "color: #{Text.toColor(@user.row.auth_address)}"}, 206 | if @owned 207 | @editable_user_name.render(@user.row.user_name) 208 | else 209 | h("a", {href: @user.getLink(), style: "color: #{Text.toColor(@user.row.auth_address)}", onclick: Page.handleLinkClick}, @user.row.user_name) 210 | ), 211 | h("div.cert_user_id", @user.row.cert_user_id) 212 | if @owned 213 | h("div.intro-full", @editable_intro.render(@user.row.intro)) 214 | else 215 | h("div.intro-full", {innerHTML: Text.renderMarked(@user.row.intro)}) 216 | h("div.follow-container", [ 217 | h("a.button.button-follow-big", {href: "#", onclick: @user.handleFollowClick}, 218 | h("span.icon-follow", "+"), 219 | if @user.isFollowed() then "Unfollow" else "Follow" 220 | ) 221 | ]), 222 | h("div.help.checkbox", {classes: {checked: @optional_helping}, onclick: @handleOptionalHelpClick}, 223 | h("div.checkbox-skin"), 224 | h("div.title", "Help distribute this user's images") 225 | ) 226 | ]) 227 | ]), 228 | h("a.user-mute", {href: "#Mute", onclick: @user.handleMuteClick}, 229 | h("div.icon.icon-mute"), 230 | "Mute #{@user.row.cert_user_id}" 231 | ), 232 | @activity_list.render(), 233 | if @user_list.users.length > 0 234 | h("h2.sep", {afterCreate: Animation.show}, [ 235 | "Following", 236 | ]) 237 | @user_list.render(".gray"), 238 | ]), 239 | h("div.col-center", [ 240 | if @owned and not @filter_post_id 241 | h("div.post-create-container", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, @post_create.render()) 242 | @post_list.render() 243 | #if @filter_post_id 244 | # h("a.more.small", {style: "color: #AAA", key: "all", href: @user.getLink(), onclick: Page.handleLinkClick, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "Show more posts by this user...") 245 | ]) 246 | ]) 247 | 248 | update: => 249 | if not @auth_address 250 | return 251 | @need_update = true 252 | Page.projector.scheduleRender() 253 | 254 | window.ContentProfile = ContentProfile -------------------------------------------------------------------------------- /js/ContentUsers.coffee: -------------------------------------------------------------------------------- 1 | class ContentUsers extends Class 2 | constructor: -> 3 | @user_list_suggested = new UserList("suggested") 4 | @user_list_suggested.limit = 9 5 | 6 | @user_list_active = new UserList("active") 7 | @user_list_active.limit = 9 8 | 9 | @user_list_recent = new UserList("recent") 10 | @user_list_recent.limit = 90 11 | 12 | @loaded = true 13 | @need_update = false 14 | @search = "" 15 | @num_users_total = null 16 | 17 | handleSuggestedMoreClick: => 18 | @user_list_suggested.limit += 90 19 | @user_list_suggested.need_update = true 20 | @user_list_suggested.loading = true 21 | Page.projector.scheduleRender() 22 | return false 23 | 24 | handleActiveMoreClick: => 25 | @user_list_active.limit += 90 26 | @user_list_active.need_update = true 27 | @user_list_active.loading = true 28 | Page.projector.scheduleRender() 29 | return false 30 | 31 | handleRecentMoreClick: => 32 | @user_list_recent.limit += 300 33 | @user_list_recent.need_update = true 34 | @user_list_recent.loading = true 35 | Page.projector.scheduleRender() 36 | return false 37 | 38 | handleSearchInput: (e=null) => 39 | @search = e.target.value 40 | if @search == "" 41 | rate_limit = 0 42 | if @search.length < 3 43 | rate_limit = 400 44 | else 45 | rate_limit = 200 46 | RateLimit rate_limit, => 47 | @log "Search", @search 48 | @user_list_recent.search = @search 49 | @user_list_recent.need_update = true 50 | @user_list_recent.limit = 15 51 | Page.projector.scheduleRender() 52 | 53 | render: => 54 | if @loaded and not Page.on_loaded.resolved then Page.on_loaded.resolve() 55 | if @need_update or not @num_users_total 56 | Page.cmd "dbQuery", "SELECT COUNT(*) AS num FROM user", (res) => 57 | @num_users_total = res[0]["num"] 58 | Page.projector.scheduleRender() 59 | if @need_update 60 | @log "Updating" 61 | @need_update = false 62 | 63 | # Update components 64 | @user_list_recent?.need_update = true 65 | @user_list_active?.need_update = true 66 | if Page.user.auth_address 67 | @user_list_suggested?.need_update = true 68 | 69 | h("div#Content.center", [ 70 | h("input.text.big.search", {placeholder: "Search in users...", value: @search, oninput: @handleSearchInput}) 71 | 72 | if not @search 73 | [ 74 | if @user_list_suggested.users.length > 0 75 | h("h2.suggested", "Suggested users") 76 | h("div.users.cards.suggested", [ 77 | @user_list_suggested.render("card") 78 | ]) 79 | if @user_list_suggested.users.length == @user_list_suggested.limit 80 | h("a.more.suggested", {href: "#", onclick: @handleSuggestedMoreClick}, "Show more...") 81 | else if @user_list_suggested.users.length > 0 and @user_list_suggested.loading 82 | h("a.more.suggested", {href: "#", onclick: @handleSuggestedMoreClick}, "Loading...") 83 | 84 | if @user_list_active.users.length > 0 85 | h("h2.active", "Most active") 86 | h("div.users.cards.active", [ 87 | @user_list_active.render("card") 88 | ]) 89 | if @user_list_active.users.length == @user_list_active.limit 90 | h("a.more.active", {href: "#", onclick: @handleActiveMoreClick}, "Show more...") 91 | else if @user_list_active.users.length > 0 and @user_list_active.loading 92 | h("a.more.active", {href: "#", onclick: @handleActiveMoreClick}, "Loading...") 93 | 94 | if @user_list_recent.users.length > 0 95 | h("h2.recent", "New users in ZeroMe") 96 | ] 97 | 98 | h("div.users.cards.recent", [ 99 | @user_list_recent.render("card") 100 | ]) 101 | if @user_list_recent.users.length == @user_list_recent.limit 102 | h("a.more.recent", {href: "#", onclick: @handleRecentMoreClick}, "Show more...") 103 | else if @user_list_recent.users.length > 0 and @user_list_recent.loading 104 | h("a.more.recent", {href: "#", onclick: @handleRecentMoreClick}, "Loading...") 105 | 106 | if @user_list_recent.users.length 107 | h("h5", {style: "text-align: center"}, "Total: #{@num_users_total} registered users") 108 | ]) 109 | 110 | update: => 111 | @need_update = true 112 | Page.projector.scheduleRender() 113 | 114 | window.ContentUsers = ContentUsers -------------------------------------------------------------------------------- /js/Head.coffee: -------------------------------------------------------------------------------- 1 | class Head extends Class 2 | constructor: -> 3 | @menu = new Menu() 4 | @follows = [] 5 | 6 | handleSelectUserClick: -> 7 | if "Merger:ZeroMe" not in Page.site_info.settings.permissions 8 | Page.cmd "wrapperPermissionAdd", "Merger:ZeroMe", => 9 | Page.updateSiteInfo => 10 | Page.content.update() 11 | else 12 | Page.cmd "certSelect", {"accepted_domains": ["zeroid.bit"], "accept_any": true} 13 | return false 14 | 15 | handleFollowMenuItemClick: (type, item) => 16 | selected = not @follows[type] 17 | @follows[type] = selected 18 | item[2] = selected 19 | @saveFollows() 20 | Page.projector.scheduleRender() 21 | return true 22 | 23 | handleMenuClick: => 24 | if not Page.site_info?.cert_user_id 25 | return @handleSelectUserClick() 26 | Page.cmd "feedListFollow", [], (@follows) => 27 | @menu.items = [] 28 | 29 | @menu.items.push ["Follow username mentions", ( (item) => 30 | return @handleFollowMenuItemClick("Mentions", item) 31 | ), @follows["Mentions"]] 32 | 33 | @menu.items.push ["Follow comments on your posts", ( (item) => 34 | return @handleFollowMenuItemClick("Comments on your posts", item) 35 | ), @follows["Comments on your posts"]] 36 | 37 | @menu.items.push ["Follow new followers", ( (item) => 38 | return @handleFollowMenuItemClick("New followers", item) 39 | ), @follows["New followers"]] 40 | 41 | @menu.items.push ["Hide \"Hello ZeroMe!\" messages", ( (item) => 42 | Page.local_storage.settings.hide_hello_zerome = not Page.local_storage.settings.hide_hello_zerome 43 | item[2] = Page.local_storage.settings.hide_hello_zerome 44 | Page.projector.scheduleRender() 45 | Page.saveLocalStorage() 46 | Page.content.need_update = true 47 | return false 48 | ), Page.local_storage.settings.hide_hello_zerome] 49 | 50 | if (key for key of Page.user_hubs).length > 1 51 | @menu.items.push ["---"] 52 | for key, val of Page.user_hubs 53 | ((key) => 54 | @menu.items.push ["Use hub #{key}", ( (item) => 55 | Page.local_storage.settings.hub = key 56 | Page.saveLocalStorage() 57 | Page.checkUser() 58 | ), Page.user.row.site == key] 59 | )(key) 60 | 61 | @menu.toggle() 62 | Page.projector.scheduleRender() 63 | return false 64 | 65 | saveFollows: => 66 | out = {} 67 | if @follows["Mentions"] 68 | out["Mentions"] = [" 69 | SELECT 70 | 'mention' AS type, 71 | comment.date_added AS date_added, 72 | 'a comment' AS title, 73 | '@' || user_name || ': ' || comment.body AS body, 74 | '?Post/' || json.site || '/' || REPLACE(post_uri, '_', '/') AS url 75 | FROM comment 76 | LEFT JOIN json USING (json_id) 77 | WHERE 78 | comment.body LIKE '%@#{Page.user.row.user_name}%' 79 | 80 | UNION 81 | 82 | SELECT 83 | 'mention' AS type, 84 | post.date_added AS date_added, 85 | 'In ' || json.user_name || \"'s post\" AS title, 86 | '@' || json.user_name || ': ' || post.body AS body, 87 | '?Post/' || json.site || '/' || REPLACE(json.directory, 'data/users/', '') || '/' || post_id AS url 88 | FROM post 89 | LEFT JOIN json USING (json_id) 90 | WHERE 91 | post.body LIKE '%@#{Page.user.row.user_name}%' 92 | ", [""]] 93 | 94 | if @follows["Comments on your posts"] 95 | out["Comments on your posts"] = [" 96 | SELECT 97 | 'comment' AS type, 98 | comment.date_added AS date_added, 99 | 'Your post' AS title, 100 | '@' || json.user_name || ': ' || comment.body AS body, 101 | '?Post/' || site || '/' || REPLACE(post_uri, '_', '/') AS url 102 | FROM comment 103 | LEFT JOIN json USING (json_id) 104 | WHERE 105 | post_uri LIKE '#{Page.user.auth_address}%' 106 | ", [""]] 107 | 108 | if @follows["New followers"] 109 | out["New followers"] = [" 110 | SELECT 111 | 'follow' AS type, 112 | follow.date_added AS date_added, 113 | json.user_name || ' started following you' AS title, 114 | '' AS body, 115 | '?Profile/' || json.hub || REPLACE(json.directory, 'data/users', '') AS url 116 | FROM follow 117 | LEFT JOIN json USING(json_id) 118 | WHERE 119 | auth_address = '#{Page.user.auth_address}' 120 | GROUP BY json.directory 121 | ", [""]] 122 | 123 | Page.cmd "feedFollow", [out] 124 | 125 | 126 | 127 | render: => 128 | h("div.head.center", [ 129 | h("a.logo", {href: "?Home", onclick: Page.handleLinkClick}, h("img", {src: "img/logo.svg", height: 40, onerror: "this.src='img/logo.png'; this.onerror=null;"})), 130 | if Page.user?.hub 131 | # Registered user 132 | h("div.right.authenticated", [ 133 | h("div.user", 134 | h("a.name.link", {href: Page.user.getLink(), onclick: Page.handleLinkClick}, Page.user.row.user_name), 135 | h("a.address", {href: "#Select+user", onclick: @handleSelectUserClick}, Page.site_info.cert_user_id) 136 | ), 137 | h("a.settings", {href: "#Settings", onclick: Page.returnFalse, onmousedown: @handleMenuClick}, "\u22EE") 138 | @menu.render() 139 | ]) 140 | else if not Page.user?.hub and Page.site_info?.cert_user_id 141 | # Cert selected, but not registered 142 | h("div.right.selected", [ 143 | h("div.user", 144 | h("a.name.link", {href: "?Create+profile", onclick: Page.handleLinkClick}, "Create profile"), 145 | h("a.address", {href: "#Select+user", onclick: @handleSelectUserClick}, Page.site_info.cert_user_id) 146 | ), 147 | @menu.render() 148 | h("a.settings", {href: "#Settings", onclick: Page.returnFalse, onmousedown: @handleMenuClick}, "\u22EE") 149 | ]) 150 | else if not Page.user?.hub and Page.site_info 151 | # No cert selected 152 | h("div.right.unknown", [ 153 | h("div.user", 154 | h("a.name.link", {href: "#Select+user", onclick: @handleSelectUserClick}, "Visitor"), 155 | h("a.address", {href: "#Select+user", onclick: @handleSelectUserClick}, "Select your account") 156 | ), 157 | @menu.render() 158 | h("a.settings", {href: "#Settings", onclick: Page.returnFalse, onmousedown: @handleMenuClick}, "\u22EE") 159 | ]) 160 | else 161 | h("div.right.unknown") 162 | ]) 163 | 164 | window.Head = Head 165 | -------------------------------------------------------------------------------- /js/Post.coffee: -------------------------------------------------------------------------------- 1 | class Post extends Class 2 | constructor: (row, @item_list) -> 3 | @liked = false 4 | @commenting = false 5 | @submitting_like = false 6 | @owned = false 7 | @editable_comments = {} 8 | @field_comment = new Autosize({placeholder: "Add your comment", onsubmit: @handleCommentSubmit, title_submit: "Send"}) 9 | @comment_limit = 3 10 | @menu = null 11 | @meta = null 12 | @css_style = "" 13 | @setRow(row) 14 | 15 | setRow: (row) -> 16 | @row = row 17 | if @row.meta 18 | @meta = new PostMeta(@, JSON.parse(@row.meta)) 19 | if Page.user 20 | @liked = Page.user.likes[@row.key] 21 | @user = new User({hub: row.site, auth_address: row.directory.replace("data/users/", "")}) 22 | @user.row = row 23 | @owned = @user.auth_address == Page.user?.auth_address 24 | if @owned 25 | @editable_body = new Editable("div.body", @handlePostSave, @handlePostDelete) 26 | @editable_body.render_function = Text.renderMarked 27 | @editable_body.empty_text = " " 28 | 29 | getLink: -> 30 | "?Post/#{@user.hub}/#{@user.auth_address}/#{@row.post_id}" 31 | 32 | handlePostSave: (body, cb) => 33 | Page.user.getData Page.user.hub, (data) => 34 | post_index = i for post, i in data.post when post.post_id == @row.post_id 35 | data.post[post_index].body = body 36 | Page.user.save data, Page.user.hub, (res) => 37 | cb(res) 38 | 39 | handlePostDelete: (cb) => 40 | Page.user.getData Page.user.hub, (data) => 41 | post_index = i for post, i in data.post when post.post_id == @row.post_id 42 | data.post.splice(post_index, 1) 43 | if @meta?.meta?.img 44 | Page.cmd "fileDelete", "#{@user.getPath()}/#{@row.post_id}.jpg", => 45 | Page.user.save data, Page.user.hub, (res) => 46 | cb(res) 47 | else 48 | Page.user.save data, Page.user.hub, (res) => 49 | cb(res) 50 | 51 | handleLikeClick: (e) => 52 | @submitting_like = true 53 | [site, post_uri] = @row.key.split("-") 54 | if Page.user.likes[post_uri] 55 | Animation.flashOut(e.currentTarget.firstChild) 56 | Page.user.dislike site, post_uri, => 57 | @submitting_like = false 58 | @unfollow() 59 | else 60 | Animation.flashIn(e.currentTarget.firstChild) 61 | Page.user.like site, post_uri, => 62 | @submitting_like = false 63 | @follow() 64 | return false 65 | 66 | handleCommentClick: => 67 | if @field_comment.node 68 | @field_comment.node.focus() 69 | else 70 | @commenting = true 71 | setTimeout ( => 72 | @field_comment.node.focus() 73 | ), 600 74 | return false 75 | 76 | handleCommentSubmit: => 77 | if not @field_comment.attrs.value then return 78 | timer_loading = setTimeout ( => @field_comment.loading = true ), 100 # Only add loading message if takes more than 100ms 79 | [site, post_uri] = @row.key.split("-") 80 | Page.user.comment site, post_uri, @field_comment.attrs.value, (res) => 81 | clearInterval(timer_loading) 82 | @field_comment.loading = false 83 | if res 84 | @field_comment.setValue("") 85 | @follow() 86 | 87 | handleCommentSave: (comment_id, body, cb) => 88 | Page.user.getData @row.site, (data) => 89 | comment_index = i for comment, i in data.comment when comment.comment_id == comment_id 90 | data.comment[comment_index].body = body 91 | Page.user.save data, @row.site, (res) => 92 | cb(res) 93 | 94 | handleCommentDelete: (comment_id, cb) => 95 | Page.user.getData @row.site, (data) => 96 | comment_index = i for comment, i in data.comment when comment.comment_id == comment_id 97 | data.comment.splice(comment_index, 1) 98 | Page.user.save data, @row.site, (res) => 99 | cb(res) 100 | @unfollow() 101 | 102 | handleMoreCommentsClick: => 103 | @comment_limit += 10 104 | return false 105 | 106 | handleReplyClick: (e) => 107 | user_name = e.currentTarget.attributes.user_name.value 108 | if @field_comment.attrs.value 109 | @field_comment.setValue("#{@field_comment.attrs.value}\n@#{user_name}: ") 110 | else 111 | @field_comment.setValue("@#{user_name}: ") 112 | @handleCommentClick(e) 113 | return false 114 | 115 | getEditableComment: (comment_uri) -> 116 | if not @editable_comments[comment_uri] 117 | [user_address, comment_id] = comment_uri.split("_") 118 | 119 | handleCommentSave = (body, cb) => 120 | @handleCommentSave(parseInt(comment_id), body, cb) 121 | 122 | handleCommentDelete = (cb) => 123 | @handleCommentDelete(parseInt(comment_id), cb) 124 | 125 | @editable_comments[comment_uri] = new Editable("div.body", handleCommentSave, handleCommentDelete) 126 | @editable_comments[comment_uri].render_function = Text.renderMarked 127 | 128 | return @editable_comments[comment_uri] 129 | 130 | getPostUri: => 131 | return "#{@user.auth_address}_#{@row.post_id}" 132 | 133 | handleSettingsClick: => 134 | @css_style = "z-index: #{@row.date_added}; position: relative" 135 | Page.cmd "feedListFollow", [], (follows) => 136 | if not @menu 137 | @menu = new Menu() 138 | followed = follows["Post follow"] and @getPostUri() in follows["Post follow"][1] 139 | @menu.items = [] 140 | @menu.items.push ["Follow in newsfeed", ( => if followed then @unfollow() else @follow() ), followed] 141 | @menu.items.push ["Mute user", @user.handleMuteClick] 142 | @menu.items.push ["Permalink", @getLink()] 143 | if @owned 144 | @menu.items.push ["Edit", ( (e) => @editable_body.handleEditClick(e) )] 145 | @menu.toggle() 146 | return false 147 | 148 | unfollow: => 149 | Page.cmd "feedListFollow", [], (follows) => 150 | if not follows["Post follow"] 151 | return 152 | followed_uris = follows["Post follow"][1] 153 | 154 | index = followed_uris.indexOf @getPostUri() 155 | if index == -1 156 | return 157 | 158 | followed_uris.splice(index, 1) 159 | if followed_uris.length == 0 160 | delete follows["Post follow"] 161 | @log "Unfollow", follows 162 | Page.cmd "feedFollow", [follows] 163 | 164 | follow: => 165 | Page.cmd "feedListFollow", [], (follows) => 166 | if not follows["Post follow"] 167 | follows["Post follow"] = [""" 168 | SELECT 169 | "comment" AS type, 170 | comment.date_added AS date_added, 171 | "a followed post" AS title, 172 | '@' || user_name || ': ' || comment.body AS body, 173 | '?Post/' || json.site || '/' || REPLACE(post_uri, '_', '/') AS url 174 | FROM comment 175 | LEFT JOIN json USING (json_id) 176 | WHERE post_uri IN (:params) 177 | """, []] 178 | followed_uris = follows["Post follow"][1] 179 | followed_uris.push @getPostUri() 180 | Page.cmd "feedFollow", [follows] 181 | 182 | renderComments: => 183 | if not @row.comments and not @commenting 184 | return [] 185 | if @row.selected 186 | comment_limit = @comment_limit + 50 187 | else 188 | comment_limit = @comment_limit 189 | h("div.comment-list", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp, animate_scrollfix: true, animate_noscale: true}, [ 190 | if @commenting then h("div.comment-create", {enterAnimation: Animation.slideDown}, 191 | @field_comment.render() 192 | ), 193 | @row.comments?[0..comment_limit-1].map (comment) => 194 | user_address = comment.directory.replace("data/users/", "") 195 | comment_uri = user_address+"_"+comment.comment_id 196 | owned = user_address == Page.user?.auth_address 197 | user_link = "?Profile/"+comment.hub+"/"+user_address+"/"+comment.cert_user_id 198 | h("div.comment", {id: comment_uri, key: comment_uri, animate_scrollfix: true, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 199 | h("div.user", [ 200 | h("a.name.link", {href: user_link, style: "color: #{Text.toColor(user_address)}", onclick: Page.handleLinkClick}, comment.user_name), 201 | h("span.sep", " \u00B7 "), 202 | h("span.address", {title: user_address}, comment.cert_user_id), 203 | h("span.sep", " \u2015 "), 204 | h("a.added.link", {href: "#", title: Time.date(comment.date_added, "long")}, Time.since(comment.date_added)), 205 | h("a.icon.icon-reply", {href: "#Reply", onclick: @handleReplyClick, user_name: comment.user_name}, "Reply") 206 | ]) 207 | if owned 208 | @getEditableComment(comment_uri).render(comment.body) 209 | else if comment.body?.length > 5000 210 | h("div.body.maxheight", {innerHTML: Text.renderMarked(comment.body), afterCreate: Maxheight.apply}) 211 | else 212 | h("div.body", {innerHTML: Text.renderMarked(comment.body) }) 213 | ]) 214 | if @row.comments?.length > comment_limit 215 | h("a.more", {href: "#More", onclick: @handleMoreCommentsClick, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, "Show more comments...") 216 | ]) 217 | 218 | render: => 219 | [site, post_uri] = @row.key.split("-") 220 | h("div.post", {key: @row.key, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp, animate_scrollfix: true, classes: {selected: @row.selected}, style: @css_style}, [ 221 | h("div.user", [ 222 | @user.renderAvatar({href: @user.getLink(), onclick: Page.handleLinkClick}), 223 | h("a.name.link", {href: @user.getLink(), onclick: Page.handleLinkClick, style: "color: #{Text.toColor(@user.auth_address)}"}, 224 | @row.user_name 225 | ), 226 | h("span.sep", " \u00B7 "), 227 | h("span.address", {title: @user.auth_address}, @row.cert_user_id), 228 | h("span.sep", " \u2015 "), 229 | h("a.added.link", {href: @getLink(), title: Time.date(@row.date_added, "long"), onclick: Page.handleLinkClick}, Time.since(@row.date_added)), 230 | if @menu then @menu.render(".menu-right"), 231 | h("a.settings", {href: "#Settings", onclick: Page.returnFalse, onmousedown: @handleSettingsClick}, "\u22EE") 232 | ]) 233 | if @owned 234 | @editable_body.render(@row.body) 235 | else 236 | h("div.body", {classes: {maxheight: not @row.selected and @row.body?.length > 3000}, innerHTML: Text.renderMarked(@row.body), afterCreate: Maxheight.apply, afterUpdate: Maxheight.apply}) 237 | if @meta 238 | @meta.render() 239 | h("div.actions", [ 240 | h("a.icon.icon-comment.link", {href: "#Comment", onclick: @handleCommentClick}, "Comment"), 241 | h("a.like.link", {classes: {active: Page.user?.likes[post_uri], loading: @submitting_like, "like-zero": @row.likes == 0}, href: "#Like", onclick: @handleLikeClick}, 242 | h("div.icon.icon-heart", {classes: {active: Page.user?.likes[post_uri]}}), 243 | if @row.likes then @row.likes 244 | ) 245 | # h("a.icon.icon-share.link", {href: "#Share"}, "Share"), 246 | ]), 247 | @renderComments() 248 | ]) 249 | 250 | window.Post = Post 251 | -------------------------------------------------------------------------------- /js/PostCreate.coffee: -------------------------------------------------------------------------------- 1 | class PostCreate extends Class 2 | constructor: -> 3 | @field_post = new Autosize({ 4 | placeholder: "Write something...", 5 | class: "postfield", 6 | onfocus: @startEdit, 7 | onblur: @startEdit 8 | }) 9 | @upload = new Uploadable(@handleUpload) 10 | @upload.resize_width = 900 11 | @upload.resize_height = 700 12 | @is_editing = false 13 | @image = new ImagePreview() 14 | 15 | startEdit: => 16 | @is_editing = true 17 | Page.projector.scheduleRender() 18 | 19 | handleUpload: (base64uri, width, height) => 20 | @startEdit() 21 | @image.base64uri = base64uri 22 | @image.width = width 23 | @image.height = height 24 | @upload.getPreviewData base64uri, 10, 10, (preview_data) => 25 | @image.preview_data = preview_data 26 | Page.projector.scheduleRender() 27 | 28 | handleImageClose: => 29 | @image.height = 0 30 | @image.base64uri = "" 31 | return false 32 | 33 | handlePostSubmit: => 34 | @field_post.loading = true 35 | if @image.height 36 | meta = {} 37 | meta["img"] = @image.preview_data 38 | else 39 | meta = null 40 | 41 | Page.user.post @field_post.attrs.value, meta, @image.base64uri?.replace(/.*base64,/, ""), (res) => 42 | @field_post.loading = false 43 | if res 44 | @field_post.setValue("") 45 | @image = new ImagePreview() 46 | document.activeElement.blur() # Clear the focus 47 | setTimeout ( -> 48 | Page.content.update() 49 | ), 100 50 | return false 51 | 52 | handleUploadClick: => 53 | if Page.server_info.rev < 1700 54 | Page.cmd "wrapperNotification", ["info", "You need ZeroNet version 0.5.0 to upload images"] 55 | else 56 | @upload.handleUploadClick() 57 | 58 | render: => 59 | user = Page.user 60 | if user == false 61 | h("div.post-create.post.empty") 62 | else if user?.hub 63 | # Registered user 64 | h("div.post-create.post", {classes: {editing: @is_editing}}, 65 | h("div.user", user.renderAvatar()), 66 | h("a.icon-image.link", {href: "#", onclick: @handleUploadClick}), 67 | @field_post.render(), 68 | if @image.base64uri 69 | h("div.image", {style: "background-image: url(#{@image.base64uri}); height: #{@image.getSize(530, 600)[1]}px", classes: {empty: false}}, [ 70 | h("a.close", {href: "#", onclick: @handleImageClose}, "×") 71 | ]) 72 | else 73 | h("div.image", {style: "height: 0px", classes: {empty: true}}) 74 | h("div.postbuttons", 75 | h("a.button.button-submit", {href: "#Submit", onclick: @handlePostSubmit}, "Submit new post"), 76 | ), 77 | h("div", {style: "clear: both"}) 78 | ) 79 | else if Page.site_info.cert_user_id 80 | # Selected cert, but no registered user 81 | h("div.post-create.post.empty.noprofile", 82 | h("div.user", h("a.avatar", href: "#", style: "background-image: url('img/unkown.png')")), 83 | h("div.select-user-container", 84 | h("a.button.button-submit.select-user", {href: "?Create+profile", onclick: Page.handleLinkClick}, [ 85 | "Create new profile" 86 | ]) 87 | ), 88 | h("textarea", {disabled: true}) 89 | ) 90 | else 91 | # No cert selected 92 | h("div.post-create.post.empty.nocert", 93 | h("div.user", h("a.avatar", href: "#", style: "background-image: url('img/unkown.png')")), 94 | h("div.select-user-container", 95 | h("a.button.button-submit.select-user", {href: "#Select+user", onclick: Page.head.handleSelectUserClick}, [ 96 | h("div.icon.icon-profile"), 97 | "Select user to post new content" 98 | ]) 99 | ), 100 | h("textarea", {disabled: true}) 101 | ) 102 | 103 | window.PostCreate = PostCreate 104 | -------------------------------------------------------------------------------- /js/PostList.coffee: -------------------------------------------------------------------------------- 1 | class PostList extends Class 2 | constructor: -> 3 | @item_list = new ItemList(Post, "key") 4 | @posts = @item_list.items 5 | @need_update = true 6 | @directories = [] 7 | @loaded = false 8 | @filter_post_ids = null 9 | @limit = 10 10 | 11 | queryComments: (post_uris, cb) => 12 | query = " 13 | SELECT 14 | post_uri, comment.body, comment.date_added, comment.comment_id, json.cert_auth_type, json.cert_user_id, json.user_name, json.hub, json.directory, json.site 15 | FROM 16 | comment 17 | LEFT JOIN json USING (json_id) 18 | WHERE 19 | ? AND date_added < #{Time.timestamp()+120} 20 | ORDER BY date_added DESC 21 | " 22 | return Page.cmd "dbQuery", [query, {post_uri: post_uris}], cb 23 | 24 | queryLikes: (post_uris, cb) => 25 | query = "SELECT post_uri, COUNT(*) AS likes FROM post_like WHERE ? GROUP BY post_uri" 26 | return Page.cmd "dbQuery", [query, {post_uri: post_uris}], cb 27 | 28 | update: => 29 | @need_update = false 30 | 31 | param = {} 32 | if @directories == "all" 33 | where = "WHERE post_id IS NOT NULL AND post.date_added < #{Time.timestamp()+120} " 34 | else 35 | where = "WHERE directory IN #{Text.sqlIn(@directories)} AND post_id IS NOT NULL AND post.date_added < #{Time.timestamp()+120} " 36 | 37 | if @filter_post_ids 38 | where += "AND post_id IN #{Text.sqlIn(@filter_post_ids)} " 39 | 40 | if Page.local_storage.settings.hide_hello_zerome 41 | where += "AND post_id > 1 " 42 | 43 | query = " 44 | SELECT 45 | * 46 | FROM 47 | post 48 | LEFT JOIN json ON (post.json_id = json.json_id) 49 | #{where} 50 | ORDER BY date_added DESC 51 | LIMIT #{@limit+1} 52 | " 53 | @logStart "Update" 54 | Page.cmd "dbQuery", [query, param], (rows) => 55 | items = [] 56 | post_uris = [] 57 | for row in rows 58 | row["key"] = row["site"]+"-"+row["directory"].replace("data/users/", "")+"_"+row["post_id"] 59 | row["post_uri"] = row["directory"].replace("data/users/", "") + "_" + row["post_id"] 60 | post_uris.push(row["post_uri"]) 61 | 62 | # Get comments for latest posts 63 | 64 | @queryComments post_uris, (comment_rows) => 65 | comment_db = {} # {Post id: posts} 66 | for comment_row in comment_rows 67 | comment_db[comment_row.site+"/"+comment_row.post_uri] ?= [] 68 | comment_db[comment_row.site+"/"+comment_row.post_uri].push(comment_row) 69 | for row in rows 70 | row["comments"] = comment_db[row.site+"/"+row.post_uri] 71 | if @filter_post_ids?.length == 1 and row.post_id == parseInt(@filter_post_ids[0]) 72 | row.selected = true 73 | @item_list.sync(rows) 74 | @loaded = true 75 | @logEnd "Update" 76 | Page.projector.scheduleRender() 77 | 78 | if @posts.length > @limit 79 | @addScrollwatcher() 80 | 81 | @queryLikes post_uris, (like_rows) => 82 | like_db = {} 83 | for like_row in like_rows 84 | like_db[like_row["post_uri"]] = like_row["likes"] 85 | 86 | for row in rows 87 | row["likes"] = like_db[row["post_uri"]] 88 | @item_list.sync(rows) 89 | Page.projector.scheduleRender() 90 | 91 | 92 | handleMoreClick: => 93 | @limit += 10 94 | @update() 95 | return false 96 | 97 | addScrollwatcher: => 98 | setTimeout ( => 99 | # Remove previous scrollwatchers for same item 100 | for item, i in Page.scrollwatcher.items 101 | if item[1] == @tag_more 102 | Page.scrollwatcher.items.splice(i, 1) 103 | break 104 | Page.scrollwatcher.add @tag_more, (tag) => 105 | if tag.getBoundingClientRect().top == 0 106 | return 107 | @limit += 10 108 | @need_update = true 109 | Page.projector.scheduleRender() 110 | ), 2000 111 | 112 | 113 | storeMoreTag: (elem) => 114 | @tag_more = elem 115 | 116 | render: => 117 | if @need_update then @update() 118 | if not @posts.length 119 | if not @loaded 120 | return null 121 | else 122 | return h("div.post-list", [ 123 | h("div.post-list-empty", {enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp}, [ 124 | h("h2", "No posts yet"), 125 | h("a", {href: "?Users", onclick: Page.handleLinkClick}, "Let's follow some users!") 126 | ]) 127 | ]) 128 | 129 | return [ 130 | h("div.post-list", @posts[0..@limit].map (post) => 131 | try 132 | post.render() 133 | catch err 134 | h("div.error", ["Post render error:", err.message]) 135 | Debug.formatException(err) 136 | ), 137 | if @posts.length > @limit 138 | h("a.more.small", {href: "#More", onclick: @handleMoreClick, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUp, afterCreate: @storeMoreTag}, "Show more posts...") 139 | ] 140 | 141 | window.PostList = PostList 142 | -------------------------------------------------------------------------------- /js/PostMeta.coffee: -------------------------------------------------------------------------------- 1 | class PostMeta extends Class 2 | constructor: (@post, @meta) -> 3 | @ 4 | 5 | afterCreateImage: (tag) => 6 | Page.scrollwatcher.add tag, => 7 | try 8 | @image_preview.preview_uri = @image_preview.getPreviewUri() 9 | Page.cmd "optionalFileInfo", @post.user.getPath()+"/"+@post.row.post_id+".jpg", (res) => 10 | @image_preview.optional_info = res 11 | Page.projector.scheduleRender() 12 | catch e 13 | @log "Image preview error: #{e}" 14 | Page.projector.scheduleRender() 15 | 16 | handleImageClick: (e) => 17 | if @image_preview.load_fullsize or @image_preview.optional_info?.is_downloaded 18 | Page.overlay.zoomImageTag(e.currentTarget, @image_preview.width, @image_preview.height) 19 | else 20 | @image_preview.load_fullsize = true 21 | @image_preview.loading = true 22 | image = new Image() 23 | image.src = "#{@post.user.getPath()}/#{@post.row.post_id}.jpg" 24 | image.onload = => 25 | @image_preview.loading = false 26 | @image_preview.optional_info.is_downloaded = 1 27 | @image_preview.optional_info.peer += 1 28 | Page.projector.scheduleRender() 29 | 30 | Page.projector.scheduleRender() 31 | return false 32 | 33 | handleOptionalHelpClick: => 34 | @post.user.hasHelp (optional_helping) => 35 | @optional_helping = optional_helping 36 | if @optional_helping 37 | Page.cmd "OptionalHelpRemove", ["data/users/#{@post.user.auth_address}", @post.user.hub] 38 | @optional_helping = false 39 | else 40 | Page.cmd "OptionalHelp", ["data/users/#{@post.user.auth_address}", "#{@post.row.user_name}'s new images", @post.user.hub] 41 | @optional_helping = true 42 | Page.content_profile.update() 43 | Page.projector.scheduleRender() 44 | return true 45 | 46 | handleImageDeleteClick: => 47 | inner_path = "#{@post.user.getPath()}/#{@post.row.post_id}.jpg" 48 | Page.cmd "optionalFileDelete", inner_path, => 49 | @image_preview.optional_info.is_downloaded = 0 50 | @image_preview.optional_info.peer -= 1 51 | Page.projector.scheduleRender() 52 | 53 | handleImageSettingsClick: (e) => 54 | if e.target.classList.contains("menu-item") 55 | return 56 | @post.user.hasHelp (helping) => 57 | if not @menu_image 58 | @menu_image = new Menu() 59 | @optional_helping = helping 60 | @menu_image.items = [] 61 | @menu_image.items.push ["Help distribute this user's new images", @handleOptionalHelpClick, ( => return @optional_helping)] 62 | @menu_image.items.push ["---"] 63 | if @image_preview.optional_info?.is_downloaded 64 | @menu_image.items.push ["Delete image", @handleImageDeleteClick] 65 | else 66 | @menu_image.items.push ["Show image", @handleImageClick, false] 67 | @menu_image.toggle() 68 | return false 69 | 70 | render: => 71 | if @meta.img 72 | if not @image_preview 73 | @image_preview = new ImagePreview() 74 | @image_preview.setPreviewData(@meta.img) 75 | [width, height] = @image_preview.getSize(530, 600) 76 | 77 | if @image_preview?.preview_uri 78 | style_preview = "background-image: url(#{@image_preview.preview_uri})" 79 | else 80 | style_preview = "" 81 | 82 | if @image_preview.load_fullsize or @image_preview.optional_info?.is_downloaded 83 | style_fullsize = "background-image: url(#{@post.user.getPath()}/#{@post.row.post_id}.jpg)" 84 | else 85 | style_fullsize = "" 86 | 87 | h("div.img.preview", { 88 | afterCreate: @afterCreateImage, 89 | style: "width: #{width}px; height: #{height}px; #{style_preview}", 90 | classes: {downloaded: @image_preview.optional_info?.is_downloaded, hasinfo: @image_preview.optional_info?.peer != null, loading: @image_preview.loading} 91 | }, 92 | h("a.fullsize", {href: "#", onclick: @handleImageClick, style: style_fullsize}), 93 | if Page.server_info.rev < 1700 94 | h("small.oldversion", "You need ZeroNet 0.5.0 to view this image") 95 | if @image_preview?.optional_info 96 | h("a.show", {href: "#", onclick: @handleImageClick}, h("div.title", "Loading...\nShow image")) 97 | if @image_preview?.optional_info 98 | h("a.details", {href: "#Settings", onclick: Page.returnFalse, onmousedown: @handleImageSettingsClick}, [ 99 | h("div.size", Text.formatSize(@image_preview.optional_info?.size)), 100 | h("div.peers.icon-profile"), @image_preview.optional_info?.peer, 101 | h("a.image-settings", "\u22EE") 102 | if @menu_image then @menu_image.render(".menu-right") 103 | ]) 104 | ) 105 | 106 | window.PostMeta = PostMeta -------------------------------------------------------------------------------- /js/Trigger.coffee: -------------------------------------------------------------------------------- 1 | class Trigger extends Class 2 | constructor: -> 3 | @trigger_off = true 4 | 5 | handleTitleClick: => 6 | if @trigger_off 7 | @trigger_off = false 8 | document.body.classList.add("trigger-on") 9 | else 10 | document.body.classList.remove("trigger-on") 11 | @trigger_off = true 12 | return false 13 | 14 | render: => 15 | h("div.Trigger", {classes: { "trigger-off": @trigger_off }}, [ 16 | h("a.icon", {"href": "#Trigger", onclick: @handleTitleClick}) 17 | ]) 18 | window.Trigger = Trigger 19 | -------------------------------------------------------------------------------- /js/User.coffee: -------------------------------------------------------------------------------- 1 | class User extends Class 2 | constructor: (row, @item_list) -> 3 | if row 4 | @setRow(row) 5 | @likes = {} 6 | @followed_users = {} 7 | @submitting_follow = false 8 | 9 | setRow: (row) -> 10 | @row = row 11 | @hub = row.hub 12 | @auth_address = row.auth_address 13 | 14 | get: (site, auth_address, cb=null) -> 15 | params = { site: site, directory: "data/users/"+auth_address } 16 | Page.cmd "dbQuery", ["SELECT * FROM json WHERE site = :site AND directory = :directory LIMIT 1", params], (res) => 17 | row = res[0] 18 | if row 19 | if row.user_name == "" 20 | row.user_name = row.cert_user_id 21 | row.auth_address = row.directory.replace("data/users/", "") 22 | @setRow(row) 23 | cb?(row) 24 | else 25 | cb(false) 26 | 27 | updateInfo: (cb=null) => 28 | @logStart "Info loaded" 29 | p_likes = new Promise() 30 | p_followed_users = new Promise() 31 | 32 | # Load followed users 33 | Page.cmd "dbQuery", ["SELECT * FROM follow WHERE json_id = #{@row.json_id}"], (res) => 34 | @followed_users = {} 35 | for row in res 36 | @followed_users[row.hub+"/"+row.auth_address] = row 37 | p_followed_users.resolve() 38 | 39 | # Load likes 40 | Page.cmd "dbQuery", ["SELECT post_like.* FROM json LEFT JOIN post_like USING (json_id) WHERE directory = 'data/users/#{@auth_address}' AND post_uri IS NOT NULL"], (res) => 41 | @likes = {} 42 | for row in res 43 | @likes[row.post_uri] = true 44 | p_likes.resolve() 45 | 46 | Promise.join(p_followed_users, p_likes).then (res1, res2) => 47 | @logEnd "Info loaded" 48 | cb?(true) 49 | 50 | isFollowed: -> 51 | return Page.user.followed_users[@hub+"/"+@auth_address] 52 | 53 | isSeeding: -> 54 | return Page.merged_sites[@hub] 55 | 56 | hasHelp: (cb) => 57 | Page.cmd "OptionalHelpList", [@hub], (helps) => 58 | cb(helps["data/users/#{@auth_address}"]) 59 | 60 | getPath: (site=@hub) -> 61 | if site == Page.userdb 62 | return "merged-ZeroMe/#{site}/data/userdb/#{@auth_address}" 63 | else 64 | return "merged-ZeroMe/#{site}/data/users/#{@auth_address}" 65 | 66 | getLink: -> 67 | return "?Profile/#{@hub}/#{@auth_address}/#{@row.cert_user_id or ''}" 68 | 69 | getAvatarLink: -> 70 | cache_invalidation = "" 71 | # Cache invalidation for local user 72 | if @auth_address == Page.user?.auth_address 73 | cache_invalidation = "?"+Page.cache_time 74 | return "merged-ZeroMe/#{@hub}/data/users/#{@auth_address}/avatar.#{@row.avatar}#{cache_invalidation}" 75 | 76 | getDefaultData: -> 77 | return { 78 | "next_post_id": 2, 79 | "next_comment_id": 1, 80 | "next_follow_id": 1, 81 | "avatar": "generate", 82 | "user_name": @row?.user_name, 83 | "hub": @hub, 84 | "intro": "Random ZeroNet user", 85 | "post": [{ 86 | "post_id": 1, 87 | "date_added": Time.timestamp(), 88 | "body": "Hello ZeroMe!" 89 | }], 90 | "post_like": {}, 91 | "comment": [], 92 | "follow": [] 93 | } 94 | 95 | getData: (site, cb) -> 96 | Page.cmd "fileGet", [@getPath(site)+"/data.json", false], (data) => 97 | data = JSON.parse(data) 98 | data ?= { 99 | "next_comment_id": 1, 100 | "user_name": @row?.user_name, 101 | "hub": @hub, 102 | "post_like": {}, 103 | "comment": [] 104 | } 105 | cb(data) 106 | 107 | renderAvatar: (attrs={}) => 108 | if @isSeeding() and (@row.avatar == "png" or @row.avatar == "jpg") 109 | attrs.style = "background-image: url('#{@getAvatarLink()}')" 110 | else 111 | attrs.style = "background: linear-gradient("+Text.toColor(@auth_address)+","+Text.toColor(@auth_address.slice(-5))+")" 112 | h("a.avatar", attrs) 113 | 114 | save: (data, site=@hub, cb=null) -> 115 | Page.cmd "fileWrite", [@getPath(site)+"/data.json", Text.fileEncode(data)], (res_write) => 116 | if Page.server_info.rev > 1400 117 | # Accidently left an unwanted modification in rev1400 fix 118 | Page.content.update() 119 | cb?(res_write) 120 | Page.cmd "sitePublish", {"inner_path": @getPath(site)+"/data.json"}, (res_sign) => 121 | @log "Save result", res_write, res_sign 122 | if site == @hub and res_write == "ok" and res_sign == "ok" 123 | @saveUserdb(data) 124 | 125 | saveUserdb: (data, cb) -> 126 | cert_provider = Page.site_info.cert_user_id.replace(/.*@/, "") 127 | if cert_provider not in ["zeroid.bit", "zeroverse.bit"] 128 | @log "Cert provider #{cert_provider} not supported by userdb!" 129 | cb(false) 130 | return false 131 | Page.cmd "fileGet", [@getPath(Page.userdb)+"/content.json", false], (userdb_data) => 132 | userdb_data = JSON.parse(userdb_data) 133 | changed = false 134 | if not userdb_data?.user 135 | userdb_data = { 136 | user: [{date_added: Time.timestamp()}] 137 | } 138 | changed = true 139 | for field in ["avatar", "hub", "intro", "user_name"] 140 | if userdb_data.user[0][field] != data[field] 141 | changed = true 142 | @log "Changed in profile:", field, userdb_data.user[0][field], "!=", data[field] 143 | userdb_data.user[0][field] = data[field] 144 | 145 | if changed 146 | Page.cmd "fileWrite", [@getPath(Page.userdb)+"/content.json", Text.fileEncode(userdb_data)], (res_write) => 147 | Page.cmd "sitePublish", {"inner_path": @getPath(Page.userdb)+"/content.json"}, (res_sign) => 148 | @log "Userdb save result", res_write, res_sign 149 | cb?(res_sign) 150 | 151 | like: (site, post_uri, cb=null) -> 152 | @log "Like", site, post_uri 153 | @likes[post_uri] = true 154 | 155 | @getData site, (data) => 156 | data.post_like[post_uri] = Time.timestamp() 157 | @save data, site, (res) => 158 | if cb then cb(res) 159 | 160 | dislike: (site, post_uri, cb=null) -> 161 | @log "Dislike", site, post_uri 162 | delete @likes[post_uri] 163 | 164 | @getData site, (data) => 165 | delete data.post_like[post_uri] 166 | @save data, site, (res) => 167 | if cb then cb(res) 168 | 169 | comment: (site, post_uri, body, cb=null) -> 170 | @getData site, (data) => 171 | data.comment.push { 172 | "comment_id": data.next_comment_id, 173 | "body": body, 174 | "post_uri": post_uri, 175 | "date_added": Time.timestamp() 176 | } 177 | data.next_comment_id += 1 178 | @save data, site, (res) => 179 | if cb then cb(res) 180 | 181 | # Add optional pattern to user's content.json 182 | checkContentJson: (cb=null) -> 183 | Page.cmd "fileGet", [@getPath(@hub)+"/content.json", false], (res) => 184 | content_json = JSON.parse(res) 185 | if content_json.optional 186 | return cb(true) 187 | 188 | content_json.optional = "(?!avatar).*jpg" 189 | Page.cmd "fileWrite", [@getPath(@hub)+"/content.json", Text.fileEncode(content_json)], (res_write) => 190 | cb(res_write) 191 | 192 | fileWrite: (file_name, content_base64, cb=null) -> 193 | if not content_base64 194 | return cb?(null) 195 | 196 | @checkContentJson => 197 | Page.cmd "fileWrite", [@getPath(@hub)+"/"+file_name, content_base64], (res_write) => 198 | cb?(res_write) 199 | 200 | post: (body, meta=null, image_base64=null, cb=null) -> 201 | @getData @hub, (data) => 202 | post = { 203 | "post_id": Time.timestamp() + data.next_post_id, 204 | "body": body, 205 | "date_added": Time.timestamp() 206 | } 207 | if meta 208 | post["meta"] = Text.jsonEncode(meta) 209 | data.post.push post 210 | data.next_post_id += 1 211 | @fileWrite post.post_id+".jpg", image_base64, (res) => 212 | @save data, @hub, (res) => 213 | if cb then cb(res) 214 | 215 | followUser: (hub, auth_address, user_name, cb=null) -> 216 | @log "Following", hub, auth_address 217 | @download() 218 | 219 | @getData @hub, (data) => 220 | follow_row = { 221 | "follow_id": data.next_follow_id, 222 | "hub": hub, 223 | "auth_address": auth_address, 224 | "user_name": user_name, 225 | "date_added": Time.timestamp() 226 | } 227 | data.follow.push follow_row 228 | @followed_users[hub+"/"+auth_address] = true 229 | data.next_follow_id += 1 230 | @save data, @hub, (res) => 231 | if cb then cb(res) 232 | 233 | Page.needSite hub # Download followed user's site if necessary 234 | 235 | unfollowUser: (hub, auth_address, cb=null) -> 236 | @log "UnFollowing", hub, auth_address 237 | delete @followed_users[hub+"/"+auth_address] 238 | 239 | @getData @hub, (data) => 240 | follow_index = i for follow, i in data.follow when follow.hub == hub and follow.auth_address == auth_address 241 | data.follow.splice(follow_index, 1) 242 | @save data, @hub, (res) => 243 | if cb then cb(res) 244 | 245 | handleFollowClick: (e) => 246 | @submitting_follow = true 247 | if not @isFollowed() 248 | Animation.flashIn(e.target) 249 | Page.user.followUser @hub, @auth_address, @row.user_name, (res) => 250 | @submitting_follow = false 251 | Page.projector.scheduleRender() 252 | else 253 | Animation.flashOut(e.target) 254 | Page.user.unfollowUser @hub, @auth_address, (res) => 255 | @submitting_follow = false 256 | Page.projector.scheduleRender() 257 | return false 258 | 259 | download: => 260 | if not Page.merged_sites[@hub] 261 | Page.cmd "mergerSiteAdd", @hub, => 262 | Page.updateSiteInfo() 263 | 264 | handleDownloadClick: (e) => 265 | @download() 266 | return false 267 | 268 | handleMuteClick: (e) => 269 | if Page.server_info.rev < 1880 270 | Page.cmd "wrapperNotification", ["info", "You need ZeroNet 0.5.2 to use this feature."] 271 | return false 272 | Page.cmd "muteAdd", [@auth_address, @row.cert_user_id, "Muted from [page](http://127.0.0.1:43110/#{Page.address}/?#{Page.history_state.url})"] 273 | return false 274 | 275 | renderList: (type="normal") => 276 | classname = "" 277 | if type == "card" then classname = ".card" 278 | link = @getLink() 279 | followed = @isFollowed() 280 | seeding = @isSeeding() 281 | if followed then title = "Unfollow" else title = "Follow" 282 | if type != "card" 283 | enterAnimation = Animation.slideDown 284 | exitAnimation = Animation.slideUp 285 | else 286 | enterAnimation = null 287 | exitAnimation = null 288 | h("div.user"+classname, {key: @hub+"/"+@auth_address, classes: {followed: followed, notseeding: !seeding}, enterAnimation: enterAnimation, exitAnimation: exitAnimation}, [ 289 | h("a.button.button-follow", {href: link, onclick: @handleFollowClick, title: title, classes: {loading: @submitting_follow}}, "+"), 290 | h("a", {href: link, onclick: Page.handleLinkClick}, @renderAvatar()), 291 | h("div.nameline", [ 292 | h("a.name.link", {href: link, onclick: Page.handleLinkClick}, @row.user_name), 293 | if type == "card" then h("span.added", Time.since(@row.date_added)) 294 | ]) 295 | if @row.followed_by 296 | h("div.intro.followedby", [ 297 | "Followed by ", 298 | h("a.name.link", {href: "?ProfileName/#{@row.followed_by}", onclick: Page.handleLinkClick}, @row.followed_by) 299 | ]) 300 | else 301 | h("div.intro", @row.intro) 302 | ]) 303 | 304 | 305 | window.User = User -------------------------------------------------------------------------------- /js/UserList.coffee: -------------------------------------------------------------------------------- 1 | class UserList extends Class 2 | constructor: (@type="new") -> 3 | @item_list = new ItemList(User, "key") 4 | @users = @item_list.items 5 | @need_update = true 6 | @limit = 5 7 | @followed_by = null 8 | @search = null 9 | 10 | update: -> 11 | @loading = true 12 | params = {} 13 | if @search 14 | search_where = "AND (json.user_name LIKE :search_like OR user.user_name LIKE :search_like OR json.cert_user_id LIKE :search_like)" 15 | params["search_like"] = "%#{@search}%" 16 | else 17 | search_where = "" 18 | if @followed_by 19 | query = """ 20 | SELECT user.user_name, follow.*, user.* 21 | FROM follow 22 | LEFT JOIN user USING (auth_address, hub) 23 | WHERE 24 | follow.json_id = #{@followed_by.row.json_id} AND user.json_id IS NOT NULL 25 | 26 | UNION 27 | 28 | SELECT user.user_name, follow.*, user.* 29 | FROM follow 30 | LEFT JOIN json ON (json.directory = 'data/userdb/' || follow.auth_address) 31 | LEFT JOIN user ON (user.json_id = json.json_id) 32 | WHERE 33 | follow.json_id = #{@followed_by.row.json_id} AND user.json_id IS NOT NULL AND 34 | follow.date_added < #{Time.timestamp()+120} 35 | ORDER BY date_added DESC 36 | LIMIT #{@limit} 37 | """ 38 | else if @type == "suggested" 39 | followed_user_addresses = (key.replace(/.*\//, "") for key, val of Page.user.followed_users) 40 | followed_user_directories = ("data/users/"+key for key in followed_user_addresses) 41 | if not followed_user_addresses.length 42 | return 43 | query = """ 44 | SELECT 45 | COUNT(DISTINCT(json.directory)) AS num, 46 | GROUP_CONCAT(DISTINCT(json.user_name)) AS followed_by, 47 | follow.*, 48 | json_suggested.avatar 49 | FROM follow 50 | LEFT JOIN json USING (json_id) 51 | LEFT JOIN json AS json_suggested ON (json_suggested.directory = 'data/users/' || follow.auth_address AND json_suggested.avatar IS NOT NULL) 52 | WHERE 53 | json.directory IN #{Text.sqlIn(followed_user_directories)} AND 54 | auth_address NOT IN #{Text.sqlIn(followed_user_addresses)} AND 55 | auth_address != '#{Page.user.auth_address}' AND 56 | date_added < #{Time.timestamp()+120} 57 | GROUP BY follow.auth_address 58 | ORDER BY num DESC 59 | LIMIT #{@limit} 60 | """ 61 | else if @type == "active" 62 | query = """ 63 | SELECT 64 | json.*, 65 | json.site AS json_site, 66 | json.directory AS json_directory, 67 | json.file_name AS json_file_name, 68 | json.cert_user_id AS json_cert_user_id, 69 | json.hub AS json_hub, 70 | json.user_name AS json_user_name, 71 | json.avatar AS json_avatar, 72 | COUNT(*) AS posts 73 | FROM 74 | post LEFT JOIN json USING (json_id) 75 | WHERE 76 | post.date_added > #{Time.timestamp() - 60*60*24*7} 77 | GROUP BY json_id 78 | ORDER BY posts DESC 79 | LIMIT #{@limit} 80 | """ 81 | else 82 | query = """ 83 | SELECT 84 | user.*, 85 | json.site AS json_site, 86 | json.directory AS json_directory, 87 | json.file_name AS json_file_name, 88 | json.cert_user_id AS json_cert_user_id, 89 | json.hub AS json_hub, 90 | json.user_name AS json_user_name, 91 | json.avatar AS json_avatar 92 | FROM 93 | user LEFT JOIN json USING (json_id) 94 | WHERE 95 | date_added < #{Time.timestamp()+120} 96 | #{search_where} 97 | ORDER BY date_added DESC 98 | LIMIT #{@limit} 99 | """ 100 | Page.cmd "dbQuery", [query, params], (rows) => 101 | rows_by_user = {} # Deduplicating 102 | followed_by_displayed = {} 103 | for row in rows 104 | if row.json_cert_user_id # File in user directory 105 | row.cert_user_id = row.json_cert_user_id 106 | row.auth_address = row.json_directory.replace("data/userdb/", "").replace("data/users/", "") 107 | 108 | if not row.auth_address # Just created user, no content.json yet 109 | continue 110 | 111 | if row.followed_by 112 | 113 | row.followed_by = (username for username in row.followed_by.split(",") when not followed_by_displayed[username])[0] 114 | followed_by_displayed[row.followed_by] = true # Only display every user once 115 | 116 | row.key = row.hub+"/"+row.auth_address 117 | if not rows_by_user[row.hub+row.auth_address] 118 | rows_by_user[row.hub+row.auth_address] = row 119 | 120 | user_rows = (val for key, val of rows_by_user) 121 | @item_list.sync(user_rows) 122 | 123 | @loading = false 124 | Page.projector.scheduleRender() 125 | 126 | render: (type="normal") => 127 | if @need_update 128 | @need_update = false 129 | setTimeout ( => @update() ), 100 # Low prioriy 130 | if not @users.length 131 | return null 132 | 133 | h("div.UserList.users"+type+"."+@type, {afterCreate: Animation.show}, @users.map (user) => 134 | user.renderList(type) 135 | ) 136 | 137 | window.UserList = UserList 138 | -------------------------------------------------------------------------------- /js/lib/Class.coffee: -------------------------------------------------------------------------------- 1 | class Class 2 | trace: true 3 | 4 | log: (args...) -> 5 | return unless @trace 6 | return if typeof console is 'undefined' 7 | args.unshift("[#{@.constructor.name}]") 8 | console.log(args...) 9 | @ 10 | 11 | logStart: (name, args...) -> 12 | return unless @trace 13 | @logtimers or= {} 14 | @logtimers[name] = +(new Date) 15 | @log "#{name}", args..., "(started)" if args.length > 0 16 | @ 17 | 18 | logEnd: (name, args...) -> 19 | ms = +(new Date)-@logtimers[name] 20 | @log "#{name}", args..., "(Done in #{ms}ms)" 21 | @ 22 | 23 | window.Class = Class -------------------------------------------------------------------------------- /js/lib/Dollar.coffee: -------------------------------------------------------------------------------- 1 | window.$ = (selector) -> 2 | if selector.startsWith("#") 3 | return document.getElementById(selector.replace("#", "")) 4 | -------------------------------------------------------------------------------- /js/lib/Promise.coffee: -------------------------------------------------------------------------------- 1 | class Promise 2 | @join: (tasks...) -> 3 | num_uncompleted = tasks.length 4 | args = new Array(num_uncompleted) 5 | promise = new Promise() 6 | 7 | for task, task_id in tasks 8 | ((task_id) -> 9 | task.then(() -> 10 | args[task_id] = Array.prototype.slice.call(arguments) 11 | num_uncompleted-- 12 | if num_uncompleted == 0 13 | for callback in promise.callbacks 14 | callback.apply(promise, args) 15 | ) 16 | )(task_id) 17 | 18 | return promise 19 | 20 | constructor: -> 21 | @resolved = false 22 | @end_promise = null 23 | @result = null 24 | @callbacks = [] 25 | 26 | resolve: -> 27 | if @resolved 28 | return false 29 | @resolved = true 30 | @data = arguments 31 | if not arguments.length 32 | @data = [true] 33 | @result = @data[0] 34 | for callback in @callbacks 35 | back = callback.apply callback, @data 36 | if @end_promise and back and back.then 37 | back.then (back_res) => 38 | @end_promise.resolve(back_res) 39 | 40 | fail: -> 41 | @resolve(false) 42 | 43 | then: (callback) -> 44 | if @resolved == true 45 | return callback.apply callback, @data 46 | 47 | @callbacks.push callback 48 | 49 | @end_promise = new Promise() 50 | return @end_promise 51 | 52 | window.Promise = Promise 53 | 54 | ### 55 | s = Date.now() 56 | log = (text) -> 57 | console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") 58 | 59 | log "Started" 60 | 61 | cmd = (query) -> 62 | p = new Promise() 63 | setTimeout ( -> 64 | p.resolve query+" Result" 65 | ), 100 66 | return p 67 | 68 | 69 | back = cmd("SELECT * FROM message").then (res) -> 70 | log res 71 | p = new Promise() 72 | setTimeout ( -> 73 | p.resolve("DONE parsing SELECT") 74 | ), 100 75 | return p 76 | .then (res) -> 77 | log "Back of messages", res 78 | return cmd("SELECT * FROM users") 79 | .then (res) -> 80 | log "End result", res 81 | 82 | log "Query started", back 83 | 84 | 85 | q1 = cmd("SELECT * FROM anything") 86 | q2 = cmd("SELECT * FROM something") 87 | 88 | Promise.join(q1, q2).then (res1, res2) -> 89 | log res1, res2 90 | ### -------------------------------------------------------------------------------- /js/lib/Property.coffee: -------------------------------------------------------------------------------- 1 | Function::property = (prop, desc) -> 2 | Object.defineProperty @prototype, prop, desc 3 | -------------------------------------------------------------------------------- /js/lib/Prototypes.coffee: -------------------------------------------------------------------------------- 1 | String::startsWith = (s) -> @[...s.length] is s 2 | String::endsWith = (s) -> s is '' or @[-s.length..] is s 3 | String::repeat = (count) -> new Array( count + 1 ).join(@) 4 | 5 | window.isEmpty = (obj) -> 6 | for key of obj 7 | return false 8 | return true 9 | -------------------------------------------------------------------------------- /js/lib/RateLimitCb.coffee: -------------------------------------------------------------------------------- 1 | last_time = {} 2 | calling = {} 3 | call_after_interval = {} 4 | 5 | # Rate limit function call and don't allow to run in parallel (until callback is called) 6 | window.RateLimitCb = (interval, fn, args=[]) -> 7 | cb = -> # Callback when function finished 8 | left = interval - (Date.now() - last_time[fn]) # Time life until next call 9 | # console.log "CB, left", left, "Calling:", calling[fn] 10 | if left <= 0 # No time left from rate limit interval 11 | delete last_time[fn] 12 | if calling[fn] # Function called within interval 13 | RateLimitCb(interval, fn, calling[fn]) 14 | delete calling[fn] 15 | else # Time left from rate limit interval 16 | setTimeout (-> 17 | delete last_time[fn] 18 | if calling[fn] # Function called within interval 19 | RateLimitCb(interval, fn, calling[fn]) 20 | delete calling[fn] 21 | ), left 22 | if last_time[fn] # Function called within interval 23 | calling[fn] = args # Schedule call and update arguments 24 | else # Not called within interval, call instantly 25 | last_time[fn] = Date.now() 26 | fn.apply(this, [cb, args...]) 27 | 28 | window.RateLimit = (interval, fn) -> 29 | if not calling[fn] 30 | call_after_interval[fn] = false 31 | fn() # First call is not delayed 32 | calling[fn] = setTimeout (-> 33 | if call_after_interval[fn] 34 | fn() 35 | delete calling[fn] 36 | delete call_after_interval[fn] 37 | ), interval 38 | else # Called within iterval, delay the call 39 | call_after_interval[fn] = true 40 | 41 | ### 42 | window.s = Date.now() 43 | window.load = (done, num) -> 44 | console.log "Loading #{num}...", Date.now()-window.s 45 | setTimeout (-> done()), 1000 46 | 47 | RateLimit 500, window.load, [0] # Called instantly 48 | RateLimit 500, window.load, [1] 49 | setTimeout (-> RateLimit 500, window.load, [300]), 300 50 | setTimeout (-> RateLimit 500, window.load, [600]), 600 # Called after 1000ms 51 | setTimeout (-> RateLimit 500, window.load, [1000]), 1000 52 | setTimeout (-> RateLimit 500, window.load, [1200]), 1200 # Called after 2000ms 53 | setTimeout (-> RateLimit 500, window.load, [3000]), 3000 # Called after 3000ms 54 | ### -------------------------------------------------------------------------------- /js/lib/anime.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Anime v1.0.0 3 | * http://anime-js.com 4 | * JavaScript animation engine 5 | * Copyright (c) 2016 Julian Garnier 6 | * http://juliangarnier.com 7 | * Released under the MIT license 8 | */ 9 | (function(r,n){"function"===typeof define&&define.amd?define([],n):"object"===typeof module&&module.exports?module.exports=n():r.anime=n()})(this,function(){var r={duration:1E3,delay:0,loop:!1,autoplay:!0,direction:"normal",easing:"easeOutElastic",elasticity:400,round:!1,begin:void 0,update:void 0,complete:void 0},n="translateX translateY translateZ rotate rotateX rotateY rotateZ scale scaleX scaleY scaleZ skewX skewY".split(" "),e=function(){return{array:function(a){return Array.isArray(a)},object:function(a){return-1< 10 | Object.prototype.toString.call(a).indexOf("Object")},html:function(a){return a instanceof NodeList||a instanceof HTMLCollection},node:function(a){return a.nodeType},svg:function(a){return a instanceof SVGElement},number:function(a){return!isNaN(parseInt(a))},string:function(a){return"string"===typeof a},func:function(a){return"function"===typeof a},undef:function(a){return"undefined"===typeof a},"null":function(a){return"null"===typeof a},hex:function(a){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a)}, 11 | rgb:function(a){return/^rgb/.test(a)},rgba:function(a){return/^rgba/.test(a)},hsl:function(a){return/^hsl/.test(a)},color:function(a){return e.hex(a)||e.rgb(a)||e.rgba(a)||e.hsl(a)}}}(),z=function(){var a={},b={Sine:function(a){return 1-Math.cos(a*Math.PI/2)},Circ:function(a){return 1-Math.sqrt(1-a*a)},Elastic:function(a,b){if(0===a||1===a)return a;var f=1-Math.min(b,998)/1E3,h=a/1-1;return-(Math.pow(2,10*h)*Math.sin(2*(h-f/(2*Math.PI)*Math.asin(1))*Math.PI/f))},Back:function(a){return a*a*(3*a-2)}, 12 | Bounce:function(a){for(var b,f=4;a<((b=Math.pow(2,--f))-1)/11;);return 1/Math.pow(4,3-f)-7.5625*Math.pow((3*b-2)/22-a,2)}};["Quad","Cubic","Quart","Quint","Expo"].forEach(function(a,d){b[a]=function(a){return Math.pow(a,d+2)}});Object.keys(b).forEach(function(c){var d=b[c];a["easeIn"+c]=d;a["easeOut"+c]=function(a,b){return 1-d(1-a,b)};a["easeInOut"+c]=function(a,b){return.5>a?d(2*a,b)/2:1-d(-2*a+2,b)/2}});a.linear=function(a){return a};return a}(),u=function(a){return e.string(a)?a:a+""},A=function(a){return a.replace(/([a-z])([A-Z])/g, 13 | "$1-$2").toLowerCase()},B=function(a){if(e.color(a))return!1;try{return document.querySelectorAll(a)}catch(b){return!1}},v=function(a){return a.reduce(function(a,c){return a.concat(e.array(c)?v(c):c)},[])},p=function(a){if(e.array(a))return a;e.string(a)&&(a=B(a)||a);return e.html(a)?[].slice.call(a):[a]},C=function(a,b){return a.some(function(a){return a===b})},N=function(a,b){var c={};a.forEach(function(a){var f=JSON.stringify(b.map(function(b){return a[b]}));c[f]=c[f]||[];c[f].push(a)});return Object.keys(c).map(function(a){return c[a]})}, 14 | D=function(a){return a.filter(function(a,c,d){return d.indexOf(a)===c})},w=function(a){var b={},c;for(c in a)b[c]=a[c];return b},t=function(a,b){for(var c in b)a[c]=e.undef(a[c])?b[c]:a[c];return a},O=function(a){a=a.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(a,b,c,e){return b+b+c+c+e+e});var b=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(a);a=parseInt(b[1],16);var c=parseInt(b[2],16),b=parseInt(b[3],16);return"rgb("+a+","+c+","+b+")"},P=function(a){a=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(a); 15 | var b=parseInt(a[1])/360,c=parseInt(a[2])/100,d=parseInt(a[3])/100;a=function(a,b,c){0>c&&(c+=1);1c?b:c<2/3?a+(b-a)*(2/3-c)*6:a};if(0==c)c=d=b=d;else var f=.5>d?d*(1+c):d+c-d*c,h=2*d-f,c=a(h,f,b+1/3),d=a(h,f,b),b=a(h,f,b-1/3);return"rgb("+255*c+","+255*d+","+255*b+")"},k=function(a){return/([\+\-]?[0-9|auto\.]+)(%|px|pt|em|rem|in|cm|mm|ex|pc|vw|vh|deg)?/.exec(a)[2]},E=function(a,b,c){return k(b)?b:-1=a.delay&&(a.begin(b),a.begin=void 0);c.current>=b.duration?(a.loop?(c.start=+new Date,"alternate"===a.direction&&y(b,!0),e.number(a.loop)&& 25 | a.loop--,c.raf=requestAnimationFrame(c.tick)):(b.ended=!0,a.complete&&a.complete(b),b.pause()),c.last=0):c.raf=requestAnimationFrame(c.tick)}}};b.seek=function(a){L(b,a/100*b.duration)};b.pause=function(){b.running=!1;cancelAnimationFrame(c.raf);X(b);var a=m.indexOf(b);-1 3 | h = elem.offsetHeight 4 | cstyle = window.getComputedStyle(elem) 5 | margin_top = cstyle.marginTop 6 | margin_bottom = cstyle.marginBottom 7 | padding_top = cstyle.paddingTop 8 | padding_bottom = cstyle.paddingBottom 9 | border_top_width = cstyle.borderTopWidth 10 | border_bottom_width = cstyle.borderBottomWidth 11 | transition = cstyle.transition 12 | 13 | if window.Animation.shouldScrollFix(elem, props) 14 | # Keep objects in the screen at same position 15 | top_after = document.body.scrollHeight 16 | next_elem = elem.nextSibling 17 | parent = elem.parentNode 18 | parent.removeChild(elem) 19 | top_before = document.body.scrollHeight 20 | console.log("Scrollcorrection down", (top_before - top_after)) 21 | window.scrollTo(window.scrollX, window.scrollY - (top_before - top_after)) 22 | if next_elem 23 | parent.insertBefore(elem, next_elem) 24 | else 25 | parent.appendChild(elem) 26 | return 27 | 28 | if props.animate_scrollfix and elem.getBoundingClientRect().top > 1600 29 | # console.log "Skip down", elem 30 | return 31 | 32 | elem.style.boxSizing = "border-box" 33 | elem.style.overflow = "hidden" 34 | if not props.animate_noscale 35 | elem.style.transform = "scale(0.6)" 36 | elem.style.opacity = "0" 37 | elem.style.height = "0px" 38 | elem.style.marginTop = "0px" 39 | elem.style.marginBottom = "0px" 40 | elem.style.paddingTop = "0px" 41 | elem.style.paddingBottom = "0px" 42 | elem.style.borderTopWidth = "0px" 43 | elem.style.borderBottomWidth = "0px" 44 | elem.style.transition = "none" 45 | 46 | setTimeout (-> 47 | elem.className += " animate-inout" 48 | elem.style.height = h+"px" 49 | elem.style.transform = "scale(1)" 50 | elem.style.opacity = "1" 51 | elem.style.marginTop = margin_top 52 | elem.style.marginBottom = margin_bottom 53 | elem.style.paddingTop = padding_top 54 | elem.style.paddingBottom = padding_bottom 55 | elem.style.borderTopWidth = border_top_width 56 | elem.style.borderBottomWidth = border_bottom_width 57 | ), 1 58 | 59 | elem.addEventListener "transitionend", -> 60 | elem.classList.remove("animate-inout") 61 | elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null 62 | elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null 63 | elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null 64 | elem.style.borderTopWidth = elem.style.borderBottomWidth = elem.style.overflow = null 65 | elem.removeEventListener "transitionend", arguments.callee, false 66 | 67 | shouldScrollFix: (elem, props) -> 68 | pos = elem.getBoundingClientRect() 69 | if props.animate_scrollfix and window.scrollY > 300 and pos.top < 0 and not document.querySelector(".noscrollfix:hover") 70 | return true 71 | else 72 | return false 73 | 74 | slideDownAnime: (elem, props) -> 75 | cstyle = window.getComputedStyle(elem) 76 | elem.style.overflowY = "hidden" 77 | anime({targets: elem, height: [0, elem.offsetHeight], easing: 'easeInOutExpo'}) 78 | 79 | slideUpAnime: (elem, remove_func, props) -> 80 | elem.style.overflowY = "hidden" 81 | anime({targets: elem, height: [elem.offsetHeight, 0], complete: remove_func, easing: 'easeInOutExpo'}) 82 | 83 | 84 | slideUp: (elem, remove_func, props) -> 85 | if window.Animation.shouldScrollFix(elem, props) and elem.nextSibling 86 | # Keep objects in the screen at same position 87 | top_after = document.body.scrollHeight 88 | next_elem = elem.nextSibling 89 | parent = elem.parentNode 90 | parent.removeChild(elem) 91 | top_before = document.body.scrollHeight 92 | console.log("Scrollcorrection down", (top_before - top_after)) 93 | window.scrollTo(window.scrollX, window.scrollY + (top_before - top_after)) 94 | if next_elem 95 | parent.insertBefore(elem, next_elem) 96 | else 97 | parent.appendChild(elem) 98 | remove_func() 99 | return 100 | 101 | if props.animate_scrollfix and elem.getBoundingClientRect().top > 1600 102 | remove_func() 103 | # console.log "Skip up", elem 104 | return 105 | 106 | elem.className += " animate-inout" 107 | elem.style.boxSizing = "border-box" 108 | elem.style.height = elem.offsetHeight+"px" 109 | elem.style.overflow = "hidden" 110 | elem.style.transform = "scale(1)" 111 | elem.style.opacity = "1" 112 | elem.style.pointerEvents = "none" 113 | 114 | setTimeout (-> 115 | cstyle = window.getComputedStyle(elem) 116 | elem.style.height = "0px" 117 | elem.style.marginTop = (0-parseInt(cstyle.borderTopWidth)-parseInt(cstyle.borderBottomWidth))+"px" 118 | elem.style.marginBottom = "0px" 119 | elem.style.paddingTop = "0px" 120 | elem.style.paddingBottom = "0px" 121 | elem.style.transform = "scale(0.8)" 122 | elem.style.opacity = "0" 123 | ), 1 124 | elem.addEventListener "transitionend", (e) -> 125 | if e.propertyName == "opacity" or e.elapsedTime >= 0.6 126 | elem.removeEventListener "transitionend", arguments.callee, false 127 | setTimeout ( -> 128 | remove_func() 129 | ), 2000 130 | 131 | 132 | showRight: (elem, props) -> 133 | elem.className += " animate" 134 | elem.style.opacity = 0 135 | elem.style.transform = "TranslateX(-20px) Scale(1.01)" 136 | setTimeout (-> 137 | elem.style.opacity = 1 138 | elem.style.transform = "TranslateX(0px) Scale(1)" 139 | ), 1 140 | elem.addEventListener "transitionend", -> 141 | elem.classList.remove("animate") 142 | elem.style.transform = elem.style.opacity = null 143 | elem.removeEventListener "transitionend", arguments.callee, false 144 | 145 | 146 | show: (elem, props) -> 147 | delay = arguments[arguments.length-2]?.delay*1000 or 1 148 | elem.className += " animate" 149 | elem.style.opacity = 0 150 | setTimeout (-> 151 | elem.style.opacity = 1 152 | ), delay 153 | elem.addEventListener "transitionend", -> 154 | elem.classList.remove("animate") 155 | elem.style.opacity = null 156 | elem.removeEventListener "transitionend", arguments.callee, false 157 | 158 | hide: (elem, remove_func, props) -> 159 | delay = arguments[arguments.length-2]?.delay*1000 or 1 160 | elem.className += " animate" 161 | setTimeout (-> 162 | elem.style.opacity = 0 163 | ), delay 164 | elem.addEventListener "transitionend", (e) -> 165 | if e.propertyName == "opacity" 166 | remove_func() 167 | elem.removeEventListener "transitionend", arguments.callee, false 168 | 169 | addVisibleClass: (elem, props) -> 170 | setTimeout -> 171 | elem.classList.add("visible") 172 | 173 | cloneAnimation: (elem, animation) -> 174 | window.requestAnimationFrame => 175 | if elem.style.pointerEvents == "none" # Fix if animation called on cloned element 176 | elem = elem.nextSibling 177 | elem.style.position = "relative" 178 | elem.style.zIndex = "2" 179 | clone = elem.cloneNode(true) 180 | cstyle = window.getComputedStyle(elem) 181 | clone.classList.remove("loading") 182 | clone.style.position = "absolute" 183 | clone.style.zIndex = "1" 184 | clone.style.pointerEvents = "none" 185 | clone.style.animation = "none" 186 | 187 | # Check the position difference between original and cloned object 188 | elem.parentNode.insertBefore(clone, elem) 189 | cloneleft = clone.offsetLeft 190 | 191 | clone.parentNode.removeChild(clone) # Remove from dom to avoid animation 192 | clone.style.marginLeft = parseInt(cstyle.marginLeft) + elem.offsetLeft - cloneleft + "px" 193 | elem.parentNode.insertBefore(clone, elem) 194 | 195 | clone.style.animation = "#{animation} 0.8s ease-in-out forwards" 196 | setTimeout ( -> clone.remove() ), 1000 197 | 198 | flashIn: (elem) -> 199 | if elem.offsetWidth > 100 200 | @cloneAnimation(elem, "flash-in-big") 201 | else 202 | @cloneAnimation(elem, "flash-in") 203 | 204 | flashOut: (elem) -> 205 | if elem.offsetWidth > 100 206 | @cloneAnimation(elem, "flash-out-big") 207 | else 208 | @cloneAnimation(elem, "flash-out") 209 | 210 | 211 | window.Animation = new Animation() -------------------------------------------------------------------------------- /js/utils/Autosize.coffee: -------------------------------------------------------------------------------- 1 | class Autosize extends Class 2 | constructor: (@attrs={}) -> 3 | @node = null 4 | 5 | @attrs.classes ?= {} 6 | @attrs.classes.loading ?= false 7 | @attrs.oninput ?= @handleInput 8 | @attrs.onkeydown ?= @handleKeydown 9 | @attrs.afterCreate ?= @storeNode 10 | @attrs.rows ?= 1 11 | @attrs.disabled ?= false 12 | @attrs.value ?= null 13 | @attrs.title_submit ?= null 14 | 15 | @property 'loading', 16 | get: -> @attrs.classes.loading 17 | set: (loading) -> 18 | @attrs.classes.loading = loading 19 | @node.value = @attrs.value 20 | @autoHeight() 21 | Page.projector.scheduleRender() 22 | 23 | storeNode: (node) => 24 | @node = node 25 | if @attrs.focused 26 | node.setSelectionRange(0,0) 27 | node.focus() 28 | 29 | setTimeout => 30 | @autoHeight() 31 | 32 | setValue: (value=null) => 33 | @attrs.value = value 34 | if @node 35 | @node.value = value 36 | @autoHeight() 37 | Page.projector.scheduleRender() 38 | 39 | autoHeight: => 40 | height_before = @node.style.height 41 | if height_before 42 | @node.style.height = "0px" 43 | h = @node.offsetHeight 44 | scrollh = @node.scrollHeight 45 | @node.style.height = height_before 46 | if scrollh > h 47 | anime({targets: @node, height: scrollh, scrollTop: 0}) 48 | else 49 | @node.style.height = height_before 50 | 51 | handleInput: (e=null) => 52 | @attrs.value = e.target.value 53 | RateLimit 300, @autoHeight 54 | 55 | handleKeydown: (e=null) => 56 | if e.which == 13 and e.ctrlKey and @attrs.onsubmit and @attrs.value.trim() 57 | @submit() 58 | 59 | submit: => 60 | @attrs.onsubmit() 61 | setTimeout ( => 62 | @autoHeight() 63 | ), 100 64 | return false 65 | 66 | render: (body=null) => 67 | if body and @attrs.value == null 68 | @setValue(body) 69 | if @loading 70 | attrs = clone(@attrs) 71 | #attrs.value = "Submitting..." 72 | attrs.disabled = true 73 | tag_textarea = h("textarea.autosize", attrs) 74 | else 75 | tag_textarea = h("textarea.autosize", @attrs) 76 | 77 | return [ 78 | tag_textarea, 79 | if @attrs.title_submit 80 | h( 81 | "a.button.button.button-submit.button-small", 82 | {href: "#Submit", onclick: @submit, classes: @attrs.classes}, 83 | @attrs.title_submit 84 | ) 85 | ] 86 | 87 | window.Autosize = Autosize 88 | -------------------------------------------------------------------------------- /js/utils/Debug.coffee: -------------------------------------------------------------------------------- 1 | class Debug 2 | formatException: (err) -> 3 | if typeof err == 'object' 4 | if err.message 5 | console.log('Message: ' + err.message) 6 | if err.stack 7 | console.log('Stacktrace:') 8 | console.log('====================') 9 | console.log(err.stack) 10 | else 11 | console.log(err) 12 | 13 | window.Debug = new Debug() -------------------------------------------------------------------------------- /js/utils/Editable.coffee: -------------------------------------------------------------------------------- 1 | class Editable extends Class 2 | constructor: (@type, @handleSave, @handleDelete) -> 3 | @node = null 4 | @editing = false 5 | @render_function = null 6 | @empty_text = "Click here to edit this field" 7 | 8 | storeNode: (node) => 9 | @node = node 10 | 11 | handleEditClick: (e) => 12 | @editing = true 13 | @field_edit = new Autosize({focused: 1, style: "height: 0px"}) 14 | return false 15 | 16 | handleCancelClick: => 17 | @editing = false 18 | return false 19 | 20 | handleDeleteClick: => 21 | Page.cmd "wrapperConfirm", ["Are you sure?", "Delete"], => 22 | @field_edit.loading = true 23 | @handleDelete (res) => 24 | @field_edit.loading = false 25 | return false 26 | 27 | handleSaveClick: => 28 | @field_edit.loading = true 29 | @handleSave @field_edit.attrs.value, (res) => 30 | @field_edit.loading = false 31 | if res 32 | @editing = false 33 | return false 34 | 35 | render: (body) => 36 | if @editing 37 | return h("div.editable.editing", {exitAnimation: Animation.slideUp}, 38 | @field_edit.render(body), 39 | h("div.editablebuttons", 40 | h("a.link", {href: "#Cancel", onclick: @handleCancelClick, tabindex: "-1"}, "Cancel"), 41 | if @handleDelete 42 | h("a.button.button-submit.button-small.button-outline", {href: "#Delete", onclick: @handleDeleteClick, tabindex: "-1"}, "Delete") 43 | h("a.button.button-submit.button-small", {href: "#Save", onclick: @handleSaveClick}, "Save") 44 | ) 45 | ) 46 | else 47 | return h("div.editable", {enterAnimation: Animation.slideDown}, 48 | h("a.icon.icon-edit", {key: @node, href: "#Edit", onclick: @handleEditClick}), 49 | if not body 50 | h(@type, h("span.empty", {onclick: @handleEditClick}, @empty_text)) 51 | else if @render_function 52 | h(@type, {innerHTML: @render_function(body)}) 53 | else 54 | h(@type, body) 55 | ) 56 | 57 | window.Editable = Editable -------------------------------------------------------------------------------- /js/utils/ImagePreview.coffee: -------------------------------------------------------------------------------- 1 | class ImagePreview extends Class 2 | constructor: -> 3 | @width = 0 4 | @height = 0 5 | @preview_data = "" 6 | @pixel_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 7 | 8 | getSize: (target_width, target_height) -> 9 | return @calcSize(@width, @height, target_width, target_height) 10 | 11 | calcSize: (source_width, source_height, target_width, target_height) -> 12 | width = target_width 13 | height = width * (source_height / source_width); 14 | if height > target_height 15 | height = target_height 16 | width = height * (source_width / source_height) 17 | return [Math.round(width), Math.round(height)] 18 | 19 | setPreviewData: (@preview_data) => 20 | [@width, @height, colors, pixels] = @preview_data.split(",") 21 | 22 | getPreviewUri: (target_width=10, target_height=10) -> 23 | @logStart "Render" 24 | [@width, @height, colors, pixels] = @preview_data.split(",") 25 | [width, height] = @getSize(target_width, target_height) 26 | 27 | colors = colors.match(/.{3}/g) 28 | pixels = pixels.split("") 29 | 30 | canvas = document.createElement("canvas") 31 | canvas.width = width 32 | canvas.height = height 33 | ctx = canvas.getContext('2d') 34 | image_data = ctx.createImageData(width, height) 35 | 36 | color_codes = {} 37 | for color, i in colors 38 | color_codes[@pixel_chars[i]] = color 39 | 40 | di = 0 41 | for pixel in pixels 42 | hex = color_codes[pixel] 43 | r = parseInt(hex[0], 16) * 17 44 | g = parseInt(hex[1], 16) * 17 45 | b = parseInt(hex[2], 16) * 17 46 | image_data.data[di] = r 47 | image_data.data[di+1] = g 48 | image_data.data[di+2] = b 49 | image_data.data[di+3] = 255 50 | di += 4 51 | 52 | #ctx.putImageData(image_data, 1, 0) 53 | #ctx.putImageData(image_data, 0, 1) 54 | ctx.putImageData(image_data, 0, 0) 55 | 56 | # Add some blur for more smooth image 57 | canvas2 = document.createElement("canvas") 58 | canvas2.width = width*3 59 | canvas2.height = height*3 60 | ctx = canvas2.getContext('2d') 61 | ctx.filter = "blur(1px)" 62 | ctx.drawImage(canvas, -5, -5, canvas.width*3 + 10, canvas.height*3 + 10) 63 | ctx.drawImage(canvas, 0, 0, canvas.width*3, canvas.height*3) 64 | 65 | back = canvas2.toDataURL("image/png") 66 | @logEnd "Render" 67 | return back 68 | 69 | window.ImagePreview = ImagePreview -------------------------------------------------------------------------------- /js/utils/ItemList.coffee: -------------------------------------------------------------------------------- 1 | class ItemList 2 | constructor: (@item_class, @key) -> 3 | @items = [] 4 | @items_bykey = {} 5 | 6 | sync: (rows, item_class, key) -> 7 | @items.splice(0, @items.length) # Empty items 8 | for row in rows 9 | current_obj = @items_bykey[row[@key]] 10 | if current_obj 11 | current_obj.row = row 12 | @items.push current_obj 13 | else 14 | item = new @item_class(row, @) 15 | @items_bykey[row[@key]] = item 16 | @items.push item 17 | 18 | deleteItem: (item) -> 19 | index = @items.indexOf(item) 20 | if index > -1 21 | @items.splice(index, 1) 22 | else 23 | console.log "Can't delete item", item 24 | delete @items_bykey[item.row[@key]] 25 | 26 | window.ItemList = ItemList -------------------------------------------------------------------------------- /js/utils/Maxheight.coffee: -------------------------------------------------------------------------------- 1 | class Maxheight 2 | apply: (elem) -> 3 | if elem.classList.contains("maxheight") and elem.scrollHeight > 500 4 | elem.classList.add("maxheight-limited") 5 | elem.onclick = (e) -> 6 | if e.target == elem 7 | elem.style.maxHeight = elem.scrollHeight+"px" 8 | elem.classList.remove("maxheight-limited") 9 | setTimeout ( -> 10 | elem.classList.remove("maxheight") 11 | elem.style.maxHeight = null 12 | ), 1000 13 | else 14 | elem.classList.remove("maxheight-limited") 15 | 16 | window.Maxheight = new Maxheight() -------------------------------------------------------------------------------- /js/utils/Menu.coffee: -------------------------------------------------------------------------------- 1 | class Menu 2 | constructor: -> 3 | @visible = false 4 | @items = [] 5 | @node = null 6 | 7 | show: => 8 | window.visible_menu?.hide() 9 | @visible = true 10 | window.visible_menu = @ 11 | 12 | hide: => 13 | @visible = false 14 | 15 | toggle: => 16 | if @visible 17 | @hide() 18 | else 19 | @show() 20 | Page.projector.scheduleRender() 21 | 22 | 23 | addItem: (title, cb, selected=false) -> 24 | @items.push([title, cb, selected]) 25 | 26 | 27 | storeNode: (node) => 28 | @node = node 29 | # Animate visible 30 | if @visible 31 | node.className = node.className.replace("visible", "") 32 | setTimeout (-> 33 | node.className += " visible" 34 | ), 10 35 | 36 | handleClick: (e) => 37 | keep_menu = false 38 | for item in @items 39 | [title, cb, selected] = item 40 | if title == e.target.textContent 41 | keep_menu = cb(item) 42 | if keep_menu != true 43 | @hide() 44 | return false 45 | 46 | renderItem: (item) => 47 | [title, cb, selected] = item 48 | if typeof(selected) == "function" 49 | selected = selected() 50 | if title == "---" 51 | h("div.menu-item-separator") 52 | else 53 | if typeof(cb) == "string" # Url 54 | href = cb 55 | onclick = true 56 | else # Callback 57 | href = "#"+title 58 | onclick = @handleClick 59 | h("a.menu-item", {href: href, onclick: onclick, key: title, classes: {"selected": selected}}, [title]) 60 | 61 | render: (class_name="") => 62 | if @visible or @node 63 | h("div.menu#{class_name}", {classes: {"visible": @visible}, afterCreate: @storeNode}, @items.map(@renderItem)) 64 | 65 | window.Menu = Menu 66 | 67 | # Hide menu on outside click 68 | document.body.addEventListener "mouseup", (e) -> 69 | if not window.visible_menu or not window.visible_menu.node 70 | return false 71 | if e.target != window.visible_menu.node.parentNode and e.target.parentNode != window.visible_menu.node and e.target.parentNode != window.visible_menu.node.parentNode and e.target.parentNode != window.visible_menu.node and e.target.parentNode.parentNode != window.visible_menu.node.parentNode 72 | window.visible_menu.hide() 73 | Page.projector.scheduleRender() -------------------------------------------------------------------------------- /js/utils/Overlay.coffee: -------------------------------------------------------------------------------- 1 | class Overlay extends Class 2 | constructor: -> 3 | @visible = false 4 | @called = false 5 | @height = 0 6 | @image_top = 0 7 | @image_left = 0 8 | @image_width = 0 9 | @image_height = 0 10 | @background_image = "" 11 | @image_transform = "" 12 | @style = "" 13 | @pos = null 14 | @tag = null 15 | 16 | zoomImageTag: (tag, target_width, target_height) => 17 | @log "Show", target_width, target_height 18 | @background_image = tag.style.backgroundImage 19 | @height = document.body.scrollHeight 20 | 21 | pos = tag.getBoundingClientRect() 22 | @original_pos = pos 23 | @image_top = parseInt(pos.top + window.scrollY) + "px" 24 | @image_left = parseInt(pos.left)+"px" 25 | @image_width = target_width 26 | @image_height = target_height 27 | ratio = pos.width/target_width 28 | @image_transform = "scale(#{ratio}) " 29 | @image_margin_left = parseInt((pos.width - target_width) / 2) 30 | @image_margin_top = parseInt((pos.height - target_height) / 2) 31 | @style = "" 32 | #@image_transform += "translateX(#{((pos.width / ratio) - (target_width / ratio)) / 2}px) " 33 | #@image_transform += "translateY(#{((pos.height / ratio) - (target_height / ratio)) / 2}px) " 34 | 35 | 36 | @called = true 37 | 38 | @tag = tag 39 | 40 | @visible = true 41 | 42 | window.requestAnimationFrame ( => 43 | #@image_width = target_width 44 | #@image_height = target_height 45 | ratio = 1 46 | @image_transform = "scale(#{ratio}) " 47 | #@image_transform += "translateX(#{((pos.width / ratio) - (target_width / ratio)) / 2}px) " 48 | #@image_transform += "translateY(#{((pos.height / ratio) - (target_height / ratio)) / 2}px) " 49 | #@image_top = (pos.top + pos.height / 2 - target_height / 2) + "px" 50 | #@image_margin_left = 0-target_width/2 51 | Page.projector.scheduleRender() 52 | ) 53 | 54 | handleClick: => 55 | @log "Hide" 56 | #@image_width = @original_pos.width 57 | #@image_height = @original_pos.height+1 58 | #@image_left = @original_pos.left+"px" 59 | #@image_margin_left = 0 60 | ratio = @original_pos.width/@image_width 61 | @image_transform = "scale(#{ratio}) " 62 | #@image_transform += "translateX(#{((@original_pos.width / ratio) - (@image_width / ratio)) / 2}px) " 63 | #@image_transform += "translateY(#{((@original_pos.height / ratio) - (@image_height / ratio)) / 2}px) " 64 | @image_margin_left = Math.floor((@original_pos.width - @image_width) / 2) 65 | @image_margin_top = Math.floor((@original_pos.height - @image_height) / 2) 66 | @log @image_margin_top, @image_margin_left, @image_width, @image_height 67 | @visible = false 68 | setTimeout ( => 69 | @log "opacity", @visible 70 | if not @visible 71 | @style = "opacity: 0" 72 | Page.projector.scheduleRender() 73 | ), 400 74 | setTimeout ( => 75 | if not @visible 76 | @called = false 77 | Page.projector.scheduleRender() 78 | ), 900 79 | return false 80 | 81 | render: => 82 | if not @called 83 | return h("div#Overlay", {classes: {visible: @visible}, onclick: @handleClick}) 84 | 85 | h("div#Overlay", {classes: {visible: @visible}, onclick: @handleClick, style: "height: #{@height}px"}, [ 86 | h("div.img", {style: """ 87 | transform: #{@image_transform}; margin-left: #{@image_margin_left}px; margin-top: #{@image_margin_top}px; 88 | top: #{@image_top}; left: #{@image_left}; 89 | width: #{@image_width}px; height: #{@image_height}px; 90 | background-image: #{@background_image}; 91 | #{@style}""" 92 | }) 93 | ]) 94 | 95 | window.Overlay = Overlay -------------------------------------------------------------------------------- /js/utils/Scrollwatcher.coffee: -------------------------------------------------------------------------------- 1 | class Scrollwatcher extends Class 2 | constructor: -> 3 | @log "Scrollwatcher" 4 | @items = [] 5 | window.onscroll = => 6 | RateLimit 200, @checkScroll 7 | @ 8 | 9 | checkScroll: => 10 | if not @items.length 11 | return 12 | view_top = window.scrollY 13 | view_bottom = window.scrollY + window.innerHeight 14 | for [item_top, tag, cb], i in @items by -1 15 | if item_top + 900 > view_top and item_top - 400 < view_bottom 16 | @items.splice(i, 1) 17 | cb(tag) 18 | 19 | add: (tag, cb) -> 20 | @items.push([tag.getBoundingClientRect().top + window.scrollY, tag, cb]) 21 | RateLimit 200, @checkScroll 22 | 23 | window.Scrollwatcher = Scrollwatcher -------------------------------------------------------------------------------- /js/utils/Text.coffee: -------------------------------------------------------------------------------- 1 | class MarkedRenderer extends marked.Renderer 2 | image: (href, title, text) -> 3 | return "![#{text}](#{href})" 4 | 5 | class Text 6 | toColor: (text, saturation=30, lightness=50) -> 7 | hash = 0 8 | for i in [0..text.length-1] 9 | hash += text.charCodeAt(i)*i 10 | hash = hash % 1777 11 | if Page.server_info?.user_settings?.theme == "dark" 12 | return "hsl(" + (hash % 360) + ",#{saturation + 5}%,#{lightness + 15}%)"; 13 | else 14 | return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)"; 15 | 16 | renderMarked: (text, options={}) => 17 | if not text 18 | return "" 19 | options["gfm"] = true 20 | options["breaks"] = true 21 | options["sanitize"] = true 22 | options["renderer"] = marked_renderer 23 | text = @fixReply(text) 24 | text = text.replace(/((?<=\s|^)http[s]?:\/\/.*?)(?=\s|$)/g, '<$1>') # Auto linkify IPv6 urls by adding <> around urls 25 | text = marked(text, options) 26 | text = text.replace(/(.*?)<\/a>/g, '$1') # Disable email auto-convert 27 | text = text.replace(/(\s|>|^)(@[^\s]{1,50}):/g, '$1$2:') # Highlight usernames 28 | text = text.replace(/(https?:\/\/)%5B(.*?)%5D/g, '$1[$2]') # Fix IPv6 links 29 | return @fixHtmlLinks text 30 | 31 | renderLinks: (text) => 32 | text = text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') # Sanitize html tags 33 | text = text.replace /(https?:\/\/[^\s)]+)/g, (match) -> 34 | return "#{match}" # UnSanitize & -> & in links 35 | text = text.replace(/\n/g, '
') 36 | text = text.replace(/(\s|>|^)(@[^\s]{1,50}):/g, '$1$2:') 37 | text = @fixHtmlLinks(text) 38 | 39 | return text 40 | 41 | emailLinks: (text) -> 42 | return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "$1@zeroid.bit") 43 | 44 | # Convert zeronet html links to relaitve 45 | fixHtmlLinks: (text) -> 46 | # Fix site links 47 | text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110\/(Me.ZeroNetwork.bit|1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH)\/\?/gi, 'href="?') 48 | if window.is_proxy 49 | text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/gi, 'href="http://zero') 50 | text = text.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") 51 | text = text.replace(/href="\/([A-Za-z0-9]{26,35})/g, 'href="http://zero/$1') # Links without 127.0.0.1 52 | else 53 | text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="') 54 | # Add no-refresh linking to local links 55 | text = text.replace(/href="\?/g, 'onclick="return Page.handleLinkClick(window.event)" href="?') 56 | return text 57 | 58 | 59 | # Convert a single link to relative 60 | fixLink: (link) -> 61 | if window.is_proxy 62 | back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero') 63 | back = back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") # Domain links 64 | back = back.replace(/\/([A-Za-z0-9]{26,35})/, "http://zero/$1") # Links without 127.0.0.1 65 | return back 66 | else 67 | return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '') 68 | 69 | toUrl: (text) -> 70 | return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "") 71 | 72 | getSiteUrl: (address) -> 73 | if window.is_proxy 74 | if "." in address # Domain 75 | return "http://"+address+"/" 76 | else 77 | return "http://zero/"+address+"/" 78 | else 79 | return "/"+address+"/" 80 | 81 | 82 | fixReply: (text) -> 83 | return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2") 84 | 85 | toBitcoinAddress: (text) -> 86 | return text.replace(/[^A-Za-z0-9]/g, "") 87 | 88 | 89 | jsonEncode: (obj) -> 90 | return unescape(encodeURIComponent(JSON.stringify(obj))) 91 | 92 | jsonDecode: (obj) -> 93 | return JSON.parse(decodeURIComponent(escape(obj))) 94 | 95 | fileEncode: (obj) -> 96 | if typeof(obj) == "string" 97 | return btoa(unescape(encodeURIComponent(obj))) 98 | else 99 | return btoa(unescape(encodeURIComponent(JSON.stringify(obj, undefined, '\t')))) 100 | 101 | utf8Encode: (s) -> 102 | return unescape(encodeURIComponent(s)) 103 | 104 | utf8Decode: (s) -> 105 | return decodeURIComponent(escape(s)) 106 | 107 | 108 | distance: (s1, s2) -> 109 | s1 = s1.toLocaleLowerCase() 110 | s2 = s2.toLocaleLowerCase() 111 | next_find_i = 0 112 | next_find = s2[0] 113 | match = true 114 | extra_parts = {} 115 | for char in s1 116 | if char != next_find 117 | if extra_parts[next_find_i] 118 | extra_parts[next_find_i] += char 119 | else 120 | extra_parts[next_find_i] = char 121 | else 122 | next_find_i++ 123 | next_find = s2[next_find_i] 124 | 125 | if extra_parts[next_find_i] 126 | extra_parts[next_find_i] = "" # Extra chars on the end doesnt matter 127 | extra_parts = (val for key, val of extra_parts) 128 | if next_find_i >= s2.length 129 | return extra_parts.length + extra_parts.join("").length 130 | else 131 | return false 132 | 133 | 134 | queryParse: (query) -> 135 | params = {} 136 | parts = query.split('&') 137 | for part in parts 138 | [key, val] = part.split("=") 139 | if val 140 | params[decodeURIComponent(key)] = decodeURIComponent(val) 141 | else 142 | params["url"] = decodeURIComponent(key) 143 | params["urls"] = params["url"].split("/") 144 | return params 145 | 146 | queryEncode: (params) -> 147 | back = [] 148 | if params.url 149 | back.push(params.url) 150 | for key, val of params 151 | if not val or key == "url" 152 | continue 153 | back.push("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}") 154 | return back.join("&") 155 | 156 | highlight: (text, search) -> 157 | parts = text.split(RegExp(search, "i")) 158 | back = [] 159 | for part, i in parts 160 | back.push(part) 161 | if i < parts.length-1 162 | back.push(h("span.highlight", {key: i}, search)) 163 | return back 164 | 165 | sqlIn: (values) -> 166 | return "("+("'#{value}'" for value in values).join(',')+")" 167 | 168 | formatSize: (size) -> 169 | size_mb = size/1024/1024 170 | if size_mb >= 1000 171 | return (size_mb/1024).toFixed(1)+" GB" 172 | else if size_mb >= 100 173 | return size_mb.toFixed(0)+" MB" 174 | else if size/1024 >= 1000 175 | return size_mb.toFixed(2)+" MB" 176 | else 177 | return (size/1024).toFixed(2)+" KB" 178 | 179 | 180 | window.is_proxy = (document.location.host == "zero" or window.location.pathname == "/") 181 | window.marked_renderer = new MarkedRenderer() 182 | window.Text = new Text() 183 | -------------------------------------------------------------------------------- /js/utils/Time.coffee: -------------------------------------------------------------------------------- 1 | class Time 2 | since: (timestamp) -> 3 | now = +(new Date)/1000 4 | if timestamp > 1000000000000 # In ms 5 | timestamp = timestamp/1000 6 | secs = now - timestamp 7 | if secs < 60 8 | back = "Just now" 9 | else if secs < 60*60 10 | back = "#{Math.round(secs/60)} minutes ago" 11 | else if secs < 60*60*24 12 | back = "#{Math.round(secs/60/60)} hours ago" 13 | else if secs < 60*60*24*3 14 | back = "#{Math.round(secs/60/60/24)} days ago" 15 | else 16 | back = "on "+@date(timestamp) 17 | back = back.replace(/^1 ([a-z]+)s/, "1 $1") # 1 days ago fix 18 | return back 19 | 20 | 21 | date: (timestamp, format="short") -> 22 | if timestamp > 1000000000000 # In ms 23 | timestamp = timestamp/1000 24 | parts = (new Date(timestamp*1000)).toString().split(" ") 25 | if format == "short" 26 | display = parts.slice(1, 4) 27 | else 28 | display = parts.slice(1, 5) 29 | return display.join(" ").replace(/( [0-9]{4})/, ",$1") 30 | 31 | 32 | timestamp: (date="") -> 33 | if date == "now" or date == "" 34 | return parseInt(+(new Date)/1000) 35 | else 36 | return parseInt(Date.parse(date)/1000) 37 | 38 | 39 | window.Time = new Time -------------------------------------------------------------------------------- /js/utils/Translate.coffee: -------------------------------------------------------------------------------- 1 | window._ = (s) -> return s -------------------------------------------------------------------------------- /js/utils/Uploadable.coffee: -------------------------------------------------------------------------------- 1 | class Uploadable extends Class 2 | constructor: (@handleSave) -> 3 | @node = null 4 | @resize_width = 50 5 | @resize_height = 50 6 | @preverse_ratio = true # Keep image originial ratio 7 | @try_png = false # Try to convert to png 8 | @png_limit = 2200 # Use png instead of jpg is smaller than this size 9 | @image_preview = new ImagePreview() 10 | @pixel_chars = @image_preview.pixel_chars 11 | @ 12 | 13 | storeNode: (node) => 14 | @node = node 15 | 16 | scaleHalf: (image) -> 17 | canvas = document.createElement("canvas") 18 | canvas.width = image.width / 2 19 | canvas.height = image.height / 2 20 | ctx = canvas.getContext("2d") 21 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height) 22 | return canvas 23 | 24 | resizeImage: (file, width, height, cb) => 25 | image = new Image() 26 | image.onload = => 27 | @log "Resize image loaded" 28 | canvas = document.createElement("canvas") 29 | if @preverse_ratio 30 | [canvas.width, canvas.height] = @image_preview.calcSize(image.width, image.height, width, height) 31 | else 32 | canvas.width = width 33 | canvas.height = height 34 | 35 | ctx = canvas.getContext("2d") 36 | ctx.fillStyle = "#FFF" 37 | ctx.fillRect(0, 0, canvas.width, canvas.height) 38 | while image.width > width * 2 39 | image = @scaleHalf(image) 40 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height) 41 | 42 | # Try to optimize to png 43 | if @try_png 44 | quant = new RgbQuant({colors: 128, method: 1}) 45 | quant.sample(canvas) 46 | quant.palette(true) 47 | canvas_quant = drawPixels(quant.reduce(canvas), width) 48 | optimizer = new CanvasTool.PngEncoder(canvas_quant, { bitDepth: 8, colourType: CanvasTool.PngEncoder.ColourType.TRUECOLOR }) 49 | image_base64uri = "data:image/png;base64," + btoa(optimizer.convert()) 50 | if image_base64uri.length > @png_limit 51 | # Too large, convert to jpg 52 | @log "PNG too large (#{image_base64uri.length} bytes), convert to jpg instead" 53 | image_base64uri = canvas.toDataURL("image/jpeg", 0.8) 54 | else 55 | image_base64uri = canvas.toDataURL("image/jpeg", 0.8) 56 | 57 | @log "Size: #{image_base64uri.length} bytes" 58 | cb image_base64uri, canvas.width, canvas.height 59 | image.onerror = (e) => 60 | @log "Image upload error", e 61 | Page.cmd "wrapperNotification", ["error", "Invalid image, only jpg format supported"] 62 | cb null 63 | if file.name 64 | image.src = URL.createObjectURL(file) 65 | else 66 | image.src = file 67 | 68 | 69 | handleUploadClick: (e) => 70 | @log "handleUploadClick", e 71 | script = document.createElement("script") 72 | script.src = "js-external/pngencoder.js" 73 | document.head.appendChild(script) 74 | input = document.createElement('input') 75 | document.body.appendChild(input) 76 | input.type = "file" 77 | input.style.visibility = "hidden" 78 | input.onchange = (e) => 79 | @log "Uploaded" 80 | @resizeImage input.files[0], @resize_width, @resize_height, (image_base64uri, width, height) => 81 | @log "Resized", width, height 82 | if image_base64uri 83 | @handleSave(image_base64uri, width, height) 84 | input.remove() 85 | input.click() 86 | return false 87 | 88 | render: (body) => 89 | h("div.uploadable", 90 | h("a.icon.icon-upload", {href: "#Upload", onclick: @handleUploadClick}) 91 | body() 92 | ) 93 | 94 | getPixelData: (data) => 95 | color_db = {} 96 | colors = [] 97 | colors_next_id = 0 98 | pixels = [] 99 | for i in [0..data.length-1] by 4 100 | 101 | r = data[i] 102 | g = data[i+1] 103 | b = data[i+2] 104 | #r = r - (r % 64) 105 | #g = g - (g % 64) 106 | #b = b - (b % 64) 107 | r = Math.round(r/17) 108 | g = Math.round(g/17) 109 | b = Math.round(b/17) 110 | hex = Number(0x1000 + r*0x100 + g*0x10 + b).toString(16).substring(1) 111 | if i == 0 112 | @log r, g, b, data[i+3], hex 113 | if !color_db[hex] 114 | color_db[hex] = @pixel_chars[colors_next_id] 115 | colors.push(hex) 116 | colors_next_id += 1 117 | pixels.push(color_db[hex]) 118 | return [colors, pixels] 119 | 120 | getPreviewData: (image_base64uri, target_width, target_height, cb) -> 121 | image = new Image() 122 | image.src = image_base64uri 123 | image.onload = => 124 | image_width = image.width 125 | image_height = image.height 126 | canvas = document.createElement("canvas") 127 | [canvas.width, canvas.height] = @image_preview.calcSize(image.width, image.height, target_width, target_height) 128 | 129 | ctx = canvas.getContext("2d") 130 | ctx.fillStyle = "#FFF" 131 | ctx.fillRect(0, 0, canvas.width, canvas.height) 132 | while image.width > target_width * 2 133 | image = @scaleHalf(image) 134 | 135 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height) 136 | 137 | quant = new RgbQuant({colors: 16, method: 1}) 138 | quant.sample(canvas) 139 | quant.palette(true) 140 | canvas = drawPixels(quant.reduce(canvas), canvas.width) 141 | ctx = canvas.getContext("2d") 142 | 143 | image_data = ctx.getImageData(0, 0, canvas.width, canvas.height) 144 | pixeldata = @getPixelData(image_data.data) 145 | 146 | back = [image_width, image_height, pixeldata[0].join(""), pixeldata[1].join("")].join(",") 147 | @log "Previewdata size:", back.length 148 | cb back 149 | 150 | 151 | window.Uploadable = Uploadable -------------------------------------------------------------------------------- /js/utils/ZeroFrame.coffee: -------------------------------------------------------------------------------- 1 | class ZeroFrame extends Class 2 | constructor: (url) -> 3 | @url = url 4 | @waiting_cb = {} 5 | @history_state = {} 6 | @wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") 7 | @connect() 8 | @next_message_id = 1 9 | @init() 10 | 11 | 12 | init: -> 13 | @ 14 | 15 | 16 | connect: -> 17 | @target = window.parent 18 | window.addEventListener("message", @onMessage, false) 19 | @cmd("innerReady") 20 | 21 | # Save scrollTop 22 | window.addEventListener "beforeunload", (e) => 23 | @log "Save scrollTop", window.pageYOffset 24 | @history_state["scrollTop"] = window.pageYOffset 25 | @cmd "wrapperReplaceState", [@history_state, null] 26 | 27 | # Restore scrollTop 28 | @cmd "wrapperGetState", [], (state) => 29 | @handleState(state) 30 | 31 | handleState: (state) -> 32 | @history_state = state if state? 33 | @log "Restore scrollTop", state, window.pageYOffset 34 | if window.pageYOffset == 0 and state 35 | window.scroll(window.pageXOffset, state.scrollTop) 36 | 37 | 38 | onMessage: (e) => 39 | message = e.data 40 | cmd = message.cmd 41 | if cmd == "response" 42 | if @waiting_cb[message.to]? 43 | @waiting_cb[message.to](message.result) 44 | else 45 | @log "Websocket callback not found:", message 46 | else if cmd == "wrapperReady" # Wrapper inited later 47 | @cmd("innerReady") 48 | else if cmd == "ping" 49 | @response message.id, "pong" 50 | else if cmd == "wrapperOpenedWebsocket" 51 | @onOpenWebsocket() 52 | else if cmd == "wrapperClosedWebsocket" 53 | @onCloseWebsocket() 54 | else if cmd == "wrapperPopState" 55 | @handleState(message.params.state) 56 | @onRequest cmd, message.params 57 | else 58 | @onRequest cmd, message.params 59 | 60 | 61 | onRequest: (cmd, message) => 62 | @log "Unknown request", message 63 | 64 | 65 | response: (to, result) -> 66 | @send {"cmd": "response", "to": to, "result": result} 67 | 68 | 69 | cmd: (cmd, params={}, cb=null) -> 70 | @send {"cmd": cmd, "params": params}, cb 71 | 72 | cmdp: (cmd, params={}) -> 73 | p = new Promise() 74 | @send {"cmd": cmd, "params": params}, (res) -> 75 | p.resolve(res) 76 | return p 77 | 78 | send: (message, cb=null) -> 79 | message.wrapper_nonce = @wrapper_nonce 80 | message.id = @next_message_id 81 | @next_message_id += 1 82 | @target.postMessage(message, "*") 83 | if cb 84 | @waiting_cb[message.id] = cb 85 | 86 | 87 | onOpenWebsocket: => 88 | @log "Websocket open" 89 | 90 | 91 | onCloseWebsocket: => 92 | @log "Websocket close" 93 | 94 | 95 | 96 | window.ZeroFrame = ZeroFrame -------------------------------------------------------------------------------- /languages/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Gæst", 3 | "Select your account": "Vælg din konto", 4 | "You need a profile for this feature": "Du skal oprette en profil først", 5 | 6 | "Follow username mentions": "Følg opslag hvor du nævnes", 7 | "Follow comments on your posts": "Følg kommentarer på dine opslag", 8 | "Follow new followers": "Følg nye brugere der følger dig", 9 | 10 | "Follow": "Følg", 11 | "Unfollow": "Følg ikke", 12 | "Help distribute this user's images": "Hjælp med at dele denne brugers billeder", 13 | "Following": "Følger", 14 | "Activity feed": "Aktiviteter", 15 | "Show more...": "Vis mere...", 16 | 17 | " started following ": " begyndte at følge: ", 18 | " commented on ": " kommenterede ", 19 | " liked ": " synes om ", 20 | "'s ": "' ", 21 | "_(post, like post)": "opslag", 22 | "_(post, comment post)": "kommentar", 23 | 24 | "Cancel": "Fortryd", 25 | "Save": "Gem", 26 | "Delete": "Slet", 27 | 28 | "User's profile site not loaded to your client yet.": "Brugerens profil side er endnu ikke fuldt downloadet endnu.", 29 | "Download user's site": "Download brugerens side", 30 | 31 | "Browse all \\u203A": "Vis alle", 32 | "New users": "Nye brugere", 33 | "Suggested users": "Foreslag", 34 | 35 | "Create new profile": "Opret ny profil", 36 | "Creating new profile...": "Opretter ny profil...", 37 | "Checking user on selected hub...": "Tjekker bruger på valgt profil side...", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "Bruger \" + Page.site_info.cert_user_id + \" findes allerede på denne profil side", 39 | "Select ID...": "Vælg bruger ID ...", 40 | "Seeded HUBs": "Delte profil sider", 41 | "Available HUBs": "Mulige profil sider", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 43 | "(Du vælger hvor din profil skal gemmes. Der er ingen forskel på indhold og du kan stadigvæk nå alle andre brugere)", 44 | 45 | "Random ZeroNet user": "Anonym ZeroNet bruger", 46 | "Hello ZeroMe!": "Hello ZeroMe!", 47 | 48 | "Loading...\\nShow image": "Indlæser...\\nVis billede", 49 | "Help distribute this user's new images": "Hjælp med at dele denne brugers nye billeder", 50 | "Delete image": "Slet billede", 51 | "Show image": "Vis billede", 52 | "'s new images": "' nye billeder", 53 | 54 | "Comment": "Kommentar", 55 | "Add your comment": "Tilføj din kommentar", 56 | "Reply": "Svar", 57 | 58 | "Everyone": "Alle", 59 | "Followed users": "Følger brugere", 60 | "Show more posts...": "Vis flere opslag...", 61 | "Show more comments...": "Vis flere kommentarer...", 62 | "No posts yet": "Ikke flere opslag", 63 | "Let's follow some users!": "Lad os følge nogle brugere!", 64 | 65 | "Select user to post new content": "Vælg bruger for at kunne opdaterer ZeroMe", 66 | "Write something...": "Skriv noget...", 67 | "Submit new post": "Gem nyt opslag", 68 | "Invalid image, only jpg format supported": "Desværre, kun jpg billeder er mulige her", 69 | 70 | "Follow in newsfeed": "Vælg i nyhedsstrøm", 71 | 72 | "New users in ZeroMe": "Nye brugere i ZeroMe", 73 | "Total: ": "I alt: ", 74 | " registered users": " brugere", 75 | 76 | " minutes ago": " minut(ter) siden", 77 | " hours ago": " time(r) siden", 78 | " days ago": " dag(e) siden", 79 | "on ": "", 80 | "Just now": "Netop nu" 81 | } -------------------------------------------------------------------------------- /languages/fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "بازدید‌کننده", 3 | "Select your account": "حساب خود را انتخاب کنید", 4 | "You need a profile for this feature": "برای این ویژگی شما به یک نمایه نیاز دارید", 5 | 6 | "Follow username mentions": "یاد‌کرد‌های نام کاربری را پیگیری کنید", 7 | "Follow comments on your posts": "نظرات روی پست خودتان را پیگیری کنید", 8 | "Follow new followers": "دنبال‌کنندگان جدید را پیگیری کنید", 9 | 10 | "Follow": "پیگیری کنید", 11 | "Unfollow": "لغو پیگیری", 12 | "Help distribute this user's images": "به انتشار تصاویر این کاربر کمک کنید", 13 | "Following": "دنبال می‌کنید", 14 | "Activity feed": "خوراک فعالیت", 15 | "Show more...": "نمایش بیشتر...", 16 | 17 | " started following ": "شروع به پیگیری کرد", 18 | " commented on ": "نظر داد بر روی", 19 | " liked ": "پسندید", 20 | "'s ": "مربوط به", 21 | "_(post, like post)": "(پست، پست پسندیدن)_", 22 | "_(post, comment post)": "(پست، پست نظر‌دهی)_", 23 | 24 | "Cancel": "لغو", 25 | "Save": "ذخیره", 26 | "Delete": "حذف", 27 | 28 | "User's profile site not loaded to your client yet.": "سایت نمایه کاربر هنوز در دستگاه شما بارگذاری نشده است", 29 | "Download user's site": "بارگذاری سایت کاربر", 30 | 31 | "Browse all \u203A": "مرور همگی \u203A", 32 | "New users": "کاربران جدید", 33 | "Suggested users": "کاربران پیشنهادی", 34 | 35 | "Create new profile": "نمایه جدید بساز", 36 | "Creating new profile...": "ساخت نمایه جدید...", 37 | "Checking user on selected hub...": "بررسی کاربر در مرکز انتخابی... ", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "کاربر\" + Page.site_info.cert_user_id + \"از قبل در این مرکز وجود داشت", 39 | "Select ID...": "انتخاب شناسه...", 40 | "Seeded HUBs": "مراکز دارای عضو فعال ", 41 | "Available HUBs": "مراکز در دسترس", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": "(با استفاده از این شما می‌توانید انتخاب کنید که نمایه شما در کجا ذخیره می‌شود. تفاوتی در محتوا نیست وشما قادر خواهید بود به همگی کاربران در هر مرکزی دسترسی داشته باشید)", 43 | 44 | "Random ZeroNet user": "کاربر تصادفی ZeroNet", 45 | "Hello ZeroMe!": "سلام ZeroMe!", 46 | 47 | "Loading...\nShow image": "بار‌گذاری...\nنمایش تصویر", 48 | "Help distribute this user's new images": " به انتشار تصاویر این کاربر کمک کنید ", 49 | "Delete image": "حذف تصویر", 50 | "Show image": "نمایش تصویر", 51 | "'s new images": "تصاویر جدید مربوط به", 52 | 53 | "Comment": "نظر", 54 | "Add your comment": "نظرت را اضافه کن", 55 | "Reply": "پاسخ", 56 | 57 | "Everyone": "همه افراد", 58 | "Followed users": "کاربران دنبال‌شده", 59 | "Show more posts...": "نمایش پست‌های بیشتر...", 60 | "Show more comments...": "نمایش نظر‌های بیشتر...", 61 | "No posts yet": "هنوز پستی نیست", 62 | "Let's follow some users!": "بیاید تعدادی از کاربران را دنبال کنید!", 63 | 64 | "Select user to post new content": "برای پست محتوی جدید، کاربری خود را انتخاب کنید", 65 | "Write something...": "چیزی بنویسید...", 66 | "Submit new post": "پست جدیدی را ارسال کنید", 67 | "Invalid image, only jpg format supported": "تصویر نامعتبر، تنها تصاویر با فرمت jpg پشتیبانی می‌شوند", 68 | 69 | "Follow in newsfeed": "پیگیری در خوراک خبری", 70 | 71 | "New users in ZeroMe": "کاربران جدید در ZeroMe", 72 | "Total: ": "در مجموع", 73 | " registered users": "کاربران ثبت‌شده", 74 | 75 | " minutes ago": "دقایقی پیش", 76 | " hours ago": "ساعاتی پیش", 77 | " days ago": "چندین روز پیش", 78 | "on ": "در ", 79 | "Just now": "همین الان" 80 | } 81 | -------------------------------------------------------------------------------- /languages/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Visiteur", 3 | "Select your account": "Sélectionnez votre compte", 4 | "You need a profile for this feature": "Vous devez avoir un profil pour accéder à cette fonctionnalité", 5 | 6 | "Follow username mentions": "Suivre les mentions d'utilisateur", 7 | "Follow comments on your posts": "Suivre les commentaires sur vos publications", 8 | "Follow new followers": "Suivre les personnes qui vous suivent", 9 | 10 | "Follow": "Suivre", 11 | "Unfollow": "Ne plus suivre", 12 | "Help distribute this user's images": "Distribuer les images de cet utilisateur", 13 | "Following": "Vous suivez", 14 | "Activity feed": "Fil d'activité", 15 | "Show more...": "Afficher plus...", 16 | 17 | " started following ": " a commencé à suivre ", 18 | " commented on ": " a commenté ce que ", 19 | " liked ": " aime ce que ", 20 | "'s ": " a ", 21 | "_(post, like post)": "publié", 22 | "_(post, comment post)": "publié", 23 | 24 | "Cancel": "Annuler", 25 | "Save": "Enregistrer", 26 | "Delete": "Supprimer", 27 | 28 | "User's profile site not loaded to your client yet.": "Le profil de l'utilisateur n'a pas encore été téléchargé.", 29 | "Download user's site": "Télécharger le site de l'utilisateur", 30 | 31 | "Browse all \\u203A": "Voir tous \\u203A", 32 | "New users": "Nouveaux utilisateurs", 33 | "Suggested users": "Utilisateurs suggérés", 34 | 35 | "Create new profile": "Créer un nouveau profil", 36 | "Creating new profile...": "Création du nouveau profil...", 37 | "Checking user on selected hub...": "Vérification de l'utilisateur sur le hub correspondant...", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "L'utilisateur ' \" + Page.site_info.cert_user_id + \" existe déjà sur ce hub", 39 | "Select ID...": "Sélectionnez l'utilisateur...", 40 | "Seeded HUBs": "Hubs partagés", 41 | "Available HUBs": "Hubs disponibles", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 43 | "(Vous choisissez ici sur quel hub votre profil est enregistré. Il n'y a aucune différence de contenu et vous aurez accès à tous les utilisateurs de ZeroMe)", 44 | 45 | "Random ZeroNet user": "Utilisateur ZeroNet aléatoire", 46 | "Hello ZeroMe!": "Bonjour ZeroMe!", 47 | 48 | "Loading...\\nShow image": "Chargement...\\nAfficher l'image", 49 | "Help distribute this user's new images": "Distribuer les nouvelles images de l'utilisateur", 50 | "Delete image": "Supprimer l'image", 51 | "Show image": "Afficher l'image", 52 | "'s new images": " nouvelles images", 53 | 54 | "Comment": "Commenter", 55 | "Add your comment": "Ajouter un commentaire", 56 | "Reply": "Répondre", 57 | 58 | "Everyone": "Tous", 59 | "Followed users": "Utilisateurs suivis", 60 | "Show more posts...": "Afficher plus de publications...", 61 | "Show more comments...": "Afficher plus de commentaires...", 62 | "No posts yet": "Aucune publication", 63 | "Let's follow some users!": "Suivez de nouveaux utilisateurs!", 64 | 65 | "Select user to post new content": "Selectionnez un identifiant pour publier", 66 | "Write something...": "Écrivez quelque chose...", 67 | "Submit new post": "Publier", 68 | "Invalid image, only jpg format supported": "Image invalide, seul le format jpg est supporté", 69 | 70 | "Follow in newsfeed": "Suivre dans le fil d'actualité", 71 | 72 | "New users in ZeroMe": "Nouveaux utilisateurs", 73 | "Total: ": "Total: ", 74 | " registered users": " utilisateurs enregistrés", 75 | 76 | " minutes ago": " minutes", 77 | " hours ago": " heures", 78 | " days ago": " jours", 79 | "on ": "le ", 80 | "Just now": "À l'instant" 81 | } 82 | -------------------------------------------------------------------------------- /languages/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Látogató", 3 | "Select your account": "Felhasználónév kiválasztása", 4 | "You need a profile for this feature": "Ennek a funckiónak az eléréséhez egy profilra van szükséged", 5 | 6 | "Follow username mentions": "Megemlítések követése", 7 | "Follow comments on your posts": "Bejegyzéseid hozzászólásainak követése", 8 | "Follow new followers": "Új követők a hírfolyamba", 9 | 10 | "Follow": "Követés", 11 | "Unfollow": "Ne kövesd", 12 | "Help distribute this user's images": "Segítség a képei terjesztésében", 13 | "Following": "Követések", 14 | "Activity feed": "Aktivitás", 15 | "Show more...": "Még több...", 16 | 17 | " started following ": " új követés: ", 18 | " commented on ": " hozzászólt ", 19 | " liked ": " kedveli ", 20 | "'s ": " ", 21 | "_(post, like post)": "bejegyzését", 22 | "_(post, comment post)": "bejegyzéséhez", 23 | 24 | "Cancel": "Mégse", 25 | "Save": "Mentés", 26 | "Delete": "Törlés", 27 | 28 | "User's profile site not loaded to your client yet.": "A felhasználó profiloldala még nincs letöltve.", 29 | "Download user's site": "Felhasználó profiljának letöltése", 30 | 31 | "Browse all \\u203A": "Összes \\u203A", 32 | "New users": "Új felhasználók", 33 | "Suggested users": "Ajánlott felhasználók", 34 | 35 | "Create new profile": "Új profil létrehozása", 36 | "Creating new profile...": "Profil létrehozása...", 37 | "Checking user on selected hub...": "Felhasználó ellenörzése...", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "A \" + Page.site_info.cert_user_id + \" felhasználó már létezik ezen a HUB-on", 39 | "Select ID...": "Felhasználónév kijelölése", 40 | "Seeded HUBs": "Csatlakozott HUB-ok", 41 | "Available HUBs": "Elérhető HUB-ok", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 43 | "(Ezzel választod ki, hogy hol legyen a profilod tárolva. Tartalomban nincs különbség és bármelyik hubrol eléred az összes felhasználót a hálózaton)", 44 | 45 | "Random ZeroNet user": "Átlagos ZeroNet felhasználó", 46 | "Hello ZeroMe!": "Szia ZeroMe!", 47 | 48 | "Loading...\\nShow image": "Betöltés...\\nKép mutatása", 49 | "Help distribute this user's new images": "Segítség a felhasználó képei terjesztésében", 50 | "Delete image": "Kép törlése", 51 | "Show image": "Kép megjelenítése", 52 | "'s new images": " új képei", 53 | 54 | "Comment": "Hozzászólás", 55 | "Add your comment": "Hozzászólásod", 56 | "Reply": "Válasz", 57 | 58 | "Everyone": "Mindenki", 59 | "Followed users": "Követett felhasználók", 60 | "Show more posts...": "Több bejegyzés mutatása...", 61 | "Show more comments...": "Több hozzászólás mutatása...", 62 | "No posts yet": "Nincs még bejegyzés", 63 | "Let's follow some users!": "Kövess valakit!", 64 | 65 | "Select user to post new content": "Tartalom elküldéséhez válaszd ki a felhasználónevet", 66 | "Write something...": "Írj valamit...", 67 | "Submit new post": "Új bejegyzés elküldése", 68 | "Invalid image, only jpg format supported": "Hibás kép, csak jpg formátum támogatott", 69 | 70 | "Follow in newsfeed": "Követés a hírfolyamban", 71 | 72 | "New users in ZeroMe": "Új felhasználók a ZeroMe-n", 73 | "Total: ": "Összesen: ", 74 | " registered users": " regisztrált felhasználó", 75 | 76 | " minutes ago": " perce", 77 | " hours ago": " órája", 78 | " days ago": " napja", 79 | "on ": "", 80 | "Just now": "Épp most" 81 | } -------------------------------------------------------------------------------- /languages/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Visitatore", 3 | "Select your account": "Seleziona il tuo account", 4 | "You need a profile for this feature": "Hai bisogno di un profilo per questa funzionalità", 5 | 6 | "Follow username mentions": "Segui le menzioni di username", 7 | "Follow comments on your posts": "Segui commenti sui tuoi messaggi", 8 | "Follow new followers": "Segui nuovi seguaci", 9 | 10 | "Follow": "Segui", 11 | "Unfollow": "Smetti di seguire", 12 | "Help distribute this user's images": "Aiuta a distribuire le immagini di questo utente", 13 | "Following": "Seguendo", 14 | "Activity feed": "Note dell'attività", 15 | "Show more...": "Mostra ancora...", 16 | "Mute ": "Silenzia ", 17 | 18 | " started following ": " iniziato a seguire: ", 19 | " commented on ": " commentato su ", 20 | " liked ": " approva ", 21 | "'s ": " ", 22 | "_(post, like post)": "_(messaggio, approva messaggio)", 23 | "_(post, comment post)": "_(messaggio, commenta messaggio)", 24 | 25 | "Cancel": "Annulla", 26 | "Save": "Salva", 27 | "Delete": "Cancella", 28 | 29 | "User's profile site not loaded to your client yet.": "Il sito dell'utente non è ancora stato caricato sul tuo client.", 30 | "Download user's site": "Scarica il sito dell'utente", 31 | 32 | "Browse all \\u203A": "Naviga tutti \\u203A", 33 | "New users": "Nuovi utenti", 34 | "Suggested users": "Utenti suggeriti", 35 | 36 | "Create new profile": "Crea nuovo profilo", 37 | "Creating new profile...": "Creazione nuovo profilo...", 38 | "Checking user on selected hub...": "Controllo utente sull'hub selezionato...", 39 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "L'utente \" + Page.site_info.cert_user_id + \" esiste già su questo hub", 40 | "Select ID...": "Seleziona ID ...", 41 | "Seeded HUBs": "HUB mantenuti", 42 | "Available HUBs": "HUB disponibili", 43 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 44 | "(Con questo si sceglie dove salvare il profilo. Non c'è differenza sul contenuto e sarai in grado di raggiungere tutti gli utenti da ogni hub)", 45 | 46 | "Random ZeroNet user": "Utente ZeroNet casuale", 47 | "Hello ZeroMe!": "Ciao ZeroMe!", 48 | 49 | "Loading...\\nShow image": "Caricamento...\\nMostra immagine", 50 | "Help distribute this user's new images": "Aiuta a distribuire le nuove immagini di questo utente", 51 | "Delete image": "Cancella immagine", 52 | "Show image": "Mostra immagine", 53 | "'s new images": " nuove immagini", 54 | 55 | "Comment": "Commenta", 56 | "Add your comment": "Aggiunti il tuo commento", 57 | "Reply": "Rispondi", 58 | 59 | "Everyone": "Tutti", 60 | "Followed users": "Utenti seguiti", 61 | "Show more posts...": "Mostra altri messaggi...", 62 | "Show more comments...": "Mostra altri commenti...", 63 | "No posts yet": "Ancora nessun messaggio", 64 | "Let's follow some users!": "Inizia a seguire alcuni utenti!", 65 | 66 | "Select user to post new content": "Seleziona un utente per inserire nuovo contenuto", 67 | "Write something...": "Scrivi qualcosa...", 68 | "Submit new post": "Invia nuovo messaggio", 69 | "Invalid image, only jpg format supported": "Immagine non valida, supportato solo il formato jpg", 70 | 71 | "Follow in newsfeed": "Segui il feed delle novità", 72 | 73 | "New users in ZeroMe": "Nuovi utenti su ZeroMe", 74 | "Total: ": "Totali: ", 75 | " registered users": " utenti registrati", 76 | 77 | " minutes ago": " minuti fa", 78 | " hours ago": " ore fa", 79 | " days ago": " giorni fa", 80 | "on ": "a ", 81 | "Just now": "Adesso" 82 | } 83 | -------------------------------------------------------------------------------- /languages/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Bezoeker", 3 | "Select your account": "Selecteer je profiel", 4 | "You need a profile for this feature": "Je hebt een profiel nodig voor deze feature", 5 | 6 | "Follow username mentions": "Volg gebruikersnaam vernoemingen", 7 | "Follow comments on your posts": "Volg commentaar op je posts", 8 | "Follow new followers": "Volg nieuwe volgers", 9 | 10 | "Follow": "Volg", 11 | "Unfollow": "Ontvolg", 12 | "Help distribute this user's images": "Help met het verspreiden van deze gebruiker zijn/haar afbeeldingen", 13 | "Following": "Volgend", 14 | "Activity feed": "Activiteiten feed", 15 | "Show more...": "Toon meer...", 16 | 17 | " started following ": " gestart met volgen ", 18 | " commented on ": " geplaatst op ", 19 | " liked ": " leuk gevonden ", 20 | "'s ": "'s", 21 | "_(post, like post)": "post", 22 | "_(post, comment post)": "commentar", 23 | 24 | "Cancel": "Annuleren", 25 | "Save": "Opslaan", 26 | "Delete": "Verwijderen", 27 | 28 | "User's profile site not loaded to your client yet.": "Gebruiker zijn/haar profiel site staat nog niet in je client.", 29 | "Download user's site": "Dewnload gebruiker zijn/haar site", 30 | 31 | "Browse all \\u203A": "Alles browsen \\u203A", 32 | "New users": "Nieuwe gebruikers", 33 | "Suggested users": "Gesuggereerde gebruikers", 34 | 35 | "Create new profile": "Nieuw profiel aanmaken", 36 | "Creating new profile...": "Nieuw profiel aanmaken...", 37 | "Checking user on selected hub...": "Controleren van gebruiker op geselecteerde hub...", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "Gebruiker \" + Page.site_info.cert_user_id + \" bestaat al op deze hub", 39 | "Select ID...": "Selecteer ID...", 40 | "Seeded HUBs": "Verspreidde HUBs", 41 | "Available HUBs": "Beschikbare HUBs", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": "(Met dit kies je waar je profiel wordt opgeslagen. Er is geen verschil met inhoud en je kunt alle gebruikers bereiken op elke hub)", 43 | 44 | "Random ZeroNet user": "Willekeurige ZereNet gebruiker", 45 | "Hello ZeroMe!": "Hallo ZeroMe!", 46 | 47 | "Loading...\\nShow image": "Laden...\\nToon afbeelding", 48 | "Help distribute this user's new images": "Help met het verspreiden van deze gebruiker zijn/haar nieuwe afbeeldingen", 49 | "Delete image": "Verwijder afbeelding", 50 | "Show image": "Toon afbeelding", 51 | "'s new images": "'s nieuwe afbeeldingen", 52 | 53 | "Comment": "Commentaar", 54 | "Add your comment": "Jouw commentaar toevoegen", 55 | "Reply": "Antwoord", 56 | 57 | "Everyone": "Iedereen", 58 | "Followed users": "Gevolgde gebruikers", 59 | "Show more posts...": "Toon meer posts...", 60 | "Show more comments...": "Toon meer commentaar...", 61 | "No posts yet": "Nog geen posts", 62 | "Let's follow some users!": "Laten we wat gebruikers gaan volgen!", 63 | 64 | "Select user to post new content": "Selecteer gebruiker om nieuwe inhoud mee to posten", 65 | "Write something...": "Schrijf iets...", 66 | "Submit new post": "Nieuwe post toevoegen", 67 | "Invalid image, only jpg format supported": "Invalide afbeelding, enkel het jpg formaat is ondersteund", 68 | 69 | "Follow in newsfeed": "Volgen in nieuwsfeed", 70 | 71 | "New users in ZeroMe": "Nieuwe gebruikers in ZeroMe", 72 | "Total: ": "Totaal: ", 73 | " registered users": " geregistreerde gebruikers", 74 | 75 | " minutes ago": " minuten geleden", 76 | " hours ago": " uren geleden", 77 | " days ago": "dagen geleden", 78 | "on ": "op", 79 | "Just now": "Zojuist" 80 | } -------------------------------------------------------------------------------- /languages/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Visitante", 3 | "Select your account": "Selecione sua conta", 4 | "You need a profile for this feature": "Para utilizar este recurso, você necessita um perfil", 5 | 6 | "Follow username mentions": "Acompanhar menções ao seu username", 7 | "Follow comments on your posts": "Acompanhar comentários em seus posts", 8 | "Follow new followers": "Acompanhar novos seguidores", 9 | 10 | "Follow": "Seguir", 11 | "Unfollow": "Deixar de seguir", 12 | "Help distribute this user's images": "Ajudar a distribuir as imagens deste usuário", 13 | "Following": "Seguindo", 14 | "Activity feed": "Feed de atividades", 15 | "Show more...": "Exibir mais..", 16 | 17 | " started following ": " começou a seguir ", 18 | " commented on ": " comentou em ", 19 | " liked ": " curtiu ", 20 | "'s ": "'s", 21 | "_(post, like post)": "post", 22 | "_(post, comment post)": "commentar", 23 | 24 | "Cancel": "Cancelar", 25 | "Save": "Salvar", 26 | "Delete": "Deletar", 27 | 28 | "User's profile site not loaded to your client yet.": "O hub do perfil do usuário ainda não foi carregado para seu dispositivo.", 29 | "Download user's site": "Baixar hub do usuário", 30 | 31 | "Browse all \\u203A": "Ver todos \\u203A", 32 | "New users": "Novos usuários", 33 | "Suggested users": "Usuários sugeridos", 34 | 35 | "Create new profile": "Criar novo perfil", 36 | "Creating new profile...": "Criando novo perfil..", 37 | "Checking user on selected hub...": "Checando usuário no hub selecionado..", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "O usuário \" + Page.site_info.cert_user_id + \" já existe neste hub", 39 | "Select ID...": "Selecionar ID...", 40 | "Seeded HUBs": "Hubs semeados", 41 | "Available HUBs": "Hubs disponíveis", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": "(Com isto você seleciona onde seu perfil é armazenado. Não há diferença no conteúdo e você poderá alcançar todos os usuários à partir de qualquer hub)", 43 | 44 | "Random ZeroNet user": "Usuário aleatório da ZeroNet", 45 | "Hello ZeroMe!": "Olá, ZeroMe!", 46 | 47 | "Loading...\\nShow image": "Carregando..\\nExibir imagem", 48 | "Help distribute this user's new images": "Ajudar a distribuir as novas imagens deste usuário", 49 | "Delete image": "Deletar imagem", 50 | "Show image": "Exibir imagem", 51 | "'s new images": "'s novas imagens", 52 | 53 | "Comment": "Comentar", 54 | "Add your comment": "Adicionar seu comentário", 55 | "Reply": "Responder", 56 | 57 | "Everyone": "Todos", 58 | "Followed users": "Usuários que sigo", 59 | "Show more posts...": "Exibir mais posts..", 60 | "Show more comments...": "Exibir mais comentários..", 61 | "No posts yet": "Nenhum post ainda..", 62 | "Let's follow some users!": "Bora seguir alguns usuários!", 63 | 64 | "Select user to post new content": "Selecione o usuário para postar novo conteúdo", 65 | "Write something...": "Escreva algo..", 66 | "Submit new post": "Postar", 67 | "Invalid image, only jpg format supported": "Imagem inválida. Somente o formato JPG é suportado", 68 | 69 | "Follow in newsfeed": "Seguir no feed de notícias", 70 | 71 | "New users in ZeroMe": "Novos usuários no ZeroMe", 72 | "Total: ": "Total: ", 73 | " registered users": " usuários registrados", 74 | 75 | " minutes ago": " minutos atrás", 76 | " hours ago": " horas atràs", 77 | " days ago": "dias atrás", 78 | "on ": "em ", 79 | "Just now": "Agora mesmo" 80 | } -------------------------------------------------------------------------------- /languages/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Návštevník", 3 | "Select your account": "Zvoľte svoj účet", 4 | "You need a profile for this feature": "Pre túto funkciu potrebujete profil", 5 | 6 | "Follow username mentions": "Sledovať spomenutia používateľského mena", 7 | "Follow comments on your posts": "Sledovať komentáre na vaších príspevkoch", 8 | "Follow new followers": "Sledovať nových sledovateľov", 9 | 10 | "Follow": "Sledovať", 11 | "Unfollow": "Prestať sledovať", 12 | "Help distribute this user's images": "Pomôcť s distribúciou obrázkov tohto používateľa", 13 | "Following": "Sledované", 14 | "Activity feed": "Prehľad aktivít", 15 | "Show more...": "Viac...", 16 | 17 | " started following ": " začal sledovať ", 18 | " commented on ": " komentoval na ", 19 | " liked ": " like-ol ", 20 | "'s ": "", 21 | "_(post, like post)": "_(post, like-núť post)", 22 | "_(post, comment post)": "_(post, komentovať post)", 23 | 24 | "Cancel": "Zrušiť", 25 | "Save": "Uložiť", 26 | "Delete": "Vymazať", 27 | 28 | "User's profile site not loaded to your client yet.": "Stránka používateľského účtu ešte nebola načítaná do vašeho klinta.", 29 | "Download user's site": "Stiahnuť stránku používateľa", 30 | 31 | "Browse all \\u203A": "Prezrieť všetko \\u203A", 32 | "New users": "Nový používatelia", 33 | "Suggested users": "Doporučený používatelia", 34 | 35 | "Create new profile": "Vytvoriť nový profil", 36 | "Creating new profile...": "Vytvára sa nový profil...", 37 | "Checking user on selected hub...": "Kontroluje sa používateľ na zvolenom hub-e...", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "Používateľ \" + Page.site_info.cert_user_id + \" už na tomto hub-e existuje", 39 | "Select ID...": "Zvoliť ID...", 40 | "Seeded HUBs": "Seed-ovaných Hub-ov", 41 | "Available HUBs": "Dostupných Hub-ov", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 43 | "(S týmto si zvolíš kde bude tvoj profil uložený. Žiadny rozdiel medzi obsahom nieje a budeš vidieť všetkých používateľov z ostatných hub-ov)", 44 | 45 | "Random ZeroNet user": "Náhodný ZeroNet používateľ", 46 | "Hello ZeroMe!": "Ahoj ZeroMe!", 47 | 48 | "Loading...\\nShow image": "Načítava sa...\\nUkázať Obrázok", 49 | "Help distribute this user's new images": "Pomôcť s distribúciou obrázkov tohto používateľa", 50 | "Delete image": "Vymazať obrázok", 51 | "Show image": "Ukázať obrázok", 52 | "'s new images": " nových obrázkov", 53 | 54 | "Comment": "Komentovať", 55 | "Add your comment": "Pridaj svoj komentár", 56 | "Reply": "Odpovedať", 57 | 58 | "Everyone": "Všetci", 59 | "Followed users": "Sledovaný používatelia", 60 | "Show more posts...": "Ukázať viac postov...", 61 | "Show more comments...": "Ukázať viac komentárov...", 62 | "No posts yet": "Zatiaľ žiadne príspevky", 63 | "Let's follow some users!": "Začnite sledovať nejakých používateľov!", 64 | 65 | "Select user to post new content": "Zvoľte používateľa za ktorého chcete uverejniť obsah", 66 | "Write something...": "Napíšte niečo...", 67 | "Submit new post": "Zaslať nový príspevok", 68 | "Invalid image, only jpg format supported": "Neplatný obrázok, podporovaný iba jpg formát", 69 | 70 | "Follow in newsfeed": "Sledovať v novinkách", 71 | 72 | "New users in ZeroMe": "Nový používatelia na ZeroMe", 73 | "Total: ": "Ceľkom: ", 74 | " registered users": " registrovaných používateľov", 75 | 76 | " minutes ago": " minút dozadu", 77 | " hours ago": " hodín dozadu", 78 | " days ago": " dní dozadu", 79 | "on ": "", 80 | "Just now": "Teraz", 81 | "Liked": "Like-nuté", 82 | "Followed by ": "Sledovaný/á používateľom " 83 | } 84 | -------------------------------------------------------------------------------- /languages/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "Ziyaretçi", 3 | "Select your account": "Hesabınızı seçin", 4 | "You need a profile for this feature": "Bu özelliği kullanabilmek için bir hesap seçmelisiniz", 5 | 6 | "Follow username mentions": "Kullanıcı adından bahsedenleri takip et", 7 | "Follow comments on your posts": "Gönderilerine yapılan yorumları takip et", 8 | "Follow new followers": "Yeni takipçileri takip et", 9 | 10 | "Follow": "Takip et", 11 | "Unfollow": "Takibi bırak", 12 | "Help distribute this user's images": "Bu kullanıcının resimlerinin dağıtılmasına yardımcı ol", 13 | "Following": "Takip edilenler", 14 | "Activity feed": "Etkinlikler", 15 | "Show more...": "Daha fazla göster...", 16 | 17 | " started following ": " takibe başladı: ", 18 | " commented on ": " yorumladı: ", 19 | " liked ": " beğendi: ", 20 | "'s ": " kullanıcısının ", 21 | "_(post, like post)": "gönderisini", 22 | "_(post, comment post)": "gönderisini", 23 | 24 | "Cancel": "İptal", 25 | "Save": "Kaydet", 26 | "Delete": "Sil", 27 | 28 | "User's profile site not loaded to your client yet.": "Kullanıcının profili henüz indirilmedi.", 29 | "Download user's site": "Kullanıcının sitesini indir", 30 | 31 | "Browse all \\u203A": "Tümünü gör \\u203A", 32 | "New users": "Yeni kullanıcılar", 33 | "Suggested users": "Önerilen kullanıcılar", 34 | 35 | "Create new profile": "Yeni profil oluştur", 36 | "Creating new profile...": "Yeni profil oluşturuluyor...", 37 | "Checking user on selected hub...": "Kullanıcı seçili merkezde kontrol ediliyor...", 38 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "\" + Page.site_info.cert_user_id + \" kullanıcısı bu merkezde mevcut", 39 | "Select ID...": "Kimlik seçin...", 40 | "Seeded HUBs": "Paylaşılan merkezler", 41 | "Available HUBs": "Erişilebilir merkezler", 42 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 43 | "(Burada profilinizin nerede tutulacağını seçiyorsunuz. İçerik bakımından farkları yok, herhangi bir merkezdeki herhangi bir kullanıcıya erişebileceksiniz)", 44 | 45 | "Random ZeroNet user": "Olağan bir ZeroNet kullanıcısı", 46 | "Hello ZeroMe!": "Merhaba ZeroMe!", 47 | 48 | "Loading...\\nShow image": "Yükleniyor...\\nResimi göster", 49 | "Help distribute this user's new images": "Bu kullanıcının yeni resimlerinin dağıtılmasına yardım et", 50 | "Delete image": "Resimi sil", 51 | "Show image": "Resimi göster", 52 | "'s new images": " kullanıcısına ait yeni resimler", 53 | 54 | "Comment": "Yorumla", 55 | "Add your comment": "Yorumunu ekle", 56 | "Reply": "Cevapla", 57 | 58 | "Everyone": "Herkes", 59 | "Followed users": "Takip edilen kullanıcılar", 60 | "Show more posts...": "Daha fazla gönderi göster...", 61 | "Show more comments...": "Tüm yorumları göster...", 62 | "No posts yet": "Henüz hiç gönderi yok", 63 | "Let's follow some users!": "Bir kaç kişiyi takip edelim!", 64 | 65 | "Select user to post new content": "Yeni içerik göndermek için kullanıcı seç", 66 | "Write something...": "Birşeyler yaz...", 67 | "Submit new post": "Gönder", 68 | "Invalid image, only jpg format supported": "Geçersiz resim, sadece jpeg resimler destekleniyor", 69 | 70 | "Follow in newsfeed": "Güncel beslemede takip et", 71 | 72 | "New users in ZeroMe": "Yeni kullanıcılar", 73 | "Total: ": "Toplam: ", 74 | " registered users": " kayıtlı kullanıcı", 75 | 76 | " minutes ago": " dakika", 77 | " hours ago": " saat", 78 | " days ago": " gün", 79 | "on ": " ", 80 | "Just now": "Şu anda" 81 | } 82 | -------------------------------------------------------------------------------- /languages/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "訪客", 3 | "Select your account": "選擇你的帳戶", 4 | "You need a profile for this feature": "你需要建立一個新的用戶檔案來實現這個特性", 5 | 6 | "Follow username mentions": "關注用戶名提醒", 7 | "Follow comments on your posts": "關注你的帖子評論", 8 | "Follow new followers": "關注新關注者", 9 | "Hide \\\"Hello ZeroMe!\\\" messages": "隱藏 \\\"你好 ZeroMe!\\\" 訊息", 10 | 11 | "Follow": "關注", 12 | "Unfollow": "取消關注", 13 | "Followed by ": "被關注於 ", 14 | "Help distribute this user's images": "幫助分發這個用戶的圖片", 15 | "Following": "已關注", 16 | "Activity feed": "活動資訊流", 17 | "Show more...": "顯示更多...", 18 | 19 | " started following ": " 開始關注 ", 20 | " commented on ": " 評論了 ", 21 | " liked ": " 贊了 ", 22 | "'s ": " ", 23 | "_(post, like post)": "的帖子", 24 | "_(post, comment post)": "的帖子", 25 | 26 | "Cancel": "取消", 27 | "Save": "存儲", 28 | "Delete": "刪除", 29 | 30 | "User's profile site not loaded to your client yet.": "用戶資料站點還沒有載入到你的客戶端。", 31 | "Download user's site": "下載用戶的站點", 32 | 33 | "Browse all \\u203A": "瀏覽全部 \\u203A", 34 | "New users": "新用戶", 35 | "Suggested users": "推薦用戶", 36 | 37 | "Create new profile": "建立新的用戶檔案", 38 | "Creating new profile...": "正在建立新的用戶檔案...", 39 | "Checking user on selected hub...": "正在檢查已選擇的 Hub 中的用戶...", 40 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "用戶已經存在於 \" + Page.site_info.cert_user_id + \" 這個 Hub", 41 | "Select ID...": "選擇 ID ...", 42 | "Seeded HUBs": "已做種的 Hub", 43 | "Available HUBs": "可用的 Hub", 44 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 45 | "(選擇你要在哪裡建立你的用戶檔案。發布內容沒有差別,你可以獲得任何 Hub 的任何用戶)", 46 | 47 | "Random ZeroNet user": "新 ZeroNet 用戶", 48 | "Hello ZeroMe!": "你好 ZeroMe !", 49 | 50 | "Loading...\\nShow image": "載入中...\\n顯示圖片", 51 | "Help distribute this user's new images": "幫助分發這個用戶的新圖片", 52 | "Delete image": "刪除圖片", 53 | "Show image": "顯示圖片", 54 | "'s new images": "的新图片", 55 | 56 | "Comment": "評論", 57 | "Add your comment": "加入你的評論", 58 | "Reply": "回復", 59 | 60 | "Everyone": "所有人", 61 | "Liked": "喜歡的", 62 | "Followed users": "關注的用戶", 63 | "Show more posts...": "顯示更多帖子...", 64 | "Mute user": "屏蔽用戶", 65 | "Permalink": "固定鏈接", 66 | "Show more comments...": "顯示更多評論...", 67 | "No posts yet": "還沒有帖子", 68 | "Let's follow some users!": "讓我們關注一些用戶吧!", 69 | 70 | "Select user to post new content": "選擇發布新內容的用戶", 71 | "Write something...": "寫點兒什麼吧...", 72 | "Submit new post": "提交新的帖子", 73 | "Invalid image, only jpg format supported": "無效的圖片,只有 jpg 格式受支持", 74 | 75 | "Follow in newsfeed": "在新聞源中關注", 76 | 77 | "Search in users...": "在用戶中搜尋...", 78 | "Most active": "最活躍用戶", 79 | "New users in ZeroMe": "在 ZeroMe 的新用戶", 80 | "Total: ": "總計:", 81 | " registered users": " 註冊用戶", 82 | 83 | " minutes ago": " 分鐘前", 84 | " hours ago": " 小時前", 85 | " days ago": " 天前", 86 | "on ": "", 87 | "Just now": "剛才" 88 | } 89 | -------------------------------------------------------------------------------- /languages/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Visitor": "访客", 3 | "Select your account": "选择你的帐户", 4 | "You need a profile for this feature": "你需要创建一个新的用户资料来实现这个特性", 5 | 6 | "Follow username mentions": "关注用户名提醒", 7 | "Follow comments on your posts": "关注你的帖子评论", 8 | "Follow new followers": "关注新关注者", 9 | "Hide \\\"Hello ZeroMe!\\\" messages": "隐藏 \\\"你好 ZeroMe!\\\" 消息", 10 | 11 | "Follow": "关注", 12 | "Unfollow": "取消关注", 13 | "Followed by ": "被关注于 ", 14 | "Help distribute this user's images": "帮助分发这个用户的图片", 15 | "Following": "已关注", 16 | "Activity feed": "活动信息流", 17 | "Show more...": "显示更多...", 18 | "Mute ": "屏蔽", 19 | 20 | " started following ": " 开始关注 ", 21 | " commented on ": " 评论了 ", 22 | " liked ": " 赞了 ", 23 | "'s ": " ", 24 | "_(post, like post)": "的帖子", 25 | "_(post, comment post)": "的帖子", 26 | 27 | "Cancel": "取消", 28 | "Save": "保存", 29 | "Delete": "删除", 30 | 31 | "User's profile site not loaded to your client yet.": "用户资料站点还没有加载到你的客户端。", 32 | "Download user's site": "下载用户的站点", 33 | 34 | "Browse all \\u203A": "浏览全部 \\u203A", 35 | "New users": "新用户", 36 | "Suggested users": "推荐用户", 37 | 38 | "Create new profile": "创建新的用户资料", 39 | "Creating new profile...": "正在创建新的用户资料...", 40 | "Checking user on selected hub...": "正在检查已选择的 Hub 中的用户...", 41 | "User \" + Page.site_info.cert_user_id + \" already exists on this hub": "用户已经存在于 \" + Page.site_info.cert_user_id + \" 这个 Hub", 42 | "Select ID...": "选择 ID ...", 43 | "Seeded HUBs": "已做种的 Hub", 44 | "Available HUBs": "可用的 Hub", 45 | "(With this you choose where is your profile stored. There is no difference on content and you will able to reach all users from any hub)": 46 | "(选择你要在哪里创建你的个人资料。发布内容没有差别,你可以获得任何 Hub 的任何用户)", 47 | 48 | "Random ZeroNet user": "新 ZeroNet 用户", 49 | "Hello ZeroMe!": "你好 ZeroMe!", 50 | 51 | "Loading...\\nShow image": "加载中...\\n显示图片", 52 | "Help distribute this user's new images": "帮助分发这个用户的新图片", 53 | "Delete image": "删除图片", 54 | "Show image": "显示图片", 55 | "'s new images": "的新图片", 56 | 57 | "Comment": "评论", 58 | "Add your comment": "添加你的评论", 59 | "Reply": "回复", 60 | 61 | "Everyone": "所有人", 62 | "Liked": "喜欢的", 63 | "Followed users": "关注的用户", 64 | "Show more posts...": "显示更多帖子...", 65 | "Mute user": "屏蔽用户", 66 | "Permalink": "固定链接", 67 | "Show more comments...": "显示更多评论...", 68 | "No posts yet": "还没有帖子", 69 | "Let's follow some users!": "让我们关注一些用户吧!", 70 | 71 | "Select user to post new content": "选择发布新内容的用户", 72 | "Write something...": "写点儿什么吧...", 73 | "Submit new post": "提交新的帖子", 74 | "Invalid image, only jpg format supported": "无效的图片,只有 jpg 格式受支持", 75 | 76 | "Follow in newsfeed": "在新闻源中关注", 77 | 78 | "Search in users...": "在用户中搜索...", 79 | "Most active": "最活跃的用户", 80 | "New users in ZeroMe": "在 ZeroMe 的新用户", 81 | "Total: ": "总共:", 82 | " registered users": " 注册用户", 83 | 84 | " minutes ago": " 分钟前", 85 | " hours ago": " 小时前", 86 | " days ago": " 天前", 87 | "on ": "", 88 | "Just now": "刚才" 89 | } 90 | -------------------------------------------------------------------------------- /zerome.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/ZeroMe/07e8090cf87f261d33a363d1b6a7a0eb1b93bc80/zerome.ico --------------------------------------------------------------------------------