backchannel.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. var MAXPOSTS = 10;
  2. var posts = [];
  3. var sortedBy = 'votes';
  4. var socket;
  5. var mobile = (/iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent.toLowerCase()));
  6. // note that this array applies only to this browser session!
  7. // ==> could vote twice on same post during different sessions.
  8. // use local storage??
  9. var postsVoted = [];
  10. var userObj = { userID: null, userName: null, userAffil: null };
  11. var loggedIn = false;
  12. // the Post object is modelled on EtherPad:
  13. // [the schoolid and courseid are implied in the meetingid];
  14. function assembleNewPostObj(msgBody) {
  15. // the postid is assigned at the server;
  16. var postObj = {};
  17. postObj.userid = userObj.userID;
  18. postObj.userName = userObj.userName;
  19. postObj.userAffil = userObj.userAffil;
  20. postObj.body = msgBody;
  21. postObj.reports = 0;
  22. return postObj;
  23. }
  24. function renderPosts(fresh, post) {
  25. if (fresh) $('#posts .postContainer').remove();
  26. //$('#total_posts').text(posts.length);
  27. // truncate long array of Posts;
  28. var sortedPosts = sortedBy == 'created' ? posts.sort(createdDesc) : posts.sort(votesDesc);
  29. var displayPosts = sortedPosts//.slice(0, MAXPOSTS - 1);
  30. if (post) $("#postTemplate").tmpl(post).appendTo("#posts");
  31. if (fresh) $("#postTemplate").tmpl(displayPosts).appendTo("#posts");
  32. else $('#posts').reOrder(displayPosts, 'post-')
  33. posts.forEach(function(post) {
  34. renderComments(post._id)
  35. var $post = $('#post-'+post._id);
  36. if ($post !== []) {
  37. if (post.reports.length >= 2) {
  38. if ($('#reportedContainer').length === 0) {
  39. $('#posts').append('<div id="reportedContainer"><h1>Flagged Posts</h1><div class="reportedPosts hidden"></div></div>')
  40. $('#reportedContainer h1').click(function() {
  41. $('.reportedPosts').toggleClass('hidden')
  42. })
  43. }
  44. $post.addClass('flagged');
  45. $('#reportedContainer .reportedPosts').append($post)
  46. }
  47. if (post.votes.indexOf(userID) == -1) {
  48. if (!public) $post.find('.postVoteContainer').addClass('unvoted')
  49. } else {
  50. $post.find('.vote-tally-rect').die()
  51. $post.find('.vote-tally-rect').css('cursor', 'default')
  52. }
  53. if (post.reports.indexOf(userID) !== -1) {
  54. $post.find('.voteFlag').css('cursor', 'default')
  55. $post.find('.voteFlag').css('background', '#888')
  56. }
  57. if (post.userAffil === 'Instructor') {
  58. $post.addClass('instructor');
  59. }
  60. if (commentToggle === '4f1efbb248dc57ba43000075') {
  61. $('.commenttoggle').empty();
  62. }
  63. if (public) {
  64. $post.find('.voteFlag').css({
  65. 'cursor': 'default',
  66. 'background': '#888'
  67. })
  68. $post.find('.vote-tally-rect').css('cursor', 'default');
  69. if (!post.public) $post.remove();
  70. } else {
  71. if (!post.public) $post.find('.privacy').text('Private')
  72. }
  73. }
  74. })
  75. }
  76. function renderComments(id) {
  77. var comments = [];
  78. $.each(posts, function(i, post) {
  79. if (post._id == id) {
  80. comments = post.comments;
  81. if (comments.length >= 1) {
  82. $('#post-'+id+' .commentContainer').empty();
  83. $('#post-'+id+' .commentAmt').text(comments.length+' ');
  84. $('#commentTemplate').tmpl(comments).appendTo('#post-'+id+' .commentContainer');
  85. }
  86. }
  87. })
  88. if (loggedIn) {
  89. $( '.commentForm :input' ).removeAttr( 'disabled' );
  90. }
  91. }
  92. $.fn.reOrder = function(_array, prefix) {
  93. var array = $.extend([], _array, true);
  94. return this.each(function() {
  95. prefix = prefix || "";
  96. if (array) {
  97. var reported = $('#reportedContainer');
  98. for(var i=0; i < array.length; i++) {
  99. var sel = '#' + prefix + array[i]._id;
  100. if ($(sel).length === 0)
  101. array[i] = $("#postTemplate").tmpl(array[i])
  102. else
  103. array[i] = $(sel);
  104. }
  105. $(this).find('.postContainer').remove();
  106. for(var i=0; i < array.length; i++) {
  107. $(this).append(array[i]);
  108. }
  109. $(this).append(reported)
  110. }
  111. });
  112. }
  113. function assembleVoteObj(postid, upOrDown) {
  114. return { "parentid": postid, "direction": upOrDown };
  115. }
  116. function votesDesc(a, b) {
  117. if (a.reports >= 2) {
  118. return 1;
  119. } else if (b.reports >= 2) {
  120. return -1;
  121. } else {
  122. // for descending, reverse usual order;
  123. return b.votes.length - a.votes.length;
  124. }
  125. }
  126. function createdDesc(a, b) {
  127. if (a.reports >= 2) {
  128. return 1;
  129. } else if (b.reports >= 2) {
  130. return -1;
  131. } else {
  132. // for descending, reverse usual order;
  133. return new Date(b.date).valueOf() - new Date(a.date).valueOf();
  134. }
  135. }
  136. function refreshRO() {
  137. if (roID !== false || public === true) {
  138. $('#editor div').load(rourl, function() {
  139. $('#editor').find('style').remove();
  140. })
  141. }
  142. }
  143. $(document).ready(function(){
  144. userObj.userName = public ? null : userName;
  145. userObj.userAffil = public ? null : userAffil;
  146. userObj.userID = public ? null : userID;
  147. loggedIn = public ? false : true;
  148. if (public) {
  149. $('#editor').css('overflow-y', 'auto')
  150. refreshRO();
  151. setInterval(function() {
  152. refreshRO();
  153. }, 10*1000)
  154. } else if (RO === true) {
  155. $('#editor').empty().append('<div class="readonly"></div>');
  156. $('#editor').css('overflow-y', 'auto')
  157. refreshRO();
  158. setInterval(function() {
  159. refreshRO();
  160. }, 10*1000)
  161. }
  162. $('#userBox').removeClass('hidden');
  163. $( '.commentForm :input' ).removeAttr( 'disabled' );
  164. // add event handlers;
  165. $('#backchatHeader input[type="button"]').click(function() {
  166. $('#backchatHeaderInstructions').toggle();
  167. });
  168. $('#enterPostTextarea').keyup(function() {
  169. var charLimit = 250;
  170. var charsUsed = $(this).val().length;
  171. if (charsUsed > 25) {
  172. $('#charsLeftMsg').text("characters left: " +
  173. (charLimit - charsUsed).toString());
  174. } else {
  175. $('#charsLeftMsg').text(" ");
  176. }
  177. });
  178. $('#submitPost').click(function() {
  179. var form = $( this );
  180. var body = $('#enterPostTextarea').val();
  181. if (body !== '') {
  182. var newPost = assembleNewPostObj(body);
  183. var anonymous = $('#enterPostForm').find( 'input[name=anonymous]' ).is(':checked') ? true : false;
  184. var public = $('#enterPostForm').find( 'input[name=private]' ).is(':checked') ? false : true;
  185. newPost.anonymous = anonymous;
  186. newPost.public = public;
  187. socket.emit('post', {post: newPost, lecture: lectureID});
  188. $('#enterPostTextarea').val('');
  189. }
  190. });
  191. if (!public) {
  192. $('.vote-tally-rect').live("click", function() {
  193. var that = this;
  194. var postid = $(this).parent().attr('data-postid');
  195. posts.forEach(function(post) {
  196. if (post._id === postid) {
  197. if (post.votes.indexOf(userID) == -1) {
  198. var newVoteObj = {parentid: postid, userid: userID};
  199. socket.emit('vote', {vote: newVoteObj, lecture: lectureID});
  200. $(that).die()
  201. $(that).css('cursor', 'default')
  202. $(that).parent().removeClass('unvoted');
  203. }
  204. }
  205. })
  206. });
  207. }
  208. $('#amountPosts').change(function() {
  209. MAXPOSTS = $(this).val();
  210. renderPosts();
  211. });
  212. $('#sortPosts').change(function() {
  213. var sort = $(this).val();
  214. sortedBy = sortedBy !== sort ? sort : sortedBy;
  215. renderPosts();
  216. });
  217. $('.commentForm').live('submit', function(e) {
  218. e.preventDefault();
  219. var body = $(this).find('#commentText').val();
  220. var anonymous = $( this ).find( 'input[name=anonymous]' ).is(':checked') ? true : false;
  221. if (body !== '') {
  222. var comment = {
  223. userName: userObj.userName,
  224. userAffil: userObj.userAffil,
  225. body: body,
  226. anonymous: anonymous,
  227. parentid: $(this).find('[name=postid]').val()
  228. }
  229. socket.emit('comment', {comment: comment, lecture: lectureID});
  230. $(this).find('#commentText').val('');
  231. }
  232. })
  233. $('.comments').live('click', function(e) {
  234. e.preventDefault();
  235. var id = $(this).attr('id').replace('post-', '');
  236. $('#post-'+id+' .commentContainer').toggleClass('hidden');
  237. if (!public) $('#post-'+id+' .commentForm').toggleClass('hidden');
  238. })
  239. if (!public) {
  240. $('.voteFlag').live('click', function() {
  241. var that = this;
  242. var id = $(this).parent().parent().attr('id').replace('post-', '');
  243. $.each(posts, function(i, post){
  244. if (post._id == id) {
  245. if (post.reports.indexOf(userID) == -1) {
  246. if(confirm('By flagging a comment, you are identifying it as a violation of the FinalsClub Code of Conduct: Keep it academic.')) {
  247. socket.emit('report', {report: {parentid: id, userid: userID}, lecture: lectureID});
  248. $(that).die()
  249. $(that).css('cursor', 'default')
  250. $(that).css('background', '#888')
  251. }
  252. }
  253. }
  254. })
  255. })
  256. }
  257. // XXX for demonstration purposes only
  258. $('.readonlylink').click(function(e) {
  259. e.preventDefault()
  260. $.get('/logout', function() {
  261. location.reload(true)
  262. })
  263. })
  264. //=====================================================================
  265. // create socket to server; note that we only permit websocket transport
  266. // for this demo;
  267. var loc = document.location;
  268. var port = loc.port == '' ? (loc.protocol == 'https:' ? 443 : 80) : loc.port;
  269. var url = loc.protocol + '//' + loc.hostname + ':' + port;
  270. socket = io.connect(url + '/backchannel');
  271. // incoming messages are objects with one property whose value is the
  272. // type of message:
  273. // { "posts": [ <array of Post objects> ] }
  274. // { "recentPosts": [ <array of Post objects> ] }
  275. // { "vote": [ "postid": <string>, "direction": <"up"|"down"> ] }
  276. // Unresolved: whether to send vote messages for local change of display
  277. // or new arrays of posts with updated vote counts. Vote message would not
  278. // be adequate if it changed order of posts. For now, send two new
  279. // arrays with updated vote counts and refrain from sending vote message.
  280. var messagesArrived = 0;
  281. socket.on('connect', function(){
  282. socket.emit('subscribe', lectureID, function(_packet) {
  283. posts = _packet.posts;
  284. //posts = _posts;
  285. commentToggle = _packet.toggle;
  286. renderPosts(true);
  287. });
  288. });
  289. socket.on('post', function(post) {
  290. posts.push(post);
  291. renderPosts(false, post);
  292. })
  293. socket.on('vote', function(vote) {
  294. posts = posts.map(function(post) {
  295. if(post._id == vote.parentid) {
  296. if (!public || (public && post.public)) {
  297. post.votes.push(vote.userid);
  298. $('#post-'+vote.parentid).find('.vote-tally-rect').text(post.votes.length);
  299. renderPosts();
  300. }
  301. }
  302. return post;
  303. });
  304. })
  305. socket.on('report', function(report) {
  306. posts = posts.map(function(post) {
  307. if(post._id == report.parentid) {
  308. if (!public || (public && post.public)) {
  309. post.reports.push(report.userid);
  310. if (post.reports.length >= 2) {
  311. $('#post-'+post._id).addClass('flagged');
  312. }
  313. renderPosts();
  314. }
  315. }
  316. return post;
  317. });
  318. })
  319. socket.on('comment', function(comment) {
  320. posts = posts.map(function(post) {
  321. if (post._id == comment.parentid) {
  322. if (!public || (public && post.public)) {
  323. if (!post.comments) {
  324. post.comments = [];
  325. }
  326. post.comments.push(comment);
  327. post.date = new Date();
  328. if (sortedBy == 'created') renderPosts();
  329. renderComments(comment.parentid);
  330. }
  331. }
  332. return post;
  333. });
  334. })
  335. socket.on('disconnect', function(){
  336. // XXX something here
  337. });
  338. $('#enterPostTextarea').val("");
  339. });