Compare commits

...

269 Commits

Author SHA1 Message Date
Julian Lam
1d81f43249 0.0.5 2013-08-13 14:10:22 -04:00
Julian Lam
645d0f60e4 dropping back to 0.0.4 for republish of 0.0.5 2013-08-13 14:09:43 -04:00
Julian Lam
e1d7fed1b6 Merge pull request #167 from miduga/my-commits
Typo with ending bracket
2013-08-13 11:09:09 -07:00
Miguel Ángel Durán García
deff75ad06 Typo with ending bracket 2013-08-13 19:37:01 +02:00
Julian Lam
85fb03c0f5 0.0.5 2013-08-13 13:07:19 -04:00
Julian Lam
74445c3d30 temporarily dropping back to 0.0.4 for re-issue 2013-08-13 13:06:59 -04:00
Baris Usakli
ca3629d89b forgot to commit oops 2013-08-13 13:05:15 -04:00
Baris Usakli
16a95dd408 fixed welcome back message, it wasnt showing up in some cases 2013-08-13 12:58:07 -04:00
Julian Lam
b96084fb16 0.0.5 2013-08-13 12:14:11 -04:00
Julian Lam
c3c5f5ab92 closed #165 - issue where the site title was "undefined" if no title was set in the config, parenthesis issue 2013-08-13 12:10:07 -04:00
Baris Usakli
0fef0aa582 disable load more users button if there is not more users, dont show mark all read in unread if user isnt logged in 2013-08-13 11:39:19 -04:00
Baris Usakli
1894d0a469 moved confings into redis 2013-08-13 11:25:10 -04:00
Baris Usakli
259ba9b9b5 added post settings to admin page 2013-08-13 10:51:25 -04:00
Baris Soner Usakli
46afbc0ba0 assorted config fixes 2013-08-12 19:00:31 -04:00
Baris Soner Usakli
dd3b01dfed closes #144 2013-08-12 14:49:49 -04:00
Baris Soner Usakli
48bb1b06c2 no need to store container 2013-08-12 14:35:28 -04:00
Baris Soner Usakli
9fb63d45c1 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-08-12 14:33:02 -04:00
Baris Soner Usakli
ded0200355 upgraded userlist to 3 sorted sets, run node app -upgrade after updating to this commit, added infinite scrolling to all user pages, added ops per second to redis page, changed the percentage to 90% for infinite scrolling to kick in 2013-08-12 14:32:56 -04:00
Julian Lam
64b166ca8c Merge pull request #162 from miduga/my-commits
#161 - meta.js problem with empty config var
2013-08-12 09:17:32 -07:00
Miguel Ángel Durán García
33994ed638 #161 - meta.js problem with empty config var
If config is empty, give to it an empty object value.
2013-08-12 18:06:56 +02:00
Julian Lam
4457228903 closed #134 2013-08-11 16:41:49 -04:00
Julian Lam
f4573d23b5 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-08-11 16:13:04 -04:00
Julian Lam
e8f1d645fe closed #160
new build_title function to be called via socket

also added categories.getCategoryField and refactored all calls to
getTopicField to be error-first
2013-08-11 16:12:20 -04:00
Baris Soner Usakli
149e8ac78e Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-08-11 14:50:14 -04:00
Baris Soner Usakli
24b951ab5b added api route for 403 2013-08-11 14:50:12 -04:00
Julian Lam
04d94a7462 turns out it was working as intended(tm)
closed #158
2013-08-11 13:27:05 -04:00
Julian Lam
9bf8739841 removed debug comments 2013-08-11 13:23:41 -04:00
Julian Lam
26944ae686 addressed issue #158, where topic buttons in admin panel were not working
correctly
2013-08-11 13:22:48 -04:00
Julian Lam
dd6c9e03cb added message to admin panel if no themes are found 2013-08-11 13:13:44 -04:00
Julian Lam
45c10d75f3 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-08-11 13:10:11 -04:00
Julian Lam
272806a25d closed #159 - issue where theme listing would hang if a theme folder did
not contain theme.json

Also refactored theme socket event a bit
2013-08-11 13:09:27 -04:00
Julian Lam
27f874759e added message about using "forever" module to run production version of
nodebb
2013-08-11 12:45:44 -04:00
Baris Soner Usakli
e1d6b77eb1 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-08-10 16:20:28 -04:00
Baris Soner Usakli
0a7d41dafa updated reds to 0.2.4 2013-08-10 16:20:16 -04:00
Julian Lam
644fb375db Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-08-10 16:16:05 -04:00
Julian Lam
81f94c56e0 using hgetall instead of hkeys+asynchget, simplifying meta.js, closes #157 2013-08-10 16:15:58 -04:00
Baris Soner Usakli
50c34e4f33 added infinite scrolling to unread page, issue #141 2013-08-10 16:14:50 -04:00
Baris Soner Usakli
cbd34a4411 removed console.log 2013-08-09 20:45:12 -04:00
Baris Soner Usakli
244801a0bf added infinite loading to recent page, #141 2013-08-09 20:44:08 -04:00
Baris Soner Usakli
1cc8214d16 infinite scrolling for topics in category view, changed how topics are stored in categories, using sorted sets instead of sets now, if you update to this commit run node app --upgrade to upgrade the redis schema, #141 2013-08-09 20:03:19 -04:00
Baris Soner Usakli
04e0d075ec added upgrade file and nconf check 2013-08-09 15:17:50 -04:00
Julian Lam
917468d676 Merge pull request #156 from miduga/master
Remove unneded type tags for scripts
2013-08-09 07:34:42 -07:00
Miguel Ángel Durán García
1afc4f39a8 Remove unneded type tags for scripts
In HTML5 we can omit the type tag for scripts because it's useless in
javascript.

http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#the-script
-element

"When used to include dynamic scripts, the scripts may either be
embedded inline or may be imported from an external file using the src
attribute. If the language is not that described by "text/javascript",
then the type attribute must be present, as described below. Whatever
language is used, the contents of the script element must conform with
the requirements of that language's specification."
2013-08-09 16:15:04 +02:00
Baris Usakli
02e707c3b5 removed console.log 2013-08-08 16:44:25 -04:00
Baris Usakli
34ce17e522 removed eventlisteners for scroll 2013-08-08 16:43:46 -04:00
Baris Usakli
a100eb1bb2 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-08-08 15:54:48 -04:00
Baris Usakli
b6281f020b removed console.logs 2013-08-08 15:54:39 -04:00
Julian Lam
78c1139ad9 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-08-08 15:05:00 -04:00
Julian Lam
08a45e40a2 fixed outgoing route to handle links with querystrings in it (using req.query instead of req.url). closes #154. 2013-08-08 15:04:22 -04:00
Baris Usakli
e979d1bf0d fixed home loading 2013-08-08 14:30:42 -04:00
Baris Usakli
e69928d624 strip tags in recent replies 2013-08-08 14:12:28 -04:00
Baris Usakli
8bc60be7c3 dont infinite load if there are no topics loaded 2013-08-08 14:00:40 -04:00
Baris Usakli
99ac782875 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-08-08 13:04:36 -04:00
Baris Usakli
c1c9d24ee5 deleted posts are collapsed clicking on them expands them, closes #138 2013-08-08 13:04:26 -04:00
Julian Lam
e2d0ca9669 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-08-08 13:04:11 -04:00
Julian Lam
661596e4cc updated reds package in package.json to accept any version in the 0.2.x branch 2013-08-08 13:03:54 -04:00
Baris Usakli
ac3fe1fc6f closes #93 2013-08-08 12:49:01 -04:00
Baris Usakli
0bd56196cf index topic titles too 2013-08-08 11:40:31 -04:00
Julian Lam
7251af56e3 refactored notifications library to mark all notifs read when the menu is
opened (closes #134)
2013-08-07 22:42:34 -04:00
Julian Lam
984ac21e52 updating notifications lib to not expose every method 2013-08-07 22:07:35 -04:00
Julian Lam
cb1258da9c removing aptana config file from repo 2013-08-07 21:42:39 -04:00
Baris Soner Usakli
f0759863e6 reindex all call 2013-08-06 15:46:15 -04:00
Baris Soner Usakli
0561710016 highlight search results 2013-08-04 14:48:36 -04:00
Baris Soner Usakli
8423684dee closes #145, fixed 404 not rendering 2013-08-04 14:08:05 -04:00
Baris Soner Usakli
6ff0c55dae swaped notif and search box 2013-08-03 20:57:03 -04:00
Baris Soner Usakli
d6b9a2799b full text search using reds, issue #142 2013-08-03 20:54:16 -04:00
Baris Soner Usakli
797d05a84a removed the other api_method routes 2013-08-02 14:29:40 -04:00
Baris Soner Usakli
a5acf31fc6 posts on user account page scroll to the post now 2013-08-01 18:00:32 -04:00
Baris Soner Usakli
23fcdde25e removed console.log and commented out code 2013-08-01 17:36:01 -04:00
Baris Soner Usakli
7297a26685 scroll to post 2 2013-08-01 17:27:37 -04:00
Baris Usakli
720dd9e960 scroll to posts 2013-08-01 16:11:00 -04:00
Baris Usakli
dbfd3c19a1 removed api_method route from user.js 2013-08-01 13:50:57 -04:00
Baris Soner Usakli
f20b7b5113 closes #133 2013-07-31 16:10:39 -04:00
Baris Soner Usakli
9f2838849e moved the imgur clientID to the server config file 2013-07-31 15:33:36 -04:00
Baris Soner Usakli
59381ee1d3 cleaned up webserver.js a bit, moved api routes to its own file, closes #54 2013-07-31 15:17:03 -04:00
Baris Soner Usakli
6fbecd00b0 allow - and space in username 2013-07-31 13:00:32 -04:00
Baris Soner Usakli
a0a1b14e2e only allow alphanumberic charactesr in username 2013-07-31 12:48:30 -04:00
Baris Soner Usakli
f4fdabd3ca changed to wrapper alert functions 2013-07-30 18:34:51 -04:00
Baris Soner Usakli
3f6f4e347d change some socket calls to use the callback, when user changes their email if they are using a gravatar picture it will be updated too, fixed email updating 2013-07-30 18:30:43 -04:00
Julian Lam
235553eaf6 Merge branch 'plugins'
Conflicts:
	src/posts.js
2013-07-29 16:01:00 -04:00
Julian Lam
1bee527f41 added note linking to wiki page for authoring plugins... also showing something if no plugins are installed 2013-07-29 15:57:14 -04:00
Julian Lam
3578ce1925 admin panel integration completion + styling of admin panel plugins page 2013-07-29 15:45:13 -04:00
Julian Lam
c2d7bf87e9 Merge branch 'plugins' of github.com:designcreateplay/NodeBB into plugins 2013-07-29 15:16:21 -04:00
Julian Lam
777173d9f4 adding admin panel integration to plugins (issue #143) 2013-07-29 15:15:49 -04:00
Julian Lam
0a3a970b52 fixed issue where if a hook had no attached methods, it wouldn't work at all... or put simply... nodebb'd break if you had no plugins, heh. 2013-07-29 15:14:41 -04:00
Baris Soner Usakli
b46d81e3e4 removed console.log 2013-07-29 13:37:16 -04:00
Baris Soner Usakli
3273c0558b anon users always see topics as unread 2013-07-29 13:33:54 -04:00
Baris Soner Usakli
6b2be772e7 removed test route 2013-07-28 18:31:20 -04:00
Baris Soner Usakli
78f462e1e6 added a new section that only shows unread topics, added mark all read button, closes #140 2013-07-28 18:27:49 -04:00
Julian Lam
871591b92d sanity check if a plugin's method was not found on hook call 2013-07-28 14:24:34 -04:00
Baris Soner Usakli
811d2df728 moved getTopicsByTids and getLatestTopics to topics.js 2013-07-28 14:02:50 -04:00
Julian Lam
307f6f34b9 added plugins.showInstalled method 2013-07-28 12:52:58 -04:00
Julian Lam
2d64c9979e sending pid as well as content in action:save_post_content hook 2013-07-28 03:01:58 -04:00
Julian Lam
34bb9dddb5 sticking to style guide for method names (camelcase) 2013-07-28 02:24:41 -04:00
Julian Lam
25a1d25108 tested filter-type hooks, working! :) 2013-07-28 01:37:40 -04:00
Julian Lam
ec4ab19a1e first pass at plugin system -- not tested! 2013-07-28 01:16:21 -04:00
Julian Lam
2e83ad448d added new plugins folder to repo 2013-07-27 21:45:51 -04:00
Baris Soner Usakli
b35ecd08ea use hasReadTopic 2013-07-27 13:59:14 -04:00
Baris Soner Usakli
d32b130d40 added function for marking all topics read #140 2013-07-27 13:05:03 -04:00
Baris Soner Usakli
cca83b9c5c bleh 2013-07-27 12:55:33 -04:00
Baris Soner Usakli
57a4741e9e show message if there are not recent topics 2013-07-26 15:49:03 -04:00
Baris Soner Usakli
23598361b8 closes #139 2013-07-26 15:04:18 -04:00
Baris Soner Usakli
3f8b3894d8 C:/Program Files (x86)/Git/recent shows only the last 24 hours 2013-07-26 14:48:16 -04:00
Julian Lam
557c6effdd updated screenshots for v0.0.4 2013-07-25 21:28:27 -04:00
Baris Soner Usakli
916b46618c another fix 2013-07-25 18:06:20 -04:00
Baris Usakli
2647666de6 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-25 17:02:06 -04:00
Baris Usakli
8cc3859e72 fixed for email visibility icon if data isnt set 2013-07-25 17:01:55 -04:00
Julian Lam
0ad9e84042 publishing 0.0.4 2013-07-25 16:35:10 -04:00
Baris Usakli
8f4d4664e4 changed class 2013-07-25 16:33:06 -04:00
Baris Usakli
6e5420fd64 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-25 16:20:28 -04:00
Baris Usakli
19b570cdcf closes #122 2013-07-25 16:20:18 -04:00
Julian Lam
eabba1cc1f swapped position of reply/quote button in main post in topic (#95) 2013-07-25 15:53:25 -04:00
Julian Lam
cb8c8f9e42 tweaked OG tags for topics 2013-07-25 15:49:55 -04:00
Julian Lam
7dbf18c1d0 removing authors from OG tags 2013-07-25 15:44:54 -04:00
Julian Lam
56dcdd0378 Merge branch 'master' of github.com:psychobunny/node-forum 2013-07-25 15:34:33 -04:00
Julian Lam
fe286870a3 closed #116 2013-07-25 15:34:22 -04:00
Baris Usakli
58ad74abb5 send new url when we are done 2013-07-25 15:26:35 -04:00
Baris Usakli
9160a135a9 changed to crop 2013-07-25 15:19:44 -04:00
Baris Usakli
6420e4c6a6 added this back, file extensions might change need to delete old pic 2013-07-25 14:51:31 -04:00
Baris Usakli
fa9c0e020a Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-25 14:44:52 -04:00
Baris Usakli
433c7d91eb some fixes 2013-07-25 14:44:43 -04:00
Julian Lam
2c95c19c9a Merge branch 'master' of github.com:psychobunny/node-forum 2013-07-25 14:37:14 -04:00
Julian Lam
c5f6a0d668 closed #95 2013-07-25 14:37:08 -04:00
Baris Usakli
3100bfcfd3 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-25 14:30:17 -04:00
Baris Usakli
472f40479e privacy page 2013-07-25 14:30:12 -04:00
Julian Lam
12e8423a24 closed #132 - issue where all auth routes were not rendering the header properly 2013-07-25 14:04:00 -04:00
Julian Lam
336345ee83 closed #137 2013-07-25 13:18:55 -04:00
Julian Lam
a2f46b9e59 updated app name (lowercase only) and added missing dep request 2013-07-25 13:09:11 -04:00
Julian Lam
1752be237b minor style changes to the composer (moved buttons to the bottom as well) 2013-07-25 12:36:43 -04:00
Baris Usakli
4260930523 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-25 12:31:03 -04:00
Baris Usakli
6af3b0ffc6 account settings 2013-07-25 12:30:59 -04:00
Julian Lam
53d001a751 Merge branch 'master' of github.com:psychobunny/node-forum 2013-07-25 12:30:46 -04:00
Julian Lam
5caf258238 fixing build_header refactor for user routes 2013-07-25 12:30:31 -04:00
Baris Usakli
5df7af6075 reset reconnect tries to 0 after a succesfull reconnect 2013-07-25 12:05:19 -04:00
Julian Lam
ce769a87ef refactored parallel to waterfall in topic and category routes (to allow
for better title generation)
2013-07-24 19:27:25 -04:00
Julian Lam
931beecc21 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-24 18:54:47 -04:00
Julian Lam
4588745b9a refactored all calls to app.build_header (in regular and auth routes) 2013-07-24 18:54:14 -04:00
Baris Soner Usakli
12d31517b0 removed console.log 2013-07-24 17:47:26 -04:00
Baris Soner Usakli
08e3eac008 closes #120 2013-07-24 16:29:17 -04:00
Baris Soner Usakli
5b349dbfaa more css 2013-07-24 15:46:18 -04:00
Baris Soner Usakli
370d931bc7 some css changes #95 2013-07-24 15:31:56 -04:00
Julian Lam
d6938f4818 first pass at a custom title function (not ready for prod) 2013-07-24 15:19:26 -04:00
Baris Soner Usakli
72a0082a28 admin user page fix, added routers for admin/users 2013-07-24 15:18:31 -04:00
Baris Soner Usakli
6865eb9f0f changed the drag and drop image upload, closes #108 2013-07-24 15:03:49 -04:00
Julian Lam
1f8f61d1d8 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-24 13:19:49 -04:00
Julian Lam
83b0b6a523 fixing issue with md parser 2013-07-24 13:19:36 -04:00
Baris Soner Usakli
744e4579b7 setUserFields 2013-07-24 13:12:56 -04:00
Baris Soner Usakli
2f00640b6b Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-24 12:57:44 -04:00
Baris Soner Usakli
cacc732015 fixed filename of first uploaded image, added a setUserFields method to user.js 2013-07-24 12:57:36 -04:00
Julian Lam
33e676b033 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-24 12:53:10 -04:00
Julian Lam
bea5dff563 fixed #31 - twitter profile picture now used as nodebb avatar in lieu of
email address for gravatar
2013-07-24 12:52:16 -04:00
Baris Soner Usakli
f7217f2d12 closes #130 2013-07-24 12:33:37 -04:00
Julian Lam
762cecf1c3 fixed #129 2013-07-24 12:04:32 -04:00
Julian Lam
42d1cade23 fixing websockets crash on server restart 2013-07-24 11:39:29 -04:00
Julian Lam
a990cf3e0d added missing "return" when a user creation doesn't pass all server-side
checks...
2013-07-24 11:26:49 -04:00
Julian Lam
10a42d4e08 minor var renaming 2013-07-24 11:25:14 -04:00
Julian Lam
24c235e360 fixed #128 - regression caused by removal of 'connect' package 2013-07-24 09:16:10 -04:00
Julian Lam
b5ecb9a762 removing dependency on package 'connect' 2013-07-24 01:43:39 -04:00
Julian Lam
ff065dcc2f Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-24 00:06:07 -04:00
Julian Lam
c5c6bcec4b closed #119 2013-07-24 00:05:58 -04:00
Julian Lam
6a12ecf8e7 Merge branch 'master' of github.com:psychobunny/node-forum 2013-07-23 17:22:26 -04:00
Julian Lam
cc1cc7cae4 first pass at meta tag construction abstraction (issue #116). Also addressed issue #118 2013-07-23 17:21:44 -04:00
Baris Soner Usakli
6e0c84f9e5 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-23 17:19:29 -04:00
Baris Soner Usakli
e7c9079449 anons cant follow, hide follow button if not logged in 2013-07-23 17:19:19 -04:00
Julian Lam
59029a0ef0 minor tweaks to theme engine 2013-07-23 16:43:02 -04:00
Baris Soner Usakli
bf6a38904a skip loading a topic if cant load its data 2013-07-23 14:44:13 -04:00
Baris Soner Usakli
6ab37bde09 closes #105 2013-07-23 13:17:54 -04:00
Baris Soner Usakli
8947553ecd closes #117 2013-07-23 12:35:45 -04:00
psychobunny
fe996f75b6 replaced the ascii > with the right chevron icon
http://fortawesome.github.io/Font-Awesome/icon/chevron-right/
2013-07-23 16:33:24 +08:00
psychobunny
45cb24fb53 UI: fixed recent replies in category sidebar 2013-07-23 16:18:56 +08:00
psychobunny
6f6a2e4127 closes #114. updated ajaxify to allow for get parameters to be passed in without issues. 2013-07-23 15:06:31 +08:00
Julian Lam
9e7afcf0ab fixing issue where navigating to a non-existant category_id caused NodeBB
to crash
2013-07-22 20:29:51 -04:00
Julian Lam
741a5843a1 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-22 20:08:28 -04:00
Julian Lam
84584a1b5b minor cleanup of header tpl 2013-07-22 20:08:18 -04:00
Baris Soner Usakli
96a779172b closes #106 2013-07-22 18:29:09 -04:00
Baris Soner Usakli
2237166e0f removed console.log 2013-07-22 17:59:49 -04:00
Baris Soner Usakli
85b0fe7175 images debug 2013-07-22 17:26:18 -04:00
Baris Soner Usakli
e9afd4a107 fixed broken view again 2013-07-22 17:08:07 -04:00
Baris Soner Usakli
667a102c8b added async.eachSeries to getCategories 2013-07-22 16:57:18 -04:00
Baris Soner Usakli
38aa5aa3e6 refactor to remove category_name and category_slug from topic hashes 2013-07-22 16:47:41 -04:00
psychobunny
676ffee459 removed text decoration from btn-link to match that of default nodebb anchors 2013-07-23 03:51:32 +08:00
Julian Lam
87baaacb8c Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-22 15:46:02 -04:00
Julian Lam
7a1e1b5b60 fixing issue where notifications toggle was causing an ajaxify 2013-07-22 15:45:40 -04:00
psychobunny
1e86f379d5 removed external link icon from signatures only 2013-07-23 03:34:45 +08:00
Julian Lam
93d4630433 Merge remote-tracking branch 'origin' 2013-07-22 15:31:31 -04:00
Julian Lam
123aac0862 limiting notification counts (issue #112) 2013-07-22 15:31:28 -04:00
psychobunny
240683ed24 merged conflicts. also fixed a potential issue where the external link icon would show up on internal links 2013-07-23 03:15:17 +08:00
psychobunny
e66cab23cf added a screening page for external links. removed forced target = _blank on external links for UX consideration
used http://www.deviantart.com/users/outgoing?http://www.nodebb.org/ for
inspiration
2013-07-23 03:07:27 +08:00
Julian Lam
48e14e9464 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-22 15:05:33 -04:00
Julian Lam
712eec0872 re: issue #104, added external link icon to external links 2013-07-22 15:03:33 -04:00
Baris Usakli
f0df2289e6 i keep forgetting console.logs in commits :/ 2013-07-22 14:50:34 -04:00
Baris Usakli
22d954d01f fixed for broken category view, after topic move 2013-07-22 14:40:52 -04:00
Baris Usakli
d9fa78a866 again 2013-07-22 13:06:31 -04:00
Julian Lam
99ba792e6d changing all calls to marked to instead go through
PostTools.markdownToHTML
2013-07-22 12:58:10 -04:00
Julian Lam
f40bf46656 closed #104 - anchors now have rel="nofollow" and open in a new window 2013-07-22 12:44:50 -04:00
Julian Lam
104dbea3f2 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-22 12:11:46 -04:00
Baris Usakli
52110016c7 closes #107 2013-07-22 12:07:05 -04:00
Julian Lam
770fb6d33d Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-22 12:01:33 -04:00
Julian Lam
54c75024b5 css fixes to post window on mobile screens 2013-07-22 12:01:19 -04:00
Baris Usakli
676765278e closes #111 2013-07-22 11:16:16 -04:00
Julian Lam
667c331a67 refactored User.create in preparation for twitter compatibility (issue 2013-07-22 00:24:37 -04:00
Julian Lam
5e7da6391c fixed #100 - composer window obscuring half of the page 2013-07-21 23:47:11 -04:00
Julian Lam
9626232e3b some more tweaks to make dark themes work better 2013-07-21 23:34:22 -04:00
Julian Lam
81cfe0c8d0 Merge branch 'master' of github.com:designcreateplay/NodeBB
fixing missing height in post-window, removed for some reason

Conflicts:
	public/src/modules/composer.js
2013-07-21 23:27:07 -04:00
Baris Soner Usakli
e39868c08e fixed regex 2013-07-21 16:32:59 -04:00
Baris Soner Usakli
b25033be45 removed console.log 2013-07-21 16:17:39 -04:00
Baris Soner Usakli
7c31e12e6b removed debug console.logs 2013-07-21 16:16:59 -04:00
Baris Soner Usakli
863f471020 closes #103 2013-07-21 16:16:07 -04:00
Baris Soner Usakli
aa54ac8495 updated passport and passport-twitter 2013-07-21 15:07:11 -04:00
Baris Soner Usakli
0b7da34a03 possible fix for #101 2013-07-21 14:25:42 -04:00
Baris Soner Usakli
e437b4df66 removed commented out code 2013-07-21 13:58:41 -04:00
Baris Soner Usakli
919efd4052 drag and drop image upload first pass 2013-07-21 12:29:57 -04:00
Julian Lam
6f759e840e Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-21 02:34:21 -04:00
Julian Lam
a66f2dfabe minor tweaks to positioning of composer window relative to taskbar 2013-07-21 02:34:04 -04:00
Julian Lam
7619fec78d fixing compatibility with dark themes 2013-07-21 02:29:53 -04:00
Julian Lam
18c991fd42 restyling composer window to be smaller widthwise
attempted to fix vertical-alignment of post_window relative to taskbar
2013-07-21 02:16:58 -04:00
Julian Lam
a6460169d4 - removing underline from composer style shortcut button grouping 2013-07-21 01:43:14 -04:00
Baris Soner Usakli
f7291631d8 when a post is restored update topic timestamp 2013-07-20 16:59:53 -04:00
Baris Soner Usakli
eec5249eb6 possible fix for wront sorting on /recent 2013-07-20 16:01:44 -04:00
Baris Soner Usakli
93c6228347 handle err 2013-07-20 15:23:04 -04:00
Julian Lam
97a70b808e Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-20 11:19:24 -04:00
Julian Lam
782858c728 added dynamically generated robots.txt 2013-07-20 11:19:08 -04:00
Baris Soner Usakli
343aba9eff added relative path to image upload url 2013-07-20 00:10:45 -04:00
Julian Lam
0b545c7056 removing extra console log... 2013-07-19 23:58:30 -04:00
Julian Lam
3c4b2c8075 fixed oddities in base_url and url generation for subdirectoried instances
of NodeBB
2013-07-19 23:56:30 -04:00
Baris Usakli
e5301deea2 more async in categories 2013-07-19 16:13:00 -04:00
Baris Usakli
ff9990701b changed to async.eachSeries 2013-07-19 15:26:29 -04:00
Baris Usakli
c1b170ec86 removed anchor around recent posts on account page 2013-07-19 12:03:54 -04:00
Baris Usakli
78358a8ccd forgot ] 2013-07-19 11:02:21 -04:00
Baris Usakli
04bc0cd2c9 oops 2013-07-19 10:59:52 -04:00
Baris Usakli
a4de1b247f decrease/increase post count on post deletion/restore 2013-07-19 10:59:24 -04:00
Baris Usakli
3adddf44ca removed console.log 2013-07-19 10:37:18 -04:00
Baris Usakli
68c011ce63 recent replies link to user and topic 2013-07-19 10:36:42 -04:00
Baris Soner Usakli
2f20831d25 removed extra ; oops 2013-07-18 15:45:17 -04:00
Baris Soner Usakli
7ff6cd9cb2 closes #99 2013-07-18 15:44:42 -04:00
Baris Soner Usakli
285e19fbbf closes #97 2013-07-18 15:14:06 -04:00
Baris Soner Usakli
f90eb72706 added minimum lengths into error messages 2013-07-18 14:50:58 -04:00
Baris Soner Usakli
f7d27cdef1 nconf fixes 2013-07-18 14:47:41 -04:00
Baris Soner Usakli
4d670006e1 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-18 13:50:45 -04:00
Baris Soner Usakli
65029ae169 content length changes 2013-07-18 13:50:42 -04:00
Julian Lam
d0c4302d24 removing non-existant unit tests from admin template, and setting admin theme to always be the bootstrap default (so errant themes don't render the admin panel useless) 2013-07-18 12:04:29 -04:00
Julian Lam
800ac86014 committing missing file pertaining to issue #96 2013-07-17 22:38:55 -04:00
Julian Lam
1fa900e615 added sitemap.xml to routes, closes #96 2013-07-17 22:35:16 -04:00
Julian Lam
6e91810231 introduction of theme engine parsing (themes go in /public/themes!) 2013-07-17 16:17:19 -04:00
Julian Lam
936d29d907 Merge branch 'master' of github.com:psychobunny/node-forum 2013-07-17 16:14:23 -04:00
Julian Lam
44f9fe9f6c fixing issue where the email check always failed... whoops! 2013-07-17 16:13:53 -04:00
Baris Usakli
ca81732661 show correct version on admin/index 2013-07-17 16:09:01 -04:00
Baris Usakli
03d3f59804 change to async.each for addUserInfoToPosts 2013-07-17 15:48:09 -04:00
Baris Usakli
8369e2278f Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-17 12:58:02 -04:00
Baris Usakli
2bb4aab6ac changes to online users code 2013-07-17 12:57:57 -04:00
Julian Lam
a06123579b issue #94 2013-07-17 12:21:40 -04:00
Julian Lam
1e4424d656 issue #94 again 2013-07-17 12:17:52 -04:00
Julian Lam
d3541da9dd resolving regression introduced by errant install script (again) - issue 94 2013-07-17 12:15:40 -04:00
Julian Lam
2f1ef4c2a1 fixing regression introduced where the client-side config was incorrectly generated (resolves #94) 2013-07-17 12:05:24 -04:00
Julian Lam
f63be472be updated link to roadmap and added new screenshots for readme 2013-07-17 11:33:37 -04:00
Baris Usakli
4f9a2d0b93 content check in posts 2013-07-17 11:22:09 -04:00
Baris Usakli
8d21d4f998 closes #91 2013-07-17 10:45:16 -04:00
Baris Usakli
582cd2f011 closes #89 2013-07-17 10:05:29 -04:00
Julian Lam
ed967a0e2f applied minor suggestion suggested by @damianb in issue #24
closed #88 (regression due to text selection enhancement)
2013-07-17 01:02:28 -04:00
Julian Lam
3feb977cc4 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-16 21:13:02 -04:00
Julian Lam
c2e9cd621d issue #57, just for Damian :) 2013-07-16 21:12:40 -04:00
Baris Soner Usakli
9e5ecf0ae8 Merge branch 'master' of https://github.com/designcreateplay/NodeBB 2013-07-16 20:30:54 -04:00
Baris Soner Usakli
cf5f9ad9bd share links wont refresh page 2013-07-16 20:30:46 -04:00
Julian Lam
7cdef19b94 issue #57 again - tightened the regex a bit more (escaped the dot) 2013-07-16 16:38:35 -04:00
Julian Lam
0553254a9b closed #57 2013-07-16 16:37:37 -04:00
Julian Lam
8234294be8 Merge branch 'master' of github.com:designcreateplay/NodeBB 2013-07-16 16:37:13 -04:00
Baris Soner Usakli
62919894b4 removed the bcrypt question from install, changed default to 12 2013-07-16 16:33:18 -04:00
Baris Soner Usakli
dd4ee28d26 removed ajaxify.enable, closes #85 2013-07-16 16:21:12 -04:00
Baris Soner Usakli
a5b324cbc4 closes #86 2013-07-16 16:19:50 -04:00
88 changed files with 4254 additions and 1831 deletions

3
.gitignore vendored
View File

@@ -8,5 +8,8 @@ sftp-config.json
config.json
public/config.json
public/css/*.css
public/themes/*
*.sublime-project
*.sublime-workspace
plugins/*
.project

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>node-forum</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
</buildSpec>
<natures>
<nature>com.aptana.projects.webnature</nature>
</natures>
</projectDescription>

View File

@@ -4,14 +4,14 @@ Please support NodeBB development! Check out our IndieGoGo campaign and like, sh
# NodeBB
**NodeBB** is a robust Node.js driven forum built on a redis database. It is powered by web sockets, and is compatible down to IE8.
![NodeBB Screenshot](http://i.imgur.com/mxRmLAg.png)
![NodeBB Main Category Listing](http://i.imgur.com/N8SLkcDh.png)
![NodeBB Login Page (with Social Logins)](http://i.imgur.com/q5tUUHW.png)
![NodeBB Topic Page](http://i.imgur.com/RBkFzdkh.png)
## How can I follow along/contribute?
* Our [Indiegogo campaign](https://www.indiegogo.com/projects/nodebb-the-discussion-platform-of-the-future/) is accepting contributions until August 17th, 2013
* Our feature roadmap is hosted on the [NodeBB Development/Roadmap Trello](https://trello.com/board/nodebb-development-roadmap/51c66b0d7c42d0a257002732)
* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/designcreateplay/NodeBB/wiki/Version-History-%26-Roadmap)
* If you are a developer, feel free to check out the source and submit pull requests.
* If you are a designer, NodeBB needs themes! NodeBB will accept any LESS or CSS file and use it in place of the default Twitter Bootstrap theme. Consider extending Bootstrap themes by extending the base bootstrap LESS file.

21
app.js
View File

@@ -16,7 +16,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Read config.js to grab redis info
var fs = require('fs'),
nconf = require('nconf'),
pkg = require('./package.json'),
@@ -34,8 +33,10 @@ console.log('Info: This program comes with ABSOLUTELY NO WARRANTY.');
console.log('Info: This is free software, and you are welcome to redistribute it under certain conditions.');
console.log('Info: ===');
if (!nconf.get('setup') && nconf.get('base_url')) {
nconf.set('url', nconf.get('base_url') + (nconf.get('use_port') ? ':' + nconf.get('port') : '') + '/');
if(nconf.get('upgrade')) {
require('./src/upgrade').upgrade();
} else if (!nconf.get('setup') && nconf.get('base_url')) {
nconf.set('url', nconf.get('base_url') + (nconf.get('use_port') ? ':' + nconf.get('port') : '') + nconf.get('relative_path') + '/');
nconf.set('upload_url', nconf.get('url') + 'uploads/');
global.nconf = nconf;
@@ -57,6 +58,7 @@ if (!nconf.get('setup') && nconf.get('base_url')) {
templates = require('./public/src/templates.js'),
webserver = require('./src/webserver.js'),
websockets = require('./src/websockets.js'),
plugins = require('./src/plugins'),
admin = {
'categories': require('./src/admin/categories.js')
};
@@ -70,16 +72,14 @@ if (!nconf.get('setup') && nconf.get('base_url')) {
config['ROOT_DIRECTORY'] = __dirname;
templates.init([
'header', 'footer', 'logout', 'admin/header', 'admin/footer', 'admin/index',
'header', 'footer', 'logout', 'outgoing', 'admin/header', 'admin/footer', 'admin/index',
'emails/reset', 'emails/reset_plaintext', 'emails/email_confirm', 'emails/email_confirm_plaintext',
'emails/header', 'emails/footer', 'install/header', 'install/footer', 'install/redis',
'noscript/header', 'noscript/home', 'noscript/category', 'noscript/topic'
]);
templates.ready(function() {
webserver.init();
});
templates.ready(webserver.init);
//setup scripts to be moved outside of the app in future.
function setup_categories() {
@@ -100,11 +100,16 @@ if (!nconf.get('setup') && nconf.get('base_url')) {
console.log('Info: Hardcoding uid 1 as an admin');
var user = require('./src/user.js');
user.makeAdministrator(1);
} else {
console.log('Info: Categories OK. Found ' + data.categories.length + ' categories.');
}
});
}
setup_categories();
}(global.configuration));
});
@@ -131,7 +136,7 @@ if (!nconf.get('setup') && nconf.get('base_url')) {
);
}
}
process.exit();
});
}

4
nodebb
View File

@@ -1,4 +1,6 @@
#!/bin/bash
clear
echo "Launching NodeBB in \"development\" mode"
echo "Launching NodeBB in \"development\" mode."
echo "To run the production build of NodeBB, please use \"forever\"."
echo "More Information: https://github.com/designcreateplay/NodeBB/wiki/How-to-run-NodeBB"
NODE_ENV=development supervisor --extensions 'node|js|tpl' -- app $1

View File

@@ -1,8 +1,8 @@
{
"name": "NodeBB",
"name": "nodebb",
"license": "GPLv3 or later",
"description": "NodeBB Forum",
"version": "0.0.3",
"version": "0.0.5",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",
@@ -14,13 +14,12 @@
"redis": "0.8.3",
"express": "3.2.0",
"express-namespace": "0.1.1",
"connect": "2.7.6",
"emailjs": "0.3.4",
"cookie": "0.0.6",
"connect-redis": "1.4.5",
"passport": "0.1.16",
"passport": "0.1.17",
"passport-local": "0.1.6",
"passport-twitter": "0.1.4",
"passport-twitter": "0.1.5",
"passport-google-oauth": "0.1.5",
"passport-facebook": "0.1.5",
"less-middleware": "0.1.12",
@@ -30,7 +29,11 @@
"node-imagemagick": "0.1.8",
"node-rss": "1.0.1",
"gravatar": "1.0.6",
"nconf": "~0.6.7"
"nconf": "~0.6.7",
"sitemap": "~0.6.0",
"cheerio": "~0.12.0",
"request": "~2.25.0",
"reds": "~0.2.4"
},
"bugs": {
"url": "https://github.com/designcreateplay/NodeBB/issues"

0
plugins/.gitignore vendored Normal file
View File

View File

@@ -6,15 +6,6 @@
margin-bottom: 10px;
padding: 10px;
cursor: move;
width: 800px;
}
.entry-row {
border-radius: 10px;
margin-bottom: 10px;
padding: 10px;
cursor: move;
width: 695px;
}
input.description {
@@ -94,6 +85,10 @@
&:hover {
background-color: rgba(128, 128, 128, 0.2);
}
&.no-themes {
font-style: italic;
}
}
}
@@ -141,4 +136,22 @@
}
}
}
.plugins {
li {
list-style-type: none;
background: rgba(64, 64, 64, 0.05);
padding: 1em;
border-left: 5px solid #08c;
h2 {
font-size: 16px;
margin: 0;
}
p {
font-size: 12px;
}
}
}
}

View File

@@ -20,11 +20,6 @@
&:last-child li {
border-bottom: 0;
}
color: #333;
&:hover {
color: #333
}
}
ul {
@@ -55,6 +50,7 @@
.topic-row {
border-radius: 5px;
padding-left: 20px;
border: 1px solid rgba(0, 0, 0, 0.2);
small {
vertical-align: 2px;
@@ -150,7 +146,8 @@
line-height: 16px;
margin-left: 1px;
padding: 5px 5px 5px 0px;
list-style-type: none;
li {
clear: both;
line-height: 16px;
@@ -167,6 +164,7 @@
p {
display: block;
padding-left:5px;
padding-top: 2px;
overflow: hidden;
height: 32px;
margin-bottom: 0.5em;
@@ -208,7 +206,6 @@
}
border-radius: 5px;
background: white;
padding: 0;
margin-bottom: 20px;

View File

@@ -201,6 +201,15 @@ footer.footer {
display:inline-block;
vertical-align:top;
}
.account-online-status {
.icon-circle-blank {
color:red;
}
.icon-circle {
color:green;
}
}
.user-profile-picture {
width:128px;
@@ -225,6 +234,10 @@ footer.footer {
div {
color: #333;
margin-bottom: 10px;
cursor: pointer;
p {
color: #333;
}
}
span {
padding-top: 10px;
@@ -346,7 +359,7 @@ footer.footer {
}
}
a:hover {
a:hover, .btn-link:hover, .btn-link:active, .btn-link:focus {
text-decoration:none;
}
@@ -379,7 +392,7 @@ body .navbar .nodebb-inline-block {
#admin-redis-info {
span {
display:inline-block;
width:200px;
width:220px;
}
}
@@ -399,10 +412,12 @@ body .navbar .nodebb-inline-block {
.icon-circle {
font-size: 12px;
color: green;
margin-right:3px;
}
.icon-circle-blank {
font-size: 12px;
color: red;
margin-right:3px;
}
}
@@ -497,13 +512,15 @@ body .navbar .nodebb-inline-block {
.post-window {
position: fixed;
bottom: 45px;
height: 450px;
display: none;
height: 350px;
visibility: hidden;
> div {
position: absolute;
height: 100%;
background: rgba(64, 64, 64, 0.6);
visibility: visible;
.btn-toolbar {
width: 90%;
@@ -511,18 +528,22 @@ body .navbar .nodebb-inline-block {
span {
color: white;
&:hover {
text-decoration: none;
}
}
}
input {
width: 100%;
width: 98%;
text-align: center;
background: rgba(255, 255, 255, 0.9);
border: none;
padding: 0.5em 0;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
border-radius: 0px;
margin: 1% 1% 2% 1%;
}
textarea {
@@ -531,17 +552,54 @@ body .navbar .nodebb-inline-block {
padding: 0.5em;
display: block;
width: 90%;
margin: 1em auto;
margin: 0.5em auto;
resize: none;
color: white;
height: 330px;
height: 200px;
}
#imagedrop {
text-align:center;
color:white;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height:230px;
line-height:230px;
font-size:20px;
vertical-align: middle;
}
#imagelist {
position: absolute;
bottom: 5px;
left: 0px;
padding-left:2em;
div {
margin-right:5px;
}
span {
line-height:20px;
float:left;
}
button {
padding-left:5px;
}
}
}
}
@media (max-width: 979px) {
.post-window {
position: static;
position: relative;
bottom: 0px !important;
> div {
position: static;
width: 100% !important;
}
}
}
@@ -693,6 +751,10 @@ body .navbar .nodebb-inline-block {
}
}
#mark-allread-btn {
margin-bottom:15px;
}
@-webkit-keyframes scroll-2 /* Safari and Chrome */
{
0% {top: 0px;}
@@ -761,3 +823,24 @@ body .navbar .nodebb-inline-block {
-webkit-animation: scroll-0 6s ease 0.5s infinite normal;
animation: scroll-0 6s ease 0.5s infinite normal;/* Safari and Chrome: */
}
.form-search {
float: left;
margin-top: 5px;
margin-bottom:0px;
}
.search-result-post {
width: 100%;
line-height: 16px;
padding: 5px;
overflow:hidden;
img {
display: block;
float: left;
width:48px;
height:48px;
padding-right:10px;
}
}

View File

@@ -6,10 +6,13 @@
clear: both;
.profile-image-block {
background: white;
display: inline-block;
text-align: center;
font-size: 12px;
.stats {
clear: both;
}
}
li {
@@ -18,18 +21,28 @@
&.deleted {
-moz-opacity: 0.30;
opacity: 0.30;
height:30px;
overflow-y:hidden;
}
&.deleted-expanded {
height:100%;
overflow-y:default;
}
}
.profile-block, .post-block {
border: 1px solid #f0f0f0;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 5px;
padding: 10px;
p {
line-height: 1.75em;
}
}
.profile-block {
background: #fafafa;
background: rgba(0, 0, 0, 0.02);
margin-right: -11px;
margin-left: -11px;
margin-bottom: -11px;
@@ -43,6 +56,9 @@
img.hidden-desktop {
max-width: 10px;
max-height: 10px;
padding-top: 5px;
margin-right: 5px;
}
}
.post-content {
@@ -51,18 +67,21 @@
word-wrap: break-word;
}
.post-images{
padding: 2px 5px 0 5px;
}
.post-block {
.post-buttons {
font-size: 12px;
float: right;
margin-right: 5px;
button {
button, a {
display: inline-block;
padding-left: 15px;
padding-right: 15px;
padding: 0px 15px;
border: none;
border-left: 1px solid #f0f0f0;
border-left: 1px solid rgba(0, 0, 0, 0.06);
cursor: pointer;
background: none;
font-size: 12px;
@@ -77,14 +96,20 @@
//theme this to make it yellow eventually
}
}
background: #fff;
}
&.deleted {
-moz-opacity: 0.30;
opacity: 0.30;
}
/*http://stackoverflow.com/questions/11037517/bootstrap-making-responsive-changes-to-layout*/
@media (max-width: 979px) {
.span12-tablet {
width: 100% !important;
margin-left:0px;
*width: 100% !important;
}
}
.main-post {
h3 {
@@ -164,3 +189,5 @@
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -31,23 +31,25 @@ var ajaxify = {};
var url = url.replace(/\/$/, "");
var hash = window.location.hash;
if(url.indexOf(RELATIVE_PATH.slice(1)) !== -1) {
url = url.slice(RELATIVE_PATH.length);
}
var tpl_url = templates.get_custom_map(url);
var tpl_url = templates.get_custom_map(url.split('?')[0]);
if (tpl_url == false && !templates[url]) {
if(url === '' || url === '/') {
tpl_url = 'home';
} else {
tpl_url = url.split('/')[0];
tpl_url = url.split('/')[0].split('?')[0];
}
} else if (templates[url]) {
tpl_url = url;
}
if (templates.is_available(tpl_url) && !templates.force_refresh(tpl_url)) {
if (quiet !== true) {
window.history.pushState({
@@ -66,19 +68,26 @@ var ajaxify = {};
}
app.process_page();
jQuery('#content, #footer').stop(true, true).fadeIn(200);
jQuery('#content, #footer').stop(true, true).fadeIn(200, function() {
if(window.location.hash)
hash = window.location.hash;
if(hash)
app.scrollToPost(hash.substr(1));
});
}, url, template);
socket.emit('api:meta.buildTitle', url, function(title) {
document.title = title;
});
return true;
}
return false;
}
ajaxify.onclick = function(ev) {
}
$('document').ready(function() {
if (!window.history || !window.history.pushState) return; // no ajaxification for old browsers
@@ -87,6 +96,9 @@ var ajaxify = {};
// Enhancing all anchors to ajaxify...
$(document.body).on('click', 'a', function(e) {
if (this.href == window.location.href + "#") return;
if(this.href.slice(-1) === "#") return;
var url = this.href.replace(rootUrl +'/', '');
if (this.target !== '') return;

View File

@@ -5,11 +5,12 @@ var socket,
(function() {
var showWelcomeMessage = false;
function loadConfig() {
$.ajax({
url: RELATIVE_PATH + '/config.json?v=' + new Date().getTime(),
url: RELATIVE_PATH + '/api/config',
success: function(data) {
API_URL = data.api_url;
@@ -21,8 +22,10 @@ var socket,
socket.on('event:connect', function(data) {
console.log('connected to nodebb socket: ', data);
app.username = data.username;
app.showLoginMessage();
});
socket.on('event:alert', function(data) {
app.alert(data);
});
@@ -43,6 +46,7 @@ var socket,
});
}, 1000);
reconnecting = false;
reconnectTries = 0;
socket.emit('api:updateHeader', { fields: ['username', 'picture', 'userslug'] });
}
});
@@ -86,9 +90,11 @@ var socket,
uid = el.parents('li').attr('data-uid');
if (uid && jQuery.inArray(uid, users) !== -1) {
el.prepend('<i class="icon-circle"></i>&nbsp;');
el.find('i').remove();
el.prepend('<i class="icon-circle"></i>');
} else {
el.prepend('<i class="icon-circle-blank"></i>&nbsp;');
el.find('i').remove();
el.prepend('<i class="icon-circle-blank"></i>');
}
el.processed = true;
@@ -190,7 +196,29 @@ var socket,
}
}
app.alertSuccess = function(message, timeout) {
if(!timeout)
timeout = 2000;
app.alert({
title: 'Success',
message: message,
type: 'success',
timeout: timeout
});
}
app.alertError = function(message, timeout) {
if(!timeout)
timeout = 2000;
app.alert({
title: 'Error',
message: message,
type: 'error',
timeout: timeout
});
}
app.current_room = null;
app.enter_room = function(room) {
@@ -208,57 +236,148 @@ var socket,
}
};
app.populate_online_users = function() {
var uids = [];
jQuery('.post-row').each(function() {
uids.push(this.getAttribute('data-uid'));
});
socket.emit('api:user.get_online_users', uids);
}
app.process_page = function() {
function populate_online_users() {
var uids = [];
jQuery('.post-row').each(function() {
uids.push(this.getAttribute('data-uid'));
});
socket.emit('api:user.get_online_users', uids);
}
// here is where all modules' onNavigate should be called, I think.
require(['mobileMenu'], function(mobileMenu) {
mobileMenu.onNavigate();
});
app.populate_online_users();
populate_online_users();
var url = window.location.href,
parts = url.split('/'),
active = parts[parts.length-1];
jQuery('#main-nav li').removeClass('active');
if(active) {
jQuery('#main-nav li a').each(function() {
var href = this.getAttribute('href');
if(active.match(/^users/))
active = 'users';
if (href && href.match(active)) {
jQuery(this.parentNode).addClass('active');
return false;
}
});
}
setTimeout(function() {
window.scrollTo(0, 1); // rehide address bar on mobile after page load completes.
}, 100);
}
app.showLoginMessage = function() {
function showAlert() {
app.alert({
type: 'success',
title: 'Welcome Back ' + app.username + '!',
message: 'You have successfully logged in!',
timeout: 5000
});
}
if(showWelcomeMessage) {
showWelcomeMessage = false;
if(document.readyState !== 'complete') {
$(document).ready(showAlert);
} else {
showAlert();
}
}
}
app.addCommasToNumbers = function() {
$('.formatted-number').each(function(index, element) {
$(element).html(app.addCommas($(element).html()));
});
}
app.createNewPosts = function(data) {
data.posts[0].display_moderator_tools = 'none';
var html = templates.prepare(templates['topic'].blocks['posts']).parse(data),
uniqueid = new Date().getTime(),
tempContainer = jQuery('<div id="' + uniqueid + '"></div>')
.appendTo("#post-container")
.hide()
.append(html)
.fadeIn('slow');
for(var x=0,numPosts=data.posts.length;x<numPosts;x++) {
socket.emit('api:post.privileges', data.posts[x].pid);
}
tempContainer.replaceWith(tempContainer.contents());
infiniteLoaderActive = false;
app.populate_online_users();
app.addCommasToNumbers();
}
app.infiniteLoaderActive = false;
app.loadMorePosts = function(tid, callback) {
if(app.infiniteLoaderActive)
return;
app.infiniteLoaderActive = true;
socket.emit('api:topic.loadMore', {
tid: tid,
after: document.querySelectorAll('#post-container li[data-pid]').length
}, function(data) {
app.infiniteLoaderActive = false;
if(data.posts.length) {
app.createNewPosts(data);
if(callback)
callback();
}
});
}
app.scrollToPost = function(pid) {
if(!pid)
return;
var container = $(document.body),
scrollTo = $('#post_anchor_' + pid);
if(!scrollTo.length) {
var tid = $('#post-container').attr('data-tid');
app.loadMorePosts(tid, function() {
app.scrollToPost(pid);
});
return;
}
container.animate({
scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop() - $('#header-menu').height()
});
}
jQuery('document').ready(function() {
// On menu click, change "active" state
var menuEl = document.querySelector('.nav'),
liEls = menuEl.querySelectorAll('li'),
logoutEl = document.getElementById('logout'),
parentEl;
menuEl.addEventListener('click', function(e) {
parentEl = e.target.parentNode;
if (parentEl.nodeName === 'LI') {
for(var x=0,numLis=liEls.length;x<numLis;x++) {
if (liEls[x] !== parentEl) liEls[x].className = '';
else parentEl.className = 'active';
}
}
}, false);
addTouchEvents();
$('#search-form').on('submit', function() {
var input = $(this).find('input');
ajaxify.go("search/"+input.val(), null, "search");
input.val('');
return false;
})
});
showWelcomeMessage = location.href.indexOf('loggedin') !== -1;
loadConfig();
function addTouchEvents() {
return; // later.

View File

@@ -11,27 +11,65 @@
var postcount = $('#postcount');
postcount.html(app.addCommas(postcount.html()));
var editLink = $('#editLink');
var followBtn = $('#follow-btn');
if( yourid !== theirid) {
editLink.hide();
if(isFollowing)
var unfollowBtn = $('#unfollow-btn');
if(yourid !== theirid) {
if(isFollowing) {
followBtn.hide();
else
unfollowBtn.show();
} else {
followBtn.show();
unfollowBtn.hide();
}
}
else {
followBtn.hide();
}
followBtn.on('click', function() {
followBtn.remove();
socket.emit('api:user.follow', {uid: theirid});
socket.emit('api:user.follow', {uid: theirid}, function(success) {
var username = $('.account-username a').html();
if(success) {
followBtn.hide();
unfollowBtn.show();
app.alertSuccess('You are now following ' + username + '!');
} else {
app.alertError('There was an error following' + username + '!');
}
});
return false;
});
unfollowBtn.on('click', function() {
socket.emit('api:user.unfollow', {uid: theirid}, function(success) {
var username = $('.account-username a').html();
if(success) {
followBtn.show();
unfollowBtn.hide();
app.alertSuccess('You are no longer following ' + username + '!');
} else {
app.alertError('There was an error unfollowing ' + username + '!');
}
});
return false;
});
$('.user-recent-posts .topic-row').on('click', function() {
ajaxify.go($(this).attr('topic-url'));
});
var onlineStatus = $('.account-online-status');
socket.on('api:user.isOnline', function(online) {
if(online) {
onlineStatus.find('span span').text('online');
onlineStatus.find('i').attr('class', 'icon-circle');
} else {
onlineStatus.find('span span').text('offline');
onlineStatus.find('i').attr('class', 'icon-circle-blank');
}
});
socket.emit('api:user.isOnline', theirid);
});
}());

View File

@@ -85,7 +85,11 @@ $(document).ready(function() {
type: type
};
socket.emit('api:user.changePicture', userData);
socket.emit('api:user.changePicture', userData, function(success) {
if(!success) {
app.alertError('There was an error changing picture!');
}
});
}
var selectedImageType = '';
@@ -102,7 +106,21 @@ $(document).ready(function() {
signature:$('#inputSignature').val()
};
socket.emit('api:user.updateProfile', userData);
socket.emit('api:user.updateProfile', userData, function(data) {
if(data.success) {
app.alertSuccess('Your profile has been updated successfully!');
if(data.picture) {
$('#user-current-picture').attr('src', data.picture);
$('#user_label img').attr('src', data.picture);
}
if(data.gravatarpicture) {
$('#user-gravatar-picture').attr('src', data.gravatarpicture);
gravatarPicture = data.gravatarpicture;
}
} else {
app.alertError('There was an error updating your profile!');
}
});
return false;
});
@@ -231,24 +249,29 @@ $(document).ready(function() {
password_confirm.on('keyup', onPasswordConfirmChanged);
$('#changePasswordBtn').on('click', function() {
if(passwordvalid && passwordsmatch && currentPassword.val()) {
socket.emit('api:user.changePassword', {
'currentPassword': currentPassword.val(),
'newPassword': password.val()
socket.emit('api:user.changePassword', {'currentPassword': currentPassword.val(),'newPassword': password.val() }, function(data) {
currentPassword.val('');
password.val('');
password_confirm.val('');
password_notify.html('');
password_confirm_notify.html('');
passwordsmatch = false;
passwordvalid = false;
if(data.err) {
app.alertError(data.err);
return;
}
app.alertSuccess('Your password is updated!');
});
}
return false;
});
socket.on('api:user.changePassword', function(data) {
currentPassword.val('');
password.val('');
password_confirm.val('');
password_notify.html('');
password_confirm_notify.html('');
passwordsmatch = false;
passwordvalid = false;
});
}());
});

View File

@@ -0,0 +1,20 @@
(function() {
var yourid = templates.get('yourid'),
theirid = templates.get('theirid');
$(document).ready(function() {
var editLink = $('#editLink');
var settingsLink = $('#settingsLink');
if(yourid === "0") {
editLink.hide();
settingsLink.hide();
}
else if(yourid !== theirid) {
editLink.hide();
settingsLink.hide();
}
});
}());

View File

@@ -0,0 +1,22 @@
$(document).ready(function() {
$('#submitBtn').on('click', function() {
var settings = {
showemail: $('#showemailCheckBox').is(':checked')?1:0
};
socket.emit('api:user.saveSettings', settings, function(success) {
if(success) {
app.alertSuccess('Settings saved!');
} else {
app.alertError('There was an error saving settings!');
}
});
return false;
});
});

View File

@@ -74,6 +74,30 @@ jQuery('.blockclass').each(function() {
jQuery('.category_name, .category_description, .blockclass').on('change', function(ev) {
modified(ev.target);
});
jQuery('.entry-row button').each(function(index, element) {
var disabled = $(element).attr('data-disabled');
if(disabled == "0" || disabled == "")
$(element).html('Disable');
else
$(element).html('Enable');
});
jQuery('.entry-row button').on('click', function(ev) {
var btn = jQuery(this);
var categoryRow = btn.parents('li');
var cid = categoryRow.attr('data-cid');
var disabled = btn.html() == "Disable" ? "1":"0";
categoryRow.remove();
modified_categories[cid] = modified_categories[cid] || {};
modified_categories[cid]['disabled'] = disabled;
save();
return false;
});
});
}());

View File

@@ -0,0 +1,38 @@
var nodebb_admin = nodebb_admin || {};
(function() {
var plugins = {
init: function() {
var pluginsList = $('.plugins'),
numPlugins = pluginsList[0].querySelectorAll('li').length,
pluginID, pluginTgl;
if (numPlugins > 0) {
pluginsList.on('click', 'button[data-action="toggleActive"]', function() {
pluginID = $(this).parents('li').attr('data-plugin-id');
socket.emit('api:admin.plugins.toggle', pluginID);
});
socket.on('api:admin.plugins.toggle', function(status) {
pluginTgl = document.querySelector('.plugins li[data-plugin-id="' + status.id + '"] button');
pluginTgl.innerHTML = '<i class="icon-off"></i> ' + (status.active ? 'Dea' : 'A') + 'ctivate';
app.alert({
alert_id: 'plugin_toggled_' + status.id,
title: 'Plugin Enabled',
message: 'You may need to restart NodeBB in order for these changes to be reflected.',
type: 'notify',
timeout: 5000
})
});
} else {
pluginsList.append('<li><p><i>No plugins found.</i></p></li>');
}
}
};
jQuery(document).ready(function() {
nodebb_admin.plugins = plugins;
nodebb_admin.plugins.init();
});
})();

View File

@@ -7,7 +7,7 @@ var nodebb_admin = (function(nodebb_admin) {
themes.render = function(bootswatch) {
var themeFrag = document.createDocumentFragment(),
themeEl = document.createElement('li'),
themeContainer = document.querySelector('#content .themes'),
themeContainer = document.querySelector('#bootstrap_themes'),
numThemes = bootswatch.themes.length;
for(var x=0;x<numThemes;x++) {
@@ -39,33 +39,36 @@ var nodebb_admin = (function(nodebb_admin) {
(function() {
var scriptEl = document.createElement('script');
scriptEl.src = 'http://api.bootswatch.com?callback=nodebb_admin.themes.render';
scriptEl.src = 'http://api.bootswatch.com/2/?callback=nodebb_admin.themes.render';
document.body.appendChild(scriptEl);
var themeContainer = document.querySelector('#content .themes');
themeContainer.addEventListener('click', function(e) {
if (e.target.hasAttribute('data-action')) {
switch(e.target.getAttribute('data-action')) {
case 'preview':
var cssSrc = $(e.target).parents('li').attr('data-css'),
cssEl = document.getElementById('base-theme');
var bootstrapThemeContainer = document.querySelector('#bootstrap_themes'),
installedThemeContainer = document.querySelector('#installed_themes'),
themeEvent = function(e) {
if (e.target.hasAttribute('data-action')) {
switch(e.target.getAttribute('data-action')) {
case 'preview':
var cssSrc = $(e.target).parents('li').attr('data-css'),
cssEl = document.getElementById('base-theme');
cssEl.href = cssSrc;
break;
case 'use':
var parentEl = $(e.target).parents('li'),
cssSrc = parentEl.attr('data-css'),
cssName = parentEl.attr('data-theme');
socket.emit('api:config.set', {
key: 'theme:id', value: 'bootswatch:' + cssName
});
socket.emit('api:config.set', {
key: 'theme:src', value: cssSrc
});
break;
cssEl.href = cssSrc;
break;
case 'use':
var parentEl = $(e.target).parents('li'),
cssSrc = parentEl.attr('data-css'),
cssName = parentEl.attr('data-theme');
socket.emit('api:config.set', {
key: 'theme:id', value: 'bootswatch:' + cssName
});
socket.emit('api:config.set', {
key: 'theme:src', value: cssSrc
});
break;
}
}
}
}, false);
};
bootstrapThemeContainer.addEventListener('click', themeEvent);
installedThemeContainer.addEventListener('click', themeEvent);
var revertEl = document.getElementById('revert_theme');
revertEl.addEventListener('click', function() {
@@ -76,4 +79,40 @@ var nodebb_admin = (function(nodebb_admin) {
}
});
}, false);
// Installed Themes
socket.emit('api:admin.themes.getInstalled', function(themes) {
var instListEl = document.getElementById('installed_themes'),
themeFrag = document.createDocumentFragment(),
liEl = document.createElement('li');
if (themes.length > 0) {
for(var x=0,numThemes=themes.length;x<numThemes;x++) {
liEl.setAttribute('data-theme', themes[x].id);
liEl.setAttribute('data-css', themes[x].src);
liEl.innerHTML = '<img src="' + themes[x].screenshot + '" />' +
'<div>' +
'<div class="pull-right">' +
'<button class="btn btn-primary" data-action="use">Use</button> ' +
'<button class="btn" data-action="preview">Preview</button>' +
'</div>' +
'<h4>' + themes[x].name + '</h4>' +
'<p>' +
themes[x].description +
(themes[x].url ? ' (<a href="' + themes[x].url + '">Homepage</a>)' : '') +
'</p>' +
'</div>' +
'<div class="clear">';
themeFrag.appendChild(liEl.cloneNode(true));
}
} else {
// No themes found
liEl.className = 'no-themes';
liEl.innerHTML = 'No installed themes found';
themeFrag.appendChild(liEl);
}
instListEl.innerHTML = '';
instListEl.appendChild(themeFrag);
});
})();

View File

@@ -32,6 +32,23 @@ $(document).ready(function() {
socket.emit('api:admin.topics.getMore', {
limit: 10,
after: lastTid
}, function(topics) {
var btnEl = document.getElementById('topics_loadmore');
topics = JSON.parse(topics);
if (topics.length > 0) {
var html = templates.prepare(templates['admin/topics'].blocks['topics']).parse({
topics: topics
}),
topicsListEl = document.querySelector('.topics');
topicsListEl.innerHTML += html;
btnEl.innerHTML = 'Load More Topics';
} else {
// Exhausted all topics
btnEl.className += ' disabled';
btnEl.innerHTML = 'No more topics';
}
});
}
}, false);
@@ -40,9 +57,9 @@ $(document).ready(function() {
var topicEls = topicsListEl.querySelectorAll('li'),
numTopics = topicEls.length;
for(var x=0;x<numTopics;x++) {
if (topicEls[x].getAttribute('data-pinned')) topicEls[x].querySelector('[data-action="pin"]').className += ' active';
if (topicEls[x].getAttribute('data-locked')) topicEls[x].querySelector('[data-action="lock"]').className += ' active';
if (topicEls[x].getAttribute('data-deleted')) topicEls[x].querySelector('[data-action="delete"]').className += ' active';
if (topicEls[x].getAttribute('data-pinned') === '1') topicEls[x].querySelector('[data-action="pin"]').className += ' active';
if (topicEls[x].getAttribute('data-locked') === '1') topicEls[x].querySelector('[data-action="lock"]').className += ' active';
if (topicEls[x].getAttribute('data-deleted') === '1') topicEls[x].querySelector('[data-action="delete"]').className += ' active';
topicEls[x].removeAttribute('data-pinned');
topicEls[x].removeAttribute('data-locked');
topicEls[x].removeAttribute('data-deleted');
@@ -95,24 +112,4 @@ socket.on('api:topic.restore', function(response) {
$(btnEl).removeClass('active');
}
});
socket.on('api:admin.topics.getMore', function(topics) {
var btnEl = document.getElementById('topics_loadmore');
topics = JSON.parse(topics);
console.log(topics);
if (topics.length > 0) {
var html = templates.prepare(templates['admin/topics'].blocks['topics']).parse({
topics: topics
}),
topicsListEl = document.querySelector('.topics');
topicsListEl.innerHTML += html;
btnEl.innerHTML = 'Load More Topics';
} else {
// Exhausted all topics
btnEl.className += ' disabled';
btnEl.innerHTML = 'No more topics';
}
});

View File

@@ -79,8 +79,9 @@
jQuery('document').ready(function() {
var yourid = templates.get('yourid');
var timeoutId = 0;
var yourid = templates.get('yourid'),
timeoutId = 0,
loadingMoreUsers = false;
var url = window.location.href,
parts = url.split('/'),
@@ -114,8 +115,7 @@
socket.removeAllListeners('api:admin.user.search');
socket.on('api:admin.user.search', function(data) {
var html = templates.prepare(templates['admin/users'].blocks['users']).parse({
var html = templates.prepare(templates['admin/users'].blocks['users']).parse({
users: data
}),
userListEl = document.querySelector('.users');
@@ -139,6 +139,45 @@
initUsers();
});
function onUsersLoaded(users) {
var html = templates.prepare(templates['admin/users'].blocks['users']).parse({ users: users });
$('#users-container').append(html);
}
function loadMoreUsers() {
var set = '';
if(active === 'latest') {
set = 'users:joindate';
} else if(active === 'sort-posts') {
set = 'users:postcount';
} else if(active === 'sort-reputation') {
set = 'users:reputation';
}
if(set) {
loadingMoreUsers = true;
socket.emit('api:users.loadMore', {
set: set,
after: $('#users-container').children().length
}, function(data) {
if(data.users.length) {
onUsersLoaded(data.users);
}
loadingMoreUsers = false;
});
}
}
$('#load-more-users-btn').on('click', loadMoreUsers);
$(window).off('scroll').on('scroll', function() {
var bottom = (document.body.offsetHeight - $(window).height()) * 0.9;
if (document.body.scrollTop > bottom && !loadingMoreUsers) {
loadMoreUsers();
}
});
});
}());

View File

@@ -6,24 +6,28 @@
googleEl = document.getElementById('google-share'),
twitter_url = templates.get('twitter-intent-url'),
facebook_url = templates.get('facebook-share-url'),
google_url = templates.get('google-share-url');
google_url = templates.get('google-share-url'),
loadingMoreTopics = false;
app.enter_room(room);
twitterEl.addEventListener('click', function() {
window.open(twitter_url, '_blank', 'width=550,height=420,scrollbars=no,status=no');
return false;
}, false);
facebookEl.addEventListener('click', function() {
window.open(facebook_url, '_blank', 'width=626,height=436,scrollbars=no,status=no');
return false;
}, false);
googleEl.addEventListener('click', function() {
window.open(google_url, '_blank', 'width=500,height=570,scrollbars=no,status=no');
return false;
}, false);
var new_post = document.getElementById('new_post');
new_post.onclick = function() {
require(['composer'], function(cmp) {
cmp.push(0, cid);
cmp.push(0, cid);
});
}
@@ -31,7 +35,8 @@
'event:new_topic'
]);
socket.on('event:new_topic', function(data) {
function onNewTopic(data) {
var html = templates.prepare(templates['category'].blocks['topics']).parse({ topics: [data] }),
topic = document.createElement('div'),
container = document.getElementById('topics-container'),
@@ -56,10 +61,9 @@
container.insertBefore(topic, null);
$(topic).hide().fadeIn('slow');
}
}
ajaxify.enable();
});
socket.on('event:new_topic', onNewTopic);
socket.emit('api:categories.getRecentReplies', cid);
socket.on('api:categories.getRecentReplies', function(posts) {
@@ -76,15 +80,54 @@
for (var i=0,numPosts=posts.length; i<numPosts; i++) {
var dateString = utils.relativeTime(posts[i].timestamp);
li.setAttribute('data-pid', posts[i].pid);
li.innerHTML = '<img title="' + posts[i].username + '" style="width: 48px; height: 48px; /*temporary*/" class="img-polaroid" src="' + posts[i].picture + '" class="" />' +
'<p>' +
'<strong>' + posts[i].username + '</strong>: ' + posts[i].content +
'</p>' +
'<span>posted ' + utils.relativeTime(posts[i].timestamp) + ' ago</span>';
li.innerHTML = '<a href="/users/' + posts[i].userslug + '"><img title="' + posts[i].username + '" style="width: 48px; height: 48px; /*temporary*/" class="img-polaroid" src="' + posts[i].picture + '" class="" /></a>' +
'<a href="/topic/'+ posts[i].topicSlug + '#' + posts[i].pid + '">' +
'<p>' +
'<strong>' + posts[i].username + '</strong>: ' + posts[i].content +
'</p>' +
'<span>posted ' + utils.relativeTime(posts[i].timestamp) + ' ago</span>' +
'</a>';
frag.appendChild(li.cloneNode(true));
recent_replies.appendChild(frag);
}
});
function onTopicsLoaded(topics) {
var html = templates.prepare(templates['category'].blocks['topics']).parse({ topics: topics }),
container = $('#topics-container');
jQuery('#topics-container, .category-sidebar').removeClass('hidden');
jQuery('#category-no-topics').remove();
container.append(html);
}
function loadMoreTopics(cid) {
loadingMoreTopics = true;
socket.emit('api:category.loadMore', {
cid: cid,
after: $('#topics-container').children().length
}, function(data) {
if(data.topics.length) {
onTopicsLoaded(data.topics);
}
loadingMoreTopics = false;
});
}
$(window).off('scroll').on('scroll', function(ev) {
var bottom = (document.body.offsetHeight - $(window).height()) * 0.9;
if (document.body.scrollTop > bottom && !loadingMoreTopics) {
loadMoreTopics(cid);
}
});
})();

View File

@@ -9,12 +9,7 @@
if(parseInt(followersCount, 10) === 0) {
$('#no-followers-notice').show();
}
var editLink = $('#editLink');
if(yourid !== theirid) {
editLink.hide();
}
$('.reputation').each(function(index, element) {
$(element).html(app.addCommas($(element).html()));
});

View File

@@ -9,20 +9,25 @@
if(parseInt(followingCount, 10) === 0) {
$('#no-following-notice').show();
}
var editLink = $('#editLink');
if(yourid !== theirid) {
editLink.hide();
$('.unfollow-btn').hide();
}
else {
$('.unfollow-btn').on('click',function() {
var removeBtn = $(this);
var unfollowBtn = $(this);
var followingUid = $(this).attr('followingUid');
removeBtn.parent().remove();
socket.emit('api:user.unfollow', {uid: followingUid});
socket.emit('api:user.unfollow', {uid: followingUid}, function(success) {
var username = unfollowBtn.attr('data-username');
if(success) {
unfollowBtn.parent().remove();
app.alertSuccess('You are no longer following ' + username + '!');
} else {
app.alertError('There was an error unfollowing ' + username + '!');
}
});
return false;
});
}

View File

@@ -6,7 +6,7 @@
user_label = document.getElementById('user_label'),
active_record = document.getElementById('active_record'),
right_menu = document.getElementById('right-menu');
socket.emit('user.count', {});
socket.on('user.count', function(data) {
num_users.innerHTML = "We currently have <b>" + data.count + "</b> registered users.";
@@ -41,9 +41,9 @@
});
socket.emit('api:updateHeader', { fields: ['username', 'picture', 'userslug'] });
socket.on('api:updateHeader', function(data) {
var rightMenu = $('#right-menu');
if (data.uid > 0) {
@@ -52,14 +52,14 @@
if(data['userslug'])
userLabel.attr('href','/users/' + data['userslug']);
if(data['picture'])
userLabel.find('img').attr('src',data['picture']+"?s=24&default=identicon");
userLabel.find('img').attr('src',data['picture']);
if(data['username'])
userLabel.find('span').html(data['username']);
}
else {
var userli = $('<li> \
<a id="user_label" href="/users/'+data['userslug']+'"> \
<img src="'+data['picture']+"?s=24&default=identicon"+'"/> \
<img src="'+data['picture']+'"/> \
<span>'+data['username']+'</span> \
</a> \
</li>');
@@ -85,9 +85,17 @@
// Notifications dropdown
var notifContainer = document.getElementsByClassName('notifications')[0],
notifTrigger = notifContainer.querySelector('a'),
notifList = document.getElementById('notif-list');
notifTrigger.addEventListener('click', function() {
if (notifContainer.className.indexOf('open') === -1) socket.emit('api:notifications.get');
notifList = document.getElementById('notif-list'),
notifIcon = document.querySelector('.notifications a i');
notifTrigger.addEventListener('click', function(e) {
e.preventDefault();
if (notifContainer.className.indexOf('open') === -1) {
socket.emit('api:notifications.get');
socket.emit('api:notifications.mark_all_read', null, function() {
notifIcon.className = 'icon-circle-blank';
utils.refreshTitle();
});
}
});
notifList.addEventListener('click', function(e) {
var target;
@@ -100,16 +108,15 @@
var nid = parseInt(target.getAttribute('data-nid'));
if (nid > 0) socket.emit('api:notifications.mark_read', nid);
}
})
});
socket.on('api:notifications.get', function(data) {
var notifFrag = document.createDocumentFragment(),
notifEl = document.createElement('li'),
notifIcon = document.querySelector('.notifications a i'),
numRead = data.read.length,
numUnread = data.unread.length,
x;
notifList.innerHTML = '';
if (data.read.length + data.unread.length > 0) {
if ((data.read.length + data.unread.length) > 0) {
for(x=0;x<numUnread;x++) {
notifEl.setAttribute('data-nid', data.unread[x].nid);
notifEl.className = 'unread';
@@ -128,24 +135,16 @@
}
notifList.appendChild(notifFrag);
socket.emit('api:notifications.removeFlag');
notifIcon.className = 'icon-circle-blank';
});
socket.on('api:notifications.hasFlag', function(flag) {
var notifIcon = document.querySelector('.notifications a i');
if(notifIcon) {
if (flag > 0) notifIcon.className = 'icon-circle active';
else notifIcon.className = 'icon-circle-blank';
}
if (data.unread.length > 0) notifIcon.className = 'icon-circle active';
else notifIcon.className = 'icon-circle-blank';
});
socket.on('event:new_notification', function() {
document.querySelector('.notifications a i').className = 'icon-circle active';
utils.refreshTitle();
});
socket.emit('api:notifications.hasFlag');
socket.on('chatMessage', function(data) {
var username = data.username;
var fromuid = data.fromuid;
var message = data.message;

View File

@@ -28,7 +28,7 @@
data: loginData,
success: function(data, textStatus, jqXHR) {
$('#login-error-notify').hide();
window.location.replace(RELATIVE_PATH + "/");
window.location.replace(RELATIVE_PATH + "/?loggedin");
},
error : function(data, textStatus, jqXHR) {
$('#login-error-notify').show().delay(1000).fadeOut(250);

View File

@@ -1,4 +1,6 @@
(function() {
var loadingMoreTopics = false;
app.enter_room('recent_posts');
ajaxify.register_events([
@@ -44,7 +46,35 @@
socket.on('event:new_post', function(data) {
++newPostCount;
updateAlertText();
});
function onTopicsLoaded(topics) {
var html = templates.prepare(templates['recent'].blocks['topics']).parse({ topics: topics }),
container = $('#topics-container');
$('#category-no-topics').remove();
container.append(html);
}
function loadMoreTopics() {
loadingMoreTopics = true;
socket.emit('api:topics.loadMoreRecentTopics', {after:$('#topics-container').children().length}, function(data) {
if(data.topics && data.topics.length) {
onTopicsLoaded(data.topics);
}
loadingMoreTopics = false;
});
}
$(window).off('scroll').on('scroll', function() {
var bottom = (document.body.offsetHeight - $(window).height()) * 0.9;
if (document.body.scrollTop > bottom && !loadingMoreTopics) {
loadMoreTopics();
}
});
})();

View File

@@ -22,12 +22,13 @@
if(username.value.length < 3) {
username_notify.innerHTML = 'Username too short';
username_notify.className = 'label label-important';
}
else if(!usernamevalid) {
} else if(username.value.length > 13) {
username_notify.innerHTML = 'Username too long';
username_notify.className = 'label label-important';
} else if(!usernamevalid) {
username_notify.innerHTML = 'Invalid username';
username_notify.className = 'label label-important';
}
else {
} else {
socket.emit('user.exists', {username: username.value});
}
});

View File

@@ -0,0 +1,14 @@
(function() {
$(document).ready(function() {
var searchQuery = $('#topics-container').attr('data-search-query');
$('.search-result-text').each(function() {
var text = $(this).html();
var regex = new RegExp(searchQuery, 'gi');
text = text.replace(regex, '<span class="label label-success">'+searchQuery+'</span>');
$(this).html(text);
});
});
})();

View File

@@ -8,18 +8,12 @@
deleted: templates.get('deleted'),
pinned: templates.get('pinned')
},
topic_name = templates.get('topic_name'),
infiniteLoaderActive = false;
function addCommasToNumbers() {
$('.formatted-number').each(function(index, element) {
$(element).html(app.addCommas($(element).html()));
});
}
topic_name = templates.get('topic_name');
jQuery('document').ready(function() {
addCommasToNumbers();
app.addCommasToNumbers();
var room = 'topic_' + tid,
adminTools = document.getElementById('thread-tools');
@@ -207,18 +201,17 @@
}, false);
// Infinite scrolling of posts
window.addEventListener('scroll', function() {
var windowHeight = document.body.offsetHeight - $(window).height(),
half = windowHeight / 2;
$(window).off('scroll').on('scroll', function() {
var bottom = (document.body.offsetHeight - $(window).height()) * 0.9;
if (document.body.scrollTop > half && !infiniteLoaderActive) {
infiniteLoaderActive = true;
socket.emit('api:topic.loadMore', {
tid: tid,
after: document.querySelectorAll('#post-container li[data-pid]').length
});
if (document.body.scrollTop > bottom && !app.infiniteLoaderActive && $('#post-container').children().length) {
app.loadMorePosts(tid);
}
});
$('.post-container').on('click', '.deleted', function(ev) {
$(this).toggleClass('deleted-expanded');
});
});
var reply_fn = function() {
@@ -232,7 +225,7 @@
if (thread_state.locked !== '1') {
require(['composer'], function(cmp) {
cmp.push(tid, null, null, selectionText + '\n\n');
cmp.push(tid, null, null, selectionText.length > 0 ? selectionText + '\n\n' : '');
});
}
};
@@ -259,16 +252,14 @@
pid = ids[0],
uid = ids[1];
if (thread_state.locked !== '1') {
var element = $(this).find('i');
if(element.attr('class') == 'icon-star-empty') {
element.attr('class', 'icon-star');
socket.emit('api:posts.favourite', {pid: pid, room_id: app.current_room});
}
else {
element.attr('class', 'icon-star-empty');
socket.emit('api:posts.unfavourite', {pid: pid, room_id: app.current_room});
}
var element = $(this).find('i');
if(element.attr('class') == 'icon-star-empty') {
element.attr('class', 'icon-star');
socket.emit('api:posts.favourite', {pid: pid, room_id: app.current_room});
}
else {
element.attr('class', 'icon-star-empty');
socket.emit('api:posts.unfavourite', {pid: pid, room_id: app.current_room});
}
});
@@ -276,10 +267,7 @@
var pid = ($(this).attr('id') || $(this.parentNode).attr('id')).split('_')[1];
var main = $(this).parents('.main-post');
// if(main.length > 0)
// app.open_post_window('edit', tid, topic_name, pid);
// else
// app.open_post_window('edit', tid, "", pid);
require(['composer'], function(cmp) {
cmp.push(null, null, pid);
});
@@ -316,7 +304,7 @@
'event:topic_deleted', 'event:topic_restored', 'event:topic:locked',
'event:topic_unlocked', 'event:topic_pinned', 'event:topic_unpinned',
'event:topic_moved', 'event:post_edited', 'event:post_deleted', 'event:post_restored',
'api:posts.favourite', 'chatMessage'
'api:posts.favourite'
]);
@@ -324,6 +312,8 @@
var activeEl = $('#thread_active_users');
if(activeEl.length)
activeEl.html(data);
app.populate_online_users();
});
socket.on('event:rep_up', function(data) {
@@ -334,25 +324,7 @@
adjust_rep(-1, data.pid, data.uid);
});
socket.on('event:new_post', function(data) {
data.posts[0].display_moderator_tools = 'none';
var html = templates.prepare(templates['topic'].blocks['posts']).parse(data),
uniqueid = new Date().getTime(),
tempContainer = jQuery('<div id="' + uniqueid + '"></div>')
.appendTo("#post-container")
.hide()
.append(html)
.fadeIn('slow');
for(var x=0,numPosts=data.posts.length;x<numPosts;x++) {
socket.emit('api:post.privileges', data.posts[x].pid);
}
tempContainer.replaceWith(tempContainer.contents());
infiniteLoaderActive = false;
addCommasToNumbers();
});
socket.on('event:new_post', app.createNewPosts);
socket.on('event:topic_deleted', function(data) {
if (data.tid === tid && data.status === 'ok') {

102
public/src/forum/unread.js Normal file
View File

@@ -0,0 +1,102 @@
(function() {
var loadingMoreTopics = false;
app.enter_room('recent_posts');
ajaxify.register_events([
'event:new_topic',
'event:new_post'
]);
var newTopicCount = 0, newPostCount = 0;
$('#new-topics-alert').on('click', function() {
$(this).hide();
});
socket.on('event:new_topic', function(data) {
++newTopicCount;
updateAlertText();
});
function updateAlertText() {
var text = '';
if(newTopicCount > 1)
text = 'There are ' + newTopicCount + ' new topics';
else if(newTopicCount === 1)
text = 'There is 1 new topic';
else
text = 'There are no new topics';
if(newPostCount > 1)
text += ' and ' + newPostCount + ' new posts.';
else if(newPostCount === 1)
text += ' and 1 new post.';
else
text += ' and no new posts.';
text += ' Click here to reload.';
$('#new-topics-alert').html(text).fadeIn('slow');
}
socket.on('event:new_post', function(data) {
++newPostCount;
updateAlertText();
});
$('#mark-allread-btn').on('click', function() {
var btn = $(this);
socket.emit('api:topics.markAllRead', {} , function(success) {
if(success) {
btn.remove();
$('#topics-container').empty();
$('#category-no-topics').removeClass('hidden');
app.alertSuccess('All topics marked as read!');
} else {
app.alertError('There was an error marking topics read!');
}
});
});
function onTopicsLoaded(topics) {
var html = templates.prepare(templates['unread'].blocks['topics']).parse({ topics: topics }),
container = $('#topics-container');
$('#category-no-topics').remove();
container.append(html);
}
function loadMoreTopics() {
loadingMoreTopics = true;
socket.emit('api:topics.loadMoreUnreadTopics', {after:parseInt($('#topics-container').attr('data-next-start'), 10)}, function(data) {
if(data.topics && data.topics.length) {
onTopicsLoaded(data.topics);
$('#topics-container').attr('data-next-start', data.nextStart);
}
loadingMoreTopics = false;
});
}
$(window).off('scroll').on('scroll', function() {
var bottom = (document.body.offsetHeight - $(window).height()) * 0.9;
if (document.body.scrollTop > bottom && !loadingMoreTopics) {
loadMoreTopics();
}
});
if($("body").height() <= $(window).height() && $('#topics-container').children().length)
$('#load-more-btn').show();
$('#load-more-btn').on('click', function() {
loadMoreTopics();
});
})();

View File

@@ -2,6 +2,7 @@
$(document).ready(function() {
var timeoutId = 0;
var loadingMoreUsers = false;
var url = window.location.href,
parts = url.split('/'),
@@ -74,6 +75,46 @@
$(element).html(app.addCommas($(element).html()));
});
function onUsersLoaded(users) {
var html = templates.prepare(templates['users'].blocks['users']).parse({ users: users });
$('#users-container').append(html);
}
function loadMoreUsers() {
var set = '';
if(active === 'users-latest' || active === 'users') {
set = 'users:joindate';
} else if(active === 'users-sort-posts') {
set = 'users:postcount';
} else if(active === 'users-sort-reputation') {
set = 'users:reputation';
}
if(set) {
loadingMoreUsers = true;
socket.emit('api:users.loadMore', {
set: set,
after: $('#users-container').children().length
}, function(data) {
if(data.users.length) {
onUsersLoaded(data.users);
} else {
$('#load-more-users-btn').addClass('disabled');
}
loadingMoreUsers = false;
});
}
}
$('#load-more-users-btn').on('click', loadMoreUsers);
$(window).off('scroll').on('scroll', function() {
var bottom = (document.body.offsetHeight - $(window).height()) * 0.9;
if (document.body.scrollTop > bottom && !loadingMoreUsers) {
loadMoreUsers();
}
});
});
}());

View File

@@ -35,7 +35,7 @@ define(['taskbar'], function(taskbar) {
taskbar.discard('chat', uuid);
});
chatModal.on('click', function(e){
chatModal.on('click', function(e) {
module.bringModalToTop(chatModal);
});

View File

@@ -1,20 +1,92 @@
define(['taskbar'], function(taskbar) {
var composer = {
initialized: false,
active: 0,
active: undefined,
taskbar: taskbar,
posts: {},
postContainer: undefined,
};
function loadFile(file) {
var reader = new FileReader();
var dropDiv = $('#imagedrop');
var imagelist = $('#imagelist');
var uuid = dropDiv.parents('[data-uuid]').attr('data-uuid');
var posts = composer.posts[uuid];
$(reader).on('loadend', function(e) {
var bin = this.result;
bin = bin.split(',')[1];
var img = {
name: file.name,
data: bin
};
posts.images.push(img);
var imageLabel = $('<div class="label"><span>'+ file.name +'</span></div>');
var closeButton = $('<button class="close">&times;</button>');
closeButton.on('click', function(e) {
imageLabel.remove();
var index = posts.images.indexOf(img);
if(index !== -1) {
posts.images.splice(index, 1);
}
});
imageLabel.append(closeButton);
imagelist.append(imageLabel);
dropDiv.hide();
});
reader.readAsDataURL(file);
}
function initializeFileReader() {
jQuery.event.props.push( "dataTransfer" );
if(window.FileReader) {
var drop = $('#imagedrop');
$(composer.postContainer).on('dragenter dragover', function() {
drop.show();
});
function cancel(e) {
e.preventDefault();
return false;
}
drop.on('dragover', cancel);
drop.on('dragenter', cancel);
drop.on('drop', function(e) {
e.preventDefault();
var uuid = drop.parents('[data-uuid]').attr('data-uuid');
var posts = composer.posts[uuid];
var dt = e.dataTransfer;
var files = dt.files;
for (var i=0; i<files.length; i++) {
loadFile(files[i]);
}
return false;
});
}
}
composer.init = function() {
if (!composer.initialized) {
// Create the fixed bottom bar
var taskbar = document.getElementById('taskbar');
var taskbar = document.getElementById('taskbar');
composer.postContainer = document.createElement('div');
composer.postContainer.className = 'post-window row-fluid';
composer.postContainer.innerHTML = '<div class="span10 offset1">' +
composer.postContainer.innerHTML = '<div class="span5">' +
'<input type="text" tabIndex="1" placeholder="Enter your topic title here..." />' +
'<div class="btn-toolbar">' +
'<div class="btn-group formatting-bar">' +
@@ -23,20 +95,28 @@ define(['taskbar'], function(taskbar) {
'<span class="btn btn-link" tabindex="-1"><i class="icon-list"></i></span>' +
'<span class="btn btn-link" tabindex="-1"><i class="icon-link"></i></span>' +
'</div>' +
'<div class="btn-group action-bar" style="float: right; margin-right: -12px">' +
'<button data-action="post" class="btn" tabIndex="3"><i class="icon-ok"></i> Submit</button>' +
'</div>' +
'<div style="position:relative;">'+
'<div id="imagedrop" class=""><div>Drag and Drop Images Here</div></div>'+
'<textarea tabIndex="2"></textarea>' +
'<div id="imagelist"></div>'+
'</div>'+
'<div class="btn-toolbar">' +
'<div class="btn-group action-bar" style="float: right; margin-right: -8px">' +
'<button data-action="minimize" class="btn hidden-phone" tabIndex="4"><i class="icon-download-alt"></i> Minimize</button>' +
'<button class="btn" data-action="discard" tabIndex="5"><i class="icon-remove"></i> Discard</button>' +
'<button data-action="post" class="btn" tabIndex="3"><i class="icon-ok"></i> Submit</button>' +
'</div>' +
'</div>' +
'<textarea tabIndex="2"></textarea>' +
'</div>';
document.body.insertBefore(composer.postContainer, taskbar);
initializeFileReader();
socket.on('api:composer.push', function(threadData) {
if (!threadData.error) {
var uuid = utils.generateUUID();
var uuid = utils.generateUUID();
composer.taskbar.push('composer', uuid, {
title: (!threadData.cid ? (threadData.title || '') : 'New Topic'),
@@ -48,7 +128,8 @@ define(['taskbar'], function(taskbar) {
cid: threadData.cid,
pid: threadData.pid,
title: threadData.title || '',
body: threadData.body || ''
body: threadData.body || '',
images: []
};
composer.load(uuid);
} else {
@@ -141,6 +222,9 @@ define(['taskbar'], function(taskbar) {
break;
}
});
window.addEventListener('resize', function() {
if (composer.active !== undefined) composer.reposition(composer.active);
});
composer.initialized = true;
}
@@ -156,12 +240,18 @@ define(['taskbar'], function(taskbar) {
}
composer.load = function(post_uuid) {
var post_data = composer.posts[post_uuid],
var post_data = composer.posts[post_uuid],
titleEl = composer.postContainer.querySelector('input'),
bodyEl = composer.postContainer.querySelector('textarea');
bodyEl = composer.postContainer.querySelector('textarea'),
dropDiv = $(composer.postContainer).find('#imagedrop'),
imagelist = $(composer.postContainer).find('#imagelist');
composer.postContainer.style.display = 'block';
// composer.postContainer.style.bottom = composer.btnContainer.offsetHeight + "px";
dropDiv.hide();
imagelist.empty();
composer.reposition(post_uuid);
composer.active = post_uuid;
composer.postContainer.setAttribute('data-uuid', post_uuid);
if (parseInt(post_data.tid) > 0) {
titleEl.value = 'Replying to: ' + post_data.title;
@@ -186,17 +276,45 @@ define(['taskbar'], function(taskbar) {
}
}
composer.reposition = function(post_uuid) {
var postWindowEl = composer.postContainer.querySelector('.span5'),
taskbarBtn = document.querySelector('#taskbar [data-uuid="' + post_uuid + '"]'),
btnRect = taskbarBtn.getBoundingClientRect(),
taskbarRect = document.getElementById('taskbar').getBoundingClientRect(),
windowRect, leftPos;
composer.postContainer.style.display = 'block';
windowRect = postWindowEl.getBoundingClientRect();
leftPos = btnRect.left + btnRect.width - windowRect.width;
postWindowEl.style.left = (leftPos > 0 ? leftPos : 0) + 'px';
composer.postContainer.style.bottom = taskbarRect.height + "px";
}
composer.post = function(post_uuid) {
// Check title and post length
var postData = composer.posts[post_uuid],
titleEl = composer.postContainer.querySelector('input'),
bodyEl = composer.postContainer.querySelector('textarea');
if (titleEl.value.length <= 3) {
titleEl.value = titleEl.value.trim();
bodyEl.value = bodyEl.value.trim();
if (titleEl.value.length < config.minimumTitleLength) {
return app.alert({
type: 'error',
timeout: 5000,
timeout: 2000,
title: 'Title too short',
message: "Please enter a longer title.",
message: "Please enter a longer title. At least " + config.minimumTitleLength+ " characters.",
alert_id: 'post_error'
});
}
if (bodyEl.value.length < config.minimumPostLength) {
return app.alert({
type: 'error',
timeout: 2000,
title: 'Content too short',
message: "Please enter a longer post. At least " + config.minimumPostLength + " characters.",
alert_id: 'post_error'
});
}
@@ -206,18 +324,21 @@ define(['taskbar'], function(taskbar) {
socket.emit('api:topics.post', {
'title' : titleEl.value,
'content' : bodyEl.value,
'category_id' : postData.cid
'category_id' : postData.cid,
images: composer.posts[post_uuid].images
});
} else if (parseInt(postData.tid) > 0) {
socket.emit('api:posts.reply', {
'topic_id' : postData.tid,
'content' : bodyEl.value
'content' : bodyEl.value,
images: composer.posts[post_uuid].images
});
} else if (parseInt(postData.pid) > 0) {
socket.emit('api:posts.edit', {
pid: postData.pid,
content: bodyEl.value,
title: titleEl.value
title: titleEl.value,
images: composer.posts[post_uuid].images
});
}
@@ -226,6 +347,7 @@ define(['taskbar'], function(taskbar) {
composer.discard = function(post_uuid) {
if (composer.posts[post_uuid]) {
$(composer.postContainer).find('#imagedrop').html('');
delete composer.posts[post_uuid];
composer.minimize();
taskbar.discard('composer', post_uuid);
@@ -234,6 +356,7 @@ define(['taskbar'], function(taskbar) {
composer.minimize = function(uuid) {
composer.postContainer.style.display = 'none';
composer.active = undefined;
taskbar.minimize('composer', uuid);
}

View File

@@ -106,7 +106,7 @@
}
templates.getTemplateNameFromUrl = function(url) {
var parts = url.split('/');
var parts = url.split('?')[0].split('/');
for(var i=0; i<parts.length; ++i) {
if (templates.is_available(parts[i])) {
@@ -123,7 +123,7 @@
var api_url = (url === '' || url === '/') ? 'home' : url;
var tpl_url = templates.get_custom_map(api_url);
var tpl_url = templates.get_custom_map(api_url.split('?')[0]);
var trimmed = api_url;

View File

@@ -86,12 +86,12 @@
// from http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
isEmailValid: function(email) {
// var re = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
var valid = email.indexOf('@') !== -1 ? true : false;
return re.test(email);
var valid = email.indexOf('@') !== -1 ? true : false;
return valid;
},
isUserNameValid: function(name) {
return (name && name !== "" && !(/^\s*$/.test(name)));
return (name && name !== "" && (/^[a-zA-Z0-9 _-]{3,14}$/.test(name)));
},
isPasswordValid: function(password) {
@@ -107,6 +107,32 @@
return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) {
return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
});
},
buildMetaTags: function(tagsArr) {
var tags = '',
tag;
for(var x=0,numTags=tagsArr.length;x<numTags;x++) {
if (tags.length > 0) tags += "\n\t";
tag = '<meta';
for(y in tagsArr[x]) {
tag += ' ' + y + '="' + tagsArr[x][y] + '"';
}
tag += ' />';
tags += tag;
}
return tags;
},
refreshTitle: function() {
var a = document.createElement('a');
a.href = document.location;
socket.emit('api:meta.buildTitle', a.pathname.slice(1), function(title) {
document.title = title;
});
}
}

View File

@@ -3,13 +3,14 @@
<div class="account-username-box">
<div class="account-username-box">
<span class="account-username">
<a href="/users/{userslug}">{username}</a>
</span>
<div class="account-sub-links inline-block pull-right">
<span id="followersLink" class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span id="followingLink" class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="settingsLink" class="pull-right"><a href="/users/{userslug}/settings">settings</a></span>
<span class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="editLink" class="pull-right"><a href="/users/{userslug}/edit">edit</a></span>
</div>
</div>
@@ -20,8 +21,12 @@
<div class="account-picture-block">
<img src="{picture}" class="user-profile-picture img-polaroid"/>
</div>
<div class="account-online-status">
<span><i class="icon-circle-blank"></i> <span>offline</span></span>
</div>
<div id="user-actions">
<a id="follow-btn" href="#" class="btn">Follow</a>
<a id="follow-btn" href="#" class="btn hide">Follow</a>
<a id="unfollow-btn" href="#" class="btn hide">Unfollow</a>
</div>
</div>
@@ -29,7 +34,7 @@
<h4>profile</h4>
<div class="inline-block">
<div class="account-bio-block">
<span class="account-bio-label">email</span>
<span class="account-bio-label">email</span><i class="icon-eye-close {emailClass}" title="Email hidden"></i>
<span>{email}</span>
<br/>
@@ -81,13 +86,10 @@
<div class="span6 user-recent-posts">
<h4>recent posts </h4>
<!-- BEGIN posts -->
<a href="/topic/{posts.tid}/{posts.pid}">
<div class="topic-row img-polaroid clearfix">
<span>{posts.content}</span>
<span class="pull-right">{posts.relativeTime} ago</span>
</div>
</a>
<div class="topic-row img-polaroid clearfix" topic-url="topic/{posts.tid}/#{posts.pid}">
<span>{posts.content}</span>
<span class="pull-right">{posts.relativeTime} ago</span>
</div>
<!-- END posts -->
</div>
</div>
@@ -101,4 +103,5 @@
<input type="hidden" template-variable="theirid" value="{theirid}" />
<input type="hidden" template-type="boolean" template-variable="isFollowing" value="{isFollowing}" />
<script type="text/javascript" src="{relative_path}/src/forum/account.js"></script>
<script type="text/javascript" src="{relative_path}/src/forum/account.js"></script>
<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>

View File

@@ -40,9 +40,9 @@
</div>
<div class="modal-body">
<form id="uploadForm" action="/users/uploadpicture" method="post" enctype="multipart/form-data">
<input id="userPhotoInput" type="file" name="userPhoto" >
<input id="imageUploadCsrf" type="hidden" name="_csrf" value="">
<form id="uploadForm" action="{relative_path}/users/uploadpicture" method="post" enctype="multipart/form-data">
<input id="userPhotoInput" type="file" name="userPhoto" />
<input id="imageUploadCsrf" type="hidden" name="_csrf" value="" />
</form>
<div id="upload-progress-box" class="progress progress-striped active hide">
@@ -62,12 +62,13 @@
<div class="account-username-box">
<span class="account-username">
<a href="/users/{userslug}">{username}</a> >
<a href="/users/{userslug}">{username}</a> <i class="icon-chevron-right"></i>
<a href="/users/{userslug}/edit">edit</a>
</span>
<div class="account-sub-links inline-block pull-right">
<span id="followersLink" class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span id="followingLink" class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="settingsLink" class="pull-right"><a href="/users/{userslug}/settings">settings</a></span>
<span class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="editLink" class="pull-right"><a href="/users/{userslug}/edit">edit</a></span>
</div>
</div>
@@ -170,4 +171,4 @@
<input type="hidden" template-variable="gravatarpicture" value="{gravatarpicture}" />
<input type="hidden" template-variable="uploadedpicture" value="{uploadedpicture}" />
<script type="text/javascript" src="{relative_path}/src/forum/accountedit.js"></script>
<script type="text/javascript" src="{relative_path}/src/forum/accountedit.js"></script>

View File

@@ -0,0 +1,34 @@
<div class="well">
<div class="account-username-box">
<span class="account-username">
<a href="/users/{userslug}">{username}</a> <i class="icon-chevron-right"></i>
<a href="/users/{userslug}/settings">settings</a>
</span>
<div class="account-sub-links inline-block pull-right">
<span id="settingsLink" class="pull-right"><a href="/users/{userslug}/settings">settings</a></span>
<span class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="editLink" class="pull-right"><a href="/users/{userslug}/edit">edit</a></span>
</div>
</div>
<div class="row-fluid">
<div class="span6">
<h4>privacy</h4>
<label class="checkbox">
<input id="showemailCheckBox" type="checkbox" {showemail}> Show my email
</label>
</div>
<div class="span6">
</div>
</div>
<div class="form-actions">
<a id="submitBtn" href="#" class="btn btn-primary">Save changes</a>
</div>
</div>
<script type="text/javascript" src="{relative_path}/src/forum/accountsettings.js"></script>

View File

@@ -27,6 +27,7 @@
<option value="category-orange">category-orange</option>
</select>
<input data-name="description" placeholder="Category Description" value="{categories.description}" class="category_description input-medium description"></input>
<button type="submit" class="btn" data-disabled="{categories.disabled}">Disable</button>
</form>
</li>

View File

@@ -6,7 +6,7 @@
<script>
var RELATIVE_PATH = "{relative_path}";
</script>
<link id="base-theme" href="{cssSrc}" rel="stylesheet" media="screen">
<link id="base-theme" href="{relative_path}/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="{relative_path}/vendor/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
<link rel="stylesheet" href="{relative_path}/vendor/fontawesome/css/font-awesome.min.css">
<script type="text/javascript" src="http://code.jquery.com/jquery.js"></script>
@@ -70,10 +70,11 @@
<ul class="nav nav-list">
<li class="nav-header">NodeBB</li>
<li class='active'><a href='{relative_path}/admin/index'><i class='icon-home'></i> Home</a></li>
<li class=''><a href='{relative_path}/admin/categories'><i class='icon-folder-close-alt'></i> Categories</a></li>
<li class=''><a href='{relative_path}/admin/categories/active'><i class='icon-folder-close-alt'></i> Categories</a></li>
<li class=''><a href='{relative_path}/admin/users/latest'><i class='icon-user'></i> Users</a></li>
<li class=''><a href='{relative_path}/admin/topics'><i class='icon-book'></i> Topics</a></li>
<li class=''><a href='{relative_path}/admin/themes'><i class='icon-th'></i> Themes</a></li>
<li class=''><a href='{relative_path}/admin/plugins'><i class='icon-code-fork'></i> Plugins</a></li>
<li class=''><a href='{relative_path}/admin/settings'><i class='icon-cogs'></i> Settings</a></li>
<li class=''><a href='{relative_path}/admin/redis'><i class='icon-hdd'></i> Redis</a></li>
<li class=''><a href="{relative_path}/admin/motd"><i class="icon-comment"></i> MOTD</a></li>
@@ -87,14 +88,14 @@
<li class="nav-header">Unit Tests</li>
<ul class="nav nav-list">
<li class=''><a href='{relative_path}/admin/testing/categories'>Categories</a></li>
<li class=''><a href='{relative_path}/admin/testing/topics'>Topics</a></li>
<!--<li class=''><a href='{relative_path}/admin/testing/topics'>Topics</a></li>
<li class=''><a href='{relative_path}/admin/testing/posts'>Posts</a></li>
<li class=''><a href='{relative_path}/admin/testing/accounts'>Accounts</a></li>
<li class=''><a href='{relative_path}/admin/testing/chat'>Chat</a></li>
<li class=''><a href='{relative_path}/admin/testing/notifications'>Notifications</a></li>
<li class=''><a href='{relative_path}/admin/testing/friends'>Friends</a></li>
<li class=''><a href='{relative_path}/admin/testing/feed'>RSS Feed</a></li>
<li class=''><a href='{relative_path}/admin/testing/emails'>Emails</a></li>
<li class=''><a href='{relative_path}/admin/testing/emails'>Emails</a></li>-->
</ul>
</ul>
</div><!--/.well -->

View File

@@ -8,7 +8,7 @@
<a target="_blank" href="http://www.nodebb.org" class="btn btn-large"><i class="icon-github-alt"></i> Get Themes</a>
<a target="_blank" href="http://www.nodebb.org" class="btn btn-large"><i class="icon-twitter"></i> dcplabs</a>
</p>
<p><small>You are running <strong>NodeBB v0.0.1</strong>. This is where we will check to make sure your <strong>NodeBB</strong> is latest, etc.</small></p>
<p><small>You are running <strong>NodeBB v{version}</strong>. This is where we will check to make sure your <strong>NodeBB</strong> is latest, etc.</small></p>
</div>

View File

@@ -0,0 +1,25 @@
<h1>Plugins</h1>
<ul class="plugins">
<!-- BEGIN plugins -->
<li data-plugin-id="{plugins.id}">
<h2>{plugins.name}</h2>
<div class="pull-right">
<button data-action="toggleActive" class="btn btn-primary">{plugins.activeText}</button>
</div>
<p>{plugins.description}</p>
<p>For more information: <a href="{plugins.url}">{plugins.url}</a></p>
</li>
<!-- END plugins -->
</ul>
<div class="alert">
<p>
<strong>Interesed in writing plugins for NodeBB?</strong>
</p>
<p>
Full documentation regarding plugin authoring can be found in the <a target="_blank" href="https://github.com/designcreateplay/NodeBB/wiki/Writing-Plugins-for-NodeBB">NodeBB Wiki</a>.
</p>
</div>
<script type="text/javascript" src="{relative_path}/src/forum/admin/plugins.js"></script>

View File

@@ -16,6 +16,7 @@
<hr/>
<span>Total Connections Received</span> <span class="text-right">{total_connections_received}</span><br/>
<span>Total Commands Processed</span> <span class="text-right">{total_commands_processed}</span><br/>
<span>Instantaneous Ops. Per Second</span> <span class="text-right">{instantaneous_ops_per_sec}</span><br/>
<hr/>
<span>Keyspace Hits</span> <span class="text-right">{keyspace_hits}</span><br/>
<span>Keyspace Misses</span> <span class="text-right">{keyspace_misses}</span><br/>

View File

@@ -5,7 +5,9 @@
<div class="alert">
<form>
<label>Site Title</label>
<input type="text" placeholder="My Forum" data-field="title" />
<input type="text" placeholder="Your Community Name" data-field="title" />
<label>Site Description</label>
<input type="text" class="input-xxlarge" placeholder="A short description about your community" data-field="description" />
</form>
</div>
@@ -46,6 +48,16 @@
</div>
</form>
<form>
<h3>Post Settings</h3>
<div class="alert alert-notify">
<strong>Post Delay</strong><br /> <input type="text" class="" value="10000" data-field="postDelay"><br />
<strong>Minimum Title Length</strong><br /> <input type="text" class="" value="3" data-field="minimumTitleLength"><br />
<strong>Minimum Post Length</strong><br /> <input type="text" class="" value="8" data-field="minimumPostLength"><br />
</div>
</form>
<button class="btn btn-large btn-primary" id="save">Save</button>
<script>

View File

@@ -1,12 +1,20 @@
<h1>Themes</h1>
<hr />
<h3>Custom Themes</h3>
<p>
The following themes are currently installed in this NodeBB instance.
</p>
<ul class="themes" id="installed_themes">
<li><i class="icon-refresh icon-spin"></i> Checking for installed themes...</li>
</ul>
<h3>Bootswatch Themes</h3>
<p>
NodeBB Themes are powered by Bootswatch, a repository containing themes built
with Bootstrap as a base theme.
</p>
<ul class="themes">
<ul class="themes" id="bootstrap_themes">
<li><i class="icon-refresh icon-spin"></i> Loading Themes</li>
</ul>

View File

@@ -14,7 +14,7 @@
<span id="user-notfound-notify" class="label label-important hide">User not found!</span><br/>
</div>
<ul class="users">
<ul id="users-container" class="users">
<!-- BEGIN users -->
<div class="users-box" data-uid="{users.uid}" data-admin="{users.administrator}" data-username="{users.username}">
<a href="/users/{users.userslug}">
@@ -41,6 +41,9 @@
<!-- END users -->
</ul>
<div class="text-center {loadmore_display}">
<button id="load-more-users-btn" class="btn">Load More</button>
</div>
<input type="hidden" template-variable="yourid" value="{yourid}" />

View File

@@ -14,6 +14,21 @@
<strong>There are no topics in this category.</strong><br />
Why don't you try posting one?
</div>
<div>
<button id="new_post" class="btn btn-primary btn-large {show_topic_button}">New Topic</button>
<div class="inline-block pull-right">
<a target="_blank" href="../{category_id}.rss"><i class="icon-rss-sign icon-2x"></i></a>&nbsp;
<a href="#" id="facebook-share"><i class="icon-facebook-sign icon-2x"></i></a>&nbsp;
<a href="#" id="twitter-intent"><i class="icon-twitter-sign icon-2x"></i></a>&nbsp;
<a href="#" id="google-share"><i class="icon-google-plus-sign icon-2x"></i></a>&nbsp;
</div>
</div>
<hr class="{show_sidebar}" />
<div class="category row">
<div class="{topic_row_size}">
<ul id="topics-container">
@@ -26,7 +41,7 @@
38
</span>
</div> -->
<div class="span12 topic-row img-polaroid">
<div class="span12 topic-row">
<div class="latest-post visible-desktop">
<div class="pull-right">
<img style="width: 48px; height: 48px; /*temporary*/" src="{topics.teaser_userpicture}" />
@@ -47,18 +62,9 @@
</li></a>
<!-- END topics -->
</ul>
<hr class="{show_sidebar}" />
<button id="new_post" class="btn btn-primary btn-large {show_topic_button}">New Topic</button>
</div>
<div class="span3 {show_sidebar} category-sidebar mobile-sidebar">
<div class="sidebar-block img-polaroid">
<div class="block-header">
<a target="_blank" href="../{category_id}.rss"><i class="icon-rss-sign icon-2x"></i></a>&nbsp;
<a href="#" id="facebook-share"><i class="icon-facebook-sign icon-2x"></i></a>&nbsp;
<a href="#" id="twitter-intent"><i class="icon-twitter-sign icon-2x"></i></a>&nbsp;
<a href="#" id="google-share"><i class="icon-google-plus-sign icon-2x"></i></a>&nbsp;
</div>
</div>
<div class="sidebar-block img-polaroid">
<div class="block-header">
Recent Replies

View File

@@ -7,6 +7,7 @@
"admin/redis[^]*": "admin/redis",
"admin/index[^]*": "admin/index",
"admin/themes[^]*": "admin/themes",
"admin/plugins[^]*": "admin/plugins",
"admin/settings[^]*": "admin/settings",
"admin/twitter[^]*": "admin/twitter",
"admin/facebook[^]*": "admin/facebook",
@@ -23,11 +24,14 @@
"users[^]*edit": "accountedit",
"users[^]*following": "following",
"users[^]*followers": "followers",
"users[^]*settings": "accountsettings",
"users/[^]*": "account",
"recent": "recent",
"unread": "unread",
"popular": "category",
"active": "category"
"active": "category",
"search": "search"
},
"force_refresh": {
"logout": true

View File

@@ -3,12 +3,13 @@
<div class="account-username-box">
<span class="account-username">
<a href="/users/{userslug}">{username}</a> >
<a href="/users/{userslug}">{username}</a> <i class="icon-chevron-right"></i>
<a href="/users/{userslug}/followers">followers</a>
</span>
<div class="account-sub-links inline-block pull-right">
<span id="followersLink" class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span id="followingLink" class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="settingsLink" class="pull-right"><a href="/users/{userslug}/settings">settings</a></span>
<span class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="editLink" class="pull-right"><a href="/users/{userslug}/edit">edit</a></span>
</div>
</div>
@@ -41,4 +42,5 @@
<input type="hidden" template-variable="theirid" value="{theirid}" />
<input type="hidden" template-variable="followersCount" value="{followersCount}" />
<script type="text/javascript" src="{relative_path}/src/forum/followers.js"></script>
<script type="text/javascript" src="{relative_path}/src/forum/followers.js"></script>
<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>

View File

@@ -5,19 +5,19 @@
<div class="account-username-box">
<span class="account-username">
<a href="/users/{userslug}">{username}</a> >
<a href="/users/{userslug}">{username}</a> <i class="icon-chevron-right"></i>
<a href="/users/{userslug}/following">following</a>
</span>
<div class="account-sub-links inline-block pull-right">
<span id="followersLink" class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span id="followingLink" class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="settingsLink" class="pull-right"><a href="/users/{userslug}/settings">settings</a></span>
<span class="pull-right"><a href="/users/{userslug}/followers">followers</a></span>
<span class="pull-right"><a href="/users/{userslug}/following">following</a></span>
<span id="editLink" class="pull-right"><a href="/users/{userslug}/edit">edit</a></span>
</div>
</div>
<div>
<!-- BEGIN following -->
<div class="users-box">
<a href="/users/{following.userslug}">
<img src="{following.picture}" class="img-polaroid"/>
@@ -33,9 +33,8 @@
<span class='postcount'>{following.postcount}</span>
<i class='icon-pencil'></i>
</div>
<a id="unfollow-btn" href="#" class="btn unfollow-btn" followingUid="{following.uid}">Unfollow</a>
<a id="unfollow-btn" href="#" class="btn unfollow-btn" followingUid="{following.uid}" data-username="{following.username}">Unfollow</a>
</div>
<!-- END following -->
</div>
<div id="no-following-notice" class="alert alert-warning hide">This user isn't following anyone :(</div>
@@ -46,3 +45,4 @@
<input type="hidden" template-variable="followingCount" value="{followingCount}" />
<script type="text/javascript" src="{relative_path}/src/forum/following.js"></script>
<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>

View File

@@ -1,24 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" CONTENT="NodeBB">
<meta name="keywords" content="" />
<meta name="description" content="" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<script>
var RELATIVE_PATH = "{relative_path}";
</script>
<title>{browserTitle}</title>
{meta_tags}
<link href="{cssSrc}" rel="stylesheet" media="screen">
<link href="{relative_path}/vendor/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
<link rel="stylesheet" href="{relative_path}/vendor/fontawesome/css/font-awesome.min.css">
<script type="text/javascript" src="http://code.jquery.com/jquery.js"></script>
<script type="text/javascript" src="{relative_path}/vendor/jquery/js/jquery-ui-1.10.3.custom.min.js"></script>
<script type="text/javascript" src="{relative_path}/vendor/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="{relative_path}/socket.io/socket.io.js"></script>
<script type="text/javascript" src="{relative_path}/src/app.js"></script>
<script>
var RELATIVE_PATH = "{relative_path}";
</script>
<script src="http://code.jquery.com/jquery.js"></script>
<script src="{relative_path}/vendor/jquery/js/jquery-ui-1.10.3.custom.min.js"></script>
<script src="{relative_path}/vendor/bootstrap/js/bootstrap.min.js"></script>
<script src="{relative_path}/socket.io/socket.io.js"></script>
<script src="{relative_path}/src/app.js"></script>
<script src="{relative_path}/vendor/requirejs/require.js"></script>
<script src="{relative_path}/vendor/bootbox/bootbox.min.js"></script>
<script>
@@ -27,14 +22,13 @@
waitSeconds: 3
});
</script>
<script type="text/javascript" src="{relative_path}/src/templates.js"></script>
<script type="text/javascript" src="{relative_path}/src/ajaxify.js"></script>
<script type="text/javascript" src="{relative_path}/src/jquery.form.js"></script>
<script type="text/javascript" src="{relative_path}/src/utils.js"></script>
<script src="{relative_path}/src/templates.js"></script>
<script src="{relative_path}/src/ajaxify.js"></script>
<script src="{relative_path}/src/jquery.form.js"></script>
<script src="{relative_path}/src/utils.js"></script>
<link rel="stylesheet" type="text/css" href="{relative_path}/css/nodebb.css" />
</head>
<body>
@@ -50,16 +44,13 @@
<span class="icon-bar"></span>
</button>
<div class="nav-collapse collapse">
<ul class="nav nodebb-inline-block">
<ul id="main-nav" class="nav nodebb-inline-block">
<li>
<a href="/recent">Recent <!--<span class="badge badge-inverse">3</span>--></a>
</li>
<!--<li>
<a href="/popular">Popular</a>
<a href="/recent">Recent</a>
</li>
<li>
<a href="/active">Active</a>
</li>-->
<a href="/unread">Unread</a>
</li>
<li>
<a href="/users">Users</a>
</li>
@@ -71,6 +62,12 @@
<li><a href="#"><i class="icon-refresh icon-spin"></i> Loading Notifications</a></li>
</ul>
</li>
<form id="search-form"
class="form-search form-inline" action="" method="GET">
<input type="text" name="query" class="input-medium search-query">
<button type="submit" class="btn hide">Search</button>
</form>
</ul>
</div>
</div>

View File

@@ -14,7 +14,7 @@
<!-- BEGIN posts -->
<div class="category-box">
<div class="post-preview">
<img src="{categories.posts.picture}" class="pull-left" >
<img src="{categories.posts.picture}" class="pull-left" />
<p class=""><strong>{categories.posts.username}</strong>: {categories.posts.content}</p>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<div class="hero-unit">
<h2 class="form-signin-heading">Now Leaving NodeBB</h2>
<p>
You are now leaving NodeBB.
</p>
<br />
<p>
<a href="{url}" rel="nofollow" class="btn btn-large">Continue to {url}</a>
<a href="{home}" class="btn btn-large btn-inverse">Return to NodeBB</a>
</p>
</div>

View File

@@ -7,15 +7,13 @@
</div>
<div class="alert alert-warning hide {no_topics_message}" id="category-no-topics">
<strong>There are no topics in this category.</strong><br />
Why don't you try posting one?
</div>
<a href="/recent">
<div class="alert hide" id="new-topics-alert"></div>
</a>
<div class="alert alert-warning hide {no_topics_message}" id="category-no-topics">
<strong>There are no recent topics.</strong>
</div>
<div class="category row">
<div class="{topic_row_size}">

View File

@@ -0,0 +1,50 @@
<div class="container">
<ul class="breadcrumb">
<li><a href="/">Home</a><span class="divider">/</span></li>
<li class="active">Search</li>
</ul>
</div>
<div class="alert alert-warning {show_no_results}" id="no-search-results">
<strong>No search results for {search_query}.</strong>
</div>
<div class="category row">
<div class="span12">
<ul id="topics-container" data-search-query="{search_query}">
<!-- BEGIN topics -->
<a href="../../topic/{topics.slug}" id="tid-{topics.tid}">
<li class="category-item">
<div class="row-fluid">
<div class="span12 img-polaroid">
<div class="search-result-post">
<img src="{topics.teaser_userpicture}" />
<strong>{topics.teaser_username}</strong>: <span class="search-result-text">{topics.title}</span>
</div>
</div>
</div>
</li>
</a>
<!-- END topics -->
<!-- BEGIN posts -->
<a href="../../topic/{posts.topicSlug}#{posts.pid}" id="tid-{posts.tid}">
<li class="category-item">
<div class="row-fluid">
<div class="span12 img-polaroid">
<div class="search-result-post">
<img src="{posts.picture}" />
<strong>{posts.username}</strong>: <span class="search-result-text">{posts.content}</span>
</div>
</div>
</div>
</li>
</a>
<!-- END posts -->
</ul>
</div>
</div>
<script type="text/javascript" src="{relative_path}/src/forum/search.js"></script>

View File

@@ -15,12 +15,12 @@
</ul>
</div>
<ul id="post-container" class="post-container container">
<ul id="post-container" class="post-container container" data-tid="{topic_id}">
<!-- BEGIN main_posts -->
<a name="{main_posts.pid}"></a>
<a id="post_anchor_{main_posts.pid}" name="{main_posts.pid}"></a>
<li class="row post-row main-post" data-pid="{main_posts.pid}" data-uid="{main_posts.uid}" data-deleted="{main_posts.deleted}">
<div class="span12">
<div class="post-block">
@@ -29,7 +29,7 @@
<div class="hover-overlay">
{main_posts.username}<br />
<i class="icon-star"></i><span class="user_rep_{main_posts.uid}">{main_posts.user_rep}</span>
<i class="icon-pencil"></i><span class="user_posts_{main_posts.uid}">8</span>
<i class="icon-pencil"></i><span class="user_posts_{main_posts.uid}">{main_posts.user_postcount}</span>
</div>
</a>
<h3><p id="topic_title_{main_posts.pid}" class="topic-title">{topic_name}</p></h3>
@@ -38,28 +38,38 @@
<hr />
<div class="topic-buttons pull-left">
<a href="/users/{main_posts.userslug}" class="username-field btn hidden-phone">{main_posts.username}</a>
<a target="_blank" class="btn hidden-phone" href="../{topic_id}.rss" title="RSS Feed"><i class="icon-rss-sign"></i></a>
<button class="btn follow" type="button" title="Be notified of new replies in this topic"><i class="icon-eye-open"></i></button>
<button id="ids_{main_posts.pid}_{main_posts.uid}" class="btn edit {main_posts.display_moderator_tools}" type="button" title="Edit"><i class="icon-pencil"></i></button>
<button id="ids_{main_posts.pid}_{main_posts.uid}" class="btn delete {main_posts.display_moderator_tools}" type="button" title="Delete"><i class="icon-trash"></i></button>
<button id="quote_{main_posts.pid}_{main_posts.uid}" class="btn quote" type="button" title="Quote"><i class="icon-quote-left"></i></button>
<button id="favs_{main_posts.pid}_{main_posts.uid}" class="favourite btn" type="button">
<span>Favourite</span>
<span class="post_rep_{main_posts.pid}">{main_posts.post_rep} </span><i class="{main_posts.fav_star_class}"></i></button>
<button class="btn btn-primary btn post_reply hidden-phone" type="button">Reply <i class="icon-reply"></i></button>
<div class="btn-group">
<button class="btn follow" type="button" title="Be notified of new replies in this topic"><i class="icon-eye-open"></i></button>
<button id="favs_{main_posts.pid}_{main_posts.uid}" class="favourite btn" type="button">
<span>Favourite</span>
<span class="post_rep_{main_posts.pid}">{main_posts.post_rep} </span><i class="{main_posts.fav_star_class}"></i>
</button>
</div>
<div class="btn-group">
<button id="quote_{main_posts.pid}_{main_posts.uid}" class="btn quote" type="button" title="Quote"><i class="icon-quote-left"></i></button>
<button class="btn btn-primary btn post_reply" type="button">Reply <i class="icon-reply"></i></button>
</div>
</div>
<div style="clear:both; margin-bottom: 10px;"></div>
<div id="content_{main_posts.pid}" class="post-content">{main_posts.content}</div>
<div id="images_{main_posts.pid}" class="post-images">
<!-- BEGIN uploadedImages -->
<i class="icon-picture icon-1"></i><a href="{main_posts.uploadedImages.url}"> {main_posts.uploadedImages.name}</a><br/>
<!-- END uploadedImages -->
</div>
<div class="post-signature">{main_posts.signature}</div>
<div class="profile-block">
<img class="hidden-desktop" src="{main_posts.picture}?s=10&default=identicon" align="left" /> posted by <strong><a class="" href="/users/{main_posts.userslug}">{main_posts.username}</a></strong> {main_posts.relativeTime} ago
<img class="hidden-desktop" src="{main_posts.picture}" align="left" /> posted by <strong><a class="username-field" href="/users/{main_posts.userslug}">{main_posts.username}</a></strong> {main_posts.relativeTime} ago
<span class="{main_posts.edited-class} hidden-phone">| last edited by <strong><a href="/users/{main_posts.editorslug}">{main_posts.editorname}</a></strong> {main_posts.relativeEditTime} ago</span>
<span class="{main_posts.edited-class}"><i class="icon-edit visible-phone" title="edited by {main_posts.editorname} {main_posts.relativeEditTime} ago"></i></span>
<div class="post-buttons visible-phone">
<div class="post_reply"><i class="icon-reply"></i></div>
<button class="post_reply btn-link"><i class="icon-reply"></i></button>
</div>
<div class="post-buttons">
<a href="../{topic_id}.rss" target="_blank"><i class="icon-rss"></i></a>
</div>
</div>
</div>
@@ -68,29 +78,36 @@
<!-- END main_posts -->
<!-- BEGIN posts -->
<a name="{posts.pid}"></a>
<li class="row post-row" data-pid="{posts.pid}" data-uid="{posts.uid}" data-username="{posts.username}" data-deleted="{posts.deleted}">
<div class="span1 profile-image-block visible-desktop">
<a id="post_anchor_{posts.pid}" name="{posts.pid}"></a>
<li class="row-fluid post-row" data-pid="{posts.pid}" data-uid="{posts.uid}" data-username="{posts.username}" data-deleted="{posts.deleted}">
<div class="span1 profile-image-block hidden-phone hidden-tablet">
<!--<i class="icon-spinner icon-spin icon-2x pull-left"></i>-->
<a href="/users/{posts.userslug}">
<img src="{posts.picture}" align="left" class="img-polaroid"/>
</a>
<i class="icon-star"></i><span class="user_rep_{posts.uid} formatted-number">{posts.user_rep}</span>
<div id="ids_{posts.pid}_{posts.uid}" class="chat hidden-phone" title="Chat"><i class="icon-comment"></i></div>
<div class="stats">
<i class="icon-star"></i><span class="user_rep_{posts.uid} formatted-number">{posts.user_rep}</span>
<div id="ids_{posts.pid}_{posts.uid}" class="chat hidden-phone" title="Chat"><i class="icon-comment"></i></div>
</div>
</div>
<div class="span11">
<div class="span11 span12-tablet">
<div class="post-block">
<div id="content_{posts.pid}" class="post-content">{posts.content}</div>
<div id="images_{posts.pid}" class="post-images">
<!-- BEGIN uploadedImages -->
<i class="icon-picture icon-1"></i><a href="{posts.uploadedImages.url}"> {posts.uploadedImages.name}</a><br/>
<!-- END uploadedImages -->
</div>
<div class="post-signature">{posts.signature}</div>
<div class="profile-block">
<span class="post-buttons">
<button id="ids_{posts.pid}_{posts.uid}" class="edit {posts.display_moderator_tools} hidden-phone" title="Edit"><i class="icon-pencil"></i></button>
<button id="ids_{posts.pid}_{posts.uid}" class="delete {posts.display_moderator_tools} hidden-phone" title="Delete"><i class="icon-trash"></i></button>
<button id="quote_{posts.pid}_{posts.uid}" class="quote hidden-phone" title="Quote"><i class="icon-quote-left"></i></button>
<button id="favs_{posts.pid}_{posts.uid}" class="favourite hidden-phone" title="Favourite"><span class="post_rep_{posts.pid}">{posts.post_rep} </span><i class="{posts.fav_star_class}"></i></button>
<button class="post_reply" title="Reply"><i class="icon-reply"></i></button>
<button id="ids_{posts.pid}_{posts.uid}" class="edit {posts.display_moderator_tools} btn-link hidden-phone" title="Edit"><i class="icon-pencil"></i></button>
<button id="ids_{posts.pid}_{posts.uid}" class="delete {posts.display_moderator_tools} btn-link hidden-phone" title="Delete"><i class="icon-trash"></i></button>
<button id="favs_{posts.pid}_{posts.uid}" class="favourite btn-link hidden-phone" title="Favourite"><span class="post_rep_{posts.pid}">{posts.post_rep} </span><i class="{posts.fav_star_class}"></i></button>
<button id="quote_{posts.pid}_{posts.uid}" class="quote btn-link hidden-phone" title="Quote"><i class="icon-quote-left"></i></button>
<button class="post_reply btn-link" title="Reply"><i class="icon-reply"></i></button>
</span>
<img class="hidden-desktop" src="{posts.picture}?s=10&default=identicon" align="left" /> posted by <strong><a class="username-field" href="/users/{posts.userslug}">{posts.username}</a></strong> {posts.relativeTime} ago
<img class="hidden-desktop" src="{posts.picture}" align="left" /> posted by <strong><a class="username-field" href="/users/{posts.userslug}">{posts.username}</a></strong> {posts.relativeTime} ago
<span class="{posts.edited-class} hidden-phone">| last edited by <strong><a href="/users/{posts.editorslug}">{posts.editorname}</a></strong> {posts.relativeEditTime} ago</span>
<span class="{posts.edited-class}"><i class="icon-edit visible-phone" title="edited by {posts.editorname} {posts.relativeEditTime} ago"></i></span>
</div>

View File

@@ -0,0 +1,55 @@
<div class="container">
<ul class="breadcrumb">
<li><a href="/">Home</a><span class="divider">/</span></li>
<li class="active">{category_name}</li>
<div id="category_active_users"></div>
</ul>
</div>
<a href="/unread">
<div class="alert hide" id="new-topics-alert"></div>
</a>
<div class="alert alert-warning {no_topics_message}" id="category-no-topics">
<strong>There are no unread topics.</strong>
</div>
<div>
<button id="mark-allread-btn" class="btn {show_markallread_button}">Mark All Read</button>
</div>
<div class="category row">
<div class="{topic_row_size}">
<ul id="topics-container" data-next-start="{nextStart}">
<!-- BEGIN topics -->
<a href="../../topic/{topics.slug}" id="tid-{topics.tid}">
<li class="category-item {topics.deleted-class}">
<div class="row-fluid">
<div class="span12 topic-row img-polaroid">
<div class="latest-post visible-desktop">
<div class="pull-right">
<img style="width: 48px; height: 48px; /*temporary*/" src="{topics.teaser_userpicture}" />
<p><strong>{topics.teaser_username}</strong>: {topics.teaser_text}</p>
<span>posted {topics.teaser_timestamp} ago</span>
</div>
</div>
<div>
<h3><span class="topic-title"><span class="badge {topics.badgeclass}">{topics.postcount}</span>{topics.title}</span></h3>
<small>
<strong><i class="{topics.pin-icon}"></i><i class="{topics.lock-icon}"></i></strong>
Posted {topics.relativeTime} ago by
<strong>{topics.username}</strong>.
</small>
</div>
</div>
</div>
</li>
</a>
<!-- END topics -->
</ul>
<button id="load-more-btn" class="btn hide">Load More</button>
</div>
</div>
<script type="text/javascript" src="{relative_path}/src/forum/unread.js"></script>

View File

@@ -14,7 +14,7 @@
<span id="user-notfound-notify" class="label label-important hide">User not found!</span><br/>
</div>
<ul class="users">
<ul id="users-container" class="users">
<!-- BEGIN users -->
<div class="users-box">
<a href="/users/{users.userslug}">
@@ -36,4 +36,8 @@
</ul>
</div>
<div class="text-center {loadmore_display}">
<button id="load-more-users-btn" class="btn">Load More</button>
</div>
<script type="text/javascript" src="{relative_path}/src/forum/users.js"></script>

0
public/themes/.gitignore vendored Normal file
View File

View File

@@ -18,7 +18,8 @@ var RDB = require('./../redis.js'),
icon: data.icon,
blockclass: data.blockclass,
slug: slug,
topic_count: 0
topic_count: 0,
disabled: 0
});
RDB.set('categoryslug:' + slug + ':cid', cid);
@@ -41,25 +42,12 @@ var RDB = require('./../redis.js'),
var slug = cid + '/' + utils.slugify(category[key]);
RDB.hset('category:' + cid, 'slug', slug);
RDB.set('categoryslug:' + slug + ':cid', cid);
RDB.smembers('categories:' + cid + ':tid', function(err, tids) {
var pipe = RDB.multi();
for (var tid in tids) {
pipe.set(schema.topics(tid).category_name, category[key]);
pipe.set(schema.topics(tid).category_slug, slug);
}
pipe.exec();
});
}
}
updated.push(cid);
}
socket.emit('event:alert', {
title: 'Updated Categories',
message: 'Category IDs ' + updated.join(', ') + ' was successfully updated.',

View File

@@ -9,192 +9,93 @@ var RDB = require('./redis.js'),
Categories.getCategoryById = function(category_id, current_user, callback) {
Categories.getCategoryData(category_id, function(categoryData) {
if(!categoryData) {
callback(false);
return;
Categories.getCategoryData(category_id, function(err, categoryData) {
if (err) return callback(err);
var category_name = categoryData.name,
category_slug = categoryData.slug,
category_description = categoryData.description;
function getTopicIds(next) {
Categories.getTopicIds(category_id, 0, 19, next);
}
RDB.smembers('categories:' + category_id + ':tid', function(err, tids) {
var category_name = categoryData.name;
category_slug = categoryData.slug;
function getActiveUsers(next) {
Categories.getActiveUsers(category_id, next);
}
RDB.smembers('cid:' + category_id + ':active_users', function(err, active_users) {
async.parallel([getTopicIds, getActiveUsers], function(err, results) {
var tids = results[0],
active_users = results[1];
var categoryData = {
'category_name' : category_name,
'show_sidebar' : 'show',
'show_topic_button': 'show',
'no_topics_message': 'hidden',
'topic_row_size': 'span9',
'category_id': category_id,
'active_users': [],
'topics' : [],
'twitter-intent-url': 'https://twitter.com/intent/tweet?url=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug) + '&text=' + encodeURIComponent(category_name),
'facebook-share-url': 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug),
'google-share-url': 'https://plus.google.com/share?url=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug)
};
var categoryData = {
'category_name' : category_name,
'category_description': category_description,
'show_sidebar' : 'show',
'show_topic_button': 'inline-block',
'no_topics_message': 'hidden',
'topic_row_size': 'span9',
'category_id': category_id,
'active_users': [],
'topics' : [],
'twitter-intent-url': 'https://twitter.com/intent/tweet?url=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug) + '&text=' + encodeURIComponent(category_name),
'facebook-share-url': 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug),
'google-share-url': 'https://plus.google.com/share?url=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug)
};
function getTopics(next) {
Categories.getTopicsByTids(tids, current_user, function(topics) {
// Float pinned topics to the top
topics = topics.sort(function(a, b) {
if (a.pinned !== b.pinned) return b.pinned - a.pinned;
else {
return b.lastposttime - a.lastposttime;
}
});
next(null, topics);
}, category_id);
}
function getModerators(next) {
Categories.getModerators(category_id, function(moderators) {
next(null, moderators);
});
}
function getTopics(next) {
topics.getTopicsByTids(tids, current_user, function(topicsData) {
next(null, topicsData);
}, category_id);
}
function getModerators(next) {
Categories.getModerators(category_id, next);
}
function getActiveUsers(next) {
user.getMultipleUserFields(active_users, ['username', 'userslug', 'picture'], function(users) {
next(null, users);
});
}
function getActiveUsers(next) {
user.getMultipleUserFields(active_users, ['username', 'userslug', 'picture'], function(users) {
next(null, users);
});
}
if (tids.length === 0) {
getModerators(function(err, moderators) {
categoryData.moderator_block_class = moderators.length > 0 ? '' : 'none';
categoryData.moderators = moderators;
categoryData.show_sidebar = 'hidden';
categoryData.no_topics_message = 'show';
if (tids.length === 0) {
getModerators(function(err, moderators) {
categoryData.moderator_block_class = moderators.length > 0 ? '' : 'none';
categoryData.moderators = moderators;
categoryData.show_sidebar = 'hidden';
categoryData.no_topics_message = 'show';
callback(null, categoryData);
});
} else {
async.parallel([getTopics, getModerators, getActiveUsers], function(err, results) {
categoryData.topics = results[0];
categoryData.moderator_block_class = results[1].length > 0 ? '' : 'none';
categoryData.moderators = results[1];
categoryData.active_users = results[2];
callback(null, categoryData);
});
}
callback(categoryData);
});
} else {
async.parallel([getTopics, getModerators, getActiveUsers], function(err, results) {
categoryData.topics = results[0];
categoryData.moderator_block_class = results[1].length > 0 ? '' : 'none';
categoryData.moderators = results[1];
categoryData.active_users = results[2];
callback(categoryData);
});
}
});
});
});
}
// not the permanent location for this function
Categories.getLatestTopics = function(current_user, start, end, callback) {
RDB.zrevrange('topics:recent', 0, -1, function(err, tids) {
var latestTopics = {
'category_name' : 'Recent',
'show_sidebar' : 'hidden',
'show_topic_button' : 'hidden',
'no_topics_message' : 'hidden',
'topic_row_size': 'span12',
'category_id': false,
'topics' : []
};
if (!tids.length) {
callback(latestTopics);
return;
}
Categories.getTopicsByTids(tids, current_user, function(topicData) {
latestTopics.topics = topicData;
callback(latestTopics);
});
Categories.getCategoryTopics = function(cid, start, stop, uid, callback) {
Categories.getTopicIds(cid, start, stop, function(err, tids) {
topics.getTopicsByTids(tids, uid, function(topicsData) {
callback(topicsData);
}, cid);
});
}
// not the permanent location for this function
Categories.getTopicsByTids = function(tids, current_user, callback, category_id /*temporary*/) {
var retrieved_topics = [];
var topicCountToLoad = tids.length;
function getTopicInfo(topicData, callback) {
function getUserName(next) {
user.getUserField(topicData.uid, 'username', function(username) {
next(null, username);
});
}
function hasReadTopic(next) {
topics.hasReadTopic(topicData.tid, current_user, function(hasRead) {
next(null, hasRead);
});
}
function getTeaserInfo(next) {
topics.getTeaser(topicData.tid, function(err, teaser) {
next(null, teaser || {});
});
}
// temporary. I don't think this call should belong here
function getPrivileges(next) {
Categories.privileges(category_id, current_user, function(user_privs) {
next(null, user_privs);
});
}
async.parallel([getUserName, hasReadTopic, getTeaserInfo, getPrivileges], function(err, results) {
var username = results[0],
hasReadTopic = results[1],
teaserInfo = results[2],
privileges = results[3];
callback({
username: username,
hasread: hasReadTopic,
teaserInfo: teaserInfo,
privileges: privileges
});
});
}
function isTopicVisible(topicData, topicInfo) {
var deleted = parseInt(topicData.deleted, 10) !== 0;
return !deleted || (deleted && topicInfo.privileges.view_deleted) || topicData.uid === current_user;
}
for(var i=0; i<tids.length; ++i) {
topics.getTopicData(tids[i], function(topicData) {
getTopicInfo(topicData, function(topicInfo) {
topicData['pin-icon'] = topicData.pinned === '1' ? 'icon-pushpin' : 'none';
topicData['lock-icon'] = topicData.locked === '1' ? 'icon-lock' : 'none';
topicData['deleted-class'] = topicData.deleted === '1' ? 'deleted' : '';
topicData.relativeTime = utils.relativeTime(topicData.timestamp);
topicData.username = topicInfo.username;
topicData.badgeclass = (topicInfo.hasread && current_user != 0) ? '' : 'badge-important';
topicData.teaser_text = topicInfo.teaserInfo.text || '',
topicData.teaser_username = topicInfo.teaserInfo.username || '';
topicData.teaser_userpicture = topicInfo.teaserInfo.picture || '';
topicData.teaser_timestamp = topicInfo.teaserInfo.timestamp ? utils.relativeTime(topicInfo.teaserInfo.timestamp) : '';
if (isTopicVisible(topicData, topicInfo))
retrieved_topics.push(topicData);
else
--topicCountToLoad;
if(retrieved_topics.length === topicCountToLoad) {
callback(retrieved_topics);
}
});
});
}
Categories.getTopicIds = function(cid, start, stop, callback) {
RDB.zrevrange('categories:' + cid + ':tid', start, stop, callback);
}
Categories.getActiveUsers = function(cid, callback) {
RDB.smembers('cid:' + cid + ':active_users', callback);
}
Categories.getAllCategories = function(callback, current_user) {
@@ -204,16 +105,20 @@ var RDB = require('./redis.js'),
});
}
Categories.getModerators = function(cid, callback) {
RDB.smembers('cid:' + cid + ':moderators', function(err, mods) {
if (mods.length === 0)
return callback([]);
if(!err) {
if(mods && mods.length) {
user.getMultipleUserFields(mods, ['username'], function(moderators) {
callback(null, moderators);
});
} else {
callback(null, []);
}
} else {
callback(err, null);
}
user.getMultipleUserFields(mods, ['username'], function(moderators) {
callback(moderators);
});
});
}
@@ -240,7 +145,7 @@ var RDB = require('./redis.js'),
}
Categories.isTopicsRead = function(cid, uid, callback) {
RDB.smembers('categories:' + cid + ':tid', function(err, tids) {
RDB.zrange('categories:' + cid + ':tid', 0, -1, function(err, tids) {
topics.hasReadTopics(tids, uid, function(hasRead) {
@@ -282,6 +187,11 @@ var RDB = require('./redis.js'),
Categories.getRecentReplies = function(cid, count, callback) {
RDB.zrevrange('categories:recent_posts:cid:' + cid, 0, (count<10)?10:count, function(err, pids) {
if(err) {
console.log(err);
callback([]);
return;
}
if (pids.length == 0) {
callback([]);
@@ -297,14 +207,51 @@ var RDB = require('./redis.js'),
});
}
Categories.getCategoryData = function(cid, callback) {
RDB.hgetall('category:' + cid, function(err, data) {
if(err === null)
callback(data);
else
Categories.moveRecentReplies = function(tid, oldCid, cid, callback) {
topics.getPids(tid, function(err, pids) {
if(!err) {
function movePost(pid, callback) {
posts.getPostField(pid, 'timestamp', function(timestamp) {
RDB.zrem('categories:recent_posts:cid:' + oldCid, pid);
RDB.zadd('categories:recent_posts:cid:' + cid, timestamp, pid);
});
}
async.each(pids, movePost, function(err) {
if(!err) {
callback(null, 1)
} else {
console.log(err);
callback(err, null);
}
});
} else {
console.log(err);
callback(err, null);
}
});
}
Categories.getCategoryData = function(cid, callback) {
RDB.exists('category:' + cid, function(err, exists) {
if (exists) RDB.hgetall('category:' + cid, callback);
else callback(new Error('No category found!'));
});
}
Categories.getCategoryField = function(cid, field, callback) {
RDB.hget('category:' + cid, field, callback);
}
Categories.getCategoryFields = function(cid, fields, callback) {
RDB.hmgetObject('category:' + cid, fields, function(err, data) {
if(err === null)
callback(data);
else
console.log(err);
});
}
Categories.setCategoryField = function(cid, field, value) {
RDB.hset('category:' + cid, field, value);
@@ -315,32 +262,41 @@ var RDB = require('./redis.js'),
}
Categories.getCategories = function(cids, callback, current_user) {
if (!cids || cids.length === 0) {
if (!cids || !Array.isArray(cids) || cids.length === 0) {
callback({'categories' : []});
return;
}
var categories = [];
for(var i=0; i<cids.length; ++i) {
Categories.getCategoryData(cids[i], function(categoryData) {
function getCategory(cid, callback) {
Categories.getCategoryData(cid, function(err, categoryData) {
if(!categoryData)
if(err) {
callback(err);
return;
}
Categories.hasReadCategory(categoryData.cid, current_user, function(hasRead) {
Categories.hasReadCategory(cid, current_user, function(hasRead) {
categoryData['badgeclass'] = (parseInt(categoryData.topic_count,10) === 0 || (hasRead && current_user != 0)) ? '' : 'badge-important';
categories.push(categoryData);
if(categories.length === cids.length)
callback({'categories': categories});
callback(null);
}) ;
});
}
};
});
}
async.eachSeries(cids, getCategory, function(err) {
if(err) {
console.log(err);
callback(null);
return;
}
callback({'categories': categories});
});
};
}(exports));
}(exports));

View File

@@ -28,8 +28,11 @@ var RDB = require('./redis.js'),
RDB.sadd('pid:' + pid + ':users_favourited', uid);
RDB.hincrby('post:' + pid, 'reputation', 1);
if (uid !== uid_of_poster)
user.incrementUserFieldBy(uid_of_poster, 'reputation', 1);
if (uid !== uid_of_poster) {
user.incrementUserFieldBy(uid_of_poster, 'reputation', 1, function(err, newreputation) {
RDB.zadd('users:reputation', newreputation, uid_of_poster);
});
}
if (room_id) {
io.sockets.in(room_id).emit('event:rep_up', {uid: uid !== uid_of_poster ? uid_of_poster : 0, pid: pid});
@@ -63,8 +66,11 @@ var RDB = require('./redis.js'),
RDB.srem('pid:' + pid + ':users_favourited', uid);
RDB.hincrby('post:' + pid, 'reputation', -1);
if (uid !== uid_of_poster)
user.incrementUserFieldBy(uid_of_poster, 'reputation', -1);
if (uid !== uid_of_poster) {
user.incrementUserFieldBy(uid_of_poster, 'reputation', -1, function(err, newreputation) {
RDB.zadd('users:reputation', newreputation, uid_of_poster);
});
}
if (room_id) {
io.sockets.in(room_id).emit('event:rep_down', {uid: uid !== uid_of_poster ? uid_of_poster : 0, pid: pid});

View File

@@ -67,7 +67,12 @@
};
Feed.updateCategory = function(cid) {
categories.getCategoryById(cid, 0, function(categoryData) {
categories.getCategoryById(cid, 0, function(err, categoryData) {
if (err) {
console.log('Error: Could not update RSS feed for category ' + cid);
return;
}
var location = '/category/' + categoryData.category_id + '/' + categoryData.category_name,
xml_url = '/category' + cid + '.rss';

33
src/imgur.js Normal file
View File

@@ -0,0 +1,33 @@
var request = require('request');
(function(imgur) {
var clientID = '';
imgur.upload = function(image, type, callback) {
var options = {
url: 'https://api.imgur.com/3/upload.json',
headers: {
'Authorization': 'Client-ID ' + clientID
}
};
var post = request.post(options, function(err, req, body){
try{
callback(err, JSON.parse(body));
} catch(e) {
callback(err, body);
}
});
var upload = post.form({type:type, image:image});
}
imgur.setClientID = function(id) {
clientID = id;
}
}(exports));

View File

@@ -3,6 +3,7 @@ var async = require('async'),
fs = require('fs'),
url = require('url'),
path = require('path'),
meta = require('./meta'),
install = {
questions: [
'base_url|Publically accessible URL of this installation? (http://localhost)',
@@ -11,18 +12,19 @@ var async = require('async'),
'redis:host|Host IP or address of your Redis instance? (127.0.0.1)',
'redis:port|Host port of your Redis instance? (6379)',
'redis:password|Password of your Redis database? (no password)',
'secret|Your NodeBB secret? (keyboard mash for a bit here)',
'bcrypt_rounds|The number of rounds to use for bcrypt.genSalt? (10)'
'secret|Your NodeBB secret? (keyboard mash for a bit here)'
],
defaults: {
"base_url": 'http://localhost',
"port": 4567,
"use_port": true,
"redis:host": '127.0.0.1',
"redis:port": 6379,
"redis:password": '',
"redis": {
"host": '127.0.0.1',
"port": 6379,
"password": ''
},
"secret": utils.generateUUID(),
"bcrypt_rounds": 10,
"bcrypt_rounds": 12,
"upload_path": '/public/uploads'
},
ask: function(question, callback) {
@@ -40,7 +42,29 @@ var async = require('async'),
async.eachSeries(install.questions, function(question, next) {
var question = question.split('|');
install.ask(question[1], function(value) {
if (value !== '') config[question[0]] = value;
switch(question[0]) {
case 'use_port':
value = value.toLowerCase();
if (['y', 'yes', ''].indexOf(value) === -1) config[question[0]] = false;
break;
case 'redis:host':
config.redis = config.redis || {};
if (value !== '') config.redis.host = value;
break;
case 'redis:port':
config.redis = config.redis || {};
if (value !== '') config.redis.port = value;
break;
case 'redis:password':
config.redis = config.redis || {};
if (value !== '') config.redis.password = value;
break;
default:
if (value !== '') config[question[0]] = value;
break;
}
next();
});
}, function() {
@@ -55,10 +79,16 @@ var async = require('async'),
port: config.port
},
api_url: protocol + '//' + host + (config.use_port ? ':' + config.port : '') + relative_path + '/api/',
relative_path: relative_path
relative_path: relative_path
};
server_conf.base_url = protocol + '//' + host;
server_conf.relative_path = relative_path;
server_conf.imgurClientID = '';
meta.config.set('postDelay', 10000);
meta.config.set('minimumPostLength', 8);
meta.config.set('minimumTitleLength', 3);
install.save(server_conf, client_conf, callback);
});

View File

@@ -1,7 +1,8 @@
var user = require('./user.js'),
bcrypt = require('bcrypt'),
RDB = require('./redis.js');
RDB = require('./redis.js'),
path = require('path');
(function(Login){
@@ -53,7 +54,7 @@ var user = require('./user.js'),
}
}
Login.loginViaTwitter = function(twid, handle, callback) {
Login.loginViaTwitter = function(twid, handle, photos, callback) {
user.get_uid_by_twitter_id(twid, function(uid) {
if (uid !== null) {
// Existing User
@@ -62,13 +63,22 @@ var user = require('./user.js'),
});
} else {
// New User
user.create(handle, null, null, function(err, uid) {
user.create(handle, undefined, undefined, function(err, uid) {
if (err !== null) {
callback(err);
} else {
// Save twitter-specific information to the user
user.setUserField(uid, 'twid', twid);
RDB.hset('twid:uid', twid, uid);
// Save their photo, if present
if (photos && photos.length > 0) {
var photoUrl = photos[0].value;
photoUrl = path.dirname(photoUrl) + '/' + path.basename(photoUrl, path.extname(photoUrl)).slice(0, -6) + 'bigger' + path.extname(photoUrl);
user.setUserField(uid, 'uploadedpicture', photoUrl);
user.setUserField(uid, 'picture', photoUrl);
}
callback(null, {
uid: uid
});
@@ -98,7 +108,7 @@ var user = require('./user.js'),
user.get_uid_by_email(email, function(uid) {
if (!uid) {
user.create(handle, null, email, function(err, uid) {
user.create(handle, undefined, email, function(err, uid) {
if (err !== null) {
callback(err);
} else success(uid);
@@ -129,7 +139,7 @@ var user = require('./user.js'),
user.get_uid_by_email(email, function(uid) {
if (!uid) {
user.create(name, null, email, function(err, uid) {
user.create(name, undefined, email, function(err, uid) {
if (err !== null) {
callback(err);
} else success(uid);

View File

@@ -1,45 +1,113 @@
var utils = require('./../public/src/utils.js'),
RDB = require('./redis.js'),
async = require('async');
async = require('async'),
path = require('path'),
fs = require('fs');
(function(Meta) {
Meta.config = {
get: function(callback) {
var config = {};
async.waterfall([
function(next) {
RDB.hkeys('config', function(err, keys) {
next(err, keys);
});
},
function(keys, next) {
async.each(keys, function(key, next) {
RDB.hget('config', key, function(err, value) {
if (!err) {
config[key] = value;
}
next(err);
});
}, next);
}
], function(err) {
RDB.hgetall('config', function(err, config) {
if (!err) {
config = config || {};
config.status = 'ok';
callback(config);
} else callback({
status: 'error'
});
} else {
callback({
status: 'error'
});
}
});
},
getFields: function(fields, callback) {
RDB.hmgetObject('config', fields, callback);
},
set: function(field, value, callback) {
RDB.hset('config', field, value, function(err, res) {
callback(err);
if(callback)
callback(err, res);
});
},
remove: function(field) {
RDB.hdel('config', field);
}
}
Meta.themes = {
get: function(callback) {
var themePath = path.join(__dirname, '../', 'public/themes');
fs.readdir(themePath, function(err, files) {
var themeArr = [];
async.each(files, function(file, next) {
fs.lstat(path.join(themePath, file), function(err, stats) {
if(stats.isDirectory()) {
var themeDir = file,
themeConfPath = path.join(themePath, themeDir, 'theme.json');
fs.exists(themeConfPath, function(exists) {
if (exists) {
fs.readFile(themeConfPath, function(err, conf) {
conf = JSON.parse(conf);
conf.src = global.nconf.get('url') + 'themes/' + themeDir + '/' + conf.src;
if (conf.screenshot) conf.screenshot = global.nconf.get('url') + 'themes/' + themeDir + '/' + conf.screenshot;
else conf.screenshot = global.nconf.get('url') + 'images/themes/default.png';
themeArr.push(conf);
next();
});
} else next();
});
} else next();
});
}, function(err) {
callback(err, themeArr);
});
});
}
}
Meta.title = {
build: function(urlFragment, current_user, callback) {
var self = this,
user = require('./user');
async.parallel({
title: function(next) {
self.parseFragment(urlFragment, next);
},
notifCount: function(next) {
user.notifications.getUnreadCount(current_user, next);
}
}, function(err, values) {
var title;
if (err) title = global.config.title || 'NodeBB';
else title = (values.notifCount > 0 ? '(' + values.notifCount + ') ' : '') + (values.title ? values.title + ' | ' : '') + (global.config.title || 'NodeBB');
callback(null, title);
});
},
parseFragment: function(urlFragment, callback) {
if (urlFragment === '') {
callback(null, 'Index');
} else if (urlFragment === 'recent') {
callback(null, 'Recent Topics');
} else if (urlFragment === 'unread') {
callback(null, 'Unread Topics');
} else if (urlFragment === 'users') {
callback(null, 'Registered Users');
} else if (/^category\/\d+\/?/.test(urlFragment)) {
var cid = urlFragment.match(/category\/(\d+)/)[1];
require('./categories').getCategoryField(cid, 'name', function(err, name) {
callback(null, name);
});
} else if (/^topic\/\d+\/?/.test(urlFragment)) {
var tid = urlFragment.match(/topic\/(\d+)/)[1];
require('./topics').getTopicField(tid, 'title', function(err, title) {
callback(null, title);
});
} else callback(null);
}
}
}(exports));

View File

@@ -1,121 +1,137 @@
var RDB = require('./redis.js'),
async = require('async'),
utils = require('../public/src/utils.js');
utils = require('../public/src/utils.js'),
(function(Notifications) {
Notifications.get = function(nid, callback) {
RDB.hmget('notifications:' + nid, 'text', 'score', 'path', 'datetime', 'uniqueId', function(err, notification) {
callback({
nid: nid,
text: notification[0],
score: notification[1],
path: notification[2],
datetime: notification[3],
uniqueId: notification[4]
notifications = {
get: function(nid, callback) {
RDB.hmget('notifications:' + nid, 'text', 'score', 'path', 'datetime', 'uniqueId', function(err, notification) {
callback({
nid: nid,
text: notification[0],
score: notification[1],
path: notification[2],
datetime: notification[3],
uniqueId: notification[4]
});
});
});
}
Notifications.create = function(text, score, path, uniqueId, callback) {
/*
* Score guide:
* 0 Low priority messages (probably unused)
* 5 Normal messages
* 10 High priority messages
*
* uniqueId is used solely to override stale nids.
* If a new nid is pushed to a user and an existing nid in the user's
* (un)read list contains the same uniqueId, it will be removed, and
* the new one put in its place.
*/
RDB.incr('notifications:next_nid', function(err, nid) {
RDB.hmset(
'notifications:' + nid,
'text', text || '',
'score', score || 5,
'path', path || null,
'datetime', Date.now(),
'uniqueId', uniqueId || utils.generateUUID(),
function(err, status) {
if (status === 'OK') callback(nid);
},
create: function(text, score, path, uniqueId, callback) {
/*
* Score guide:
* 0 Low priority messages (probably unused)
* 5 Normal messages
* 10 High priority messages
*
* uniqueId is used solely to override stale nids.
* If a new nid is pushed to a user and an existing nid in the user's
* (un)read list contains the same uniqueId, it will be removed, and
* the new one put in its place.
*/
RDB.incr('notifications:next_nid', function(err, nid) {
RDB.hmset(
'notifications:' + nid,
'text', text || '',
'score', score || 5,
'path', path || null,
'datetime', Date.now(),
'uniqueId', uniqueId || utils.generateUUID(),
function(err, status) {
if (status === 'OK') callback(nid);
});
});
});
}
},
push: function(nid, uids, callback) {
if (!Array.isArray(uids)) uids = [uids];
Notifications.push = function(nid, uids, callback) {
if (!Array.isArray(uids)) uids = [uids];
var numUids = uids.length,
x;
var numUids = uids.length,
x;
Notifications.get(nid, function(notif_data) {
for(x=0;x<numUids;x++) {
if (parseInt(uids[x]) > 0) {
(function(uid) {
Notifications.remove_by_uniqueId(notif_data.uniqueId, uid, function() {
RDB.zadd('uid:' + uid + ':notifications:unread', notif_data.score, nid);
RDB.set('uid:' + uid + ':notifications:flag', 1);
global.io.sockets.in('uid_' + uid).emit('event:new_notification');
if (callback) callback(true);
});
})(uids[x]);
notifications.get(nid, function(notif_data) {
for(x=0;x<numUids;x++) {
if (parseInt(uids[x]) > 0) {
(function(uid) {
notifications.remove_by_uniqueId(notif_data.uniqueId, uid, function() {
RDB.zadd('uid:' + uid + ':notifications:unread', notif_data.score, nid);
RDB.set('uid:' + uid + ':notifications:flag', 1);
global.io.sockets.in('uid_' + uid).emit('event:new_notification');
if (callback) callback(true);
});
})(uids[x]);
}
}
}
});
}
Notifications.remove_by_uniqueId = function(uniqueId, uid, callback) {
async.parallel([
function(next) {
RDB.zrange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) {
if (nids && nids.length > 0) {
async.each(nids, function(nid, next) {
Notifications.get(nid, function(nid_info) {
if (nid_info.uniqueId === uniqueId) RDB.zrem('uid:' + uid + ':notifications:unread', nid);
});
},
remove_by_uniqueId: function(uniqueId, uid, callback) {
async.parallel([
function(next) {
RDB.zrange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) {
if (nids && nids.length > 0) {
async.each(nids, function(nid, next) {
notifications.get(nid, function(nid_info) {
if (nid_info.uniqueId === uniqueId) RDB.zrem('uid:' + uid + ':notifications:unread', nid);
next();
});
}, function(err) {
next();
});
}, function(err) {
next();
});
} else next();
});
},
function(next) {
RDB.zrange('uid:' + uid + ':notifications:read', 0, -1, function(err, nids) {
if (nids && nids.length > 0) {
async.each(nids, function(nid, next) {
Notifications.get(nid, function(nid_info) {
if (nid_info.uniqueId === uniqueId) RDB.zrem('uid:' + uid + ':notifications:read', nid);
} else next();
});
},
function(next) {
RDB.zrange('uid:' + uid + ':notifications:read', 0, -1, function(err, nids) {
if (nids && nids.length > 0) {
async.each(nids, function(nid, next) {
notifications.get(nid, function(nid_info) {
if (nid_info.uniqueId === uniqueId) RDB.zrem('uid:' + uid + ':notifications:read', nid);
next();
});
}, function(err) {
next();
});
}, function(err) {
next();
});
} else next();
} else next();
});
}
], function(err) {
if (!err) callback(true);
});
},
mark_read: function(nid, uid, callback) {
if (parseInt(uid) > 0) {
notifications.get(nid, function(notif_data) {
RDB.zrem('uid:' + uid + ':notifications:unread', nid);
RDB.zadd('uid:' + uid + ':notifications:read', notif_data.score, nid);
if (callback) callback();
});
}
], function(err) {
if (!err) callback(true);
});
}
},
mark_read_multiple: function(nids, uid, callback) {
if (!Array.isArray(nids) && parseInt(nids, 10) > 0) nids = [nids];
Notifications.mark_read = function(nid, uid, callback) {
if (parseInt(uid) > 0) {
Notifications.get(nid, function(notif_data) {
RDB.zrem('uid:' + uid + ':notifications:unread', nid);
RDB.zadd('uid:' + uid + ':notifications:read', notif_data.score, nid);
if (callback) callback(true);
async.each(nids, function(nid, next) {
notifications.mark_read(nid, uid, function(err) {
if (!err) next(null);
});
}, function(err) {
if (callback) callback(err);
});
},
mark_all_read: function(uid, callback) {
RDB.zrange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) {
if (err) return callback(err);
if (nids.length > 0) {
notifications.mark_read_multiple(nids, uid, function(err) {
callback(err);
});
} else callback();
});
}
}
Notifications.mark_read_multiple = function(nids, uid, callback) {
async.each(nids, function(nid, next) {
Notifications.mark_read(nid, uid, function(success) {
if (success) next(null);
});
}, function(err) {
if (callback && !err) callback(true);
});
}
}(exports));
module.exports = {
get: notifications.get,
create: notifications.create,
push: notifications.push,
mark_read: notifications.mark_read_multiple,
mark_all_read: notifications.mark_all_read
}

203
src/plugins.js Normal file
View File

@@ -0,0 +1,203 @@
var fs = require('fs'),
path = require('path'),
RDB = require('./redis.js'),
async = require('async'),
plugins = {
libraries: [],
loadedHooks: {},
init: function() {
if (this.initialized) return;
if (global.env === 'development') console.log('Info: [plugins] Initializing plugins system');
var _self = this;
// Read the list of activated plugins and require their libraries
async.waterfall([
function(next) {
RDB.smembers('plugins:active', next);
},
function(plugins, next) {
async.each(plugins, function(plugin) {
// TODO: Update this check to also check node_modules
var pluginPath = path.join(__dirname, '../plugins/', plugin);
fs.exists(pluginPath, function(exists) {
if (exists) {
fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) {
if (err) return next(err);
var pluginData = JSON.parse(data);
_self.libraries[pluginData.id] = require(path.join(pluginPath, pluginData.library));
if (pluginData.hooks) {
for(var x=0,numHooks=pluginData.hooks.length;x<numHooks;x++) {
_self.registerHook(pluginData.id, pluginData.hooks[x]);
}
}
if (global.env === 'development') console.log('Info: [plugins] Loaded plugin: ' + pluginData.id);
next();
});
} else {
if (global.env === 'development') console.log('Info: [plugins] Plugin \'' + plugin + '\' not found');
next(); // Ignore this plugin silently
}
})
}, next);
}
], function(err) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] NodeBB encountered a problem while loading plugins', err.message);
return;
}
if (global.env === 'development') console.log('Info: [plugins] Plugins OK');
});
},
initialized: false,
registerHook: function(id, data) {
/*
`data` is an object consisting of (* is required):
`data.hook`*, the name of the NodeBB hook
`data.method`*, the method called in that plugin
`data.callbacked`, whether or not the hook expects a callback (true), or a return (false). Only used for filters. (Default: false)
(Not implemented) `data.priority`, the relative priority of the method when it is eventually called (default: 10)
*/
var _self = this;
if (data.hook && data.method) {
_self.loadedHooks[data.hook] = _self.loadedHooks[data.hook] || [];
_self.loadedHooks[data.hook].push([id, data.method]);
if (global.env === 'development') console.log('Info: [plugins] Hook registered: ' + data.hook + ' will call ' + id);
} else return;
},
fireHook: function(hook, args, callback) {
// TODO: Implement priority hook firing
var _self = this
hookList = this.loadedHooks[hook];
if (hookList && Array.isArray(hookList)) {
if (global.env === 'development') console.log('Info: [plugins] Firing hook: \'' + hook + '\'');
var hookType = hook.split(':')[0];
switch(hookType) {
case 'filter':
// Filters only take one argument, so only args[0] will be passed in
var returnVal = (Array.isArray(args) ? args[0] : args);
async.each(hookList, function(hookObj, next) {
if (hookObj.callbacked) {
_self.libraries[hookObj[0]][hookObj[1]](returnVal, function(err, afterVal) {
returnVal = afterVal;
next(err);
});
} else {
returnVal = _self.libraries[hookObj[0]][hookObj[1]](returnVal);
next();
}
}, function(err) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] Problem executing hook: ' + hook);
}
callback(returnVal);
});
break;
case 'action':
async.each(hookList, function(hookObj) {
if (
_self.libraries[hookObj[0]] &&
_self.libraries[hookObj[0]][hookObj[1]] &&
typeof _self.libraries[hookObj[0]][hookObj[1]] === 'function'
) {
_self.libraries[hookObj[0]][hookObj[1]].apply(_self.libraries[hookObj[0]], args);
} else {
if (global.env === 'development') console.log('Info: [plugins] Expected method \'' + hookObj[1] + '\' in plugin \'' + hookObj[0] + '\' not found, skipping.');
}
});
break;
default:
// Do nothing...
break;
}
} else {
// Otherwise, this hook contains no methods
var returnVal = (Array.isArray(args) ? args[0] : args);
if (callback) callback(returnVal);
}
},
isActive: function(id, callback) {
RDB.sismember('plugins:active', id, callback);
},
toggleActive: function(id, callback) {
this.isActive(id, function(err, active) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] Could not toggle active state on plugin \'' + id + '\'');
return;
}
RDB[(active ? 'srem' : 'sadd')]('plugins:active', id, function(err, success) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] Could not toggle active state on plugin \'' + id + '\'');
return;
}
callback({
id: id,
active: !active
});
});
});
},
showInstalled: function(callback) {
// TODO: Also check /node_modules
var _self = this;
moduleBasePath = path.join(__dirname, '../plugins');
async.waterfall([
function(next) {
fs.readdir(moduleBasePath, next);
},
function(files, next) {
var plugins = [];
async.each(files, function(file, next) {
var modulePath = path.join(moduleBasePath, file),
configPath;
async.waterfall([
function(next) {
fs.stat(path.join(moduleBasePath, file), next);
},
function(stats, next) {
if (stats.isDirectory()) fs.readFile(path.join(modulePath, 'plugin.json'), next);
else next(new Error('not-a-directory'));
},
function(configJSON, next) {
var config = JSON.parse(configJSON);
_self.isActive(config.id, function(err, active) {
if (err) next(new Error('no-active-state'));
delete config.library;
delete config.hooks;
config.active = active;
config.activeText = '<i class="icon-off"></i> ' + (active ? 'Dea' : 'A') + 'ctivate';
next(null, config);
});
}
], function(err, config) {
if (err) return next(); // Silently fail
plugins.push(config);
next();
});
}, function(err) {
next(null, plugins);
});
}
], function(err, plugins) {
callback(err, plugins);
});
}
}
plugins.init();
module.exports = plugins;

View File

@@ -4,12 +4,12 @@ var RDB = require('./redis.js'),
threadTools = require('./threadTools.js'),
user = require('./user.js'),
async = require('async'),
marked = require('marked'),
utils = require('../public/src/utils');
marked.setOptions({
breaks: true
});
utils = require('../public/src/utils'),
plugins = require('./plugins'),
reds = require('reds'),
postSearch = reds.createSearch('nodebbpostsearch'),
topicSearch = reds.createSearch('nodebbtopicsearch');
(function(PostTools) {
PostTools.isMain = function(pid, tid, callback) {
@@ -59,15 +59,23 @@ marked.setOptions({
posts.setPostField(pid, 'edited', Date.now());
posts.setPostField(pid, 'editor', uid);
postSearch.remove(pid, function() {
postSearch.index(content, pid);
});
posts.getPostField(pid, 'tid', function(tid) {
PostTools.isMain(pid, tid, function(isMainPost) {
if (isMainPost)
if (isMainPost) {
topics.setTopicField(tid, 'title', title);
topicSearch.remove(tid, function() {
topicSearch.index(title, tid);
});
}
io.sockets.in('topic_' + tid).emit('event:post_edited', {
pid: pid,
title: title,
content: marked(content || '')
content: PostTools.markdownToHTML(content)
});
});
});
@@ -75,26 +83,40 @@ marked.setOptions({
PostTools.privileges(pid, uid, function(privileges) {
if (privileges.editable) {
success();
plugins.fireHook('filter:save_post_content', content, function(parsedContent) {
content = parsedContent;
success();
});
}
});
}
PostTools.delete = function(uid, pid) {
var success = function() {
var success = function() {
posts.setPostField(pid, 'deleted', 1);
postSearch.remove(pid);
posts.getPostField(pid, 'tid', function(tid) {
io.sockets.in('topic_' + tid).emit('event:post_deleted', {
posts.getPostFields(pid, ['tid', 'uid'], function(postData) {
user.decrementUserFieldBy(postData.uid, 'postcount', 1, function(err, postcount) {
RDB.zadd('users:postcount', postcount, postData.uid);
});
io.sockets.in('topic_' + postData.tid).emit('event:post_deleted', {
pid: pid
});
// Delete the thread if it is the last undeleted post
threadTools.get_latest_undeleted_pid(tid, function(err, pid) {
threadTools.get_latest_undeleted_pid(postData.tid, function(err, pid) {
if (err && err.message === 'no-undeleted-pids-found') {
threadTools.delete(tid, -1, function(err) {
if (err) console.log('Error: Could not delete topic (tid: ' + tid + ')');
threadTools.delete(postData.tid, -1, function(err) {
if (err) console.log('Error: Could not delete topic (tid: ' + postData.tid + ')');
});
} else {
posts.getPostField(pid, 'timestamp', function(timestamp) {
topics.updateTimestamp(postData.tid, timestamp);
});
}
});
});
@@ -108,13 +130,24 @@ marked.setOptions({
}
PostTools.restore = function(uid, pid) {
var success = function() {
var success = function() {
posts.setPostField(pid, 'deleted', 0);
posts.getPostField(pid, 'tid', function(tid) {
io.sockets.in('topic_' + tid).emit('event:post_restored', {
posts.getPostFields(pid, ['tid', 'uid', 'content'], function(postData) {
user.incrementUserFieldBy(postData.uid, 'postcount', 1);
io.sockets.in('topic_' + postData.tid).emit('event:post_restored', {
pid: pid
});
threadTools.get_latest_undeleted_pid(postData.tid, function(err, pid) {
posts.getPostField(pid, 'timestamp', function(timestamp) {
topics.updateTimestamp(postData.tid, timestamp);
});
});
postSearch.index(postData.content, pid);
});
};
@@ -125,5 +158,36 @@ marked.setOptions({
});
}
PostTools.markdownToHTML = function(md, isSignature) {
var marked = require('marked'),
cheerio = require('cheerio');
marked.setOptions({
breaks: true
});
if (md && md.length > 0) {
var parsedContentDOM = cheerio.load(marked(md));
var domain = global.nconf.get('url');
parsedContentDOM('a').each(function() {
this.attr('rel', 'nofollow');
var href = this.attr('href');
if (href && !href.match(domain)) {
this.attr('href', domain + 'outgoing?url=' + encodeURIComponent(href));
if (!isSignature) this.append(' <i class="icon-external-link"></i>');
}
});
html = parsedContentDOM.html();
} else {
html = '<p></p>';
}
return html;
}
}(exports));

View File

@@ -1,17 +1,17 @@
var RDB = require('./redis.js'),
utils = require('./../public/src/utils.js'),
schema = require('./schema.js'),
marked = require('marked'),
user = require('./user.js'),
topics = require('./topics.js'),
favourites = require('./favourites.js'),
threadTools = require('./threadTools.js'),
postTools = require('./postTools'),
feed = require('./feed.js'),
async = require('async');
marked.setOptions({
breaks: true
});
async = require('async'),
plugins = require('./plugins'),
reds = require('reds'),
nconf = require('nconf'),
postSearch = reds.createSearch('nodebbpostsearch');
(function(Posts) {
@@ -25,21 +25,20 @@ marked.setOptions({
callback(posts);
});
} else {
callback({
error: 'no-posts'
});
callback([]);
}
});
}
Posts.addUserInfoToPost = function(post, callback) {
user.getUserFields(post.uid, ['username', 'userslug', 'reputation', 'picture', 'signature'], function(userData) {
user.getUserFields(post.uid, ['username', 'userslug', 'reputation', 'postcount', 'picture', 'signature'], function(userData) {
post.username = userData.username || 'anonymous';
post.userslug = userData.userslug || '';
post.user_rep = userData.reputation || 0;
post.user_postcount = userData.postcount || 0;
post.picture = userData.picture || require('gravatar').url('', {}, https=global.nconf.get('https'));
post.signature = marked(userData.signature || '');
post.signature = postTools.markdownToHTML(userData.signature, true);
if(post.editor !== '') {
user.getUserFields(post.editor, ['username', 'userslug'], function(editorData) {
@@ -58,75 +57,56 @@ marked.setOptions({
var returnData = [];
function getPostSummary(pid, callback) {
Posts.getPostFields(pid, ['pid', 'content', 'uid', 'timestamp', 'deleted'], function(postData) {
Posts.addUserInfoToPost(postData, function() {
Posts.getPostFields(pid, ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted'], function(postData) {
if(postData.deleted === '1') {
return callback(null);
}
if(postData.deleted !== '1') {
returnData.push(postData);
}
callback(null);
Posts.addUserInfoToPost(postData, function() {
topics.getTopicField(postData.tid, 'slug', function(err, topicSlug) {
if(postData.content)
postData.content = utils.strip_tags(postTools.markdownToHTML(postData.content));
postData.topicSlug = topicSlug;
returnData.push(postData);
callback(null);
});
});
});
}
async.eachSeries(pids, getPostSummary, function(err) {
if(!err) {
callback(returnData);
} else {
console.log(err);
}
});
};
Posts.getPostData = function(pid, callback) {
RDB.hgetall('post:' + pid, function(err, data) {
if(err === null)
if(err === null) {
callback(data);
}
else
console.log(err);
});
}
Posts.getPostFields = function(uid, fields, callback) {
RDB.hmget('post:' + uid, fields, function(err, data) {
Posts.getPostFields = function(pid, fields, callback) {
RDB.hmgetObject('post:' + pid, fields, function(err, data) {
if(err === null) {
var returnData = {};
for(var i=0, ii=fields.length; i<ii; ++i) {
returnData[fields[i]] = data[i];
}
callback(returnData);
callback(data);
}
else
else {
console.log(err);
}
});
}
Posts.getPostsByPids = function(pids, callback) {
var posts = [],
loaded = 0;
for(var i=0, ii=pids.length; i<ii; ++i) {
(function(index, pid) {
Posts.getPostData(pid, function(postData) {
if(postData) {
postData.relativeTime = utils.relativeTime(postData.timestamp);
postData.post_rep = postData.reputation;
postData['edited-class'] = postData.editor !== '' ? '' : 'none';
postData['relativeEditTime'] = postData.edited !== '0' ? utils.relativeTime(postData.edited) : '';
postData.content = marked(postData.content || '');
posts[index] = postData;
}
++loaded;
if(loaded === pids.length)
callback(posts);
});
}(i, pids[i]));
}
}
Posts.getPostField = function(pid, field, callback) {
RDB.hget('post:' + pid, field, function(err, data) {
if(err === null)
@@ -140,26 +120,44 @@ marked.setOptions({
RDB.hset('post:' + pid, field, value);
}
Posts.getPostFields = function(pid, fields, callback) {
RDB.hmget('post:' + pid, fields, function(err, data) {
if(err === null) {
var returnData = {};
for(var i=0, ii=fields.length; i<ii; ++i) {
returnData[fields[i]] = data[i];
}
callback(returnData);
Posts.getPostsByPids = function(pids, callback) {
var posts = [];
function iterator(pid, callback) {
Posts.getPostData(pid, function(postData) {
if(postData) {
postData.relativeTime = utils.relativeTime(postData.timestamp);
postData.post_rep = postData.reputation;
postData['edited-class'] = postData.editor !== '' ? '' : 'none';
postData['relativeEditTime'] = postData.edited !== '0' ? utils.relativeTime(postData.edited) : '';
postData.content = postTools.markdownToHTML(postData.content);
if(postData.uploadedImages) {
postData.uploadedImages = JSON.parse(postData.uploadedImages);
} else {
postData.uploadedImages = [];
}
posts.push(postData);
}
callback(null);
});
}
async.eachSeries(pids, iterator, function(err) {
if(!err) {
callback(posts);
} else {
callback([]);
}
else
console.log(err);
});
});
}
Posts.get_cid_by_pid = function(pid, callback) {
Posts.getPostField(pid, 'tid', function(tid) {
if (tid) {
topics.getTopicField(tid, 'cid', function(cid) {
topics.getTopicField(tid, 'cid', function(err, cid) {
if (cid) {
callback(cid);
} else {
@@ -170,70 +168,65 @@ marked.setOptions({
});
}
Posts.reply = function(socket, tid, uid, content) {
if (uid < 1) {
socket.emit('event:alert', {
title: 'Reply Unsuccessful',
message: 'You don&apos;t seem to be logged in, so you cannot reply.',
type: 'error',
timeout: 2000
});
Posts.emitContentTooShortAlert = function(socket) {
socket.emit('event:alert', {
type: 'error',
timeout: 2000,
title: 'Content too short',
message: "Please enter a longer post. At least " + config.minimumPostLength + " characters.",
alert_id: 'post_error'
});
}
Posts.emitTooManyPostsAlert = function(socket) {
socket.emit('event:alert', {
title: 'Too many posts!',
message: 'You can only post every '+ config.postDelay/1000 + ' seconds.',
type: 'error',
timeout: 2000
});
}
Posts.reply = function(tid, uid, content, images, callback) {
if(content) {
content = content.trim();
}
if (!content || content.length < config.minimumPostLength) {
callback(new Error('content-too-short'), null);
return;
}
user.getUserField(uid, 'lastposttime', function(lastposttime) {
if(Date.now() - lastposttime < config.post_delay) {
socket.emit('event:alert', {
title: 'Too many posts!',
message: 'You can only post every '+ (config.post_delay / 1000) + ' seconds.',
type: 'error',
timeout: 2000
});
if(Date.now() - lastposttime < config.postDelay) {
callback(new Error('too-many-posts'), null);
return;
}
Posts.create(uid, tid, content, function(pid) {
if (pid > 0) {
RDB.rpush('tid:' + tid + ':posts', pid);
Posts.create(uid, tid, content, images, function(postData) {
if (postData) {
topics.addPostToTopic(tid, postData.pid);
topics.markUnRead(tid);
RDB.del('tid:' + tid + ':read_by_uid');
Posts.get_cid_by_pid(pid, function(cid) {
Posts.get_cid_by_pid(postData.pid, function(cid) {
RDB.del('cid:' + cid + ':read_by_uid', function(err, data) {
topics.markAsRead(tid, uid);
});
});
Posts.getTopicPostStats(socket);
// Send notifications to users who are following this topic
threadTools.notify_followers(tid, uid);
socket.emit('event:alert', {
title: 'Reply Successful',
message: 'You have successfully replied. Click here to view your reply.',
type: 'notify',
timeout: 2000
});
postData.content = postTools.markdownToHTML(postData.content);
postData.post_rep = 0;
postData.relativeTime = utils.relativeTime(postData.timestamp)
postData.fav_star_class = 'icon-star-empty';
postData['edited-class'] = 'none';
postData.uploadedImages = JSON.parse(postData.uploadedImages);
var timestamp = Date.now();
var socketData = {
'posts' : [
{
'pid' : pid,
'content' : marked(content || ''),
'uid' : uid,
'post_rep' : 0,
'timestamp' : timestamp,
'relativeTime': utils.relativeTime(timestamp),
'fav_star_class' :'icon-star-empty',
'edited-class': 'none',
'editor': '',
}
postData
]
};
@@ -241,80 +234,124 @@ marked.setOptions({
io.sockets.in('topic_' + tid).emit('event:new_post', socketData);
io.sockets.in('recent_posts').emit('event:new_post', socketData);
});
callback(null, 'Reply successful');
} else {
socket.emit('event:alert', {
title: 'Reply Unsuccessful',
message: 'Your reply could not be posted at this time. Please try again later.',
type: 'notify',
timeout: 2000
});
callback(new Error('reply-error'), null);
}
});
});
};
Posts.create = function(uid, tid, content, callback) {
Posts.create = function(uid, tid, content, images, callback) {
if (uid === null) {
callback(-1);
callback(null);
return;
}
topics.isLocked(tid, function(locked) {
if (!locked || locked === '0') {
RDB.incr('global:next_post_id', function(err, pid) {
RDB.handle(err);
var timestamp = Date.now();
RDB.hmset('post:' + pid, {
'pid': pid,
'uid': uid,
'tid': tid,
'content': content,
'timestamp': timestamp,
'reputation': 0,
'editor': '',
'edited': 0,
'deleted': 0
});
topics.increasePostCount(tid);
topics.setTopicField(tid, 'lastposttime', timestamp);
topics.addToRecent(tid, timestamp);
RDB.incr('totalpostcount');
plugins.fireHook('filter:save_post_content', content, function(content) {
var timestamp = Date.now(),
postData = {
'pid': pid,
'uid': uid,
'tid': tid,
'content': content,
'timestamp': timestamp,
'reputation': 0,
'editor': '',
'edited': 0,
'deleted': 0,
'uploadedImages': ''
};
topics.getTopicField(tid, 'cid', function(cid) {
RDB.handle(err);
RDB.hmset('post:' + pid, postData);
feed.updateTopic(tid, cid);
topics.increasePostCount(tid);
topics.updateTimestamp(tid, timestamp);
RDB.zadd('categories:recent_posts:cid:' + cid, Date.now(), pid);
RDB.incr('totalpostcount');
topics.getTopicField(tid, 'cid', function(err, cid) {
RDB.handle(err);
// this is a bit of a naive implementation, defn something to look at post-MVP
RDB.scard('cid:' + cid + ':active_users', function(amount) {
if (amount > 10) {
RDB.spop('cid:' + cid + ':active_users');
feed.updateTopic(tid, cid);
RDB.zadd('categories:recent_posts:cid:' + cid, timestamp, pid);
RDB.zadd('categories:' + cid + ':tid', timestamp, tid);
// this is a bit of a naive implementation, defn something to look at post-MVP
RDB.scard('cid:' + cid + ':active_users', function(amount) {
if (amount > 10) {
RDB.spop('cid:' + cid + ':active_users');
}
RDB.sadd('cid:' + cid + ':active_users', uid);
});
});
user.onNewPostMade(uid, tid, pid, timestamp);
uploadPostImages(postData, images, function(err, uploadedImages) {
if(err) {
console.log('Uploading images failed!');
} else {
postData.uploadedImages = JSON.stringify(uploadedImages);
Posts.setPostField(pid, 'uploadedImages', postData.uploadedImages);
}
RDB.sadd('cid:' + cid + ':active_users', uid);
callback(postData);
});
plugins.fireHook('action:save_post_content', [pid, content]);
postSearch.index(content, pid);
});
user.onNewPostMade(uid, tid, pid, timestamp);
if (callback)
callback(pid);
});
} else {
callback(-1);
callback(null);
}
});
}
function uploadPostImages(postData, images, callback) {
var imgur = require('./imgur');
imgur.setClientID(global.nconf.get('imgurClientID'));
var uploadedImages = [];
function uploadImage(image, callback) {
imgur.upload(image.data, 'base64', function(err, data) {
if(err) {
callback(err);
} else {
if(data.success) {
var img= {url:data.data.link, name:image.name};
uploadedImages.push(img);
callback(null);
} else {
callback(data);
}
}
});
}
if(!images) {
callback(null, uploadedImages);
} else {
async.each(images, uploadImage, function(err) {
if(!err) {
callback(null, uploadedImages);
} else {
console.log(err);
callback(err, null);
}
});
}
}
Posts.getPostsByUid = function(uid, start, end, callback) {
@@ -338,7 +375,7 @@ marked.setOptions({
topics: data[0]?data[0]:0,
posts: data[1]?data[1]:0
};
socket.emit('post.stats', stats);
}
else
@@ -346,4 +383,28 @@ marked.setOptions({
});
}
Posts.reIndexPids = function(pids, callback) {
function reIndex(pid, callback) {
Posts.getPostField(pid, 'content', function(content) {
postSearch.remove(pid, function() {
if(content && content.length) {
postSearch.index(content, pid);
}
callback(null);
});
});
}
async.each(pids, reIndex, function(err) {
if(err) {
callback(err, null);
} else {
callback(null, 'Posts reindexed');
}
});
}
}(exports));

View File

@@ -1,11 +1,12 @@
(function(RedisDB) {
var redis = require('redis'),
var redis = require('redis'),
nconf = require('nconf'),
utils = require('./../public/src/utils.js');
RedisDB.exports = redis.createClient(global.nconf.get('redis:port'), global.nconf.get('redis:host'));
RedisDB.exports = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host'));
if( global.nconf.get('redis:password') ) {
RedisDB.exports.auth(global.nconf.get('redis:password'));
if(nconf.get('redis:password')) {
RedisDB.exports.auth(nconf.get('redis:password'));
}
RedisDB.exports.handle = function(error) {
@@ -37,28 +38,26 @@
};
/*
* A lot of redis calls come back like this:
* [key, value, key, value, key, value]
* this is a simple utility fn to turn this into an object.
*/
RedisDB.exports.objectify = function(arr) {
var obj = {};
for (var i = 0; i < arr.length; i += 2) {
obj[arr[i]] = arr[i+1];
}
return obj;
};
* gets fields of a hash as an object instead of an array
*/
RedisDB.exports.hmgetObject = function (key, fields, callback) {
RedisDB.exports.hmget(key, fields, function(err, data) {
if(err === null) {
var returnData = {};
for(var i=0, ii=fields.length; i<ii; ++i) {
returnData[fields[i]] = data[i];
}
callback(null, returnData);
}
else {
console.log(err);
callback(err, null);
}
});
}
/*
* Similar to .objectify, this utility function splits the data array into two arrays
*/
RedisDB.exports.splitify = function(arr) {
var arr1 = [], arr2 = [];
for (var i = 0; i < arr.length; i += 2) {
arr1.push(arr[i]);
arr2.push(arr[i+1]);
}
return [arr1,arr2];
};
}(module));

View File

@@ -1,8 +1,10 @@
var user = require('./../user.js'),
topics = require('./../topics.js'),
RDB = require('./../redis.js')
categories = require('./../categories.js');
RDB = require('./../redis.js'),
pkg = require('./../../package.json'),
categories = require('./../categories.js'),
plugins = require('../plugins');
(function(Admin) {
Admin.isAdmin = function(req, res, next) {
@@ -14,7 +16,6 @@ var user = require('./../user.js'),
Admin.build_header = function(res) {
return templates['admin/header'].parse({
cssSrc: global.config['theme:src'] || global.nconf.get('relative_path') + '/vendor/bootstrap/css/bootstrap.min.css',
csrf:res.locals.csrf_token,
relative_path: global.nconf.get('relative_path')
});
@@ -23,7 +24,12 @@ var user = require('./../user.js'),
Admin.create_routes = function(app) {
(function() {
var routes = ['categories', 'users', 'topics', 'settings', 'themes', 'twitter', 'facebook', 'gplus', 'redis', 'motd'];
var routes = [
'categories/active', 'categories/disabled', 'users', 'topics', 'settings', 'themes',
'twitter', 'facebook', 'gplus', 'redis', 'motd',
'users/latest', 'users/sort-posts', 'users/sort-reputation',
'users/search', 'plugins'
];
for (var i=0, ii=routes.length; i<ii; i++) {
(function(route) {
@@ -45,7 +51,6 @@ var user = require('./../user.js'),
}());
//todo consolidate.
app.get('/admin', Admin.isAdmin, function(req, res) {
res.send(Admin.build_header(res) + app.create_route('admin/index') + templates['admin/footer']);
});
@@ -54,94 +59,134 @@ var user = require('./../user.js'),
res.send(Admin.build_header(res) + app.create_route('admin/index') + templates['admin/footer']);
});
app.get('/api/admin/index', function(req, res) {
res.json({version:pkg.version});
});
function api_method(req, res) {
switch(req.params.method) {
case 'users' :
if (req.params.tab == 'search') {
res.json({search_display: 'block', users: []});
}
else if(req.params.tab == 'latest') {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.joindate - a.joindate;
});
res.json({search_display: 'none', users:data, yourid:req.user.uid});
});
}
else if(req.params.tab == 'sort-posts') {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.postcount - a.postcount;
});
res.json({search_display: 'none', users:data, yourid:req.user.uid});
});
}
else if(req.params.tab == 'sort-reputation') {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.reputation - a.reputation;
});
res.json({search_display: 'none', users:data, yourid:req.user.uid});
});
}
else {
user.getUserList(function(data) {
res.json({search_display: 'none', users:data, yourid:req.user.uid});
});
}
app.get('/api/admin/users/search', function(req, res) {
res.json({search_display: 'block', loadmore_display:'none', users: []});
});
app.get('/api/admin/users/latest', function(req, res) {
user.getUsers('users:joindate', 0, 49, function(err, data) {
res.json({ search_display: 'none', loadmore_display:'block', users:data, yourid:req.user.uid });
});
});
app.get('/api/admin/users/sort-posts', function(req, res) {
user.getUsers('users:postcount', 0, 49, function(err, data) {
res.json({ search_display: 'none', loadmore_display:'block', users:data, yourid:req.user.uid });
});
});
app.get('/api/admin/users/sort-reputation', function(req, res) {
user.getUsers('users:reputation', 0, 49, function(err, data) {
res.json({ search_display: 'none', loadmore_display:'block', users:data, yourid:req.user.uid });
});
});
app.get('/api/admin/users', function(req, res) {
user.getUsers('users:joindate', 0, 49, function(err, data) {
res.json({ search_display: 'none', users:data, yourid:req.user.uid });
});
});
app.get('/api/admin/categories', function(req, res) {
categories.getAllCategories(function(data) {
res.json(data);
});
});
app.get('/api/admin/categories/active', function(req, res) {
categories.getAllCategories(function(data) {
data.categories = data.categories.filter(function(category) {
return (!category.disabled || category.disabled === "0");
});
res.json(data);
});
});
app.get('/api/admin/categories/disabled', function(req, res) {
categories.getAllCategories(function(data) {
data.categories = data.categories.filter(function(category) {
return category.disabled === "1";
});
res.json(data);
});
});
app.get('/api/admin/topics', function(req, res) {
topics.getAllTopics(10, null, function(topics) {
res.json({
topics: topics
});
});
});
app.get('/api/admin/redis', function(req, res) {
RDB.info(function(err, data) {
data = data.split("\r\n");
var finalData = {};
for(var i in data) {
break;
case 'categories':
if (req.params.tab == 'disabled') {
res.json({categories: []});
} else {
categories.getAllCategories(function(data) {
res.json(data);
});
}
break;
case 'topics':
topics.getAllTopics(10, null, function(topics) {
res.json({
topics: topics
});
});
break;
case 'redis':
RDB.info(function(err, data) {
data = data.split("\r\n");
var finalData = {};
for(var i in data) {
try {
data[i] = data[i].replace(/:/,"\":\"");
var json = "{\"" + data[i] + "\"}";
var jsonObject = JSON.parse(json);
for(var key in jsonObject) {
finalData[key] = jsonObject[key];
}
}catch(err){
}
}
if(data[i].indexOf(':') == -1 || !data[i])
continue;
try {
data[i] = data[i].replace(/:/,"\":\"");
var json = "{\"" + data[i] + "\"}";
res.json(finalData);
});
break;
default :
res.json({});
}
}
var jsonObject = JSON.parse(json);
for(var key in jsonObject) {
finalData[key] = jsonObject[key];
}
} catch(err){
console.log('invalid redis status', i, data[i], err);
}
}
res.json(finalData);
});
});
app.get('/api/admin/:method/:tab?*', api_method);
app.get('/api/admin/:method*', api_method);
app.get('/api/admin/plugins', function(req, res) {
plugins.showInstalled(function(err, plugins) {
if (err || !Array.isArray(plugins)) plugins = [];
res.json(200, {
plugins: plugins
});
});
});
app.get('/api/admin/settings', function(req, res) {
res.json(200, {});
});
app.get('/api/admin/motd', function(req, res) {
res.json(200, {});
});
app.get('/api/admin/themes', function(req, res) {
res.json(200, {});
});
app.get('/api/admin/twitter', function(req, res) {
res.json(200, {});
});
app.get('/api/admin/facebook', function(req, res) {
res.json(200, {});
});
app.get('/api/admin/gplus', function(req, res) {
res.json(200, {});
});
app.get('/api/admin/testing/categories', function(req, res) {
res.json(200, {});
});
};

232
src/routes/api.js Normal file
View File

@@ -0,0 +1,232 @@
var user = require('./../user.js'),
auth = require('./authentication.js'),
topics = require('./../topics.js'),
categories = require('./../categories.js')
utils = require('./../../public/src/utils.js'),
pkg = require('../../package.json'),
meta = require('./../meta.js');
(function(Api) {
Api.create_routes = function(app) {
app.get('/api/get_templates_listing', function(req, res) {
utils.walk(global.configuration.ROOT_DIRECTORY + '/public/templates', function(err, data) {
res.json(data);
});
});
app.get('/api/config', function(req, res, next) {
meta.config.getFields(['postDelay', 'minimumTitleLength', 'minimumPostLength'], function(err, metaConfig) {
if(err) return next();
var clientConfig = require('../../public/config.json');
for (var attrname in metaConfig) {
clientConfig[attrname] = metaConfig[attrname];
}
res.json(200, clientConfig);
})
});
app.get('/api/home', function(req, res) {
var uid = (req.user) ? req.user.uid : 0;
categories.getAllCategories(function(data) {
data.categories = data.categories.filter(function(category) {
return (!category.disabled || category.disabled === "0");
});
function iterator(category, callback) {
categories.getRecentReplies(category.cid, 2, function(posts) {
category["posts"] = posts;
category["post_count"] = posts.length>2 ? 2 : posts.length;
callback(null);
});
}
require('async').each(data.categories, iterator, function(err) {
data.motd_class = (config.show_motd === '1' || config.show_motd === undefined) ? '' : 'none';
data.motd = marked(config.motd || "# NodeBB v" + pkg.version + "\nWelcome to NodeBB, the discussion platform of the future.\n\n<a target=\"_blank\" href=\"http://www.nodebb.org\" class=\"btn btn-large\"><i class=\"icon-comment\"></i> Get NodeBB</a> <a target=\"_blank\" href=\"https://github.com/designcreateplay/NodeBB\" class=\"btn btn-large\"><i class=\"icon-github-alt\"></i> Fork us on Github</a> <a target=\"_blank\" href=\"https://twitter.com/dcplabs\" class=\"btn btn-large\"><i class=\"icon-twitter\"></i> @dcplabs</a>");
res.json(data);
});
}, uid);
});
app.get('/api/login', function(req, res) {
var data = {},
login_strategies = auth.get_login_strategies(),
num_strategies = login_strategies.length;
if (num_strategies == 0) {
data = {
'login_window:spansize': 'span12',
'alternate_logins:display': 'none'
};
} else {
data = {
'login_window:spansize': 'span6',
'alternate_logins:display': 'block'
}
for (var i=0, ii=num_strategies; i<ii; i++) {
data[login_strategies[i] + ':display'] = 'active';
}
}
data.token = res.locals.csrf_token;
res.json(data);
});
app.get('/api/register', function(req, res) {
var data = {},
login_strategies = auth.get_login_strategies(),
num_strategies = login_strategies.length;
if (num_strategies == 0) {
data = {
'register_window:spansize': 'span12',
'alternate_logins:display': 'none'
};
} else {
data = {
'register_window:spansize': 'span6',
'alternate_logins:display': 'block'
}
for (var i=0, ii=num_strategies; i<ii; i++) {
data[login_strategies[i] + ':display'] = 'active';
}
}
data.token = res.locals.csrf_token;
res.json(data);
});
app.get('/api/topic/:id/:slug?', function(req, res) {
var uid = (req.user) ? req.user.uid : 0;
topics.getTopicWithPosts(req.params.id, uid, function(err, data) {
res.json(data);
});
});
app.get('/api/category/:id/:slug?', function(req, res, next) {
var uid = (req.user) ? req.user.uid : 0;
categories.getCategoryById(req.params.id, uid, function(err, data) {
if (!err)
res.json(data);
else
next();
}, req.params.id, uid);
});
app.get('/api/recent', function(req, res) {
var uid = (req.user) ? req.user.uid : 0;
topics.getLatestTopics(uid, 0, 19, function(data) {
res.json(data);
});
});
app.get('/api/unread', function(req, res) {
var uid = (req.user) ? req.user.uid : 0;
topics.getUnreadTopics(uid, 0, 19, function(data) {
res.json(data);
});
});
app.get('/api/confirm/:id', function(req, res) {
user.email.confirm(req.params.id, function(data) {
if (data.status === 'ok') {
res.json({
'alert-class': 'alert-success',
title: 'Email Confirmed',
text: 'Thank you for vaidating your email. Your account is now fully activated.'
});
} else {
res.json({
'alert-class': 'alert-error',
title: 'An error occurred...',
text: 'There was a problem validating your email address. Perhaps the code was invalid or has expired.'
});
}
});
});
app.get('/api/outgoing', function(req, res) {
var url = req.query.url;
if (url) {
res.json({
url: url,
home: global.nconf.get('url')
});
} else {
res.status(404);
res.redirect(global.nconf.get('relative_path') + '/404');
}
});
app.get('/api/search', function(req, res) {
return res.json({
show_no_results:'hide',
search_query:'',
posts:[]
});
});
app.get('/api/search/:term', function(req, res, next) {
var reds = require('reds');
var postSearch = reds.createSearch('nodebbpostsearch');
var topicSearch = reds.createSearch('nodebbtopicsearch');
function search(searchObj, callback) {
searchObj
.query(query = req.params.term).type('or')
.end(callback);
}
function searchPosts(callback) {
search(postSearch, function(err, pids) {
if(err)
return callback(err, null);
posts.getPostSummaryByPids(pids, function(posts) {
callback(null, posts);
});
})
}
function searchTopics(callback) {
search(topicSearch, function(err, tids) {
if(err)
return callback(err, null);
console.log(tids);
topics.getTopicsByTids(tids, 0, function(topics) {
callback(null, topics);
}, 0);
});
}
async.parallel([searchPosts, searchTopics], function(err, results) {
if (err)
return next();
var noresults = !results[0].length && !results[1].length;
return res.json({
show_no_results: noresults?'show':'hide',
search_query:req.params.term,
posts:results[0],
topics:results[1]
});
});
});
app.get('/api/404', function(req, res) {
res.json({});
});
app.get('/api/403', function(req, res) {
res.json({});
});
}
}(exports));

View File

@@ -5,8 +5,8 @@
passportGoogle = require('passport-google-oauth').OAuth2Strategy,
passportFacebook = require('passport-facebook').Strategy,
login_strategies = [],
user_module = require('./../user.js'),
nconf = require('nconf'),
users = require('../user'),
login_module = require('./../login.js');
passport.use(new passportLocal(function(user, password, next) {
@@ -20,9 +20,9 @@
passport.use(new passportTwitter({
consumerKey: global.config['social:twitter:key'],
consumerSecret: global.config['social:twitter:secret'],
callbackURL: config.url + 'auth/twitter/callback'
callbackURL: nconf.get('url') + 'auth/twitter/callback'
}, function(token, tokenSecret, profile, done) {
login_module.loginViaTwitter(profile.id, profile.username, function(err, user) {
login_module.loginViaTwitter(profile.id, profile.username, profile.photos, function(err, user) {
if (err) { return done(err); }
done(null, user);
});
@@ -35,7 +35,7 @@
passport.use(new passportGoogle({
clientID: global.config['social:google:id'],
clientSecret: global.config['social:google:secret'],
callbackURL: config.url + 'auth/google/callback'
callbackURL: nconf.get('url') + 'auth/google/callback'
}, function(accessToken, refreshToken, profile, done) {
login_module.loginViaGoogle(profile.id, profile.displayName, profile.emails[0].value, function(err, user) {
if (err) { return done(err); }
@@ -50,7 +50,7 @@
passport.use(new passportFacebook({
clientID: global.config['social:facebook:app_id'],
clientSecret: global.config['social:facebook:secret'],
callbackURL: config.url + 'auth/facebook/callback'
callbackURL: nconf.get('url') + 'auth/facebook/callback'
}, function(accessToken, refreshToken, profile, done) {
login_module.loginViaFacebook(profile.id, profile.displayName, profile.emails[0].value, function(err, user) {
if (err) { return done(err); }
@@ -84,11 +84,15 @@
Auth.create_routes = function(app) {
app.get('/logout', function(req, res) {
console.log('info: [Auth] Session ' + req.sessionID + ' logout (uid: ' + global.uid + ')');
login_module.logout(req.sessionID, function(logout) {
req.logout();
res.send(app.build_header(res) + templates['logout'] + templates['footer']);
});
if (req.user && req.user.uid > 0) {
console.log('info: [Auth] Session ' + req.sessionID + ' logout (uid: ' + req.user.uid + ')');
login_module.logout(req.sessionID, function(logout) {
req.logout();
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + templates['logout'] + templates['footer']);
});
});
} else res.redirect('/');
});
if (login_strategies.indexOf('twitter') !== -1) {
@@ -121,11 +125,16 @@
app.get('/reset/:code', function(req, res) {
res.send(app.build_header(res) + templates['reset_code'].parse({ reset_code: req.params.code }) + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + templates['reset_code'].parse({ reset_code: req.params.code }) + templates['footer']);
});
});
app.get('/reset', function(req, res) {
res.send(app.build_header(res) + templates['reset'] + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
console.log(header);
res.send(header + templates['reset'] + templates['footer']);
});
});
@@ -134,7 +143,7 @@
});
app.post('/register', function(req, res) {
user_module.create(req.body.username, req.body.password, req.body.email, function(err, uid) {
users.create(req.body.username, req.body.password, req.body.email, function(err, uid) {
if (err === null && uid > 0) {
req.login({

View File

@@ -24,20 +24,13 @@ var RDB = require('../redis.js');
res.send(templates['install/header'] + app.create_route('install/basic') + templates['install/footer']);
});
app.get('/api/install/basic', function(req, res) {
res.send('{}');
});
function api_method(req, res) {
switch(req.params.method) {
case 'basic' :
res.send('{}');
break;
default :
res.send('{}');
}
}
app.get('/api/install/:method/:tab?*', api_method);
app.get('/api/install/:method*', api_method);
app.get('/api/install', function(req, res) {
res.send('{}');
});
};

View File

@@ -1,5 +1,6 @@
var user = require('./../user.js'),
posts = require('./../posts.js'),
postTools = require('../postTools'),
fs = require('fs'),
utils = require('./../../public/src/utils.js'),
path = require('path'),
@@ -24,23 +25,33 @@ var user = require('./../user.js'),
});
app.get('/users', function(req, res) {
res.send(app.build_header(res) + app.create_route("users", "users") + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("users", "users") + templates['footer']);
});
});
app.get('/users-latest', function(req, res) {
res.send(app.build_header(res) + app.create_route("users-latest", "users") + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("users-latest", "users") + templates['footer']);
});
});
app.get('/users-sort-posts', function(req, res) {
res.send(app.build_header(res) + app.create_route("users-sort-posts", "users") + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("users-sort-posts", "users") + templates['footer']);
});
});
app.get('/users-sort-reputation', function(req, res) {
res.send(app.build_header(res) + app.create_route("users-sort-reputation", "users") + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("users-sort-reputation", "users") + templates['footer']);
});
});
app.get('/users-search', function(req, res) {
res.send(app.build_header(res) + app.create_route("users-search", "users") + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("users-search", "users") + templates['footer']);
});
});
app.get('/users/:userslug', function(req, res, next) {
@@ -56,7 +67,9 @@ var user = require('./../user.js'),
return;
}
res.send(app.build_header(res) + app.create_route('users/'+req.params.userslug, 'account') + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route('users/'+req.params.userslug, 'account') + templates['footer']);
});
});
});
@@ -68,7 +81,25 @@ var user = require('./../user.js'),
user.getUserField(req.user.uid, 'userslug', function(userslug) {
if(req.params.userslug && userslug === req.params.userslug) {
res.send(app.build_header(res) + app.create_route('users/'+req.params.userslug+'/edit','accountedit') + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route('users/'+req.params.userslug+'/edit','accountedit') + templates['footer']);
});
} else {
return res.redirect('/404');
}
});
});
app.get('/users/:userslug/settings', function(req, res) {
if(!req.user)
return res.redirect('/403');
user.getUserField(req.user.uid, 'userslug', function(userslug) {
if(req.params.userslug && userslug === req.params.userslug) {
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route('users/'+req.params.userslug+'/settings','accountsettings') + templates['footer']);
})
} else {
return res.redirect('/404');
}
@@ -96,21 +127,19 @@ var user = require('./../user.js'),
}
user.getUserField(req.user.uid, 'uploadedpicture', function(oldpicture) {
if(!oldpicture) {
uploadUserPicture(req.user.uid, req.files.userPhoto.name, req.files.userPhoto.path, res);
uploadUserPicture(req.user.uid, path.extname(req.files.userPhoto.name), req.files.userPhoto.path, res);
return;
}
var absolutePath = path.join(global.configuration['ROOT_DIRECTORY'], config.upload_path, path.basename(oldpicture));
var absolutePath = path.join(global.configuration['ROOT_DIRECTORY'], global.nconf.get('upload_path'), path.basename(oldpicture));
fs.unlink(absolutePath, function(err) {
if(err) {
if(err) {
console.error('[%d] %s', Date.now(), + err);
}
uploadUserPicture(req.user.uid, path.extname(req.files.userPhoto.name), req.files.userPhoto.path, res);
});
});
});
@@ -124,7 +153,7 @@ var user = require('./../user.js'),
}
var filename = uid + '-profileimg' + extension;
var uploadPath = path.join(global.configuration['ROOT_DIRECTORY'], config.upload_path, filename);
var uploadPath = path.join(global.configuration['ROOT_DIRECTORY'], global.nconf.get('upload_path'), filename);
// @todo move to proper logging code - this should only be temporary
console.log('Info: Attempting upload to: '+ uploadPath);
@@ -135,20 +164,17 @@ var user = require('./../user.js'),
is.on('end', function() {
fs.unlinkSync(tempPath);
var imageUrl = config.upload_url + filename;
res.json({ path: imageUrl });
var imageUrl = global.nconf.get('upload_url') + filename;
user.setUserField(uid, 'uploadedpicture', imageUrl);
user.setUserField(uid, 'picture', imageUrl);
var im = require('node-imagemagick');
im.resize({
require('node-imagemagick').crop({
srcPath: uploadPath,
dstPath: uploadPath,
width: 128
}, function(err, stdout, stderr) {
dstPath: uploadPath,
width: 128,
height: 128
}, function(err, stdout, stderr){
if (err) {
// @todo: better logging method; for now, send to stderr.
// ideally, this should be happening in another process
@@ -156,6 +182,8 @@ var user = require('./../user.js'),
// to crash the main process
console.error('[%d] %s', Date.now(), + err);
}
res.json({ path: imageUrl });
});
});
@@ -178,7 +206,9 @@ var user = require('./../user.js'),
return;
}
res.send(app.build_header(res) + app.create_route('users/'+req.params.userslug+'/following','following') + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route('users/'+req.params.userslug+'/following','following') + templates['footer']);
});
});
});
@@ -192,55 +222,16 @@ var user = require('./../user.js'),
res.redirect('/404');
return;
}
res.send(app.build_header(res) + app.create_route('users/'+req.params.userslug+'/followers','followers') + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route('users/'+req.params.userslug+'/followers','followers') + templates['footer']);
});
});
});
function api_method(req, res) {
app.get('/api/users/:userslug/following', function(req, res) {
var callerUID = req.user ? req.user.uid : 0;
var userslug = req.params.userslug;
var section = req.params.section ? String(req.params.section).toLowerCase() : null;
if (!section && !userslug) {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.joindate - a.joindate;
});
res.json({ search_display: 'none', users: data });
});
}
else if(section === 'following') {
getFollowing(req, res, callerUID);
}
else if(section === 'followers') {
getFollowers(req, res, callerUID);
}
else if (section === 'edit') {
getUserDataByUserSlug(userslug, callerUID, function(userData) {
res.json(userData);
});
} else {
getUserDataByUserSlug(userslug, callerUID, function(userData) {
if(userData) {
user.isFollowing(callerUID, userData.theirid, function(isFollowing) {
posts.getPostsByUid(userData.theirid, 0, 9, function(posts) {
userData.posts = posts;
userData.isFollowing = isFollowing;
userData.signature = marked(userData.signature || '');
res.json(userData);
});
});
} else {
res.json(404, { error: 'User not found!' }) ;
}
});
}
}
function getFollowing(req, res, callerUid) {
getUserDataByUserSlug(req.params.userslug, callerUid, function(userData) {
getUserDataByUserSlug(req.params.userslug, callerUID, function(userData) {
if(userData) {
user.getFollowing(userData.uid, function(followingData) {
userData.following = followingData;
@@ -252,10 +243,12 @@ var user = require('./../user.js'),
res.json(404, { error: 'User not found!' }) ;
}
});
}
});
function getFollowers(req, res, callerUid) {
getUserDataByUserSlug(req.params.userslug, callerUid, function(userData) {
app.get('/api/users/:userslug/followers', function(req, res) {
var callerUID = req.user ? req.user.uid : 0;
getUserDataByUserSlug(req.params.userslug, callerUID, function(userData) {
if(userData) {
user.getFollowers(userData.uid, function(followersData) {
userData.followers = followersData;
@@ -266,43 +259,90 @@ var user = require('./../user.js'),
res.json(404, { error: 'User not found!' }) ;
}
});
}
});
app.get('/api/users/:userslug?/:section?', api_method);
app.get('/api/users/:userslug/edit', function(req, res) {
var callerUID = req.user ? req.user.uid : 0;
getUserDataByUserSlug(req.params.userslug, callerUID, function(userData) {
res.json(userData);
});
});
app.get('/api/users/:userslug/settings', function(req, res) {
var callerUID = req.user ? req.user.uid : 0;
user.get_uid_by_userslug(req.params.userslug, function(uid) {
if(!uid) {
res.json(404, { error: 'User not found!' }) ;
return;
}
if(uid !== callerUID || callerUID === "0") {
res.json(403, { error: 'Not allowed!' });
return;
}
user.getUserFields(uid, ['username','userslug','showemail'], function(userData) {
if(userData) {
if(userData.showemail && userData.showemail === "1")
userData.showemail = "checked";
else
userData.showemail = "";
res.json(userData);
} else {
res.json(404, { error: 'User not found!' }) ;
}
});
});
});
app.get('/api/users/:userslug', function(req, res) {
var callerUID = req.user ? req.user.uid : 0;
getUserDataByUserSlug(req.params.userslug, callerUID, function(userData) {
if(userData) {
user.isFollowing(callerUID, userData.theirid, function(isFollowing) {
posts.getPostsByUid(userData.theirid, 0, 9, function(posts) {
userData.posts = posts.filter(function(p) {return p.deleted !== "1";});
userData.isFollowing = isFollowing;
userData.signature = postTools.markdownToHTML(userData.signature, true);
res.json(userData);
});
});
} else {
res.json(404, { error: 'User not found!' }) ;
}
});
});
app.get('/api/users', getUsersSortedByJoinDate);
app.get('/api/users-sort-posts', getUsersSortedByPosts);
app.get('/api/users-sort-reputation', getUsersSortedByReputation);
app.get('/api/users-latest', getUsersSortedByJoinDate);
app.get('/api/users-search', getUsersForSearch);
function getUsersSortedByJoinDate(req, res) {
user.getUsers('users:joindate', 0, 49, function(err, data) {
res.json({ search_display: 'none',loadmore_display:'block', users:data });
});
}
function getUsersSortedByPosts(req, res) {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.postcount - a.postcount;
});
res.json({ search_display: 'none', users:data });
user.getUsers('users:postcount', 0, 49, function(err, data) {
res.json({ search_display: 'none',loadmore_display:'block', users:data });
});
}
function getUsersSortedByReputation(req, res) {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.reputation - a.reputation;
});
res.json({ search_display: 'none', users:data });
});
}
function getUsersSortedByJoinDate(req, res) {
user.getUserList(function(data) {
data = data.sort(function(a, b) {
return b.joindate - a.joindate;
});
res.json({ search_display: 'none', users:data });
user.getUsers('users:reputation', 0, 49, function(err, data) {
res.json({ search_display: 'none', loadmore_display:'block', users:data });
});
}
function getUsersForSearch(req, res) {
res.json({ search_display: 'block', users: [] });
res.json({ search_display: 'block', loadmore_display:'none', users: [] });
}
function getUserDataByUserSlug(userslug, callerUID, callback) {
@@ -312,7 +352,7 @@ var user = require('./../user.js'),
callback(null);
return;
}
user.getUserData(uid, function(data) {
if(data) {
data.joindate = utils.relativeTime(data.joindate);
@@ -323,6 +363,19 @@ var user = require('./../user.js'),
data.age = new Date().getFullYear() - new Date(data.birthday).getFullYear();
}
function canSeeEmail() {
return callerUID === uid || (data.email && (data.showemail && data.showemail === "1"));
}
if(!canSeeEmail())
data.email = "";
if(callerUID === uid && (!data.showemail || data.showemail === "0"))
data.emailClass = "";
else
data.emailClass = "hide";
data.uid = uid;
data.yourid = callerUID;
data.theirid = uid;
@@ -341,10 +394,7 @@ var user = require('./../user.js'),
});
}
};
}(exports));

View File

@@ -14,7 +14,6 @@
return {
/* sets */
tid: 'topics:tid',
read_by_uid: 'tid:' + tid + ':read_by_uid',
/* sorted sets */

70
src/sitemap.js Normal file
View File

@@ -0,0 +1,70 @@
var path = require('path'),
async = require('async'),
sm = require('sitemap'),
url = require('url'),
categories = require('./categories'),
topics = require('./topics'),
sitemap = {
getStaticUrls: function(callback) {
callback(null, [
{ url: '', changefreq: 'weekly', priority: '0.6' },
{ url: 'recent', changefreq: 'daily', priority: '0.4' },
{ url: 'users', changefreq: 'daily', priority: '0.4' }
]);
},
getDynamicUrls: function(callback) {
var returnUrls = [];
async.parallel([
function(next) {
var categoryUrls = [];
categories.getAllCategories(function(data) {
data.categories.forEach(function(category) {
categoryUrls.push({
url: path.join('category', category.slug),
changefreq: 'weekly',
priority: '0.4'
});
});
next(null, categoryUrls);
}, 0);
},
function(next) {
var topicUrls = [];
topics.getAllTopics(null, null, function(topics) {
topics.forEach(function(topic) {
topicUrls.push({
url: path.join('topic', topic.slug),
changefreq: 'daily',
priority: '0.6'
});
});
next(null, topicUrls);
});
}
], function(err, data) {
if (!err) {
returnUrls = returnUrls.concat(data[0]).concat(data[1]);
}
callback(null, returnUrls);
});
},
render: function(callback) {
async.parallel([sitemap.getStaticUrls, sitemap.getDynamicUrls], function(err, urls) {
var urls = urls[0].concat(urls[1]),
map = sm.createSitemap({
hostname: global.nconf.get('url'),
cacheTime: 600000,
urls: urls
}),
xml = map.toXML(function(xml) {
callback(xml);
});
});
}
}
module.exports = sitemap;

View File

@@ -4,7 +4,9 @@ var RDB = require('./redis.js'),
user = require('./user.js'),
async = require('async'),
notifications = require('./notifications.js'),
posts = require('./posts');
posts = require('./posts'),
reds = require('reds'),
topicSearch = reds.createSearch('nodebbtopicsearch');
(function(ThreadTools) {
@@ -19,7 +21,7 @@ var RDB = require('./redis.js'),
//todo: break early if one condition is true
function getCategoryPrivileges(next) {
topics.getTopicField(tid, 'cid', function(cid) {
topics.getTopicField(tid, 'cid', function(err, cid) {
categories.privileges(cid, uid, function(privileges) {
next(null, privileges);
});
@@ -88,6 +90,8 @@ var RDB = require('./redis.js'),
topics.setTopicField(tid, 'deleted', 1);
ThreadTools.lock(tid, uid);
topicSearch.remove(tid);
io.sockets.in('topic_' + tid).emit('event:topic_deleted', {
tid: tid,
status: 'ok'
@@ -116,6 +120,10 @@ var RDB = require('./redis.js'),
tid: tid
});
}
topics.getTopicField(tid, 'title', function(err, title) {
topicSearch.index(title, tid);
});
}
});
}
@@ -125,7 +133,10 @@ var RDB = require('./redis.js'),
if (privileges.editable) {
topics.setTopicField(tid, 'pinned', 1);
topics.getTopicField(tid, 'cid', function(err, cid) {
RDB.zadd('categories:' + cid + ':tid', Math.pow(2,53), tid);
});
if (socket) {
io.sockets.in('topic_' + tid).emit('event:topic_pinned', {
tid: tid,
@@ -146,7 +157,9 @@ var RDB = require('./redis.js'),
if (privileges.editable) {
topics.setTopicField(tid, 'pinned', 0);
topics.getTopicFields(tid, ['cid', 'lastposttime'], function(topicData) {
RDB.zadd('categories:' + topicData.cid + ':tid', topicData.lastposttime, tid);
});
if (socket) {
io.sockets.in('topic_' + tid).emit('event:topic_unpinned', {
tid: tid,
@@ -164,21 +177,28 @@ var RDB = require('./redis.js'),
ThreadTools.move = function(tid, cid, socket) {
topics.getTopicField(tid, 'cid', function(oldCid) {
topics.getTopicFields(tid, ['cid', 'lastposttime'], function(topicData) {
var oldCid = topicData.cid;
var multi = RDB.multi();
multi.zrem('categories:' + oldCid + ':tid', tid);
multi.zadd('categories:' + cid + ':tid', topicData.lastposttime, tid);
multi.exec(function(err, result) {
RDB.smove('categories:' + oldCid + ':tid', 'categories:' + cid + ':tid', tid, function(err, result) {
if (!err && result === 1) {
topics.setTopicField(tid, 'cid', cid);
categories.moveRecentReplies(tid, oldCid, cid, function(err, data) {
if(err) {
console.log(err);
}
});
categories.incrementCategoryFieldBy(oldCid, 'topic_count', -1);
categories.incrementCategoryFieldBy(cid, 'topic_count', 1);
categories.getCategories([cid], function(data) {
topics.setTopicField(tid, 'category_name', data.categories[0].name);
topics.setTopicField(tid, 'category_slug', data.categories[0].slug);
});
socket.emit('api:topic.move', {
status: 'ok'
});
@@ -238,7 +258,7 @@ var RDB = require('./redis.js'),
async.parallel([
function(next) {
topics.getTopicField(tid, 'title', function(title) {
topics.getTopicField(tid, 'title', function(err, title) {
topics.getTeaser(tid, function(err, teaser) {
if (!err) {
notifications.create(teaser.username + ' has posted a reply to: "' + title + '"', null, '/topic/' + tid, 'topic:' + tid, function(nid) {

View File

@@ -10,7 +10,9 @@ var RDB = require('./redis.js')
postTools = require('./postTools'),
async = require('async'),
feed = require('./feed.js'),
favourites = require('./favourites.js');
favourites = require('./favourites.js'),
reds = require('reds'),
topicSearch = reds.createSearch('nodebbtopicsearch');
marked.setOptions({
breaks: true
@@ -18,6 +20,8 @@ marked.setOptions({
(function(Topics) {
Topics.getTopicData = function(tid, callback) {
RDB.hgetall('topic:' + tid, function(err, data) {
if(err === null)
@@ -39,7 +43,9 @@ marked.setOptions({
Topics.getTopicPosts = function(tid, start, end, current_user, callback) {
posts.getPostsByTid(tid, start, end, function(postData) {
if(Array.isArray(postData) && !postData.length)
return callback([]);
function getFavouritesData(next) {
var pids = [];
for(var i=0; i<postData.length; ++i)
@@ -51,16 +57,15 @@ marked.setOptions({
}
function addUserInfoToPosts(next) {
var done = 0;
for(var i=0, ii=postData.length; i<ii; ++i) {
posts.addUserInfoToPost(postData[i], function() {
++done;
if(done === postData.length)
next(null, null);
function iterator(post, callback) {
posts.addUserInfoToPost(post, function() {
callback(null);
});
}
async.each(postData, iterator, function(err) {
next(err, null);
});
}
function getPrivileges(next) {
@@ -77,15 +82,202 @@ marked.setOptions({
postData[i].fav_star_class = fav_data[postData[i].pid] ? 'icon-star' : 'icon-star-empty';
postData[i]['display_moderator_tools'] = (postData[i].uid == current_user || privileges.editable) ? 'show' : 'none';
}
callback(postData);
});
});
}
Topics.getCategoryData = function(tid, callback) {
Topics.getTopicField(tid, 'cid', function(err, cid) {
categories.getCategoryData(cid, callback);
});
}
Topics.getLatestTopics = function(current_user, start, end, callback) {
var timestamp = Date.now();
var args = [ 'topics:recent', '+inf', timestamp - 86400000, 'WITHSCORES', 'LIMIT', start, end - start + 1];
RDB.zrevrangebyscore(args, function(err, tids) {
var latestTopics = {
'category_name' : 'Recent',
'show_sidebar' : 'hidden',
'show_topic_button' : 'hidden',
'no_topics_message' : 'hidden',
'topic_row_size': 'span12',
'category_id': false,
'topics' : []
};
if (!tids || !tids.length) {
latestTopics.no_topics_message = 'show';
callback(latestTopics);
return;
}
Topics.getTopicsByTids(tids, current_user, function(topicData) {
latestTopics.topics = topicData;
callback(latestTopics);
});
});
}
Topics.getUnreadTopics = function(uid, start, stop, callback) {
var unreadTopics = {
'category_name' : 'Unread',
'show_sidebar' : 'hidden',
'show_topic_button' : 'hidden',
'show_markallread_button': 'show',
'no_topics_message' : 'hidden',
'topic_row_size': 'span12',
'topics' : []
};
RDB.zrevrange('topics:recent', start, stop, function (err, tids) {
function noUnreadTopics() {
unreadTopics.no_topics_message = 'show';
unreadTopics.show_markallread_button = 'hidden';
callback(unreadTopics);
}
function sendUnreadTopics(topicIds) {
Topics.getTopicsByTids(topicIds, uid, function(topicData) {
unreadTopics.topics = topicData;
unreadTopics.nextStart = start + tids.length;
if(uid === 0)
unreadTopics.show_markallread_button = 'hidden';
callback(unreadTopics);
});
}
if (!tids || !tids.length) {
noUnreadTopics();
return;
}
if(uid === 0) {
sendUnreadTopics(tids);
} else {
Topics.hasReadTopics(tids, uid, function(read) {
var unreadTids = tids.filter(function(tid, index, self) {
return read[index] === 0;
});
if (!unreadTids || !unreadTids.length) {
noUnreadTopics();
return;
}
sendUnreadTopics(unreadTids);
});
}
});
}
Topics.getTopicsByTids = function(tids, current_user, callback, category_id) {
var retrieved_topics = [];
if(!Array.isArray(tids) || tids.length === 0) {
callback(retrieved_topics);
return;
}
function getTopicInfo(topicData, callback) {
function getUserName(next) {
user.getUserField(topicData.uid, 'username', function(username) {
next(null, username);
});
}
function hasReadTopic(next) {
Topics.hasReadTopic(topicData.tid, current_user, function(hasRead) {
next(null, hasRead);
});
}
function getTeaserInfo(next) {
Topics.getTeaser(topicData.tid, function(err, teaser) {
next(null, teaser || {});
});
}
// temporary. I don't think this call should belong here
function getPrivileges(next) {
categories.privileges(category_id, current_user, function(user_privs) {
next(null, user_privs);
});
}
async.parallel([getUserName, hasReadTopic, getTeaserInfo, getPrivileges], function(err, results) {
var username = results[0],
hasReadTopic = results[1],
teaserInfo = results[2],
privileges = results[3];
callback({
username: username,
hasread: hasReadTopic,
teaserInfo: teaserInfo,
privileges: privileges
});
});
}
function isTopicVisible(topicData, topicInfo) {
var deleted = parseInt(topicData.deleted, 10) !== 0;
return !deleted || (deleted && topicInfo.privileges.view_deleted) || topicData.uid === current_user;
}
function loadTopic(tid, callback) {
Topics.getTopicData(tid, function(topicData) {
if(!topicData) {
return callback(null);
}
getTopicInfo(topicData, function(topicInfo) {
topicData['pin-icon'] = topicData.pinned === '1' ? 'icon-pushpin' : 'none';
topicData['lock-icon'] = topicData.locked === '1' ? 'icon-lock' : 'none';
topicData['deleted-class'] = topicData.deleted === '1' ? 'deleted' : '';
topicData.relativeTime = utils.relativeTime(topicData.timestamp);
topicData.username = topicInfo.username;
topicData.badgeclass = (topicInfo.hasread && current_user != 0) ? '' : 'badge-important';
topicData.teaser_text = topicInfo.teaserInfo.text || '',
topicData.teaser_username = topicInfo.teaserInfo.username || '';
topicData.teaser_userpicture = topicInfo.teaserInfo.picture || '';
topicData.teaser_timestamp = topicInfo.teaserInfo.timestamp ? utils.relativeTime(topicInfo.teaserInfo.timestamp) : '';
if (isTopicVisible(topicData, topicInfo))
retrieved_topics.push(topicData);
callback(null);
});
});
}
async.eachSeries(tids, loadTopic, function(err) {
if(!err) {
callback(retrieved_topics);
}
});
}
Topics.getTopicWithPosts = function(tid, current_user, callback) {
threadTools.exists(tid, function(exists) {
if (!exists) return callback(new Error('Topic tid \'' + tid + '\' not found'));
if (!exists)
return callback(new Error('Topic tid \'' + tid + '\' not found'));
Topics.markAsRead(tid, current_user);
@@ -96,8 +288,7 @@ marked.setOptions({
}
function getTopicPosts(next) {
Topics.getTopicPosts(tid, 0, -1, current_user, function(topicPosts, privileges) {
Topics.getTopicPosts(tid, 0, 10, current_user, function(topicPosts, privileges) {
next(null, topicPosts);
});
}
@@ -107,19 +298,29 @@ marked.setOptions({
next(null, privData);
});
}
function getCategoryData(next) {
Topics.getCategoryData(tid, next);
}
async.parallel([getTopicData, getTopicPosts, getPrivileges], function(err, results) {
if (err) console.log(err.message);
async.parallel([getTopicData, getTopicPosts, getPrivileges, getCategoryData], function(err, results) {
if (err) {
console.log(err.message);
callback(err, null);
return;
}
var topicData = results[0],
topicPosts = results[1],
privileges = results[2];
privileges = results[2],
categoryData = results[3];
var main_posts = topicPosts.splice(0, 1);
callback(null, {
'topic_name':topicData.title,
'category_name':topicData.category_name,
'category_slug':topicData.category_slug,
'category_name':categoryData.name,
'category_slug':categoryData.slug,
'locked': topicData.locked,
'deleted': topicData.deleted,
'pinned': topicData.pinned,
@@ -144,7 +345,7 @@ marked.setOptions({
function getReadStatus(next) {
if (uid && parseInt(uid) > 0) {
RDB.sismember(schema.topics(tid).read_by_uid, uid, function(err, read) {
Topics.hasReadTopic(tid, uid, function(read) {
next(null, read);
});
} else {
@@ -173,7 +374,8 @@ marked.setOptions({
topicData.teaser_text = teaser.text || '';
topicData.teaser_username = teaser.username || '';
topicData.teaser_timestamp = teaser.timestamp ? utils.relativeTime(teaser.timestamp) : '';
topicData.teaser_userpicture = teaser.picture;
callback(topicData);
});
}
@@ -215,20 +417,42 @@ marked.setOptions({
});
});
}
Topics.markAllRead = function(uid, callback) {
RDB.smembers('topics:tid', function(err, tids) {
if(err) {
console.log(err);
callback(err, null);
return;
}
if(tids && tids.length) {
for(var i=0; i<tids.length; ++i) {
Topics.markAsRead(tids[i], uid);
}
}
callback(null, true);
});
}
Topics.getTitleByPid = function(pid, callback) {
posts.getPostField(pid, 'tid', function(tid) {
Topics.getTopicField(tid, 'title', function(title) {
Topics.getTopicField(tid, 'title', function(err, title) {
callback(title);
});
});
}
Topics.markUnRead = function(tid) {
RDB.del('tid:' + tid + ':read_by_uid');
}
Topics.markAsRead = function(tid, uid) {
RDB.sadd(schema.topics(tid).read_by_uid, uid);
Topics.getTopicField(tid, 'cid', function(cid) {
Topics.getTopicField(tid, 'cid', function(err, cid) {
categories.isTopicsRead(cid, uid, function(read) {
if(read) {
@@ -252,10 +476,10 @@ marked.setOptions({
Topics.hasReadTopic = function(tid, uid, callback) {
RDB.sismember(schema.topics(tid).read_by_uid, uid, function(err, hasRead) {
if(err === null) {
callback(hasRead);
}
else {
} else {
console.log(err);
callback(false);
}
@@ -265,7 +489,7 @@ marked.setOptions({
Topics.getTeasers = function(tids, callback) {
var teasers = [];
if (Array.isArray(tids)) {
async.each(tids, function(tid, next) {
async.eachSeries(tids, function(tid, next) {
Topics.getTeaser(tid, function(err, teaser_info) {
if (err) teaser_info = {};
teasers.push(teaser_info);
@@ -285,10 +509,12 @@ marked.setOptions({
user.getUserFields(postData.uid, ['username', 'picture'], function(userData) {
var stripped = postData.content,
timestamp = postData.timestamp;
if(postData.content)
stripped = utils.strip_tags(marked(postData.content));
if(postData.content) {
stripped = postData.content.replace(/>.+\n\n/, '');
stripped = utils.strip_tags(postTools.markdownToHTML(stripped));
}
callback(null, {
"text": stripped,
"username": userData.username,
@@ -301,32 +527,40 @@ marked.setOptions({
});
}
Topics.post = function(socket, uid, title, content, category_id) {
Topics.emitTitleTooShortAlert = function(socket) {
socket.emit('event:alert', {
type: 'error',
timeout: 2000,
title: 'Title too short',
message: "Please enter a longer title. At least " + config.minimumTitleLength + " characters.",
alert_id: 'post_error'
});
}
Topics.post = function(uid, title, content, category_id, images, callback) {
if (!category_id)
throw new Error('Attempted to post without a category_id');
if(content)
content = content.trim();
if(title)
title = title.trim();
if (uid === 0) {
socket.emit('event:alert', {
title: 'Thank you for posting',
message: 'Since you are unregistered, your post is awaiting approval. Click here to register now.',
type: 'warning',
timeout: 7500,
clickfn: function() {
ajaxify.go('register');
}
});
return; // for now, until anon code is written.
callback(new Error('not-logged-in'), null);
return;
} else if(!title || title.length < config.minimumTitleLength) {
callback(new Error('title-too-short'), null);
return;
} else if (!content || content.length < config.miminumPostLength) {
callback(new Error('content-too-short'), null);
return;
}
user.getUserField(uid, 'lastposttime', function(lastposttime) {
if(Date.now() - lastposttime < config.post_delay) {
socket.emit('event:alert', {
title: 'Too many posts!',
message: 'You can only post every '+ (config.post_delay / 1000) + ' seconds.',
type: 'error',
timeout: 2000
});
if(Date.now() - lastposttime < config.postDelay) {
callback(new Error('too-many-posts'), null);
return;
}
@@ -336,7 +570,7 @@ marked.setOptions({
// Global Topics
if (uid == null) uid = 0;
if (uid !== null) {
RDB.sadd(schema.topics().tid, tid);
RDB.sadd('topics:tid', tid);
} else {
// need to add some unique key sent by client so we can update this with the real uid later
RDB.lpush(schema.topics().queued_tids, tid);
@@ -359,25 +593,9 @@ marked.setOptions({
'pinned': 0
});
topicSearch.index(title, tid);
RDB.set('topicslug:' + slug + ':tid', tid);
posts.create(uid, tid, content, function(pid) {
if (pid > 0) {
RDB.lpush(schema.topics(tid).posts, pid);
// Auto-subscribe the post creator to the newly created topic
threadTools.toggleFollow(tid, uid);
// Notify any users looking at the category that a new topic has arrived
Topics.getTopicForCategoryView(tid, uid, function(topicData) {
io.sockets.in('category_' + category_id).emit('event:new_topic', topicData);
io.sockets.in('recent_posts').emit('event:new_topic', topicData);
});
posts.getTopicPostStats(socket);
}
});
user.addTopicIdToUser(uid, tid);
// let everyone know that there is an unread topic in this category
@@ -387,35 +605,45 @@ marked.setOptions({
// in future it may be possible to add topics to several categories, so leaving the door open here.
RDB.sadd('categories:' + category_id + ':tid', tid);
categories.getCategories([category_id], function(data) {
Topics.setTopicField(tid, 'category_name', data.categories[0].name);
Topics.setTopicField(tid, 'category_slug', data.categories[0].slug);
});
RDB.zadd('categories:' + category_id + ':tid', timestamp, tid);
RDB.hincrby('category:' + category_id, 'topic_count', 1);
RDB.incr('totaltopiccount');
feed.updateCategory(category_id);
socket.emit('event:alert', {
title: 'Thank you for posting',
message: 'You have successfully posted. Click here to view your post.',
type: 'notify',
timeout: 2000
posts.create(uid, tid, content, images, function(postData) {
if (postData) {
RDB.lpush(schema.topics(tid).posts, postData.pid);
// Auto-subscribe the post creator to the newly created topic
threadTools.toggleFollow(tid, uid);
// Notify any users looking at the category that a new topic has arrived
Topics.getTopicForCategoryView(tid, uid, function(topicData) {
io.sockets.in('category_' + category_id).emit('event:new_topic', topicData);
io.sockets.in('recent_posts').emit('event:new_topic', topicData);
});
callback(null, postData);
}
});
});
});
};
Topics.getTopicField = function(tid, field, callback) {
RDB.hget('topic:' + tid, field, function(err, data) {
if(err === null)
RDB.hget('topic:' + tid, field, callback);
}
Topics.getTopicFields = function(tid, fields, callback) {
RDB.hmgetObject('topic:' + tid, fields, function(err, data) {
if(err === null) {
callback(data);
else
}
else {
console.log(err);
});
}
});
}
Topics.setTopicField = function(tid, field, value) {
@@ -427,13 +655,55 @@ marked.setOptions({
}
Topics.isLocked = function(tid, callback) {
Topics.getTopicField(tid, 'locked', function(locked) {
Topics.getTopicField(tid, 'locked', function(err, locked) {
callback(locked);
});
}
Topics.addToRecent = function(tid, timestamp) {
Topics.updateTimestamp = function(tid, timestamp) {
RDB.zadd(schema.topics().recent, timestamp, tid);
Topics.setTopicField(tid, 'lastposttime', timestamp);
}
Topics.addPostToTopic = function(tid, pid) {
RDB.rpush('tid:' + tid + ':posts', pid);
}
Topics.getPids = function(tid, callback) {
RDB.lrange('tid:' + tid + ':posts', 0, -1, callback);
}
Topics.reIndexTopic = function(tid, callback) {
Topics.getPids(tid, function(err, pids) {
if(err) {
callback(err);
} else {
posts.reIndexPids(pids, function(err) {
if(err) {
callback(err);
} else {
callback(null);
}
});
}
});
}
Topics.reIndexAll = function(callback) {
RDB.smembers('topics:tid', function(err, tids) {
if(err) {
callback(err, null);
} else {
async.each(tids, Topics.reIndexTopic, function(err) {
if(err) {
callback(err, null);
} else {
callback(null, 'All topics reindexed.');
}
});
}
});
}
}(exports));

97
src/upgrade.js Normal file
View File

@@ -0,0 +1,97 @@
var RDB = require('./redis.js'),
async = require('async');
function upgradeCategory(cid, callback) {
RDB.type('categories:'+ cid +':tid', function(err, type) {
if (type === 'set') {
RDB.smembers('categories:' + cid + ':tid', function(err, tids) {
function moveTopic(tid, callback) {
RDB.hget('topic:' + tid, 'timestamp', function(err, timestamp) {
if(err)
return callback(err);
RDB.zadd('temp_categories:'+ cid + ':tid', timestamp, tid);
callback(null);
});
}
async.each(tids, moveTopic, function(err) {
if(!err) {
console.log('renaming ' + cid);
RDB.rename('temp_categories:' + cid + ':tid', 'categories:' + cid + ':tid');
callback(null);
}
else
callback(err);
});
});
} else {
console.log('category already upgraded '+ cid);
callback(null);
}
});
}
function upgradeUser(uid, callback) {
RDB.hmgetObject('user:' + uid, ['joindate', 'postcount', 'reputation'], function(err, userData) {
if(err)
return callback(err);
RDB.zadd('users:joindate', userData.joindate, uid);
RDB.zadd('users:postcount', userData.postcount, uid);
RDB.zadd('users:reputation', userData.reputation, uid);
callback(null);
});
}
exports.upgrade = function() {
console.log('upgrading nodebb now');
var schema = [
function upgradeCategories(next) {
console.log('upgrading categories');
RDB.lrange('categories:cid', 0, -1, function(err, cids) {
async.each(cids, upgradeCategory, function(err) {
if(!err)
next(null, 'upgraded categories');
else
next(err, null);
});
});
},
function upgradeUsers(next) {
console.log('upgrading users');
RDB.lrange('userlist', 0, -1, function(err, uids) {
async.each(uids, upgradeUser, function(err) {
if(!err)
next(null, 'upgraded users');
else
next(err, null);
});
});
}
];
async.series(schema, function(err, results) {
if(!err)
console.log('upgrade complete');
else
console.log(err);
process.exit();
});
}

View File

@@ -10,81 +10,93 @@ var utils = require('./../public/src/utils.js'),
async = require('async');
(function(User) {
User.create = function(username, password, email, callback) {
username = username.trim(), email = email.trim();
// @todo use node-validator?
if(!utils.isEmailValid(email) || !utils.isUserNameValid(username) || !utils.isPasswordValid(password)) {
callback('Invalid email/username/password!', 0);
return;
}
var userslug = utils.slugify(username);
User.exists(userslug, function(exists) {
if(exists) {
callback('Username taken!', 0);
return;
username = username.trim();
if (email !== undefined) email = email.trim();
async.parallel([
function(next) {
if (email !== undefined) next(!utils.isEmailValid(email) ? new Error('Invalid Email!') : null);
else next();
},
function(next) {
next(!utils.isUserNameValid(username) ? new Error('Invalid Username!') : null);
},
function(next) {
if (password !== undefined) next(!utils.isPasswordValid(password) ? new Error('Invalid Password!') : null);
else next();
},
function(next) {
User.exists(userslug, function(exists) {
next(exists ? new Error('Username taken!') : null);
});
},
function(next) {
if (email !== undefined) {
User.isEmailAvailable(email, function(available) {
next(!available ? new Error('Email taken!') : null);
});
} else next();
}
User.isEmailAvailable(email, function(available) {
if(!available) {
callback('Email taken!', 0);
return;
], function(err, results) {
if (err) return callback(err, 0); // FIXME: Maintaining the 0 for backwards compatibility. Do we need this?
RDB.incr('global:next_user_id', function(err, uid) {
RDB.handle(err);
var gravatar = User.createGravatarURLFromEmail(email);
var timestamp = Date.now();
RDB.hmset('user:'+uid, {
'uid': uid,
'username' : username,
'userslug' : userslug,
'fullname': '',
'location':'',
'birthday':'',
'website':'',
'email' : email || '',
'signature':'',
'joindate' : timestamp,
'picture': gravatar,
'gravatarpicture' : gravatar,
'uploadedpicture': '',
'reputation': 0,
'postcount': 0,
'lastposttime': 0,
'administrator': (uid == 1) ? 1 : 0,
'showemail': 0
});
RDB.set('username:' + username + ':uid', uid);
RDB.set('userslug:'+ userslug +':uid', uid);
if (email !== undefined) {
RDB.set('email:' + email +':uid', uid);
User.sendConfirmationEmail(email);
}
RDB.incr('global:next_user_id', function(err, uid) {
RDB.incr('usercount', function(err, count) {
RDB.handle(err);
var gravatar = User.createGravatarURLFromEmail(email);
RDB.hmset('user:'+uid, {
'uid': uid,
'username' : username,
'userslug' : userslug,
'fullname': '',
'location':'',
'birthday':'',
'website':'',
'email' : email,
'signature':'',
'joindate' : Date.now(),
'picture': gravatar,
'gravatarpicture' : gravatar,
'uploadedpicture': '',
'reputation': 0,
'postcount': 0,
'lastposttime': 0,
'administrator': (uid == 1) ? 1 : 0
});
RDB.set('username:' + username + ':uid', uid);
RDB.set('email:' + email +':uid', uid);
RDB.set('userslug:'+ userslug +':uid', uid);
if(email) {
User.sendConfirmationEmail(email);
}
RDB.incr('usercount', function(err, count) {
RDB.handle(err);
io.sockets.emit('user.count', {count: count});
});
RDB.lpush('userlist', uid);
io.sockets.emit('user.latest', {userslug: userslug, username: username});
if (password) {
User.hashPassword(password, function(hash) {
User.setUserField(uid, 'password', hash);
});
}
callback(null, uid);
io.sockets.emit('user.count', {count: count});
});
RDB.zadd('users:joindate', timestamp, uid);
RDB.zadd('users:postcount', 0, uid);
RDB.zadd('users:reputation', 0, uid);
io.sockets.emit('user.latest', {userslug: userslug, username: username});
if (password !== undefined) {
User.hashPassword(password, function(hash) {
User.setUserField(uid, 'password', hash);
});
}
callback(null, uid);
});
});
};
@@ -103,7 +115,9 @@ var utils = require('./../public/src/utils.js'),
RDB.del('followers:' + uid);
RDB.del('following:' + uid);
RDB.lrem('userlist', 1, uid);
RDB.zrem('users:joindate', uid);
RDB.zrem('users:postcount', uid);
RDB.zrem('users:reputation', uid);
callback(true);
});
@@ -124,13 +138,9 @@ var utils = require('./../public/src/utils.js'),
}
User.getUserFields = function(uid, fields, callback) {
RDB.hmget('user:' + uid, fields, function(err, data) {
RDB.hmgetObject('user:' + uid, fields, function(err, data) {
if(err === null) {
for(var i = 0, returnData = {}, ii=fields.length; i<ii; ++i) {
returnData[fields[i]] = data[i];
}
callback(returnData);
callback(data);
} else {
console.log(err);
}
@@ -139,7 +149,7 @@ var utils = require('./../public/src/utils.js'),
User.getMultipleUserFields = function(uids, fields, callback) {
if(uids.length === 0) {
callback({});
callback([]);
return;
}
@@ -156,7 +166,7 @@ var utils = require('./../public/src/utils.js'),
});
}
async.each(uuids, iterator, function(err) {
async.eachSeries(uuids, iterator, function(err) {
if(!err) {
callback(returnData);
} else {
@@ -180,9 +190,10 @@ var utils = require('./../public/src/utils.js'),
});
}
User.updateProfile = function(socket, uid, data) {
User.updateProfile = function(uid, data, callback) {
var fields = ['email', 'fullname', 'website', 'location', 'birthday', 'signature'];
var returnData = {success:false};
function isSignatureValid(next) {
if(data['signature'] !== undefined && data['signature'].length > 150) {
@@ -193,61 +204,69 @@ var utils = require('./../public/src/utils.js'),
}
function isEmailAvailable(next) {
if(data['email'] !== undefined) {
User.getUserField(uid, 'email', function(email) {
if(email !== data['email']) {
User.isEmailAvailable(data['email'], function(available) {
if(!available) {
next({error:'Email not available!'}, false);
}
});
} else {
next(null, true);
}
});
} else {
next(null, true);
if(!data['email']) {
return next(null, true);
}
User.getUserField(uid, 'email', function(email) {
if(email !== data['email']) {
User.isEmailAvailable(data['email'], function(available) {
if(!available) {
next({error:'Email not available!'}, false);
} else {
next(null, true);
}
});
} else {
next(null, true);
}
});
}
async.series([isSignatureValid, isEmailAvailable], function(err, results) {
if(err) {
socket.emit('event:alert', {
title: 'Error',
message: err.error,
type: 'error',
timeout: 2000
});
console.log(err);
callback(returnData);
} else {
updateFields();
async.each(fields, updateField, function(err) {
if(err) {
console.log(err);
callback(returnData);
} else {
returnData.success = true;
callback(returnData);
}
});
}
});
function updateFields() {
for(var i = 0, key, ii = fields.length; i < ii; ++i) {
key = fields[i];
function updateField(field, callback) {
if(data[field] !== undefined) {
if(field === 'email') {
var gravatarpicture = User.createGravatarURLFromEmail(data[field]);
User.setUserField(uid, 'gravatarpicture', gravatarpicture);
User.getUserFields(uid, ['email', 'picture', 'uploadedpicture'], function(userData) {
RDB.del('email:' + userData['email'] + ':uid');
RDB.set('email:' + data['email'] + ':uid', uid);
User.setUserField(uid, field, data[field]);
if(userData.picture !== userData.uploadedpicture) {
returnData.picture = gravatarpicture;
User.setUserField(uid, 'picture', gravatarpicture);
}
returnData.gravatarpicture = gravatarpicture;
callback(null);
});
return;
} else if(field === 'signature') {
data[field] = utils.strip_tags(data[field]);
}
if(data[key] !== undefined) {
if(key === 'email') {
User.setUserField(uid, 'gravatarpicture', User.createGravatarURLFromEmail(data[key]));
user.getUserField(uid, 'email', function(email) {
RDB.del('email:' + email + ':uid');
RDB.set('email:' + data['email'] + ':uid', uid);
});
} else if(key === 'signature') {
data[key] = utils.strip_tags(data[key]);
}
User.setUserField(uid, field, data[field]);
User.setUserField(uid, key, data[key]);
}
callback(null);
} else {
callback(null);
}
socket.emit('event:alert', {
title: 'Success',
message: 'Your profile has been updated successfully!',
type: 'success',
timeout: 2000
});
}
}
@@ -263,15 +282,9 @@ var utils = require('./../public/src/utils.js'),
});
}
User.changePassword = function(socket, uid, data, callback) {
User.changePassword = function(uid, data, callback) {
if(!utils.isPasswordValid(data.newPassword)) {
socket.emit('event:alert', {
title: 'Error',
message: 'Invalid password!',
type: 'error',
timeout: 2000
});
callback(false);
callback({err:'Invalid password!'});
return;
}
@@ -279,7 +292,7 @@ var utils = require('./../public/src/utils.js'),
bcrypt.compare(data.currentPassword, user_password, function(err, res) {
if(err) {
console.log(err);
callback(false);
callback({err:'bcrpyt compare error!'});
return;
}
@@ -287,22 +300,10 @@ var utils = require('./../public/src/utils.js'),
User.hashPassword(data.newPassword, function(hash) {
User.setUserField(uid, 'password', hash);
socket.emit('event:alert', {
title: 'Success',
message: 'Your password is updated!',
type: 'success',
timeout: 2000
});
callback(true);
callback({err:null});
});
} else {
socket.emit('event:alert', {
title: 'Warning',
message: 'Your current password is not correct!',
type: 'warning',
timeout: 2000
});
callback(false);
callback({err:'Your current password is not correct!'});
}
});
});
@@ -312,32 +313,39 @@ var utils = require('./../public/src/utils.js'),
RDB.hset('user:' + uid, field, value);
}
User.incrementUserFieldBy = function(uid, field, value) {
RDB.hincrby('user:' + uid, field, value);
User.setUserFields = function(uid, data) {
RDB.hmset('user:' + uid, data);
}
User.getUserList = function(callback) {
User.incrementUserFieldBy = function(uid, field, value, callback) {
RDB.hincrby('user:' + uid, field, value, callback);
}
User.decrementUserFieldBy = function(uid, field, value, callback) {
RDB.hincrby('user:' + uid, field, -value, callback);
}
User.getUsers = function(set, start, stop, callback) {
var data = [];
RDB.lrange('userlist', 0, -1, function(err, uids) {
RDB.handle(err);
RDB.zrevrange(set, start, stop, function(err, uids) {
if(err) {
return callback(err, null);
}
function iterator(uid, callback) {
User.getUserData(uid, function(userData) {
data.push(userData);
if(userData) {
data.push(userData);
}
callback(null);
});
}
async.each(uids, iterator, function(err) {
if(!err) {
callback(data);
} else {
console.log(err);
callback(null);
}
async.eachSeries(uids, iterator, function(err) {
callback(err, data);
});
});
}
@@ -395,7 +403,10 @@ var utils = require('./../public/src/utils.js'),
User.onNewPostMade = function(uid, tid, pid, timestamp) {
User.addPostIdToUser(uid, pid);
User.incrementUserFieldBy(uid, 'postcount', 1);
User.incrementUserFieldBy(uid, 'postcount', 1, function(err, newpostcount) {
RDB.zadd('users:postcount', newpostcount, uid);
});
User.setUserField(uid, 'lastposttime', timestamp);
User.sendPostNotificationToFollowers(uid, tid, pid);
@@ -552,7 +563,7 @@ var utils = require('./../public/src/utils.js'),
});
}
async.each(uids, iterator, function(err) {
async.eachSeries(uids, iterator, function(err) {
callback(returnData);
});
}
@@ -560,7 +571,7 @@ var utils = require('./../public/src/utils.js'),
User.sendPostNotificationToFollowers = function(uid, tid, pid) {
User.getUserField(uid, 'username', function(username) {
RDB.smembers('followers:' + uid, function(err, followers) {
topics.getTopicField(tid, 'slug', function(slug) {
topics.getTopicField(tid, 'slug', function(err, slug) {
var message = username + ' made a new post';
notifications.create(message, 5, global.nconf.get('url') + 'topic/' + slug + '#' + pid, 'notification_'+ Date.now(), function(nid) {
@@ -596,7 +607,7 @@ var utils = require('./../public/src/utils.js'),
};
User.latest = function(socket) {
RDB.lrange('userlist', 0, 0, function(err, uid) {
RDB.zrevrange('users:joindate', 0, 0, function(err, uid) {
RDB.handle(err);
User.getUserFields(uid, ['username', 'userslug'], function(userData) {
@@ -637,7 +648,7 @@ var utils = require('./../public/src/utils.js'),
});
}
async.each(uids, iterator, function(err) {
async.eachSeries(uids, iterator, function(err) {
callback(usernames);
});
}
@@ -656,7 +667,7 @@ var utils = require('./../public/src/utils.js'),
});
}
async.each(uids, iterator, function(err) {
async.eachSeries(uids, iterator, function(err) {
callback(userslugs);
});
}
@@ -726,14 +737,14 @@ var utils = require('./../public/src/utils.js'),
User.isModerator = function(uid, cid, callback) {
RDB.sismember('cid:' + cid + ':moderators', uid, function(err, exists) {
// @todo handle error
RDB.handle(err);
callback(!!exists);
});
}
User.isAdministrator = function(uid, callback) {
RDB.sismember('administrators', uid, function(err, exists) {
// @todo handle error
RDB.handle(err);
callback(!!exists);
});
}
@@ -904,29 +915,6 @@ var utils = require('./../public/src/utils.js'),
}
};
User.get_online_users = function(socket, uids) {
RDB.sismembers('users:online', uids, function(err, data) {
RDB.handle(err);
socket.emit('api:user.get_online_users', data);
});
};
User.go_online = function(uid) {
RDB.sadd('users:online', uid, function(err) {
if (err) {
RDB.handle(err);
}
});
};
User.go_offline = function(uid) {
RDB.srem('users:online', uid, function(err) {
if (err) {
RDB.handle(err);
}
});
};
User.active = {
get_record : function(socket) {
RDB.mget(['global:active_user_record', 'global:active_user_record_date'], function(err, data) {
@@ -1000,11 +988,17 @@ var utils = require('./../public/src/utils.js'),
User.notifications = {
get: function(uid, callback) {
var maxNotifs = 15;
async.parallel({
unread: function(next) {
RDB.zrevrangebyscore('uid:' + uid + ':notifications:unread', 10, 0, function(err, nids) {
// @todo handle err
var unread = [];
// Cap the number of notifications returned
if (nids.length > maxNotifs) nids.length = maxNotifs;
if (nids && nids.length > 0) {
async.eachSeries(nids, function(nid, next) {
notifications.get(nid, function(notif_data) {
@@ -1023,6 +1017,10 @@ var utils = require('./../public/src/utils.js'),
RDB.zrevrangebyscore('uid:' + uid + ':notifications:read', 10, 0, function(err, nids) {
// @todo handle err
var read = [];
// Cap the number of notifications returned
if (nids.length > maxNotifs) nids.length = maxNotifs;
if (nids && nids.length > 0) {
async.eachSeries(nids, function(nid, next) {
notifications.get(nid, function(notif_data) {
@@ -1039,34 +1037,31 @@ var utils = require('./../public/src/utils.js'),
}
}, function(err, notifications) {
// While maintaining score sorting, sort by time
var readCount = notifications.read.length,
unreadCount = notifications.unread.length;
notifications.read.sort(function(a, b) {
if (a.score === b.score) {
return (a.datetime - b.datetime) > 0 ? -1 : 1;
}
});
notifications.unread.sort(function(a, b) {
if (a.score === b.score) {
return (a.datetime - b.datetime) > 0 ? -1 : 1;
}
});
// Limit the number of notifications to `maxNotifs`, prioritising unread notifications
if (notifications.read.length + notifications.unread.length > maxNotifs) {
notifications.read.length = maxNotifs - notifications.unread.length;
}
callback(notifications);
});
},
hasFlag: function(uid, callback) {
RDB.get('uid:1:notifications:flag', function(err, flag) {
if (err) {
RDB.handle(err);
}
callback(flag === 1);
});
},
removeFlag: function(uid) {
RDB.del('uid:' + uid + ':notifications:flag', function(err) {
if (err) {
RDB.handle(err);
}
});
getUnreadCount: function(uid, callback) {
RDB.zcount('uid:' + uid + ':notifications:unread', 0, 10, callback);
}
}
}(exports));

View File

@@ -18,6 +18,7 @@ var express = require('express'),
notifications = require('./notifications.js'),
admin = require('./routes/admin.js'),
userRoute = require('./routes/user.js'),
apiRoute = require('./routes/api.js'),
installRoute = require('./routes/install.js'),
testBed = require('./routes/testbed.js'),
auth = require('./routes/authentication.js'),
@@ -26,19 +27,39 @@ var express = require('express'),
(function(app) {
var templates = null;
app.build_header = function(res) {
return templates['header'].parse({
cssSrc: global.config['theme:src'] || global.nconf.get('relative_path') + '/vendor/bootstrap/css/bootstrap.min.css',
title: global.config['title'] || 'NodeBB',
csrf:res.locals.csrf_token,
relative_path: global.nconf.get('relative_path')
});
/**
* `options` object requires: req, res
* accepts: metaTags
*/
app.build_header = function(options, callback) {
var defaultMetaTags = [
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
{ name: 'content-type', content: 'text/html; charset=UTF-8' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ property: 'og:site_name', content: global.config.title || 'NodeBB' },
],
metaString = utils.buildMetaTags(defaultMetaTags.concat(options.metaTags || [])),
templateValues = {
cssSrc: global.config['theme:src'] || global.nconf.get('relative_path') + '/vendor/bootstrap/css/bootstrap.min.css',
title: global.config['title'] || 'NodeBB',
browserTitle: global.config['title'] || 'NodeBB',
csrf: options.res.locals.csrf_token,
relative_path: global.nconf.get('relative_path'),
meta_tags: metaString
};
// meta.build_title(options.title, (options.req.user ? options.req.user.uid : 0), function(err, title) {
// if (!err) templateValues.browserTitle = title;
// callback(null, templates['header'].parse(templateValues));
// });
callback(null, templates['header'].parse(templateValues));
};
// Middlewares
app.use(express.favicon(path.join(__dirname, '../', 'public', 'favicon.ico')));
app.use(require('less-middleware')({ src: path.join(__dirname, '../', 'public') }));
//app.use(express.static(path.join(__dirname, '../', 'public')));
app.use(global.nconf.get('relative_path'), express.static(path.join(__dirname, '../', 'public')));
app.use(express.bodyParser()); // Puts POST vars in request.body
app.use(express.cookieParser()); // If you want to parse cookies (res.cookies)
@@ -128,12 +149,12 @@ var express = require('express'),
userRoute.create_routes(app);
installRoute.create_routes(app);
testBed.create_routes(app);
apiRoute.create_routes(app);
// Basic Routes (entirely client-side parsed, goal is to move the rest of the crap in this file into this one section)
(function() {
var routes = ['login', 'register', 'account', 'recent', 'popular', 'active', '403', '404'];
var routes = ['login', 'register', 'account', 'recent', 'unread', 'popular', 'active', '403', '404'];
for (var i=0, ii=routes.length; i<ii; i++) {
(function(route) {
@@ -146,8 +167,10 @@ var express = require('express'),
});
return;
}
res.send(app.build_header(res) + app.create_route(route) + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route(route) + templates['footer']);
});
});
}(routes[i]));
}
@@ -155,21 +178,38 @@ var express = require('express'),
app.get('/', function(req, res) {
categories.getAllCategories(function(returnData) {
async.parallel({
"header": function(next) {
app.build_header({
req: req,
res: res,
metaTags: [
{ name: "title", content: global.config.title || 'NodeBB' },
{ name: "description", content: global.config.description || '' },
{ property: 'og:title', content: 'Index | ' + (global.config.title || 'NodeBB') },
{ property: "og:type", content: 'website' }
]
}, next);
},
"categories": function(next) {
categories.getAllCategories(function(returnData) {
next(null, returnData);
}, 0);
}
}, function(err, data) {
res.send(
app.build_header(res) +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/home'].parse(returnData) + '\n\t</noscript>' +
data.header +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/home'].parse(data.categories) + '\n\t</noscript>' +
app.create_route('') +
templates['footer']
);
}, 0);
})
});
app.get('/topic/:topic_id/:slug?', function(req, res) {
var tid = req.params.topic_id;
if (tid.match('.rss')) {
if (tid.match(/^\d+\.rss$/)) {
fs.readFile('feeds/topics/' + tid, function (err, data) {
if (err) {
res.type('text').send(404, "Unable to locate an rss feed at this location.");
@@ -181,14 +221,48 @@ var express = require('express'),
return;
}
async.waterfall([
function(next) {
topics.getTopicWithPosts(tid, ((req.user) ? req.user.uid : 0), function(err, topicData) {
next(err, topicData);
});
},
function(topicData, next) {
var posts = topicData.posts.push(topicData.main_posts[0]),
lastMod = 0,
timestamp;
for(var x=0,numPosts=topicData.posts.length;x<numPosts;x++) {
timestamp = parseInt(topicData.posts[x].timestamp, 10);
if (timestamp > lastMod) lastMod = timestamp;
}
var topic_url = tid + (req.params.slug ? '/' + req.params.slug : '');
topics.getTopicWithPosts(tid, ((req.user) ? req.user.uid : 0), function(err, topic) {
app.build_header({
req: req,
res: res,
metaTags: [
{ name: "title", content: topicData.topic_name },
{ property: 'og:title', content: topicData.topic_name + ' | ' + (global.config.title || 'NodeBB') },
{ property: "og:type", content: 'article' },
{ property: "og:url", content: global.nconf.get('url') + 'topic/' + topicData.slug },
{ property: 'og:image', content: topicData.main_posts[0].picture },
{ property: "article:published_time", content: new Date(parseInt(topicData.main_posts[0].timestamp, 10)).toISOString() },
{ property: 'article:modified_time', content: new Date(lastMod).toISOString() },
{ property: 'article:section', content: topicData.category_name }
]
}, function(err, header) {
next(err, {
header: header,
topics: topicData
});
});
},
], function(err, data) {
if (err) return res.redirect('404');
var topic_url = tid + (req.params.slug ? '/' + req.params.slug : '');
res.send(
app.build_header(res) +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/topic'].parse(topic) + '\n\t</noscript>' +
data.header +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/topic'].parse(data.topics) + '\n\t</noscript>' +
'\n\t<script>templates.ready(function(){ajaxify.go("topic/' + topic_url + '");});</script>' +
templates['footer']
);
@@ -197,7 +271,8 @@ var express = require('express'),
app.get('/category/:category_id/:slug?', function(req, res) {
var cid = req.params.category_id;
if (cid.match('.rss')) {
if (cid.match(/^\d+\.rss$/)) {
fs.readFile('feeds/categories/' + cid, function (err, data) {
if (err) {
res.type('text').send(404, "Unable to locate an rss feed at this location.");
@@ -209,16 +284,35 @@ var express = require('express'),
return;
}
var category_url = cid + (req.params.slug ? '/' + req.params.slug : '');
categories.getCategoryById(cid, 0, function(returnData) {
if(!returnData) {
res.redirect('404');
return;
async.waterfall([
function(next) {
categories.getCategoryById(cid, 0, function(err, categoryData) {
next(err, categoryData);
});
},
function(categoryData, next) {
app.build_header({
req: req,
res: res,
metaTags: [
{ name: 'title', content: categoryData.category_name },
{ name: 'description', content: categoryData.category_description },
{ property: "og:type", content: 'website' }
]
}, function(err, header) {
next(err, {
header: header,
categories: categoryData
});
});
}
], function(err, data) {
if(err) return res.redirect('404');
var category_url = cid + (req.params.slug ? '/' + req.params.slug : '');
res.send(
app.build_header(res) +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/category'].parse(returnData) + '\n\t</noscript>' +
data.header +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/category'].parse(data.categories) + '\n\t</noscript>' +
'\n\t<script>templates.ready(function(){ajaxify.go("category/' + category_url + '");});</script>' +
templates['footer']
);
@@ -226,17 +320,29 @@ var express = require('express'),
});
app.get('/confirm/:code', function(req, res) {
res.send(app.build_header(res) + '<script>templates.ready(function(){ajaxify.go("confirm/' + req.params.code + '");});</script>' + templates['footer']);
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + '<script>templates.ready(function(){ajaxify.go("confirm/' + req.params.code + '");});</script>' + templates['footer']);
});
});
app.get('/api/:method', api_method);
app.get('/api/:method/:id', api_method);
// ok fine MUST ADD RECURSION style. I'll look for a better fix in future but unblocking baris for this:
app.get('/api/:method/:id/:section?', api_method);
app.get('/api/:method/:id*', api_method);
app.get('/sitemap.xml', function(req, res) {
var sitemap = require('./sitemap.js');
sitemap.render(function(xml) {
res.type('xml').set('Content-Length', xml.length).send(xml);
});
});
app.get('/robots.txt', function(req, res) {
res.set('Content-Type', 'text/plain');
res.send( "User-agent: *\n" +
"Disallow: \n" +
"Disallow: /admin/\n" +
"Sitemap: " + global.nconf.get('url') + "sitemap.xml");
});
app.get('/cid/:cid', function(req, res) {
categories.getCategoryData(req.params.cid, function(data){
categories.getCategoryData(req.params.cid, function(err, data) {
if(data)
res.send(data);
else
@@ -262,156 +368,42 @@ var express = require('express'),
});
});
app.get('/test', function(req, res) {
/*user.get_userslugs_by_uids([1,2], function(data) {
res.send(data);
});*/
var gravatar= require('gravatar');
var img = gravatar.url('', {}, https=false);
res.send(img);
// 'http://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e'
/* categories.getCategoryById(1,1, function(data) {
res.send(data);
},1);*/
app.get('/outgoing', function(req, res) {
if (!req.query.url) return res.redirect('/404');
app.build_header({ req: req, res: res }, function(err, header) {
res.send(
header +
'\n\t<script>templates.ready(function(){ajaxify.go("outgoing?url=' + req.query.url + '");});</script>' +
templates['footer']
);
});
});
app.get('/search', function(req, res) {
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("search", null, "search") + templates['footer']);
});
});
});
// These functions are called via ajax once the initial page is loaded to populate templates with data
function api_method(req, res) {
var uid = (req.user) ? req.user.uid : 0;
switch(req.params.method) {
case 'get_templates_listing' :
utils.walk(global.configuration.ROOT_DIRECTORY + '/public/templates', function(err, data) {
res.json(data);
});
break;
case 'home' :
categories.getAllCategories(function(data) {
var async = require('async');
function iterator(category, callback) {
categories.getRecentReplies(category.cid, 2, function(posts) {
category["posts"] = posts;
category["post_count"] = posts.length>2 ? 2 : posts.length;
callback(null);
});
}
async.each(data.categories, iterator, function(err) {
data.motd_class = (config.show_motd === '1' || config.show_motd === undefined) ? '' : 'none';
data.motd = marked(config.motd || "# NodeBB v" + pkg.version + "\nWelcome to NodeBB, the discussion platform of the future.\n\n<a target=\"_blank\" href=\"http://www.nodebb.org\" class=\"btn btn-large\"><i class=\"icon-comment\"></i> Get NodeBB</a> <a target=\"_blank\" href=\"https://github.com/designcreateplay/NodeBB\" class=\"btn btn-large\"><i class=\"icon-github-alt\"></i> Fork us on Github</a> <a target=\"_blank\" href=\"https://twitter.com/dcplabs\" class=\"btn btn-large\"><i class=\"icon-twitter\"></i> @dcplabs</a>");
res.json(data);
});
app.get('/search/:term', function(req, res) {
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + app.create_route("search/"+req.params.term, null, "search") + templates['footer']);
});
});
app.get('/reindex', function(req, res) {
topics.reIndexAll(function(err) {
if(err) {
res.json(err);
} else {
res.send('All topics reindexed');
}
});
});
});
}, uid);
break;
case 'login' :
var data = {},
login_strategies = auth.get_login_strategies(),
num_strategies = login_strategies.length;
if (num_strategies == 0) {
data = {
'login_window:spansize': 'span12',
'alternate_logins:display': 'none'
};
} else {
data = {
'login_window:spansize': 'span6',
'alternate_logins:display': 'block'
}
for (var i=0, ii=num_strategies; i<ii; i++) {
data[login_strategies[i] + ':display'] = 'active';
}
}
data.token = res.locals.csrf_token;
res.json(data);
break;
case 'register' :
var data = {},
login_strategies = auth.get_login_strategies(),
num_strategies = login_strategies.length;
if (num_strategies == 0) {
data = {
'register_window:spansize': 'span12',
'alternate_logins:display': 'none'
};
} else {
data = {
'register_window:spansize': 'span6',
'alternate_logins:display': 'block'
}
for (var i=0, ii=num_strategies; i<ii; i++) {
data[login_strategies[i] + ':display'] = 'active';
}
}
data.token = res.locals.csrf_token;
res.json(data);
break;
case 'topic' :
topics.getTopicWithPosts(req.params.id, uid, function(err, data) {
res.json(data);
});
break;
case 'category' :
categories.getCategoryById(req.params.id, uid, function(data) {
res.json(data);
}, req.params.id, uid);
break;
case 'recent' :
categories.getLatestTopics(uid, 0, 9, function(data) {
res.json(data);
});
break;
case 'popular' :
categories.getLatestTopics(uid, 0, 9, function(data) {
res.json(data);
});
break;
case 'active' :
categories.getLatestTopics(uid, 0, 9, function(data) {
res.json(data);
});
break;
case 'confirm':
user.email.confirm(req.params.id, function(data) {
if (data.status === 'ok') {
res.json({
'alert-class': 'alert-success',
title: 'Email Confirmed',
text: 'Thank you for vaidating your email. Your account is now fully activated.'
});
} else {
res.json({
'alert-class': 'alert-error',
title: 'An error occurred...',
text: 'There was a problem validating your email address. Perhaps the code was invalid or has expired.'
});
}
});
break;
default :
res.json(404, { error: 'unrecognized API endpoint' });
break;
}
}
}(WebServer));
server.listen(nconf.get('port'));

View File

@@ -1,7 +1,7 @@
var SocketIO = require('socket.io').listen(global.server, { log:false }),
cookie = require('cookie'),
connect = require('connect'),
express = require('express'),
user = require('./user.js'),
posts = require('./posts.js'),
favourites = require('./favourites.js'),
@@ -14,10 +14,19 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
postTools = require('./postTools.js'),
meta = require('./meta.js'),
async = require('async'),
RedisStoreLib = require('connect-redis')(express),
redis = require('redis'),
redisServer = redis.createClient(global.nconf.get('redis:port'), global.nconf.get('redis:host')),
RedisStore = new RedisStoreLib({
client: redisServer,
ttl: 60*60*24*14
}),
socketCookieParser = express.cookieParser(global.nconf.get('secret')),
admin = {
'categories': require('./admin/categories.js'),
'user': require('./admin/user.js')
};
},
plugins = require('./plugins');
(function(io) {
var users = {},
@@ -26,75 +35,56 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
global.io = io;
// Adapted from http://howtonode.org/socket-io-auth
io.set('authorization', function(handshakeData, accept) {
if (handshakeData.headers.cookie) {
handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
handshakeData.sessionID = connect.utils.parseSignedCookie(handshakeData.cookie['express.sid'], global.nconf.get('secret'));
if (handshakeData.cookie['express.sid'] == handshakeData.sessionID) {
return accept('Cookie is invalid.', false);
}
} else {
// No cookie sent
return accept('No cookie transmitted', false);
}
// Otherwise, continue unimpeded.
var sessionID = handshakeData.sessionID;
user.get_uid_by_session(sessionID, function(userId) {
if (userId)
users[sessionID] = userId;
else
users[sessionID] = 0;
accept(null, true);
});
});
io.sockets.on('connection', function(socket) {
var hs = socket.handshake;
var hs = socket.handshake,
sessionID, uid;
var uid = users[hs.sessionID];
// if (uid > 0) {
userSockets[uid] = userSockets[uid] || [];
userSockets[uid].push(socket);
user.go_online(uid);
socket.join('uid_' + uid);
// }
/*process.on('uncaughtException', function(err) {
// handle the error safely
console.log("error message "+err);
socket.emit('event:consolelog',{type:'uncaughtException', stack:err.stack, error:err.toString()});
});*/
// Validate the session, if present
socketCookieParser(hs, {}, function(err) {
sessionID = socket.handshake.signedCookies["express.sid"];
RedisStore.get(sessionID, function(err, sessionData) {
if (!err && sessionData && sessionData.passport && sessionData.passport.user) uid = users[sessionID] = sessionData.passport.user;
else uid = users[sessionID] = 0;
socket.emit('event:connect', {status: 1});
userSockets[uid] = userSockets[uid] || [];
userSockets[uid].push(socket);
if(uid) {
socket.join('uid_' + uid);
io.sockets.in('global').emit('api:user.isOnline', isUserOnline(uid));
user.getUserField(uid, 'username', function(username) {
socket.emit('event:connect', {status: 1, username:username});
});
}
});
});
socket.on('disconnect', function() {
// if (uid > 0) {
user.go_offline(uid);
delete users[hs.sessionID];
var index = userSockets[uid].indexOf(socket);
if(index !== -1) {
userSockets[uid].splice(index, 1);
}
for(var roomName in rooms) {
var index = userSockets[uid].indexOf(socket);
if(index !== -1) {
userSockets[uid].splice(index, 1);
}
socket.leave(roomName);
if(userSockets[uid].length === 0) {
delete users[sessionID];
if(uid)
io.sockets.in('global').emit('api:user.isOnline', isUserOnline(uid));
}
for(var roomName in rooms) {
if(rooms[roomName][hs.sessionID]) {
delete rooms[roomName][hs.sessionID];
}
updateRoomBrowsingText(roomName);
}
socket.leave(roomName);
if(rooms[roomName][socket.id]) {
delete rooms[roomName][socket.id];
}
// }
updateRoomBrowsingText(roomName);
}
});
socket.on('api:get_all_rooms', function(data) {
@@ -105,9 +95,9 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
function getUidsInRoom(room) {
var uids = [];
for(var sessionId in room) {
if(uids.indexOf(room[sessionId]) === -1)
uids.push(room[sessionId]);
for(var socketId in room) {
if(uids.indexOf(room[socketId]) === -1)
uids.push(room[socketId]);
}
return uids;
}
@@ -118,7 +108,8 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
for(var i=0; i<clients.length; ++i) {
var hs = clients[i].handshake;
if(hs && !users[hs.sessionID]) {
if(hs && !users[sessionID]) {
++anonCount;
}
}
@@ -163,17 +154,17 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
rooms[data.enter] = rooms[data.enter] || {};
if(data.leave) {
if (uid) {
rooms[data.enter][hs.sessionID] = uid;
if (uid) {
rooms[data.enter][socket.id] = uid;
if (data.leave && rooms[data.leave] && rooms[data.leave][hs.sessionID]) {
delete rooms[data.leave][hs.sessionID];
}
if (data.leave && rooms[data.leave] && rooms[data.leave][socket.id]) {
delete rooms[data.leave][socket.id];
}
updateRoomBrowsingText(data.leave);
}
if(data.leave)
updateRoomBrowsingText(data.leave);
updateRoomBrowsingText(data.enter);
if (data.enter != 'admin')
@@ -185,7 +176,6 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
socket.on('api:updateHeader', function(data) {
if(uid) {
user.getUserFields(uid, data.fields, function(fields) {
fields.uid = uid;
socket.emit('api:updateHeader', fields);
@@ -236,23 +226,36 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
user.reset.commit(socket, data.code, data.password);
});
function isUserOnline(uid) {
return !!userSockets[uid] && userSockets[uid].length > 0;
}
socket.on('api:user.get_online_users', function(data) {
user.get_online_users(socket, data);
var returnData = [];
for(var i=0; i<data.length; ++i) {
var uid = data[i];
if(isUserOnline(uid))
returnData.push(uid);
else
returnData.push(0);
}
socket.emit('api:user.get_online_users', returnData);
});
socket.on('api:user.changePassword', function(data) {
user.changePassword(socket, uid, data, function(success) {
if(success) {
socket.emit('api:user.changePassword');
}
});
socket.on('api:user.isOnline', function(uid) {
socket.emit('api:user.isOnline', isUserOnline(uid));
});
socket.on('api:user.changePassword', function(data, callback) {
user.changePassword(uid, data, callback);
});
socket.on('api:user.updateProfile', function(data) {
user.updateProfile(socket, uid, data);
socket.on('api:user.updateProfile', function(data, callback) {
user.updateProfile(uid, data, callback);
});
socket.on('api:user.changePicture', function(data) {
socket.on('api:user.changePicture', function(data, callback) {
var type = data.type;
@@ -260,63 +263,136 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
user.getUserFields(uid, ['picture'], function(fields) {
fields.uid = uid;
socket.emit('api:updateHeader', fields);
callback(true);
});
}
if(type === 'gravatar') {
if(type === 'gravatar') {
user.getUserField(uid, 'gravatarpicture', function(gravatar) {
user.setUserField(uid, 'picture', gravatar);
updateHeader();
});
}
else if(type === 'uploaded') {
} else if(type === 'uploaded') {
user.getUserField(uid, 'uploadedpicture', function(uploadedpicture) {
user.setUserField(uid, 'picture', uploadedpicture);
updateHeader();
});
} else {
callback(false);
}
});
socket.on('api:user.follow', function(data) {
user.follow(uid, data.uid, function(success) {
if(success) {
user.getUserField(data.uid, 'username', function(username) {
socket.emit('event:alert', {
title: 'Following',
message: 'You are now following ' + username + '!',
type: 'success',
timeout: 2000
});
});
}
});
socket.on('api:user.follow', function(data, callback) {
if(uid) {
user.follow(uid, data.uid, callback);
}
});
socket.on('api:user.unfollow', function(data) {
user.unfollow(uid, data.uid, function(success) {
if(success) {
user.getUserField(data.uid, 'username', function(username) {
socket.emit('event:alert', {
title: 'Unfollowed',
message: 'You are no longer following ' + username + '!',
type: 'success',
timeout: 2000
});
});
}
});
socket.on('api:user.unfollow', function(data, callback) {
if(uid) {
user.unfollow(uid, data.uid, callback);
}
});
socket.on('api:user.saveSettings', function(data, callback) {
if(uid) {
user.setUserFields(uid, {
showemail:data.showemail
});
callback(true);
}
});
socket.on('api:topics.post', function(data) {
topics.post(socket, uid, data.title, data.content, data.category_id);
topics.post(uid, data.title, data.content, data.category_id, data.images, function(err, result) {
if(err) {
if(err.message === 'not-logged-in') {
socket.emit('event:alert', {
title: 'Thank you for posting',
message: 'Since you are unregistered, your post is awaiting approval. Click here to register now.',
type: 'warning',
timeout: 7500,
clickfn: function() {
ajaxify.go('register');
}
});
} else if(err.message === 'title-too-short') {
topics.emitTitleTooShortAlert(socket);
} else if(err.message === 'content-too-short') {
posts.emitContentTooShortAlert(socket);
} else if (err.message === 'too-many-posts') {
posts.emitTooManyPostsAlert(socket);
}
return;
}
if(result) {
posts.getTopicPostStats(socket);
socket.emit('event:alert', {
title: 'Thank you for posting',
message: 'You have successfully posted. Click here to view your post.',
type: 'notify',
timeout: 2000
});
}
});
});
socket.on('api:topics.markAllRead', function(data, callback) {
topics.markAllRead(uid, function(err, success) {
if(!err && success) {
callback(true);
} else {
callback(false);
}
});
});
socket.on('api:posts.reply', function(data) {
posts.reply(socket, data.topic_id, uid, data.content);
if(uid < 1) {
socket.emit('event:alert', {
title: 'Reply Unsuccessful',
message: 'You don&apos;t seem to be logged in, so you cannot reply.',
type: 'error',
timeout: 2000
});
return;
}
posts.reply(data.topic_id, uid, data.content, data.images, function(err, result) {
if(err) {
if(err.message === 'content-too-short') {
posts.emitContentTooShortAlert(socket);
} else if(err.message === 'too-many-posts') {
posts.emitTooManyPostsAlert(socket);
} else if(err.message === 'reply-error') {
socket.emit('event:alert', {
title: 'Reply Unsuccessful',
message: 'Your reply could not be posted at this time. Please try again later.',
type: 'notify',
timeout: 2000
});
}
return;
}
if(result) {
posts.getTopicPostStats(socket);
socket.emit('event:alert', {
title: 'Reply Successful',
message: 'You have successfully replied. Click here to view your reply.',
type: 'notify',
timeout: 2000
});
}
});
});
socket.on('api:user.active.get', function() {
@@ -383,6 +459,13 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
});
socket.on('api:posts.edit', function(data) {
if(!data.title || data.title.length < topics.minimumTitleLength) {
topics.emitTitleTooShortAlert(socket);
return;
} else if (!data.content || data.content.length < require('../public/config.json').minimumPostLength) {
posts.emitContentTooShortAlert(socket);
return;
}
postTools.edit(uid, data.pid, data.title, data.content);
});
@@ -400,20 +483,16 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
});
});
socket.on('api:notifications.hasFlag', function(data) {
user.notifications.hasFlag(uid, function(flag) {
socket.emit('api:notifications.hasFlag', flag);
});
});
socket.on('api:notifications.removeFlag', function() {
user.notifications.removeFlag(uid);
});
socket.on('api:notifications.mark_read', function(nid) {
notifications.mark_read(nid, uid);
});
socket.on('api:notifications.mark_all_read', function(data, callback) {
notifications.mark_all_read(uid, function(err) {
if (!err) callback();
});
});
socket.on('api:categories.getRecentReplies', function(tid) {
categories.getRecentReplies(tid, 4, function(replies) {
socket.emit('api:categories.getRecentReplies', replies);
@@ -421,6 +500,7 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
});
socket.on('sendChatMessage', function(data) {
var touid = data.touid;
if(userSockets[touid]) {
@@ -433,7 +513,7 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
for(var x=0;x<numSockets;x++) {
userSockets[touid][x].emit('chatMessage', {fromuid:uid, username:username, message:finalMessage});
}
notifications.create(finalMessage, 5, '#', 'notification_'+uid+'_'+touid, function(nid) {
notifications.push(nid, [touid], function(success) {
@@ -546,20 +626,58 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
}
});
socket.on('api:topic.loadMore', function(data) {
socket.on('api:topic.loadMore', function(data, callback) {
var start = data.after,
end = start + 10;
end = start + 9;
topics.getTopicPosts(data.tid, start, end, uid, function(posts) {
io.sockets.in('topic_' + data.tid).emit('event:new_post', {
posts: posts
});
callback({posts:posts});
});
});
socket.on('api:category.loadMore', function(data, callback) {
var start = data.after,
end = start + 9;
categories.getCategoryTopics(data.cid, start, end, uid, function(topics) {
callback({topics:topics});
});
});
socket.on('api:admin.topics.getMore', function(data) {
socket.on('api:topics.loadMoreRecentTopics', function(data, callback) {
var start = data.after,
end = start + 9;
topics.getLatestTopics(uid, start, end, function(latestTopics) {
callback(latestTopics);
});
});
socket.on('api:topics.loadMoreUnreadTopics', function(data, callback) {
var start = data.after,
end = start + 9;
topics.getUnreadTopics(uid, start, end, function(unreadTopics) {
callback(unreadTopics);
});
});
socket.on('api:users.loadMore', function(data, callback) {
var start = data.after,
end = start + 19;
user.getUsers(data.set, start, end, function(err, data) {
if(err) {
console.log(err);
} else {
callback({users:data});
}
});
});
socket.on('api:admin.topics.getMore', function(data, callback) {
topics.getAllTopics(data.limit, data.after, function(topics) {
socket.emit('api:admin.topics.getMore', JSON.stringify(topics));
callback(JSON.stringify(topics));
});
});
@@ -594,6 +712,24 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
else
socket.emit('api:admin.user.search', null);
});
socket.on('api:admin.themes.getInstalled', function(callback) {
meta.themes.get(function(err, themeArr) {
callback(themeArr);
});
});
socket.on('api:admin.plugins.toggle', function(plugin_id) {
plugins.toggleActive(plugin_id, function(status) {
socket.emit('api:admin.plugins.toggle', status);
});
});
socket.on('api:meta.buildTitle', function(text, callback) {
meta.title.build(text, uid, function(err, title) {
callback(title);
});
});
});
}(SocketIO));