[ Index ]

PHP Cross Reference of BuddyPress

title

Body

[close]

/src/bp-templates/bp-nouveau/js/ -> buddypress-messages.js (source)

   1  /* global wp, BP_Nouveau, _, Backbone, tinymce, tinyMCE */
   2  /* jshint devel: true */
   3  /* @since 3.0.0 */
   4  /* @version 8.0.0 */
   5  window.wp = window.wp || {};
   6  window.bp = window.bp || {};
   7  
   8  ( function( bp, $ ) {
   9  
  10      // Bail if not set.
  11      if ( typeof BP_Nouveau === 'undefined' ) {
  12          return;
  13      }
  14  
  15      _.extend( bp, _.pick( wp, 'Backbone', 'ajax', 'template' ) );
  16  
  17      bp.Models      = bp.Models || {};
  18      bp.Collections = bp.Collections || {};
  19      bp.Views       = bp.Views || {};
  20  
  21      bp.Nouveau = bp.Nouveau || {};
  22  
  23      /**
  24       * [Nouveau description]
  25       * @type {Object}
  26       */
  27      bp.Nouveau.Messages = {
  28          /**
  29           * [start description]
  30           * @return {[type]} [description]
  31           */
  32          start: function() {
  33              this.views    = new Backbone.Collection();
  34              this.threads  = new bp.Collections.Threads();
  35              this.messages = new bp.Collections.Messages();
  36              this.router   = new bp.Nouveau.Messages.Router();
  37              this.box      = 'inbox';
  38  
  39              this.setupNav();
  40  
  41              Backbone.history.start( {
  42                  pushState: true,
  43                  root: BP_Nouveau.messages.rootUrl
  44              } );
  45          },
  46  
  47          setupNav: function() {
  48              var self = this;
  49  
  50              // First adapt the compose nav.
  51              $( '#compose-personal-li' ).addClass( 'last' );
  52  
  53              // Then listen to nav click and load the appropriate view.
  54              $( '#subnav a' ).on( 'click', function( event ) {
  55                  var view_id = $( event.target ).prop( 'id' ),
  56                      supportedView = [ 'inbox', 'starred', 'sentbox', 'compose' ];
  57  
  58                  if ( -1 === _.indexOf( supportedView, view_id ) || 'unsupported' === self.box ) {
  59                      return event;
  60                  }
  61  
  62                  event.preventDefault();
  63  
  64                  // Remove the editor to be sure it will be added dynamically later.
  65                  self.removeTinyMCE();
  66  
  67                  // The compose view is specific (toggle behavior).
  68                  if ( 'compose' === view_id ) {
  69                      // If it exists, it means the user wants to remove it.
  70                      if ( ! _.isUndefined( self.views.get( 'compose' ) ) ) {
  71                          var form = self.views.get( 'compose' );
  72                          form.get( 'view' ).remove();
  73                          self.views.remove( { id: 'compose', view: form } );
  74  
  75                          // Back to inbox.
  76                          if ( 'single' === self.box ) {
  77                              self.box = 'inbox';
  78                          }
  79  
  80                          // Navigate back to current box.
  81                          self.router.navigate( self.box + '/', { trigger: true } );
  82  
  83                      // Otherwise load it.
  84                      } else {
  85                          self.router.navigate( 'compose/', { trigger: true } );
  86                      }
  87  
  88                  // Other views are classic.
  89                  } else {
  90  
  91                      if ( self.box !== view_id || ! _.isUndefined( self.views.get( 'compose' ) ) ) {
  92                          self.clearViews();
  93  
  94                          self.router.navigate( view_id + '/', { trigger: true } );
  95                      }
  96                  }
  97              } );
  98          },
  99  
 100          removeTinyMCE: function() {
 101              if ( typeof tinymce !== 'undefined' ) {
 102                  var editor = tinymce.get( 'message_content' );
 103  
 104                  if ( editor !== null ) {
 105                      tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, 'message_content' );
 106                  }
 107              }
 108          },
 109  
 110          tinyMCEinit: function() {
 111              if ( typeof window.tinyMCE === 'undefined' || window.tinyMCE.activeEditor === null || typeof window.tinyMCE.activeEditor === 'undefined' ) {
 112                  return;
 113              } else {
 114                  // Mentions isn't available, so bail.
 115                  if ( _.isEmpty( bp.mentions ) ) {
 116                      return;
 117                  }
 118  
 119                  $( window.tinyMCE.activeEditor.contentDocument.activeElement )
 120                      .atwho( 'setIframe', $( '#message_content_ifr' )[0] )
 121                      .bp_mentions( {
 122                          data: [],
 123                          suffix: ' '
 124                      } );
 125              }
 126          },
 127  
 128          removeFeedback: function() {
 129              var feedback;
 130  
 131              if ( ! _.isUndefined( this.views.get( 'feedback' ) ) ) {
 132                  feedback = this.views.get( 'feedback' );
 133                  feedback.get( 'view' ).remove();
 134                  this.views.remove( { id: 'feedback', view: feedback } );
 135              }
 136          },
 137  
 138          displayFeedback: function( message, type ) {
 139              var feedback;
 140  
 141              // Make sure to remove the feedbacks.
 142              this.removeFeedback();
 143  
 144              if ( ! message ) {
 145                  return;
 146              }
 147  
 148              feedback = new bp.Views.Feedback( {
 149                  value: message,
 150                  type:  type || 'info'
 151              } );
 152  
 153              this.views.add( { id: 'feedback', view: feedback } );
 154  
 155              feedback.inject( '.bp-messages-feedback' );
 156          },
 157  
 158          clearViews: function() {
 159              // Clear views.
 160              if ( ! _.isUndefined( this.views.models ) ) {
 161                  _.each( this.views.models, function( model ) {
 162                      model.get( 'view' ).remove();
 163                  }, this );
 164  
 165                  this.views.reset();
 166              }
 167          },
 168  
 169          composeView: function() {
 170              // Remove all existing views.
 171              this.clearViews();
 172  
 173              // Create the loop view.
 174              var form = new bp.Views.messageForm( {
 175                  model: new bp.Models.Message()
 176              } );
 177  
 178              this.views.add( { id: 'compose', view: form } );
 179  
 180              form.inject( '.bp-messages-content' );
 181          },
 182  
 183          threadsView: function() {
 184              // Activate the appropriate nav.
 185              $( '#subnav ul li' ).each( function( l, li ) {
 186                  $( li ).removeClass( 'current selected' );
 187              } );
 188              $( '#subnav a#' + this.box ).closest( 'li' ).addClass( 'current selected' );
 189  
 190              // Create the loop view.
 191              var threads_list = new bp.Views.userThreads( { collection: this.threads, box: this.box } );
 192  
 193              this.views.add( { id: 'threads', view: threads_list } );
 194  
 195              threads_list.inject( '.bp-messages-content' );
 196  
 197              // Attach filters.
 198              this.displayFilters( this.threads );
 199          },
 200  
 201          displayFilters: function( collection ) {
 202              var filters_view;
 203  
 204              // Create the model.
 205              this.filters = new Backbone.Model( {
 206                  'page'         : 1,
 207                  'total_page'   : 0,
 208                  'search_terms' : '',
 209                  'box'          : this.box
 210              } );
 211  
 212              // Use it in the filters viex.
 213              filters_view = new bp.Views.messageFilters( { model: this.filters, threads: collection } );
 214  
 215              this.views.add( { id: 'filters', view: filters_view } );
 216  
 217              filters_view.inject( '.bp-messages-filters' );
 218          },
 219  
 220          singleView: function( thread ) {
 221              // Remove all existing views.
 222              this.clearViews();
 223  
 224              this.box = 'single';
 225  
 226              // Create the single thread view.
 227              var single_thread = new bp.Views.userMessages( { collection: this.messages, thread: thread } );
 228  
 229              this.views.add( { id: 'single', view: single_thread } );
 230  
 231              single_thread.inject( '.bp-messages-content' );
 232          }
 233      };
 234  
 235      bp.Models.Message = Backbone.Model.extend( {
 236          defaults: {
 237              send_to         : [],
 238              subject         : '',
 239              message_content : '',
 240              meta            : {}
 241          },
 242  
 243          sendMessage: function() {
 244              if ( true === this.get( 'sending' ) ) {
 245                  return;
 246              }
 247  
 248              this.set( 'sending', true, { silent: true } );
 249  
 250              var sent = bp.ajax.post( 'messages_send_message', _.extend(
 251                  {
 252                      nonce: BP_Nouveau.messages.nonces.send
 253                  },
 254                  this.attributes
 255              ) );
 256  
 257              this.set( 'sending', false, { silent: true } );
 258  
 259              return sent;
 260          }
 261      } );
 262  
 263      bp.Models.Thread = Backbone.Model.extend( {
 264          defaults: {
 265              id            : 0,
 266              message_id    : 0,
 267              subject       : '',
 268              excerpt       : '',
 269              content       : '',
 270              unread        : true,
 271              sender_name   : '',
 272              sender_link   : '',
 273              sender_avatar : '',
 274              count         : 0,
 275              date          : 0,
 276              display_date  : '',
 277              recipients    : []
 278          },
 279  
 280          updateReadState: function( options ) {
 281              options = options || {};
 282              options.data = _.extend(
 283                  _.pick( this.attributes, ['id', 'message_id'] ),
 284                  {
 285                      action : 'messages_thread_read',
 286                      nonce  : BP_Nouveau.nonces.messages
 287                  }
 288              );
 289  
 290              return bp.ajax.send( options );
 291          }
 292      } );
 293  
 294      bp.Models.messageThread = Backbone.Model.extend( {
 295          defaults: {
 296              id            : 0,
 297              content       : '',
 298              sender_id     : 0,
 299              sender_name   : '',
 300              sender_link   : '',
 301              sender_avatar : '',
 302              date          : 0,
 303              display_date  : ''
 304          }
 305      } );
 306  
 307      bp.Collections.Threads = Backbone.Collection.extend( {
 308          model: bp.Models.Thread,
 309  
 310          initialize : function() {
 311              this.options = { page: 1, total_page: 0 };
 312          },
 313  
 314          sync: function( method, model, options ) {
 315              options         = options || {};
 316              options.context = this;
 317              options.data    = options.data || {};
 318  
 319              // Add generic nonce.
 320              options.data.nonce = BP_Nouveau.nonces.messages;
 321  
 322              if ( 'read' === method ) {
 323                  options.data = _.extend( options.data, {
 324                      action: 'messages_get_user_message_threads'
 325                  } );
 326  
 327                  return bp.ajax.send( options );
 328              }
 329          },
 330  
 331          parse: function( resp ) {
 332  
 333              if ( ! _.isArray( resp.threads ) ) {
 334                  resp.threads = [resp.threads];
 335              }
 336  
 337              _.each( resp.threads, function( value, index ) {
 338                  if ( _.isNull( value ) ) {
 339                      return;
 340                  }
 341  
 342                  resp.threads[index].id            = value.id;
 343                  resp.threads[index].message_id    = value.message_id;
 344                  resp.threads[index].subject       = value.subject;
 345                  resp.threads[index].excerpt       = value.excerpt;
 346                  resp.threads[index].content       = value.content;
 347                  resp.threads[index].unread        = value.unread;
 348                  resp.threads[index].sender_name   = value.sender_name;
 349                  resp.threads[index].sender_link   = value.sender_link;
 350                  resp.threads[index].sender_avatar = value.sender_avatar;
 351                  resp.threads[index].count         = value.count;
 352                  resp.threads[index].date          = new Date( value.date );
 353                  resp.threads[index].display_date  = value.display_date;
 354                  resp.threads[index].recipients    = value.recipients;
 355                  resp.threads[index].star_link     = value.star_link;
 356                  resp.threads[index].is_starred    = value.is_starred;
 357              } );
 358  
 359              if ( ! _.isUndefined( resp.meta ) ) {
 360                  this.options.page       = resp.meta.page;
 361                  this.options.total_page = resp.meta.total_page;
 362              }
 363  
 364              if ( bp.Nouveau.Messages.box ) {
 365                  this.options.box = bp.Nouveau.Messages.box;
 366              }
 367  
 368              if ( ! _.isUndefined( resp.extraContent ) ) {
 369                  _.extend( this.options, _.pick( resp.extraContent, [
 370                      'beforeLoop',
 371                      'afterLoop'
 372                  ] ) );
 373              }
 374  
 375              return resp.threads;
 376          },
 377  
 378          doAction: function( action, ids, options ) {
 379              options         = options || {};
 380              options.context = this;
 381              options.data    = options.data || {};
 382  
 383              options.data = _.extend( options.data, {
 384                  action: 'messages_' + action,
 385                  nonce : BP_Nouveau.nonces.messages,
 386                  id    : ids
 387              } );
 388  
 389              return bp.ajax.send( options );
 390          }
 391      } );
 392  
 393      bp.Collections.Messages = Backbone.Collection.extend( {
 394          model: bp.Models.messageThread,
 395          options: {},
 396  
 397          sync: function( method, model, options ) {
 398              options         = options || {};
 399              options.context = this;
 400              options.data    = options.data || {};
 401  
 402              // Add generic nonce.
 403              options.data.nonce = BP_Nouveau.nonces.messages;
 404  
 405              if ( 'read' === method ) {
 406                  options.data = _.extend( options.data, {
 407                      action: 'messages_get_thread_messages'
 408                  } );
 409  
 410                  return bp.ajax.send( options );
 411              }
 412  
 413              if ( 'create' === method ) {
 414                  options.data = _.extend( options.data, {
 415                      action : 'messages_send_reply',
 416                      nonce  : BP_Nouveau.messages.nonces.send
 417                  }, model || {} );
 418  
 419                  return bp.ajax.send( options );
 420              }
 421          },
 422  
 423          parse: function( resp ) {
 424  
 425              if ( ! _.isArray( resp.messages ) ) {
 426                  resp.messages = [resp.messages];
 427              }
 428  
 429              _.each( resp.messages, function( value, index ) {
 430                  if ( _.isNull( value ) ) {
 431                      return;
 432                  }
 433  
 434                  resp.messages[index].id            = value.id;
 435                  resp.messages[index].content       = value.content;
 436                  resp.messages[index].sender_id     = value.sender_id;
 437                  resp.messages[index].sender_name   = value.sender_name;
 438                  resp.messages[index].sender_link   = value.sender_link;
 439                  resp.messages[index].sender_avatar = value.sender_avatar;
 440                  resp.messages[index].date          = new Date( value.date );
 441                  resp.messages[index].display_date  = value.display_date;
 442                  resp.messages[index].star_link     = value.star_link;
 443                  resp.messages[index].is_starred    = value.is_starred;
 444              } );
 445  
 446              if ( ! _.isUndefined( resp.thread ) ) {
 447                  this.options.thread_id      = resp.thread.id;
 448                  this.options.thread_subject = resp.thread.subject;
 449                  this.options.recipients     = resp.thread.recipients;
 450              }
 451  
 452              return resp.messages;
 453          }
 454      } );
 455  
 456      // Extend wp.Backbone.View with .prepare() and .inject().
 457      bp.Nouveau.Messages.View = bp.Backbone.View.extend( {
 458          inject: function( selector ) {
 459              this.render();
 460              $(selector).html( this.el );
 461              this.views.ready();
 462          },
 463  
 464          prepare: function() {
 465              if ( ! _.isUndefined( this.model ) && _.isFunction( this.model.toJSON ) ) {
 466                  return this.model.toJSON();
 467              } else {
 468                  return {};
 469              }
 470          }
 471      } );
 472  
 473      // Feedback view.
 474      bp.Views.Feedback = bp.Nouveau.Messages.View.extend( {
 475          tagName: 'div',
 476          className: 'bp-messages bp-user-messages-feedback',
 477          template  : bp.template( 'bp-messages-feedback' ),
 478  
 479          initialize: function() {
 480              this.model = new Backbone.Model( {
 481                  type: this.options.type || 'info',
 482                  message: this.options.value
 483              } );
 484          }
 485      } );
 486  
 487      // Hook view.
 488      bp.Views.Hook = bp.Nouveau.Messages.View.extend( {
 489          tagName: 'div',
 490          template  : bp.template( 'bp-messages-hook' ),
 491  
 492          initialize: function() {
 493              this.model = new Backbone.Model( {
 494                  extraContent: this.options.extraContent
 495              } );
 496  
 497              this.el.className = 'bp-messages-hook';
 498  
 499              if ( this.options.className ) {
 500                  this.el.className += ' ' + this.options.className;
 501              }
 502          }
 503      } );
 504  
 505      bp.Views.messageEditor = bp.Nouveau.Messages.View.extend( {
 506          template  : bp.template( 'bp-messages-editor' ),
 507  
 508          initialize: function() {
 509              this.on( 'ready', this.activateTinyMce, this );
 510          },
 511  
 512          activateTinyMce: function() {
 513              if ( typeof tinymce !== 'undefined' ) {
 514                  tinymce.EditorManager.execCommand( 'mceAddEditor', true, 'message_content' );
 515              }
 516          }
 517      } );
 518  
 519      bp.Views.messageForm = bp.Nouveau.Messages.View.extend( {
 520          tagName   : 'form',
 521          id        : 'send_message_form',
 522          className : 'standard-form',
 523          template  : bp.template( 'bp-messages-form' ),
 524  
 525          events: {
 526              'click #bp-messages-send'  : 'sendMessage',
 527              'click #bp-messages-reset' : 'resetForm'
 528          },
 529  
 530          initialize: function() {
 531              // Clone the model to set the resetted one.
 532              this.resetModel = this.model.clone();
 533  
 534              // Add the editor view.
 535              this.views.add( '#bp-message-content', new bp.Views.messageEditor() );
 536  
 537              this.model.on( 'change', this.resetFields, this );
 538  
 539              // Activate bp_mentions.
 540              this.on( 'ready', this.addMentions, this );
 541          },
 542  
 543          addMentions: function() {
 544              var sendToInput = $( this.el ).find( '#send-to-input' ),
 545                  mention = bp.Nouveau.getLinkParams( null, 'r' ) || null;
 546  
 547              // Add autocomplete to send_to field.
 548              sendToInput.bp_mentions( {
 549                  data: [],
 550                  suffix: ' '
 551              } );
 552  
 553              // Check for mention.
 554              if ( ! _.isNull( mention ) ) {
 555                  sendToInput.val( '@' + _.escape( mention ) + ' ' );
 556                  sendToInput.focus();
 557              }
 558          },
 559  
 560          resetFields: function( model ) {
 561              // Clean inputs.
 562              _.each( model.previousAttributes(), function( value, input ) {
 563                  if ( 'message_content' === input ) {
 564                      // tinyMce.
 565                      if ( undefined !== tinyMCE.activeEditor && null !== tinyMCE.activeEditor ) {
 566                          tinyMCE.activeEditor.setContent( '' );
 567                      }
 568  
 569                  // All except meta or empty value.
 570                  } else if ( 'meta' !== input && false !== value ) {
 571                      $( 'input[name="' + input + '"]' ).val( '' );
 572                  }
 573              } );
 574  
 575              // Listen to this to eventually reset your custom inputs.
 576              $( this.el ).trigger( 'message:reset', _.pick( model.previousAttributes(), 'meta' ) );
 577          },
 578  
 579          sendMessage: function( event ) {
 580              var meta = {}, errors = [], self = this;
 581              event.preventDefault();
 582  
 583              bp.Nouveau.Messages.removeFeedback();
 584  
 585              // Set the content and meta.
 586              _.each( this.$el.serializeArray(), function( pair ) {
 587                  pair.name = pair.name.replace( '[]', '' );
 588  
 589                  // Group extra fields in meta.
 590                  if ( -1 === _.indexOf( ['send_to', 'subject', 'message_content'], pair.name ) ) {
 591                      if ( _.isUndefined( meta[ pair.name ] ) ) {
 592                          meta[ pair.name ] = pair.value;
 593                      } else {
 594                          if ( ! _.isArray( meta[ pair.name ] ) ) {
 595                              meta[ pair.name ] = [ meta[ pair.name ] ];
 596                          }
 597  
 598                          meta[ pair.name ].push( pair.value );
 599                      }
 600  
 601                  // Prepare the core model.
 602                  } else {
 603                      // Send to.
 604                      if ( 'send_to' === pair.name ) {
 605                          var usernames = pair.value.match( /(^|[^@\w\-])@([a-zA-Z0-9_\-]{1,50})\b/g );
 606  
 607                          if ( ! usernames ) {
 608                              errors.push( 'send_to' );
 609                          } else {
 610                              usernames = usernames.map( function( username ) {
 611                                  username = username.trim();
 612                                  return username;
 613                              } );
 614  
 615                              if ( ! usernames || ! _.isArray( usernames ) ) {
 616                                  errors.push( 'send_to' );
 617                              }
 618  
 619                              this.model.set( 'send_to', usernames, { silent: true } );
 620                          }
 621  
 622                      // Subject and content.
 623                      } else {
 624                          // Message content.
 625                          if ( 'message_content' === pair.name && undefined !== tinyMCE.activeEditor ) {
 626                              pair.value = tinyMCE.activeEditor.getContent();
 627                          }
 628  
 629                          if ( ! pair.value ) {
 630                              errors.push( pair.name );
 631                          } else {
 632                              this.model.set( pair.name, pair.value, { silent: true } );
 633                          }
 634                      }
 635                  }
 636  
 637              }, this );
 638  
 639              if ( errors.length ) {
 640                  var feedback = '';
 641                  _.each( errors, function( e ) {
 642                      feedback += BP_Nouveau.messages.errors[ e ] + '<br/>';
 643                  } );
 644  
 645                  bp.Nouveau.Messages.displayFeedback( feedback, 'error' );
 646                  return;
 647              }
 648  
 649              // Set meta.
 650              this.model.set( 'meta', meta, { silent: true } );
 651  
 652              // Send the message.
 653              this.model.sendMessage().done( function( response ) {
 654                  // Reset the model.
 655                  self.model.set( self.resetModel );
 656  
 657                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
 658  
 659                  // Remove tinyMCE.
 660                  bp.Nouveau.Messages.removeTinyMCE();
 661  
 662                  // Remove the form view.
 663                  var form = bp.Nouveau.Messages.views.get( 'compose' );
 664                  form.get( 'view' ).remove();
 665                  bp.Nouveau.Messages.views.remove( { id: 'compose', view: form } );
 666  
 667                  bp.Nouveau.Messages.router.navigate( 'sentbox/', { trigger: true } );
 668              } ).fail( function( response ) {
 669                  if ( response.feedback ) {
 670                      bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
 671                  }
 672              } );
 673          },
 674  
 675          resetForm: function( event ) {
 676              event.preventDefault();
 677  
 678              this.model.set( this.resetModel );
 679          }
 680      } );
 681  
 682      bp.Views.userThreads = bp.Nouveau.Messages.View.extend( {
 683          tagName   : 'div',
 684  
 685          events: {
 686              'click .subject' : 'changePreview'
 687          },
 688  
 689          initialize: function() {
 690              var Views = [
 691                  new bp.Nouveau.Messages.View( { tagName: 'ul', id: 'message-threads', className: 'message-lists' } ),
 692                  new bp.Views.previewThread( { collection: this.collection } )
 693              ];
 694  
 695              _.each( Views, function( view ) {
 696                  this.views.add( view );
 697              }, this );
 698  
 699              // Load threads for the active view.
 700              this.requestThreads();
 701  
 702              this.collection.on( 'reset', this.cleanContent, this );
 703              this.collection.on( 'add', this.addThread, this );
 704          },
 705  
 706          requestThreads: function() {
 707              this.collection.reset();
 708  
 709              bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.loading, 'loading' );
 710  
 711              this.collection.fetch( {
 712                  data    : _.pick( this.options, 'box' ),
 713                  success : _.bind( this.threadsFetched, this ),
 714                  error   : this.threadsFetchError
 715              } );
 716          },
 717  
 718          threadsFetched: function() {
 719              bp.Nouveau.Messages.removeFeedback();
 720  
 721              // Display the bp_after_member_messages_loop hook.
 722              if ( this.collection.options.afterLoop ) {
 723                  this.views.add( new bp.Views.Hook( { extraContent: this.collection.options.afterLoop, className: 'after-messages-loop' } ), { at: 1 } );
 724              }
 725  
 726              // Display the bp_before_member_messages_loop hook.
 727              if ( this.collection.options.beforeLoop ) {
 728                  this.views.add( new bp.Views.Hook( { extraContent: this.collection.options.beforeLoop, className: 'before-messages-loop' } ), { at: 0 } );
 729              }
 730  
 731              // Inform the user about how to use the UI.
 732              bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.howto, 'info' );
 733          },
 734  
 735          threadsFetchError: function( collection, response ) {
 736              bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
 737          },
 738  
 739          cleanContent: function() {
 740              _.each( this.views._views['#message-threads'], function( view ) {
 741                  view.remove();
 742              } );
 743          },
 744  
 745          addThread: function( thread ) {
 746              var selected = this.collection.findWhere( { active: true } );
 747  
 748              if ( _.isUndefined( selected ) ) {
 749                  thread.set( 'active', true );
 750              }
 751  
 752              this.views.add( '#message-threads', new bp.Views.userThread( { model: thread } ) );
 753          },
 754  
 755          setActiveThread: function( active ) {
 756              if ( ! active ) {
 757                  return;
 758              }
 759  
 760              _.each( this.collection.models, function( thread ) {
 761                  if ( thread.id === active ) {
 762                      thread.set( 'active', true );
 763                  } else {
 764                      thread.unset( 'active' );
 765                  }
 766              }, this );
 767          },
 768  
 769          changePreview: function( event ) {
 770              var target = $( event.currentTarget );
 771  
 772              event.preventDefault();
 773              bp.Nouveau.Messages.removeFeedback();
 774  
 775              // If the click is done on an active conversation, open it.
 776              if ( target.closest( '.thread-item' ).hasClass( 'selected' ) ) {
 777                  bp.Nouveau.Messages.router.navigate(
 778                      'view/' + target.closest( '.thread-content' ).data( 'thread-id' ) + '/',
 779                      { trigger: true }
 780                  );
 781  
 782              // Otherwise activate the conversation and display its preview.
 783              } else {
 784                  this.setActiveThread( target.closest( '.thread-content' ).data( 'thread-id' ) );
 785  
 786                  $( '.message-action-view' ).focus();
 787              }
 788          }
 789      } );
 790  
 791      bp.Views.userThread = bp.Nouveau.Messages.View.extend( {
 792          tagName   : 'li',
 793          template  : bp.template( 'bp-messages-thread' ),
 794          className : 'thread-item',
 795  
 796          events: {
 797              'click .message-check' : 'singleSelect'
 798          },
 799  
 800          initialize: function() {
 801              if ( this.model.get( 'active' ) ) {
 802                  this.el.className += ' selected';
 803              }
 804  
 805              if ( this.model.get( 'unread' ) ) {
 806                  this.el.className += ' unread';
 807              }
 808  
 809              if ( 'sentbox' === bp.Nouveau.Messages.box ) {
 810                  var recipientsCount = this.model.get( 'recipients' ).length, toOthers = '';
 811  
 812                  if ( 2 === recipientsCount ) {
 813                      toOthers = BP_Nouveau.messages.toOthers.one;
 814                  } else if ( 2 < recipientsCount ) {
 815                      toOthers = BP_Nouveau.messages.toOthers.more.replace( '%d', Number( recipientsCount - 1 ) );
 816                  }
 817  
 818                  this.model.set( {
 819                      recipientsCount: recipientsCount,
 820                      toOthers: toOthers
 821                  }, { silent: true } );
 822              } else if ( this.model.get( 'recipientsCount' )  ) {
 823                  this.model.unset( 'recipientsCount', { silent: true } );
 824              }
 825  
 826              this.model.on( 'change:active', this.toggleClass, this );
 827              this.model.on( 'change:unread', this.updateReadState, this );
 828              this.model.on( 'change:checked', this.bulkSelect, this );
 829              this.model.on( 'remove', this.cleanView, this );
 830          },
 831  
 832          toggleClass: function( model ) {
 833              if ( true === model.get( 'active' ) ) {
 834                  $( this.el ).addClass( 'selected' );
 835              } else {
 836                  $( this.el ).removeClass( 'selected' );
 837              }
 838          },
 839  
 840          updateReadState: function( model, state ) {
 841              if ( false === state ) {
 842                  $( this.el ).removeClass( 'unread' );
 843              } else {
 844                  $( this.el ).addClass( 'unread' );
 845              }
 846          },
 847  
 848          bulkSelect: function( model ) {
 849              if ( $( '#bp-message-thread-' + model.get( 'id' ) ).length ) {
 850                  $( '#bp-message-thread-' + model.get( 'id' ) ).prop( 'checked',model.get( 'checked' ) );
 851              }
 852          },
 853  
 854          singleSelect: function( event ) {
 855              var isChecked = $( event.currentTarget ).prop( 'checked' );
 856  
 857              // To avoid infinite loops.
 858              this.model.set( 'checked', isChecked, { silent: true } );
 859  
 860              var hasChecked = false;
 861  
 862              _.each( this.model.collection.models, function( model ) {
 863                  if ( true === model.get( 'checked' ) ) {
 864                      hasChecked = true;
 865                  }
 866              } );
 867  
 868              if ( hasChecked ) {
 869                  $( '#user-messages-bulk-actions' ).closest( '.bulk-actions-wrap' ).removeClass( 'bp-hide' );
 870  
 871                  // Inform the user about how to use the bulk actions.
 872                  bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.howtoBulk, 'info' );
 873              } else {
 874                  $( '#user-messages-bulk-actions' ).closest( '.bulk-actions-wrap' ).addClass( 'bp-hide' );
 875  
 876                  bp.Nouveau.Messages.removeFeedback();
 877              }
 878          },
 879  
 880          cleanView: function() {
 881              this.views.view.remove();
 882          }
 883      } );
 884  
 885      bp.Views.previewThread = bp.Nouveau.Messages.View.extend( {
 886          tagName: 'div',
 887          id: 'thread-preview',
 888          template  : bp.template( 'bp-messages-preview' ),
 889  
 890          events: {
 891              'click .actions button' : 'doAction',
 892              'click .actions a'      : 'doAction'
 893          },
 894  
 895          initialize: function() {
 896              this.collection.on( 'change:active', this.setPreview, this );
 897              this.collection.on( 'change:is_starred', this.updatePreview, this );
 898              this.collection.on( 'reset', this.emptyPreview, this );
 899              this.collection.on( 'remove', this.emptyPreview, this );
 900          },
 901  
 902          render: function() {
 903              // Only render if we have some content to render.
 904              if ( _.isUndefined( this.model ) || true !== this.model.get( 'active' ) ) {
 905                  return;
 906              }
 907  
 908              bp.Nouveau.Messages.View.prototype.render.apply( this, arguments );
 909          },
 910  
 911          setPreview: function( model ) {
 912              var self = this;
 913  
 914              this.model = model;
 915  
 916              if ( true === model.get( 'unread' ) ) {
 917                  this.model.updateReadState().done( function() {
 918                      self.model.set( 'unread', false );
 919                  } );
 920              }
 921  
 922              this.render();
 923          },
 924  
 925          updatePreview: function( model ) {
 926              if ( true === model.get( 'active' ) ) {
 927                  this.render();
 928              }
 929          },
 930  
 931          emptyPreview: function() {
 932              $( this.el ).html( '' );
 933          },
 934  
 935          doAction: function( event ) {
 936              var action = $( event.currentTarget ).data( 'bp-action' ), self = this, options = {}, mid,
 937                  feedback = BP_Nouveau.messages.doingAction;
 938  
 939              if ( ! action ) {
 940                  return event;
 941              }
 942  
 943              event.preventDefault();
 944  
 945              var model = this.collection.findWhere( { active: true } );
 946  
 947              if ( ! model.get( 'id' ) ) {
 948                  return;
 949              }
 950  
 951              mid = model.get( 'id' );
 952  
 953              // Open the full conversation.
 954              if ( 'view' === action ) {
 955                  bp.Nouveau.Messages.router.navigate(
 956                      'view/' + mid + '/',
 957                      { trigger: true }
 958                  );
 959  
 960                  return;
 961  
 962              // Star/Unstar actions needs to use a specific id and nonce.
 963              } else if ( 'star' === action || 'unstar' === action ) {
 964                  options.data = {
 965                      'star_nonce' : model.get( 'star_nonce' )
 966                  };
 967  
 968                  mid = model.get( 'starred_id' );
 969              }
 970  
 971              if ( ! _.isUndefined( feedback[ action ] ) ) {
 972                  bp.Nouveau.Messages.displayFeedback( feedback[ action ], 'loading' );
 973              }
 974  
 975              this.collection.doAction( action, mid, options ).done( function( response ) {
 976                  // Remove previous feedback.
 977                  bp.Nouveau.Messages.removeFeedback();
 978  
 979                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
 980  
 981                  if ( 'delete' === action || ( 'starred' === self.collection.options.box && 'unstar' === action ) ) {
 982                      // Remove from the list of messages.
 983                      self.collection.remove( model.get( 'id' ) );
 984  
 985                      // And Requery.
 986                      self.collection.fetch( {
 987                          data : _.pick( self.collection.options, ['box', 'search_terms', 'page'] )
 988                      } );
 989                  } else if ( 'unstar' === action || 'star' === action ) {
 990                      // Update the model attributes--updates the star icon.
 991                      _.each( response.messages, function( updated ) {
 992                          model.set( updated );
 993                      } );
 994                      model.set( _.first( response.messages ) );
 995                  } else if ( response.messages ) {
 996                      model.set( _.first( response.messages ) );
 997                  }
 998              } ).fail( function( response ) {
 999                  // Remove previous feedback.
1000                  bp.Nouveau.Messages.removeFeedback();
1001  
1002                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1003              } );
1004          }
1005      } );
1006  
1007      bp.Views.Pagination = bp.Nouveau.Messages.View.extend( {
1008          tagName   : 'li',
1009          className : 'last filter',
1010          template  :  bp.template( 'bp-messages-paginate' )
1011      } );
1012  
1013      bp.Views.BulkActions = bp.Nouveau.Messages.View.extend( {
1014          tagName   : 'div',
1015          template  :  bp.template( 'bp-bulk-actions' ),
1016  
1017          events : {
1018              'click #user_messages_select_all' : 'bulkSelect',
1019              'click .bulk-apply'               : 'doBulkAction'
1020          },
1021  
1022          bulkSelect: function( event ) {
1023              var isChecked = $( event.currentTarget ).prop( 'checked' );
1024  
1025              if ( isChecked ) {
1026                  $( this.el ).find( '.bulk-actions-wrap' ).removeClass( 'bp-hide' ).addClass( 'bp-show' );
1027  
1028                  // Inform the user about how to use the bulk actions.
1029                  bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.howtoBulk, 'info' );
1030              } else {
1031                  $( this.el ).find( '.bulk-actions-wrap' ).addClass( 'bp-hide' );
1032  
1033                  bp.Nouveau.Messages.removeFeedback();
1034              }
1035  
1036              _.each( this.collection.models, function( model ) {
1037                  model.set( 'checked', isChecked );
1038              } );
1039          },
1040  
1041          doBulkAction: function( event ) {
1042              var self = this, options = {}, ids, attr = 'id',
1043                  feedback = BP_Nouveau.messages.doingAction;
1044  
1045              event.preventDefault();
1046  
1047              var action = $( '#user-messages-bulk-actions' ).val();
1048  
1049              if ( ! action ) {
1050                  return;
1051              }
1052  
1053              var threads    = this.collection.where( { checked: true } );
1054              var thread_ids = _.map( threads, function( model ) {
1055                  return model.get( 'id' );
1056              } );
1057  
1058              // Default to thread ids.
1059              ids = thread_ids;
1060  
1061              // We need to get the starred ids.
1062              if ( 'star' === action || 'unstar' === action ) {
1063                  ids = _.map( threads, function( model ) {
1064                      return model.get( 'starred_id' );
1065                  } );
1066  
1067                  if ( 1 === ids.length ) {
1068                      options.data = {
1069                          'star_nonce' : threads[0].get( 'star_nonce' )
1070                      };
1071                  }
1072  
1073                  // Map with first message starred in the thread.
1074                  attr = 'starred_id';
1075              }
1076  
1077              // Message id to Thread id.
1078              var m_tid = _.object( _.map( threads, function (model) {
1079                  return [model.get( attr ), model.get( 'id' )];
1080              } ) );
1081  
1082              if ( ! _.isUndefined( feedback[ action ] ) ) {
1083                  bp.Nouveau.Messages.displayFeedback( feedback[ action ], 'loading' );
1084              }
1085  
1086              this.collection.doAction( action, ids, options ).done( function( response ) {
1087                  // Remove previous feedback.
1088                  bp.Nouveau.Messages.removeFeedback();
1089  
1090                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1091  
1092                  if ( 'delete' === action || ( 'starred' === self.collection.options.box && 'unstar' === action ) ) {
1093                      // Remove from the list of messages.
1094                      self.collection.remove( thread_ids );
1095  
1096                      // And Requery.
1097                      self.collection.fetch( {
1098                          data : _.pick( self.collection.options, ['box', 'search_terms', 'page'] )
1099                      } );
1100                  } else if ( response.messages ) {
1101                      // Update each model attributes.
1102                      _.each( response.messages, function( updated, id ) {
1103                          var model = self.collection.get( m_tid[id] );
1104                          model.set( updated );
1105                      } );
1106                  }
1107              } ).fail( function( response ) {
1108                  // Remove previous feedback.
1109                  bp.Nouveau.Messages.removeFeedback();
1110  
1111                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1112              } );
1113          }
1114      } );
1115  
1116      bp.Views.messageFilters = bp.Nouveau.Messages.View.extend( {
1117          tagName: 'ul',
1118          template:  bp.template( 'bp-messages-filters' ),
1119  
1120          events : {
1121              'search #user_messages_search'      : 'resetSearchTerms',
1122              'submit #user_messages_search_form' : 'setSearchTerms',
1123              'click #bp-messages-next-page'      : 'nextPage',
1124              'click #bp-messages-prev-page'      : 'prevPage'
1125          },
1126  
1127          initialize: function() {
1128              this.model.on( 'change', this.filterThreads, this );
1129              this.options.threads.on( 'sync', this.addPaginatation, this );
1130          },
1131  
1132          addPaginatation: function( collection ) {
1133              _.each( this.views._views, function( view ) {
1134                  if ( ! _.isUndefined( view ) ) {
1135                      _.first( view ).remove();
1136                  }
1137              } );
1138  
1139              this.views.add( new bp.Views.Pagination( { model: new Backbone.Model( collection.options ) } ) );
1140  
1141              this.views.add( '.user-messages-bulk-actions', new bp.Views.BulkActions( {
1142                  model: new Backbone.Model( BP_Nouveau.messages.bulk_actions ),
1143                  collection : collection
1144              } ) );
1145          },
1146  
1147          filterThreads: function() {
1148              bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.loading, 'loading' );
1149  
1150              this.options.threads.reset();
1151              _.extend( this.options.threads.options, _.pick( this.model.attributes, ['box', 'search_terms'] ) );
1152  
1153              this.options.threads.fetch( {
1154                  data    : _.pick( this.model.attributes, ['box', 'search_terms', 'page'] ),
1155                  success : this.threadsFiltered,
1156                  error   : this.threadsFilterError
1157              } );
1158          },
1159  
1160          threadsFiltered: function() {
1161              bp.Nouveau.Messages.removeFeedback();
1162          },
1163  
1164          threadsFilterError: function( collection, response ) {
1165              bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1166          },
1167  
1168          resetSearchTerms: function( event ) {
1169              event.preventDefault();
1170  
1171              if ( ! $( event.target ).val() ) {
1172                  $( event.target ).closest( 'form' ).submit();
1173              } else {
1174                  $( event.target ).closest( 'form' ).find( '[type=submit]' ).addClass('bp-show').removeClass('bp-hide');
1175              }
1176          },
1177  
1178          setSearchTerms: function( event ) {
1179              event.preventDefault();
1180  
1181              this.model.set( {
1182                  'search_terms': $( event.target ).find( 'input[type=search]' ).val() || '',
1183                  page: 1
1184              } );
1185          },
1186  
1187          nextPage: function( event ) {
1188              event.preventDefault();
1189  
1190              this.model.set( 'page', this.model.get( 'page' ) + 1 );
1191          },
1192  
1193          prevPage: function( event ) {
1194              event.preventDefault();
1195  
1196              this.model.set( 'page', this.model.get( 'page' ) - 1 );
1197          }
1198      } );
1199  
1200      bp.Views.userMessagesHeader = bp.Nouveau.Messages.View.extend( {
1201          tagName  : 'div',
1202          template : bp.template( 'bp-messages-single-header' ),
1203  
1204          events: {
1205              'click .actions a' : 'doAction',
1206              'click .actions button' : 'doAction'
1207          },
1208  
1209          doAction: function( event ) {
1210              var action = $( event.currentTarget ).data( 'bp-action' ), self = this, options = {},
1211                  feedback = BP_Nouveau.messages.doingAction;
1212  
1213              if ( ! action ) {
1214                  return event;
1215              }
1216  
1217              event.preventDefault();
1218  
1219              if ( ! this.model.get( 'id' ) ) {
1220                  return;
1221              }
1222  
1223              if ( 'star' === action || 'unstar' === action ) {
1224                  var opposite = {
1225                      'star'  : 'unstar',
1226                      'unstar' : 'star'
1227                  };
1228  
1229                  options.data = {
1230                      'star_nonce' : this.model.get( 'star_nonce' )
1231                  };
1232  
1233                  $( event.currentTarget ).addClass( 'bp-hide' );
1234                  $( event.currentTarget ).parent().find( '[data-bp-action="' + opposite[ action ] + '"]' ).removeClass( 'bp-hide' );
1235  
1236              }
1237  
1238              if ( ! _.isUndefined( feedback[ action ] ) ) {
1239                  bp.Nouveau.Messages.displayFeedback( feedback[ action ], 'loading' );
1240              }
1241  
1242              bp.Nouveau.Messages.threads.doAction( action, this.model.get( 'id' ), options ).done( function( response ) {
1243                  // Remove all views
1244                  if ( 'delete' === action ) {
1245                      bp.Nouveau.Messages.clearViews();
1246                  } else if ( response.messages ) {
1247                      self.model.set( _.first( response.messages ) );
1248                  }
1249  
1250                  // Remove previous feedback.
1251                  bp.Nouveau.Messages.removeFeedback();
1252  
1253                  // Display the feedback.
1254                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1255              } ).fail( function( response ) {
1256                  // Remove previous feedback.
1257                  bp.Nouveau.Messages.removeFeedback();
1258  
1259                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1260              } );
1261          }
1262      } );
1263  
1264      bp.Views.userMessagesEntry = bp.Views.userMessagesHeader.extend( {
1265          tagName  : 'li',
1266          template : bp.template( 'bp-messages-single-list' ),
1267  
1268          events: {
1269              'click [data-bp-action]' : 'doAction'
1270          },
1271  
1272          initialize: function() {
1273              this.model.on( 'change:is_starred', this.updateMessage, this );
1274          },
1275  
1276          updateMessage: function( model ) {
1277              if ( this.model.get( 'id' ) !== model.get( 'id' ) ) {
1278                  return;
1279              }
1280  
1281              this.render();
1282          }
1283      } );
1284  
1285      bp.Views.userMessages = bp.Nouveau.Messages.View.extend( {
1286          tagName  : 'div',
1287          template : bp.template( 'bp-messages-single' ),
1288  
1289          initialize: function() {
1290              // Load Messages.
1291              this.requestMessages();
1292  
1293              // Init a reply.
1294              this.reply = new bp.Models.messageThread();
1295  
1296              this.collection.on( 'add', this.addMessage, this );
1297  
1298              // Add the editor view.
1299              this.views.add( '#bp-message-content', new bp.Views.messageEditor() );
1300          },
1301  
1302          events: {
1303              'click #send_reply_button' : 'sendReply'
1304          },
1305  
1306          requestMessages: function() {
1307              var data = {};
1308  
1309              this.collection.reset();
1310  
1311              bp.Nouveau.Messages.displayFeedback( BP_Nouveau.messages.loading, 'loading' );
1312  
1313              if ( _.isUndefined( this.options.thread.attributes ) ) {
1314                  data.id = this.options.thread.id;
1315  
1316              } else {
1317                  data.id        = this.options.thread.get( 'id' );
1318                  data.js_thread = ! _.isEmpty( this.options.thread.get( 'subject' ) );
1319              }
1320  
1321              this.collection.fetch( {
1322                  data: data,
1323                  success : _.bind( this.messagesFetched, this ),
1324                  error   : this.messagesFetchError
1325              } );
1326          },
1327  
1328          messagesFetched: function( collection, response ) {
1329              if ( ! _.isUndefined( response.thread ) ) {
1330                  this.options.thread = new Backbone.Model( response.thread );
1331              }
1332  
1333              bp.Nouveau.Messages.removeFeedback();
1334  
1335              this.views.add( '#bp-message-thread-header', new bp.Views.userMessagesHeader( { model: this.options.thread } ) );
1336          },
1337  
1338          messagesFetchError: function( collection, response ) {
1339              if ( response.feedback && response.type ) {
1340                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1341              }
1342          },
1343  
1344          addMessage: function( message ) {
1345              this.views.add( '#bp-message-thread-list', new bp.Views.userMessagesEntry( { model: message } ) );
1346          },
1347  
1348          addEditor: function() {
1349              // Load the Editor
1350              this.views.add( '#bp-message-content', new bp.Views.messageEditor() );
1351          },
1352  
1353          sendReply: function( event ) {
1354              event.preventDefault();
1355  
1356              if ( true === this.reply.get( 'sending' ) ) {
1357                  return;
1358              }
1359  
1360              this.reply.set ( {
1361                  thread_id : this.options.thread.get( 'id' ),
1362                  content   : tinyMCE.activeEditor.getContent(),
1363                  sending   : true
1364              } );
1365  
1366              this.collection.sync( 'create', _.pick( this.reply.attributes, ['thread_id', 'content' ] ), {
1367                  success : _.bind( this.replySent, this ),
1368                  error   : _.bind( this.replyError, this )
1369              } );
1370          },
1371  
1372          replySent: function( response ) {
1373              var reply = this.collection.parse( response );
1374  
1375              // Reset the form.
1376              tinyMCE.activeEditor.setContent( '' );
1377              this.reply.set( 'sending', false );
1378  
1379              this.collection.add( _.first( reply ) );
1380          },
1381  
1382          replyError: function( response ) {
1383              if ( response.feedback && response.type ) {
1384                  bp.Nouveau.Messages.displayFeedback( response.feedback, response.type );
1385              }
1386          }
1387      } );
1388  
1389      bp.Nouveau.Messages.Router = Backbone.Router.extend( {
1390          routes: {
1391              'compose/'    : 'composeMessage',
1392              'view/:id/'   : 'viewMessage',
1393              'sentbox/'    : 'sentboxView',
1394              'starred/'    : 'starredView',
1395              'inbox/'      : 'inboxView',
1396              ''            : 'inboxView',
1397              '*unSupported': 'unSupported'
1398          },
1399  
1400          composeMessage: function() {
1401              bp.Nouveau.Messages.composeView();
1402          },
1403  
1404          viewMessage: function( thread_id ) {
1405              if ( ! thread_id ) {
1406                  return;
1407              }
1408  
1409              // Try to get the corresponding thread.
1410              var thread = bp.Nouveau.Messages.threads.get( thread_id );
1411  
1412              if ( undefined === thread ) {
1413                  thread    = {};
1414                  thread.id = thread_id;
1415              }
1416  
1417              bp.Nouveau.Messages.singleView( thread );
1418          },
1419  
1420          sentboxView: function() {
1421              bp.Nouveau.Messages.box = 'sentbox';
1422              bp.Nouveau.Messages.threadsView();
1423          },
1424  
1425          starredView: function() {
1426              bp.Nouveau.Messages.box = 'starred';
1427              bp.Nouveau.Messages.threadsView();
1428          },
1429  
1430          unSupported: function() {
1431              bp.Nouveau.Messages.box = 'unsupported';
1432          },
1433  
1434          inboxView: function() {
1435              bp.Nouveau.Messages.box = 'inbox';
1436              bp.Nouveau.Messages.threadsView();
1437          }
1438      } );
1439  
1440      // Launch BP Nouveau Groups.
1441      bp.Nouveau.Messages.start();
1442  
1443  } )( window.bp, jQuery );


Generated: Wed May 12 01:01:43 2021 Cross-referenced by PHPXref 0.7.1