]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - js/util.js
Kill 'hit return to send'...
[quix0rs-gnu-social.git] / js / util.js
1 /*
2  * StatusNet - a distributed open-source microblogging tool
3  * Copyright (C) 2008, StatusNet, Inc.
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Affero General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Affero General Public License for more details.
14  *
15  * You should have received a copy of the GNU Affero General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * @category  UI interaction
19  * @package   StatusNet
20  * @author    Sarven Capadisli <csarven@status.net>
21  * @author    Evan Prodromou <evan@status.net>
22  * @author    Brion Vibber <brion@status.net>
23  * @copyright 2009,2010 StatusNet, Inc.
24  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
25  * @link      http://status.net/
26  */
27
28 var SN = { // StatusNet
29     C: { // Config
30         I: { // Init
31             CounterBlackout: false,
32             MaxLength: 140,
33             PatternUsername: /^[0-9a-zA-Z\-_.]*$/,
34             HTTP20x30x: [200, 201, 202, 203, 204, 205, 206, 300, 301, 302, 303, 304, 305, 306, 307]
35         },
36
37         /**
38          * @fixme are these worth the trouble? They seem to mostly just duplicate
39          * themselves while slightly obscuring the actual selector, so it's hard
40          * to pop over to the HTML and find something.
41          *
42          * In theory, minification could reduce them to shorter variable names,
43          * but at present that doesn't happen with yui-compressor.
44          */
45         S: { // Selector
46             Disabled: 'disabled',
47             Warning: 'warning',
48             Error: 'error',
49             Success: 'success',
50             Processing: 'processing',
51             CommandResult: 'command_result',
52             FormNotice: 'form_notice',
53             NoticeDataText: 'notice_data-text',
54             NoticeTextCount: 'notice_text-count',
55             NoticeInReplyTo: 'notice_in-reply-to',
56             NoticeDataAttach: 'notice_data-attach',
57             NoticeDataAttachSelected: 'notice_data-attach_selected',
58             NoticeActionSubmit: 'notice_action-submit',
59             NoticeLat: 'notice_data-lat',
60             NoticeLon: 'notice_data-lon',
61             NoticeLocationId: 'notice_data-location_id',
62             NoticeLocationNs: 'notice_data-location_ns',
63             NoticeGeoName: 'notice_data-geo_name',
64             NoticeDataGeo: 'notice_data-geo',
65             NoticeDataGeoCookie: 'NoticeDataGeo',
66             NoticeDataGeoSelected: 'notice_data-geo_selected',
67             StatusNetInstance:'StatusNetInstance'
68         }
69     },
70
71     /**
72      * Map of localized message strings exported to script from the PHP
73      * side via Action::getScriptMessages().
74      *
75      * Retrieve them via SN.msg(); this array is an implementation detail.
76      *
77      * @access private
78      */
79     messages: {},
80
81     /**
82      * Grabs a localized string that's been previously exported to us
83      * from server-side code via Action::getScriptMessages().
84      *
85      * @example alert(SN.msg('coolplugin-failed'));
86      *
87      * @param {String} key: string key name to pull from message index
88      * @return matching localized message string
89      */
90     msg: function(key) {
91         if (typeof SN.messages[key] == "undefined") {
92             return '[' + key + ']';
93         } else {
94             return SN.messages[key];
95         }
96     },
97
98     U: { // Utils
99         /**
100          * Setup function -- DOES NOT trigger actions immediately.
101          *
102          * Sets up event handlers on the new notice form.
103          *
104          * @param {jQuery} form: jQuery object whose first matching element is the form
105          * @access private
106          */
107         FormNoticeEnhancements: function(form) {
108             if (jQuery.data(form[0], 'ElementData') === undefined) {
109                 MaxLength = form.find('#'+SN.C.S.NoticeTextCount).text();
110                 if (typeof(MaxLength) == 'undefined') {
111                      MaxLength = SN.C.I.MaxLength;
112                 }
113                 jQuery.data(form[0], 'ElementData', {MaxLength:MaxLength});
114
115                 SN.U.Counter(form);
116
117                 NDT = form.find('#'+SN.C.S.NoticeDataText);
118
119                 NDT.bind('keyup', function(e) {
120                     SN.U.Counter(form);
121                 });
122
123                 var delayedUpdate= function(e) {
124                     // Cut and paste events fire *before* the operation,
125                     // so we need to trigger an update in a little bit.
126                     // This would be so much easier if the 'change' event
127                     // actually fired every time the value changed. :P
128                     window.setTimeout(function() {
129                         SN.U.Counter(form);
130                     }, 50);
131                 };
132                 // Note there's still no event for mouse-triggered 'delete'.
133                 NDT.bind('cut', delayedUpdate)
134                    .bind('paste', delayedUpdate);
135             }
136             else {
137                 form.find('#'+SN.C.S.NoticeTextCount).text(jQuery.data(form[0], 'ElementData').MaxLength);
138             }
139
140             if ($('body')[0].id != 'conversation' && window.location.hash.length === 0 && $(window).scrollTop() == 0) {
141                 form.find('textarea').focus();
142             }
143         },
144
145         /**
146          * To be called from event handlers on the notice import form.
147          * Triggers an update of the remaining-characters counter.
148          *
149          * Additional counter updates will be suppressed during the
150          * next half-second to avoid flooding the layout engine with
151          * updates, followed by another automatic check.
152          *
153          * The maximum length is pulled from data established by
154          * FormNoticeEnhancements.
155          *
156          * @param {jQuery} form: jQuery object whose first element is the notice posting form
157          * @access private
158          */
159         Counter: function(form) {
160             SN.C.I.FormNoticeCurrent = form;
161
162             var MaxLength = jQuery.data(form[0], 'ElementData').MaxLength;
163
164             if (MaxLength <= 0) {
165                 return;
166             }
167
168             var remaining = MaxLength - SN.U.CharacterCount(form);
169             var counter = form.find('#'+SN.C.S.NoticeTextCount);
170
171             if (remaining.toString() != counter.text()) {
172                 if (!SN.C.I.CounterBlackout || remaining === 0) {
173                     if (counter.text() != String(remaining)) {
174                         counter.text(remaining);
175                     }
176                     if (remaining < 0) {
177                         form.addClass(SN.C.S.Warning);
178                     } else {
179                         form.removeClass(SN.C.S.Warning);
180                     }
181                     // Skip updates for the next 500ms.
182                     // On slower hardware, updating on every keypress is unpleasant.
183                     if (!SN.C.I.CounterBlackout) {
184                         SN.C.I.CounterBlackout = true;
185                         SN.C.I.FormNoticeCurrent = form;
186                         window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);", 500);
187                     }
188                 }
189             }
190         },
191
192         /**
193          * Pull the count of characters in the current edit field.
194          * Plugins replacing the edit control may need to override this.
195          *
196          * @param {jQuery} form: jQuery object whose first element is the notice posting form
197          * @return number of chars
198          */
199         CharacterCount: function(form) {
200             return form.find('#'+SN.C.S.NoticeDataText).val().length;
201         },
202
203         /**
204          * Called internally after the counter update blackout period expires;
205          * runs another update to make sure we didn't miss anything.
206          *
207          * @param {jQuery} form: jQuery object whose first element is the notice posting form
208          * @access private
209          */
210         ClearCounterBlackout: function(form) {
211             // Allow keyup events to poke the counter again
212             SN.C.I.CounterBlackout = false;
213             // Check if the string changed since we last looked
214             SN.U.Counter(form);
215         },
216
217         /**
218          * Helper function to rewrite default HTTP form action URLs to HTTPS
219          * so we can actually fetch them when on an SSL page in ssl=sometimes
220          * mode.
221          *
222          * It would be better to output URLs that didn't hardcode protocol
223          * and hostname in the first place...
224          *
225          * @param {String} url
226          * @return string
227          */
228         RewriteAjaxAction: function(url) {
229             // Quick hack: rewrite AJAX submits to HTTPS if they'd fail otherwise.
230             if (document.location.protocol == 'https:' && url.substr(0, 5) == 'http:') {
231                 return url.replace(/^http:\/\/[^:\/]+/, 'https://' + document.location.host);
232             } else {
233                 return url;
234             }
235         },
236
237         /**
238          * Grabs form data and submits it asynchronously, with 'ajax=1'
239          * parameter added to the rest.
240          *
241          * If a successful response includes another form, that form
242          * will be extracted and copied in, replacing the original form.
243          * If there's no form, the first paragraph will be used.
244          *
245          * @fixme can sometimes explode confusingly if returnd data is bogus
246          * @fixme error handling is pretty vague
247          * @fixme can't submit file uploads
248          *
249          * @param {jQuery} form: jQuery object whose first element is a form
250          *
251          * @access public
252          */
253         FormXHR: function(form) {
254             $.ajax({
255                 type: 'POST',
256                 dataType: 'xml',
257                 url: SN.U.RewriteAjaxAction(form.attr('action')),
258                 data: form.serialize() + '&ajax=1',
259                 beforeSend: function(xhr) {
260                     form
261                         .addClass(SN.C.S.Processing)
262                         .find('.submit')
263                             .addClass(SN.C.S.Disabled)
264                             .attr(SN.C.S.Disabled, SN.C.S.Disabled);
265                 },
266                 error: function (xhr, textStatus, errorThrown) {
267                     alert(errorThrown || textStatus);
268                 },
269                 success: function(data, textStatus) {
270                     if (typeof($('form', data)[0]) != 'undefined') {
271                         form_new = document._importNode($('form', data)[0], true);
272                         form.replaceWith(form_new);
273                     }
274                     else {
275                         form.replaceWith(document._importNode($('p', data)[0], true));
276                     }
277                 }
278             });
279         },
280
281         /**
282          * Setup function -- DOES NOT trigger actions immediately.
283          *
284          * Sets up event handlers for special-cased async submission of the
285          * notice-posting form, including some pre-post validation.
286          *
287          * Unlike FormXHR() this does NOT submit the form immediately!
288          * It sets up event handlers so that any method of submitting the
289          * form (click on submit button, enter, submit() etc) will trigger
290          * it properly.
291          *
292          * Also unlike FormXHR(), this system will use a hidden iframe
293          * automatically to handle file uploads via <input type="file">
294          * controls.
295          *
296          * @fixme tl;dr
297          * @fixme vast swaths of duplicate code and really long variable names clutter this function up real bad
298          * @fixme error handling is unreliable
299          * @fixme cookieValue is a global variable, but probably shouldn't be
300          * @fixme saving the location cache cookies should be split out
301          * @fixme some error messages are hardcoded english: needs i18n
302          * @fixme special-case for bookmarklet is confusing and uses a global var "self". Is this ok?
303          *
304          * @param {jQuery} form: jQuery object whose first element is a form
305          *
306          * @access public
307          */
308         FormNoticeXHR: function(form) {
309             SN.C.I.NoticeDataGeo = {};
310             form.append('<input type="hidden" name="ajax" value="1"/>');
311
312             // Make sure we don't have a mixed HTTP/HTTPS submission...
313             form.attr('action', SN.U.RewriteAjaxAction(form.attr('action')));
314
315             /**
316              * Show a response feedback bit under the new-notice dialog.
317              *
318              * @param {String} cls: CSS class name to use ('error' or 'success')
319              * @param {String} text
320              * @access private
321              */
322             var showFeedback = function(cls, text) {
323                 form.append(
324                     $('<p class="form_response"></p>')
325                         .addClass(cls)
326                         .text(text)
327                 );
328             };
329
330             /**
331              * Hide the previous response feedback, if any.
332              */
333             var removeFeedback = function() {
334                 form.find('.form_response').remove();
335             };
336
337             form.ajaxForm({
338                 dataType: 'xml',
339                 timeout: '60000',
340                 beforeSend: function(formData) {
341                     if (form.find('#'+SN.C.S.NoticeDataText)[0].value.length === 0) {
342                         form.addClass(SN.C.S.Warning);
343                         return false;
344                     }
345                     form
346                         .addClass(SN.C.S.Processing)
347                         .find('#'+SN.C.S.NoticeActionSubmit)
348                             .addClass(SN.C.S.Disabled)
349                             .attr(SN.C.S.Disabled, SN.C.S.Disabled);
350
351                     SN.U.normalizeGeoData(form);
352
353                     return true;
354                 },
355                 error: function (xhr, textStatus, errorThrown) {
356                     form
357                         .removeClass(SN.C.S.Processing)
358                         .find('#'+SN.C.S.NoticeActionSubmit)
359                             .removeClass(SN.C.S.Disabled)
360                             .removeAttr(SN.C.S.Disabled, SN.C.S.Disabled);
361                     removeFeedback();
362                     if (textStatus == 'timeout') {
363                         // @fixme i18n
364                         showFeedback('error', 'Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.');
365                     }
366                     else {
367                         var response = SN.U.GetResponseXML(xhr);
368                         if ($('.'+SN.C.S.Error, response).length > 0) {
369                             form.append(document._importNode($('.'+SN.C.S.Error, response)[0], true));
370                         }
371                         else {
372                             if (parseInt(xhr.status) === 0 || jQuery.inArray(parseInt(xhr.status), SN.C.I.HTTP20x30x) >= 0) {
373                                 form
374                                     .resetForm()
375                                     .find('#'+SN.C.S.NoticeDataAttachSelected).remove();
376                                 SN.U.FormNoticeEnhancements(form);
377                             }
378                             else {
379                                 // @fixme i18n
380                                 showFeedback('error', '(Sorry! We had trouble sending your notice ('+xhr.status+' '+xhr.statusText+'). Please report the problem to the site administrator if this happens again.');
381                             }
382                         }
383                     }
384                 },
385                 success: function(data, textStatus) {
386                     removeFeedback();
387                     var errorResult = $('#'+SN.C.S.Error, data);
388                     if (errorResult.length > 0) {
389                         showFeedback('error', errorResult.text());
390                     }
391                     else {
392                         if($('body')[0].id == 'bookmarklet') {
393                             // @fixme self is not referenced anywhere?
394                             self.close();
395                         }
396
397                         var commandResult = $('#'+SN.C.S.CommandResult, data);
398                         if (commandResult.length > 0) {
399                             showFeedback('success', commandResult.text());
400                         }
401                         else {
402                             // New notice post was successful. If on our timeline, show it!
403                             var notice = document._importNode($('li', data)[0], true);
404                             var notices = $('#notices_primary .notices:first');
405                             if (notices.length > 0 && SN.U.belongsOnTimeline(notice)) {
406                                 if ($('#'+notice.id).length === 0) {
407                                     var notice_irt_value = $('#'+SN.C.S.NoticeInReplyTo).val();
408                                     var notice_irt = '#notices_primary #notice-'+notice_irt_value;
409                                     if($('body')[0].id == 'conversation') {
410                                         if(notice_irt_value.length > 0 && $(notice_irt+' .notices').length < 1) {
411                                             $(notice_irt).append('<ul class="notices"></ul>');
412                                         }
413                                         $($(notice_irt+' .notices')[0]).append(notice);
414                                     }
415                                     else {
416                                         notices.prepend(notice);
417                                     }
418                                     $('#'+notice.id)
419                                         .css({display:'none'})
420                                         .fadeIn(2500);
421                                     SN.U.NoticeWithAttachment($('#'+notice.id));
422                                     SN.U.NoticeReplyTo($('#'+notice.id));
423                                 }
424                             }
425                             else {
426                                 // Not on a timeline that this belongs on?
427                                 // Just show a success message.
428                                 showFeedback('success', $('title', data).text());
429                             }
430                         }
431                         form.resetForm();
432                         form.find('#'+SN.C.S.NoticeInReplyTo).val('');
433                         form.find('#'+SN.C.S.NoticeDataAttachSelected).remove();
434                         SN.U.FormNoticeEnhancements(form);
435                     }
436                 },
437                 complete: function(xhr, textStatus) {
438                     form
439                         .removeClass(SN.C.S.Processing)
440                         .find('#'+SN.C.S.NoticeActionSubmit)
441                             .removeAttr(SN.C.S.Disabled)
442                             .removeClass(SN.C.S.Disabled);
443
444                     $('#'+SN.C.S.NoticeLat).val(SN.C.I.NoticeDataGeo.NLat);
445                     $('#'+SN.C.S.NoticeLon).val(SN.C.I.NoticeDataGeo.NLon);
446                     if ($('#'+SN.C.S.NoticeLocationNs)) {
447                         $('#'+SN.C.S.NoticeLocationNs).val(SN.C.I.NoticeDataGeo.NLNS);
448                         $('#'+SN.C.S.NoticeLocationId).val(SN.C.I.NoticeDataGeo.NLID);
449                     }
450                     $('#'+SN.C.S.NoticeDataGeo).attr('checked', SN.C.I.NoticeDataGeo.NDG);
451                 }
452             });
453         },
454
455         normalizeGeoData: function(form) {
456             SN.C.I.NoticeDataGeo.NLat = form.find('[name=lat]').val();
457             SN.C.I.NoticeDataGeo.NLon = form.find('[name=lon]').val();
458             SN.C.I.NoticeDataGeo.NLNS = form.find('[name=location_ns]').val();
459             SN.C.I.NoticeDataGeo.NLID = form.find('[name=location_id]').val();
460             SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked'); // @fixme
461
462             var cookieValue = $.cookie(SN.C.S.NoticeDataGeoCookie);
463
464             if (cookieValue !== null && cookieValue != 'disabled') {
465                 cookieValue = JSON.parse(cookieValue);
466                 SN.C.I.NoticeDataGeo.NLat = form.find('[name=lat]').val(cookieValue.NLat).val();
467                 SN.C.I.NoticeDataGeo.NLon = form.find('[name=lon]').val(cookieValue.NLon).val();
468                 if (cookieValue.NLNS) {
469                     SN.C.I.NoticeDataGeo.NLNS = form.find('[name=location_ns]').val(cookieValue.NLNS).val();
470                     SN.C.I.NoticeDataGeo.NLID = form.find('[name=location_id]').val(cookieValue.NLID).val();
471                 } else {
472                     form.find('[name=location_ns]').val('');
473                     form.find('[name=location_id]').val('');
474                 }
475             }
476             if (cookieValue == 'disabled') {
477                 SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
478             }
479             else {
480                 SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
481             }
482
483         },
484         /**
485          * Fetch an XML DOM from an XHR's response data.
486          *
487          * Works around unavailable responseXML when document.domain
488          * has been modified by Meteor or other tools, in some but not
489          * all browsers.
490          *
491          * @param {XMLHTTPRequest} xhr
492          * @return DOMDocument
493          */
494         GetResponseXML: function(xhr) {
495             try {
496                 return xhr.responseXML;
497             } catch (e) {
498                 return (new DOMParser()).parseFromString(xhr.responseText, "text/xml");
499             }
500         },
501
502         /**
503          * Setup function -- DOES NOT trigger actions immediately.
504          *
505          * Sets up event handlers on all visible notice's reply buttons to
506          * tweak the new-notice form with needed variables and focus it
507          * when pushed.
508          *
509          * (This replaces the default reply button behavior to submit
510          * directly to a form which comes back with a specialized page
511          * with the form data prefilled.)
512          *
513          * @access private
514          */
515         NoticeReply: function() {
516             if ($('#'+SN.C.S.NoticeDataText).length > 0 && $('#content .notice_reply').length > 0) {
517                 $('#content .notice').each(function() { SN.U.NoticeReplyTo($(this)); });
518             }
519         },
520
521         /**
522          * Setup function -- DOES NOT trigger actions immediately.
523          *
524          * Sets up event handlers on the given notice's reply button to
525          * tweak the new-notice form with needed variables and focus it
526          * when pushed.
527          *
528          * (This replaces the default reply button behavior to submit
529          * directly to a form which comes back with a specialized page
530          * with the form data prefilled.)
531          *
532          * @param {jQuery} notice: jQuery object containing one or more notices
533          * @access private
534          */
535         NoticeReplyTo: function(notice) {
536             notice.find('.notice_reply').live('click', function(e) {
537                 e.preventDefault();
538                 var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname.uid');
539                 /* SN.U.NoticeReplySet(nickname.text(), $($('.notice_id', notice)[0]).text()); */
540                 SN.U.NoticeInlineReplyTrigger(notice, '@' + nickname.text());
541                 return false;
542             });
543         },
544
545         /**
546          * Open up a notice's inline reply box.
547          *
548          * @param {jQuery} notice: jQuery object containing one notice
549          * @param {String} initialText
550          */
551         NoticeInlineReplyTrigger: function(notice, initialText) {
552             // Find the notice we're replying to...
553             var id = $($('.notice_id', notice)[0]).text();
554             var parentNotice = notice;
555
556             // Find the threaded replies view we'll be adding to...
557             var list = notice.closest('.notices');
558             if (list.hasClass('threaded-replies')) {
559                 // We're replying to a reply; use reply form on the end of this list.
560                 // We'll add our form at the end of this; grab the root notice.
561                 parentNotice = list.closest('.notice');
562             } else {
563                 // We're replying to a parent notice; pull its threaded list
564                 // and we'll add on the end of it. Will add if needed.
565                 list = $('ul.threaded-replies', notice);
566                 if (list.length == 0) {
567                     list = $('<ul class="notices threaded-replies xoxo"></ul>');
568                     notice.append(list);
569                 }
570             }
571
572             // See if the form's already open...
573             var replyForm = $('.notice-reply-form', list);
574             if (replyForm.length == 0) {
575                 // Remove placeholder if any
576                 $('li.notice-reply-placeholder').remove();
577
578                 // Create the reply form entry at the end
579                 var replyItem = $('li.notice-reply', list);
580                 if (replyItem.length == 0) {
581                     replyItem = $('<li class="notice-reply">' +
582                                       '<form class="notice-reply-form" method="post">' +
583                                           '<textarea name="status_textarea"></textarea>' +
584                                           '<div class="controls">' +
585                                           '<input type="hidden" name="token">' +
586                                           '<input type="hidden" name="inreplyto">' +
587                                           '<input type="submit" class="submit">' +
588                                       '</div>' +
589                                       '</form>' +
590                                   '</li>');
591
592                     var baseForm = $('#form_notice');
593                     replyForm = replyItem.find('form');
594                     replyForm.attr('action', baseForm.attr('action'));
595                     replyForm.find('input[name="token"]').val(baseForm.find('input[name=token]').val());
596                     replyForm.find('input[type="submit"]').val(SN.msg('reply_submit'));
597                     list.append(replyItem);
598
599                     replyForm.find('textarea').blur(function() {
600                         var textarea = $(this);
601                         var txt = $.trim(textarea.val());
602                         if (txt == '' || txt == textarea.data('initialText')) {
603                             // Nothing to say? Begone!
604                             replyItem.remove();
605                             if (list.find('li').length > 0) {
606                                 SN.U.NoticeInlineReplyPlaceholder(parentNotice);
607                             } else {
608                                 list.remove();
609                             }
610                         }
611                     });
612                     replyForm.submit(function(event) {
613                         var form = replyForm;
614                         $.ajax({
615                             type: 'POST',
616                             dataType: 'xml',
617                             url: SN.U.RewriteAjaxAction(form.attr('action')),
618                             data: form.serialize() + '&ajax=1',
619                             beforeSend: function(xhr) {
620                                 form
621                                     .addClass(SN.C.S.Processing)
622                                     .find('.submit')
623                                         .addClass(SN.C.S.Disabled)
624                                         .attr(SN.C.S.Disabled, SN.C.S.Disabled)
625                                         .end()
626                                     .find('textarea')
627                                         .addClass(SN.C.S.Disabled)
628                                         .attr(SN.C.S.Disabled, SN.C.S.Disabled);
629                             },
630                             error: function (xhr, textStatus, errorThrown) {
631                                 alert(errorThrown || textStatus);
632                             },
633                             success: function(data, textStatus) {
634                                 var orig_li = $('li', data)[0];
635                                 if (orig_li) {
636                                     var li = document._importNode(orig_li, true);
637                                     var id = $(li).attr('id');
638                                     if ($("#"+id).length == 0) {
639                                         replyItem.replaceWith(li);
640                                         SN.U.NoticeInlineReplyPlaceholder(parentNotice);
641                                     } else {
642                                         // Realtime came through before us...
643                                         replyItem.remove();
644                                     }
645                                 }
646                             }
647                         });
648                         event.preventDefault();
649                         return false;
650                     });
651                 }
652             }
653
654             // Override...?
655             replyForm.find('input[name=inreplyto]').val(id);
656
657             // Set focus...
658             var text = replyForm.find('textarea');
659             if (text.length == 0) {
660                 throw "No textarea";
661             }
662             var replyto = '';
663             if (initialText) {
664                 replyto = initialText + ' ';
665             }
666             text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
667             text.data('initialText', $.trim(initialText + ''));
668             text.focus();
669             if (text[0].setSelectionRange) {
670                 var len = text.val().length;
671                 text[0].setSelectionRange(len,len);
672             }
673         },
674
675         /**
676          * FIXME OBSOLETE?
677          * Updates the new notice posting form with bits for replying to the
678          * given user. Adds replyto parameter to the form, and a "@foo" to the
679          * text area.
680          *
681          * @fixme replyto is a global variable, but probably shouldn't be
682          *
683          * @param {String} nick
684          * @param {String} id
685          */
686         NoticeReplySet: function(nick,id) {
687             if (nick.match(SN.C.I.PatternUsername)) {
688                 var text = $('#'+SN.C.S.NoticeDataText);
689                 if (text.length > 0) {
690                     replyto = '@' + nick + ' ';
691                     text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
692                     $('#'+SN.C.S.FormNotice+' #'+SN.C.S.NoticeInReplyTo).val(id);
693
694                     text[0].focus();
695                     if (text[0].setSelectionRange) {
696                         var len = text.val().length;
697                         text[0].setSelectionRange(len,len);
698                     }
699                 }
700             }
701         },
702
703         /**
704          * Setup function -- DOES NOT apply immediately.
705          *
706          * Sets up event handlers for favor/disfavor forms to submit via XHR.
707          * Uses 'live' rather than 'bind', so applies to future as well as present items.
708          */
709         NoticeFavor: function() {
710             $('.form_favor').live('click', function() { SN.U.FormXHR($(this)); return false; });
711             $('.form_disfavor').live('click', function() { SN.U.FormXHR($(this)); return false; });
712         },
713
714         NoticeInlineReplyPlaceholder: function(notice) {
715             var list = notice.find('ul.threaded-replies');
716             var placeholder = $('<li class="notice-reply-placeholder">' +
717                                     '<input class="placeholder">' +
718                                 '</li>');
719             placeholder.click(function() {
720                 SN.U.NoticeInlineReplyTrigger(notice);
721             });
722             placeholder.find('input').val(SN.msg('reply_placeholder'));
723             list.append(placeholder);
724         },
725
726         /**
727          * Setup function -- DOES NOT apply immediately.
728          *
729          * Sets up event handlers for favor/disfavor forms to submit via XHR.
730          * Uses 'live' rather than 'bind', so applies to future as well as present items.
731          */
732         NoticeInlineReplySetup: function() {
733             $('.threaded-replies').each(function() {
734                 var list = $(this);
735                 var notice = list.closest('.notice');
736                 SN.U.NoticeInlineReplyPlaceholder(notice);
737             });
738         },
739
740         /**
741          * Setup function -- DOES NOT trigger actions immediately.
742          *
743          * Sets up event handlers for repeat forms to toss up a confirmation
744          * popout before submitting.
745          *
746          * Uses 'live' rather than 'bind', so applies to future as well as present items.
747          */
748         NoticeRepeat: function() {
749             $('.form_repeat').live('click', function(e) {
750                 e.preventDefault();
751
752                 SN.U.NoticeRepeatConfirmation($(this));
753                 return false;
754             });
755         },
756
757         /**
758          * Shows a confirmation dialog box variant of the repeat button form.
759          * This seems to use a technique where the repeat form contains
760          * _both_ a standalone button _and_ text and buttons for a dialog.
761          * The dialog will close after its copy of the form is submitted,
762          * or if you click its 'close' button.
763          *
764          * The dialog is created by duplicating the original form and changing
765          * its style; while clever, this is hard to generalize and probably
766          * duplicates a lot of unnecessary HTML output.
767          *
768          * @fixme create confirmation dialogs through a generalized interface
769          * that can be reused instead of hardcoded text and styles.
770          *
771          * @param {jQuery} form
772          */
773         NoticeRepeatConfirmation: function(form) {
774             var submit_i = form.find('.submit');
775
776             var submit = submit_i.clone();
777             submit
778                 .addClass('submit_dialogbox')
779                 .removeClass('submit');
780             form.append(submit);
781             submit.bind('click', function() { SN.U.FormXHR(form); return false; });
782
783             submit_i.hide();
784
785             form
786                 .addClass('dialogbox')
787                 .append('<button class="close">&#215;</button>')
788                 .closest('.notice-options')
789                     .addClass('opaque');
790
791             form.find('button.close').click(function(){
792                 $(this).remove();
793
794                 form
795                     .removeClass('dialogbox')
796                     .closest('.notice-options')
797                         .removeClass('opaque');
798
799                 form.find('.submit_dialogbox').remove();
800                 form.find('.submit').show();
801
802                 return false;
803             });
804         },
805
806         /**
807          * Setup function -- DOES NOT trigger actions immediately.
808          *
809          * Goes through all notices currently displayed and sets up attachment
810          * handling if needed.
811          */
812         NoticeAttachments: function() {
813             $('.notice a.attachment').each(function() {
814                 SN.U.NoticeWithAttachment($(this).closest('.notice'));
815             });
816         },
817
818         /**
819          * Setup function -- DOES NOT trigger actions immediately.
820          *
821          * Sets up special attachment link handling if needed. Currently this
822          * consists only of making the "more" button used for OStatus message
823          * cropping turn into an auto-expansion button that loads the full
824          * text from an attachment file.
825          *
826          * @param {jQuery} notice
827          */
828         NoticeWithAttachment: function(notice) {
829             if (notice.find('.attachment').length === 0) {
830                 return;
831             }
832
833             var attachment_more = notice.find('.attachment.more');
834             if (attachment_more.length > 0) {
835                 $(attachment_more[0]).click(function() {
836                     var m = $(this);
837                     m.addClass(SN.C.S.Processing);
838                     $.get(m.attr('href')+'/ajax', null, function(data) {
839                         m.parent('.entry-content').html($(data).find('#attachment_view .entry-content').html());
840                     });
841
842                     return false;
843                 }).attr('title', SN.msg('showmore_tooltip'));
844             }
845         },
846
847         /**
848          * Setup function -- DOES NOT trigger actions immediately.
849          *
850          * Sets up event handlers for the file-attachment widget in the
851          * new notice form. When a file is selected, a box will be added
852          * below the text input showing the filename and, if supported
853          * by the browser, a thumbnail preview.
854          *
855          * This preview box will also allow removing the attachment
856          * prior to posting.
857          */
858         NoticeDataAttach: function() {
859             NDA = $('#'+SN.C.S.NoticeDataAttach);
860             NDA.change(function(event) {
861                 var filename = $(this).val();
862                 if (!filename) {
863                     // No file -- we've been tricked!
864                     $('#'+SN.C.S.NoticeDataAttachSelected).remove();
865                     return false;
866                 }
867
868                 // @fixme appending filename straight in is potentially unsafe
869                 S = '<div id="'+SN.C.S.NoticeDataAttachSelected+'" class="'+SN.C.S.Success+'"><code>'+filename+'</code> <button class="close">&#215;</button></div>';
870                 NDAS = $('#'+SN.C.S.NoticeDataAttachSelected);
871                 if (NDAS.length > 0) {
872                     NDAS.replaceWith(S);
873                 }
874                 else {
875                     $('#'+SN.C.S.FormNotice).append(S);
876                 }
877                 $('#'+SN.C.S.NoticeDataAttachSelected+' button').click(function(){
878                     $('#'+SN.C.S.NoticeDataAttachSelected).remove();
879                     NDA.val('');
880
881                     return false;
882                 });
883                 if (typeof this.files == "object") {
884                     // Some newer browsers will let us fetch the files for preview.
885                     for (var i = 0; i < this.files.length; i++) {
886                         SN.U.PreviewAttach(this.files[i]);
887                     }
888                 }
889             });
890         },
891
892         /**
893          * Get PHP's MAX_FILE_SIZE setting for this form;
894          * used to apply client-side file size limit checks.
895          *
896          * @param {jQuery} form
897          * @return int max size in bytes; 0 or negative means no limit
898          */
899         maxFileSize: function(form) {
900             var max = $(form).find('input[name=MAX_FILE_SIZE]').attr('value');
901             if (max) {
902                 return parseInt(max);
903             } else {
904                 return 0;
905             }
906         },
907
908         /**
909          * For browsers with FileAPI support: make a thumbnail if possible,
910          * and append it into the attachment display widget.
911          *
912          * Known good:
913          * - Firefox 3.6.6, 4.0b7
914          * - Chrome 8.0.552.210
915          *
916          * Known ok metadata, can't get contents:
917          * - Safari 5.0.2
918          *
919          * Known fail:
920          * - Opera 10.63, 11 beta (no input.files interface)
921          *
922          * @param {File} file
923          *
924          * @todo use configured thumbnail size
925          * @todo detect pixel size?
926          * @todo should we render a thumbnail to a canvas and then use the smaller image?
927          */
928         PreviewAttach: function(file) {
929             var tooltip = file.type + ' ' + Math.round(file.size / 1024) + 'KB';
930             var preview = true;
931
932             var blobAsDataURL;
933             if (typeof window.createObjectURL != "undefined") {
934                 /**
935                  * createObjectURL lets us reference the file directly from an <img>
936                  * This produces a compact URL with an opaque reference to the file,
937                  * which we can reference immediately.
938                  *
939                  * - Firefox 3.6.6: no
940                  * - Firefox 4.0b7: no
941                  * - Safari 5.0.2: no
942                  * - Chrome 8.0.552.210: works!
943                  */
944                 blobAsDataURL = function(blob, callback) {
945                     callback(window.createObjectURL(blob));
946                 }
947             } else if (typeof window.FileReader != "undefined") {
948                 /**
949                  * FileAPI's FileReader can build a data URL from a blob's contents,
950                  * but it must read the file and build it asynchronously. This means
951                  * we'll be passing a giant data URL around, which may be inefficient.
952                  *
953                  * - Firefox 3.6.6: works!
954                  * - Firefox 4.0b7: works!
955                  * - Safari 5.0.2: no
956                  * - Chrome 8.0.552.210: works!
957                  */
958                 blobAsDataURL = function(blob, callback) {
959                     var reader = new FileReader();
960                     reader.onload = function(event) {
961                         callback(reader.result);
962                     }
963                     reader.readAsDataURL(blob);
964                 }
965             } else {
966                 preview = false;
967             }
968
969             var imageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml'];
970             if ($.inArray(file.type, imageTypes) == -1) {
971                 // We probably don't know how to show the file.
972                 preview = false;
973             }
974
975             var maxSize = 8 * 1024 * 1024;
976             if (file.size > maxSize) {
977                 // Don't kill the browser trying to load some giant image.
978                 preview = false;
979             }
980
981             if (preview) {
982                 blobAsDataURL(file, function(url) {
983                     var img = $('<img>')
984                         .attr('title', tooltip)
985                         .attr('alt', tooltip)
986                         .attr('src', url)
987                         .attr('style', 'height: 120px');
988                     $('#'+SN.C.S.NoticeDataAttachSelected).append(img);
989                 });
990             } else {
991                 var img = $('<div></div>').text(tooltip);
992                 $('#'+SN.C.S.NoticeDataAttachSelected).append(img);
993             }
994         },
995
996         /**
997          * Setup function -- DOES NOT trigger actions immediately.
998          *
999          * Initializes state for the location-lookup features in the
1000          * new-notice form. Seems to set up some event handlers for
1001          * triggering lookups and using the new values.
1002          *
1003          * @fixme tl;dr
1004          * @fixme there's not good visual state update here, so users have a
1005          *        hard time figuring out if it's working or fixing if it's wrong.
1006          *
1007          */
1008         NoticeLocationAttach: function() {
1009             // @fixme this should not be tied to the main notice form, as there may be multiple notice forms...
1010             var NLat = $('#'+SN.C.S.NoticeLat).val();
1011             var NLon = $('#'+SN.C.S.NoticeLon).val();
1012             var NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
1013             var NLID = $('#'+SN.C.S.NoticeLocationId).val();
1014             var NLN = $('#'+SN.C.S.NoticeGeoName).text();
1015             var NDGe = $('#'+SN.C.S.NoticeDataGeo);
1016
1017             function removeNoticeDataGeo(error) {
1018                 $('label[for='+SN.C.S.NoticeDataGeo+']')
1019                     .attr('title', jQuery.trim($('label[for='+SN.C.S.NoticeDataGeo+']').text()))
1020                     .removeClass('checked');
1021
1022                 $('#'+SN.C.S.NoticeLat).val('');
1023                 $('#'+SN.C.S.NoticeLon).val('');
1024                 $('#'+SN.C.S.NoticeLocationNs).val('');
1025                 $('#'+SN.C.S.NoticeLocationId).val('');
1026                 $('#'+SN.C.S.NoticeDataGeo).attr('checked', false);
1027
1028                 $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/' });
1029
1030                 if (error) {
1031                     $('.geo_status_wrapper').removeClass('success').addClass('error');
1032                     $('.geo_status_wrapper .geo_status').text(error);
1033                 } else {
1034                     $('.geo_status_wrapper').remove();
1035                 }
1036             }
1037
1038             function getJSONgeocodeURL(geocodeURL, data) {
1039                 SN.U.NoticeGeoStatus('Looking up place name...');
1040                 $.getJSON(geocodeURL, data, function(location) {
1041                     var lns, lid;
1042
1043                     if (typeof(location.location_ns) != 'undefined') {
1044                         $('#'+SN.C.S.NoticeLocationNs).val(location.location_ns);
1045                         lns = location.location_ns;
1046                     }
1047
1048                     if (typeof(location.location_id) != 'undefined') {
1049                         $('#'+SN.C.S.NoticeLocationId).val(location.location_id);
1050                         lid = location.location_id;
1051                     }
1052
1053                     if (typeof(location.name) == 'undefined') {
1054                         NLN_text = data.lat + ';' + data.lon;
1055                     }
1056                     else {
1057                         NLN_text = location.name;
1058                     }
1059
1060                     SN.U.NoticeGeoStatus(NLN_text, data.lat, data.lon, location.url);
1061                     $('label[for='+SN.C.S.NoticeDataGeo+']')
1062                         .attr('title', NoticeDataGeo_text.ShareDisable + ' (' + NLN_text + ')');
1063
1064                     $('#'+SN.C.S.NoticeLat).val(data.lat);
1065                     $('#'+SN.C.S.NoticeLon).val(data.lon);
1066                     $('#'+SN.C.S.NoticeLocationNs).val(lns);
1067                     $('#'+SN.C.S.NoticeLocationId).val(lid);
1068                     $('#'+SN.C.S.NoticeDataGeo).attr('checked', true);
1069
1070                     var cookieValue = {
1071                         NLat: data.lat,
1072                         NLon: data.lon,
1073                         NLNS: lns,
1074                         NLID: lid,
1075                         NLN: NLN_text,
1076                         NLNU: location.url,
1077                         NDG: true
1078                     };
1079
1080                     $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue), { path: '/' });
1081                 });
1082             }
1083
1084             if (NDGe.length > 0) {
1085                 if ($.cookie(SN.C.S.NoticeDataGeoCookie) == 'disabled') {
1086                     NDGe.attr('checked', false);
1087                 }
1088                 else {
1089                     NDGe.attr('checked', true);
1090                 }
1091
1092                 var NGW = $('#notice_data-geo_wrap');
1093                 var geocodeURL = NGW.attr('title');
1094                 NGW.removeAttr('title');
1095
1096                 $('label[for='+SN.C.S.NoticeDataGeo+']')
1097                     .attr('title', jQuery.trim($('label[for='+SN.C.S.NoticeDataGeo+']').text()));
1098
1099                 NDGe.change(function() {
1100                     if ($('#'+SN.C.S.NoticeDataGeo).attr('checked') === true || $.cookie(SN.C.S.NoticeDataGeoCookie) === null) {
1101                         $('label[for='+SN.C.S.NoticeDataGeo+']')
1102                             .attr('title', NoticeDataGeo_text.ShareDisable)
1103                             .addClass('checked');
1104
1105                         if ($.cookie(SN.C.S.NoticeDataGeoCookie) === null || $.cookie(SN.C.S.NoticeDataGeoCookie) == 'disabled') {
1106                             if (navigator.geolocation) {
1107                                 SN.U.NoticeGeoStatus('Requesting location from browser...');
1108                                 navigator.geolocation.getCurrentPosition(
1109                                     function(position) {
1110                                         $('#'+SN.C.S.NoticeLat).val(position.coords.latitude);
1111                                         $('#'+SN.C.S.NoticeLon).val(position.coords.longitude);
1112
1113                                         var data = {
1114                                             lat: position.coords.latitude,
1115                                             lon: position.coords.longitude,
1116                                             token: $('#token').val()
1117                                         };
1118
1119                                         getJSONgeocodeURL(geocodeURL, data);
1120                                     },
1121
1122                                     function(error) {
1123                                         switch(error.code) {
1124                                             case error.PERMISSION_DENIED:
1125                                                 removeNoticeDataGeo('Location permission denied.');
1126                                                 break;
1127                                             case error.TIMEOUT:
1128                                                 //$('#'+SN.C.S.NoticeDataGeo).attr('checked', false);
1129                                                 removeNoticeDataGeo('Location lookup timeout.');
1130                                                 break;
1131                                         }
1132                                     },
1133
1134                                     {
1135                                         timeout: 10000
1136                                     }
1137                                 );
1138                             }
1139                             else {
1140                                 if (NLat.length > 0 && NLon.length > 0) {
1141                                     var data = {
1142                                         lat: NLat,
1143                                         lon: NLon,
1144                                         token: $('#token').val()
1145                                     };
1146
1147                                     getJSONgeocodeURL(geocodeURL, data);
1148                                 }
1149                                 else {
1150                                     removeNoticeDataGeo();
1151                                     $('#'+SN.C.S.NoticeDataGeo).remove();
1152                                     $('label[for='+SN.C.S.NoticeDataGeo+']').remove();
1153                                 }
1154                             }
1155                         }
1156                         else {
1157                             var cookieValue = JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));
1158
1159                             $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat);
1160                             $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon);
1161                             $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS);
1162                             $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID);
1163                             $('#'+SN.C.S.NoticeDataGeo).attr('checked', cookieValue.NDG);
1164
1165                             SN.U.NoticeGeoStatus(cookieValue.NLN, cookieValue.NLat, cookieValue.NLon, cookieValue.NLNU);
1166                             $('label[for='+SN.C.S.NoticeDataGeo+']')
1167                                 .attr('title', NoticeDataGeo_text.ShareDisable + ' (' + cookieValue.NLN + ')')
1168                                 .addClass('checked');
1169                         }
1170                     }
1171                     else {
1172                         removeNoticeDataGeo();
1173                     }
1174                 }).change();
1175             }
1176         },
1177
1178         /**
1179          * Create or update a geolocation status widget in this notice posting form.
1180          *
1181          * @param {String} status
1182          * @param {String} lat (optional)
1183          * @param {String} lon (optional)
1184          * @param {String} url (optional)
1185          */
1186         NoticeGeoStatus: function(status, lat, lon, url)
1187         {
1188             var form = $('#form_notice');
1189             var wrapper = form.find('.geo_status_wrapper');
1190             if (wrapper.length == 0) {
1191                 wrapper = $('<div class="'+SN.C.S.Success+' geo_status_wrapper"><button class="close" style="float:right">&#215;</button><div class="geo_status"></div></div>');
1192                 wrapper.find('button.close').click(function() {
1193                     $('#'+SN.C.S.NoticeDataGeo).removeAttr('checked').change();
1194                 });
1195                 form.append(wrapper);
1196             }
1197             var label;
1198             if (url) {
1199                 label = $('<a></a>').attr('href', url);
1200             } else {
1201                 label = $('<span></span>');
1202             }
1203             label.text(status);
1204             if (lat || lon) {
1205                 var latlon = lat + ';' + lon;
1206                 label.attr('title', latlon);
1207                 if (!status) {
1208                     label.text(latlon)
1209                 }
1210             }
1211             wrapper.find('.geo_status').empty().append(label);
1212         },
1213
1214         /**
1215          * Setup function -- DOES NOT trigger actions immediately.
1216          *
1217          * Initializes event handlers for the "Send direct message" link on
1218          * profile pages, setting it up to display a dialog box when clicked.
1219          *
1220          * Unlike the repeat confirmation form, this appears to fetch
1221          * the form _from the original link target_, so the form itself
1222          * doesn't need to be in the current document.
1223          *
1224          * @fixme breaks ability to open link in new window?
1225          */
1226         NewDirectMessage: function() {
1227             NDM = $('.entity_send-a-message a');
1228             NDM.attr({'href':NDM.attr('href')+'&ajax=1'});
1229             NDM.bind('click', function() {
1230                 var NDMF = $('.entity_send-a-message form');
1231                 if (NDMF.length === 0) {
1232                     $(this).addClass(SN.C.S.Processing);
1233                     $.get(NDM.attr('href'), null, function(data) {
1234                         $('.entity_send-a-message').append(document._importNode($('form', data)[0], true));
1235                         NDMF = $('.entity_send-a-message .form_notice');
1236                         SN.U.FormNoticeXHR(NDMF);
1237                         SN.U.FormNoticeEnhancements(NDMF);
1238                         NDMF.append('<button class="close">&#215;</button>');
1239                         $('.entity_send-a-message button').click(function(){
1240                             NDMF.hide();
1241                             return false;
1242                         });
1243                         NDM.removeClass(SN.C.S.Processing);
1244                     });
1245                 }
1246                 else {
1247                     NDMF.show();
1248                     $('.entity_send-a-message textarea').focus();
1249                 }
1250                 return false;
1251             });
1252         },
1253
1254         /**
1255          * Return a date object with the current local time on the
1256          * given year, month, and day.
1257          *
1258          * @param {number} year: 4-digit year
1259          * @param {number} month: 0 == January
1260          * @param {number} day: 1 == 1
1261          * @return {Date}
1262          */
1263         GetFullYear: function(year, month, day) {
1264             var date = new Date();
1265             date.setFullYear(year, month, day);
1266
1267             return date;
1268         },
1269
1270         /**
1271          * Some sort of object interface for storing some structured
1272          * information in a cookie.
1273          *
1274          * Appears to be used to save the last-used login nickname?
1275          * That's something that browsers usually take care of for us
1276          * these days, do we really need to do it? Does anything else
1277          * use this interface?
1278          *
1279          * @fixme what is this?
1280          * @fixme should this use non-cookie local storage when available?
1281          */
1282         StatusNetInstance: {
1283             /**
1284              * @fixme what is this?
1285              */
1286             Set: function(value) {
1287                 var SNI = SN.U.StatusNetInstance.Get();
1288                 if (SNI !== null) {
1289                     value = $.extend(SNI, value);
1290                 }
1291
1292                 $.cookie(
1293                     SN.C.S.StatusNetInstance,
1294                     JSON.stringify(value),
1295                     {
1296                         path: '/',
1297                         expires: SN.U.GetFullYear(2029, 0, 1)
1298                     });
1299             },
1300
1301             /**
1302              * @fixme what is this?
1303              */
1304             Get: function() {
1305                 var cookieValue = $.cookie(SN.C.S.StatusNetInstance);
1306                 if (cookieValue !== null) {
1307                     return JSON.parse(cookieValue);
1308                 }
1309                 return null;
1310             },
1311
1312             /**
1313              * @fixme what is this?
1314              */
1315             Delete: function() {
1316                 $.cookie(SN.C.S.StatusNetInstance, null);
1317             }
1318         },
1319
1320         /**
1321          * Check if the current page is a timeline where the current user's
1322          * posts should be displayed immediately on success.
1323          *
1324          * @fixme this should be done in a saner way, with machine-readable
1325          * info about what page we're looking at.
1326          *
1327          * @param {DOMElement} notice: HTML chunk with formatted notice
1328          * @return boolean
1329          */
1330         belongsOnTimeline: function(notice) {
1331             var action = $("body").attr('id');
1332             if (action == 'public') {
1333                 return true;
1334             }
1335
1336             var profileLink = $('#nav_profile a').attr('href');
1337             if (profileLink) {
1338                 var authorUrl = $(notice).find('.entry-title .author a.url').attr('href');
1339                 if (authorUrl == profileLink) {
1340                     if (action == 'all' || action == 'showstream') {
1341                         // Posts always show on your own friends and profile streams.
1342                         return true;
1343                     }
1344                 }
1345             }
1346
1347             // @fixme tag, group, reply timelines should be feasible as well.
1348             // Mismatch between id-based and name-based user/group links currently complicates
1349             // the lookup, since all our inline mentions contain the absolute links but the
1350             // UI links currently on the page use malleable names.
1351
1352             return false;
1353         }
1354     },
1355
1356     Init: {
1357         /**
1358          * If user is logged in, run setup code for the new notice form:
1359          *
1360          *  - char counter
1361          *  - AJAX submission
1362          *  - location events
1363          *  - file upload events
1364          */
1365         NoticeForm: function() {
1366             if ($('body.user_in').length > 0) {
1367                 SN.U.NoticeLocationAttach();
1368
1369                 $('.'+SN.C.S.FormNotice).each(function() {
1370                     SN.U.FormNoticeXHR($(this));
1371                     SN.U.FormNoticeEnhancements($(this));
1372                 });
1373
1374                 SN.U.NoticeDataAttach();
1375             }
1376         },
1377
1378         /**
1379          * Run setup code for notice timeline views items:
1380          *
1381          * - AJAX submission for fave/repeat/reply (if logged in)
1382          * - Attachment link extras ('more' links)
1383          */
1384         Notices: function() {
1385             if ($('body.user_in').length > 0) {
1386                 SN.U.NoticeFavor();
1387                 SN.U.NoticeRepeat();
1388                 SN.U.NoticeReply();
1389                 SN.U.NoticeInlineReplySetup();
1390             }
1391
1392             SN.U.NoticeAttachments();
1393         },
1394
1395         /**
1396          * Run setup code for user & group profile page header area if logged in:
1397          *
1398          * - AJAX submission for sub/unsub/join/leave/nudge
1399          * - AJAX form popup for direct-message
1400          */
1401         EntityActions: function() {
1402             if ($('body.user_in').length > 0) {
1403                 $('.form_user_subscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
1404                 $('.form_user_unsubscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
1405                 $('.form_group_join').live('click', function() { SN.U.FormXHR($(this)); return false; });
1406                 $('.form_group_leave').live('click', function() { SN.U.FormXHR($(this)); return false; });
1407                 $('.form_user_nudge').live('click', function() { SN.U.FormXHR($(this)); return false; });
1408
1409                 SN.U.NewDirectMessage();
1410             }
1411         },
1412
1413         /**
1414          * Run setup code for login form:
1415          *
1416          * - loads saved last-used-nickname from cookie
1417          * - sets event handler to save nickname to cookie on submit
1418          *
1419          * @fixme is this necessary? Browsers do their own form saving these days.
1420          */
1421         Login: function() {
1422             if (SN.U.StatusNetInstance.Get() !== null) {
1423                 var nickname = SN.U.StatusNetInstance.Get().Nickname;
1424                 if (nickname !== null) {
1425                     $('#form_login #nickname').val(nickname);
1426                 }
1427             }
1428
1429             $('#form_login').bind('submit', function() {
1430                 SN.U.StatusNetInstance.Set({Nickname: $('#form_login #nickname').val()});
1431                 return true;
1432             });
1433         },
1434
1435         /**
1436          * Add logic to any file upload forms to handle file size limits,
1437          * on browsers that support basic FileAPI.
1438          */
1439         UploadForms: function () {
1440             $('input[type=file]').change(function(event) {
1441                 if (typeof this.files == "object" && this.files.length > 0) {
1442                     var size = 0;
1443                     for (var i = 0; i < this.files.length; i++) {
1444                         size += this.files[i].size;
1445                     }
1446
1447                     var max = SN.U.maxFileSize($(this.form));
1448                     if (max > 0 && size > max) {
1449                         var msg = 'File too large: maximum upload size is %d bytes.';
1450                         alert(msg.replace('%d', max));
1451
1452                         // Clear the files.
1453                         $(this).val('');
1454                         event.preventDefault();
1455                         return false;
1456                     }
1457                 }
1458             });
1459         }
1460     }
1461 };
1462
1463 /**
1464  * Run initialization functions on DOM-ready.
1465  *
1466  * Note that if we're waiting on other scripts to load, this won't happen
1467  * until that's done. To load scripts asynchronously without delaying setup,
1468  * don't start them loading until after DOM-ready time!
1469  */
1470 $(document).ready(function(){
1471     SN.Init.UploadForms();
1472     if ($('.'+SN.C.S.FormNotice).length > 0) {
1473         SN.Init.NoticeForm();
1474     }
1475     if ($('#content .notices').length > 0) {
1476         SN.Init.Notices();
1477     }
1478     if ($('#content .entity_actions').length > 0) {
1479         SN.Init.EntityActions();
1480     }
1481     if ($('#form_login').length > 0) {
1482         SN.Init.Login();
1483     }
1484 });