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