├── 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 | 
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 |
--------------------------------------------------------------------------------
/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 |
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 "
"
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
--------------------------------------------------------------------------------