backchannel.js 11 KB

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