PluginProbe
Friends / 4.3.0
Friends v4.3.0
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
friends / friends.js

friends.js in Friends 4.3.0, at friends.js

2,037 lines 60.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* global jQuery */
2 ( function ( $, wp, friends ) {
3 const $document = $( document );
4 let alreadySearching = '';
5 let alreadyLoading = false;
6 const bottomOffsetLoadNext = 2000;
7 let searchResultFocused = false;
8
9 wp = wp || { ajax: { send() {}, post() {} } };
10
11 $document.on(
12 'keydown',
13 'input#master-search, input.master-search, .form-autocomplete a',
14 function ( e ) {
15 const code = e.keyCode ? e.keyCode : e.which;
16 const results = $( this )
17 .closest( '.form-autocomplete' )
18 .find( '.menu .menu-item a' );
19 if ( 40 === code ) {
20 if ( false === searchResultFocused ) {
21 searchResultFocused = 0;
22 } else if ( searchResultFocused + 1 < results.length ) {
23 searchResultFocused += 1;
24 }
25 } else if ( 38 === code ) {
26 if ( false === searchResultFocused ) {
27 searchResultFocused = -1;
28 } else {
29 searchResultFocused -= 1;
30 if ( -1 === searchResultFocused ) {
31 $( this ).closest( '.form-autocomplete' ).find( 'input.master-search, input#master-search' ).focus();
32 return false;
33 }
34 }
35 } else {
36 return;
37 }
38
39 searchResultFocused = ( results.length + searchResultFocused ) % results.length;
40
41 const el = results.get( searchResultFocused );
42 if ( el ) {
43 el.focus();
44 } else {
45 searchResultFocused = results.length;
46 }
47 return false;
48 }
49 );
50
51 $document.on( 'keyup', 'input#master-search, input.master-search', function () {
52 const input = $( this );
53 const query = input.val().trim();
54
55 if ( alreadySearching === query ) {
56 return;
57 }
58 const menu = input.closest( '.form-autocomplete' ).find( '.menu' );
59
60 if ( ! query ) {
61 menu.hide();
62 return;
63 }
64
65 const searchIndicator = input
66 .closest( '.form-autocomplete-input' )
67 .find( '.form-icon' );
68
69 wp.ajax.send( 'friends-autocomplete', {
70 data: {
71 _ajax_nonce: input.data( 'nonce' ),
72 q: query,
73 },
74 beforeSend() {
75 searchIndicator.addClass( 'loading' );
76 alreadySearching = query;
77 },
78 success( results ) {
79 searchIndicator.removeClass( 'loading' );
80 searchResultFocused = false;
81 if ( results ) {
82 menu.html( results ).show();
83 } else {
84 menu.hide();
85 }
86 },
87 } );
88 } );
89
90 $document.on( 'keydown', function ( e ) {
91 const searchDialog = $( '.search-dialog' );
92 if ( searchDialog.length ) {
93 if (
94 (e.metaKey && 75 === e.keyCode ) // cmd-k.
95 || ( e.ctrlKey && 75 === e.keyCode ) // ctrl-p.
96 ) {
97 // move all the childnodes of section.navbar-section search into the .search-dialog:
98 searchDialog.append( $( '.navbar-section.search' ).children() );
99
100 // create a new dialog
101 if ( searchDialog.is( ':visible' ) ) {
102 searchDialog.hide();
103 $( '.navbar-section.search' ).append( searchDialog.children() );
104 } else {
105 searchDialog.show();
106 }
107
108 $( 'input#master-search' ).focus();
109 return false;
110 }
111
112 if ( 27 === e.keyCode ) {
113 searchDialog.hide();
114 $( '.navbar-section.search' ).append( $( '.search-dialog' ).children() );
115 }
116 }
117 if ( 69 === e.keyCode && ! e.metaKey && ! e.ctrlKey && ! $( e.target ).is( 'input, textarea' ) ) {
118 const links = $( 'header .post-edit-link' );
119 if ( 1 === links.length ) {
120 links[ 0 ].click();
121 }
122 }
123 } );
124
125 const refresh_feeds_now = function() {
126 let $this = $( this );
127 function set_status( t ) {
128 if ( ! $this.length ) {
129 $this = $( 'a.friends-refresh' );
130 }
131 if ( ! $this.find( 'span' ).length ) {
132 $this.html( '<span></span> <i class="loading"></i> ' );
133 $this.find( 'i' ).css( 'margin-left', '1em' );
134 $this.find( 'i' ).css( 'margin-right', '1em' );
135 }
136 $this.find( 'span' ).text( t );
137 }
138 set_status( friends.text_refreshing );
139
140 $.ajax( {
141 url: friends.rest_base + 'get-feeds',
142 beforeSend: function(xhr){
143 xhr.setRequestHeader( 'X-WP-Nonce', friends.rest_nonce );
144 },
145 }
146 ).done( function( feeds ) {
147 const feed_count = feeds.length;
148 let alreadyLoading = false;
149 function update_page() {
150 if ( alreadyLoading ) {
151 return;
152 }
153 wp.ajax.send( 'friends-load-next-page', {
154 data: {
155 query_vars: friends.query_vars,
156 page: friends.current_page - 1,
157 qv_sign: friends.qv_sign,
158 },
159 beforeSend() {
160 alreadyLoading = true;
161 },
162 success( newPosts ) {
163 alreadyLoading = false;
164 if ( newPosts ) {
165 $( 'section.posts' )
166 .html( newPosts );
167 }
168 },
169 } );
170 }
171 function fetch_next() {
172 if ( ! feeds.length ) {
173 $this.html( friends.text_refreshed + '<i class="dashicons dashicons-yes">' );
174 alreadyLoading = false;
175 update_page();
176 return;
177 }
178 const feed = feeds.shift();
179 $.ajax( {
180 url: friends.rest_base + 'refresh-feed',
181 method: 'POST',
182 data: { id: feed.id },
183 beforeSend: function(xhr){
184 xhr.setRequestHeader( 'X-WP-Nonce', friends.rest_nonce );
185 },
186 } ).always( function( data ) {
187 set_status( ( feed_count - feeds.length ) + ' / ' + feed_count );
188 if ( data.new_posts ) {
189 update_page();
190 }
191 setTimeout( fetch_next, 1 );
192 } );
193 }
194 fetch_next();
195 } );
196 return false;
197 };
198 $document.on( 'click', 'a.friends-refresh', refresh_feeds_now );
199
200 if ( 'true' === friends.refresh_now ) {
201 refresh_feeds_now.apply( $( 'a.friends-refresh' ) );
202 }
203
204 $( function () {
205 $( '.activitypub-follower-check' ).each( function () {
206 const $pill = $( this );
207 wp.ajax.send( 'friends_check_activitypub_follower', {
208 data: {
209 _ajax_nonce: $pill.data( 'nonce' ),
210 actor_url: $pill.data( 'actor-url' ),
211 },
212 success( result ) {
213 $pill
214 .removeClass( 'is-loading is-error' )
215 .toggleClass( 'is-following', !! result.follows )
216 .toggleClass( 'is-not-following', ! result.follows )
217 .text( result.label )
218 .attr( 'title', result.title );
219 },
220 error( result ) {
221 const message = Array.isArray( result )
222 ? result.map( function( error ) { return error.message || error.code; } ).join( ', ' )
223 : friends.text_error;
224 $pill
225 .removeClass( 'is-loading is-following is-not-following' )
226 .addClass( 'is-error' )
227 .text( friends.text_error )
228 .attr( 'title', message );
229 },
230 } );
231 } );
232
233 const standard_count = $( '.chip.post-count-standard' );
234 if ( standard_count.text().substr( 0, 3 ) === '...' || standard_count.text().substr( 0, 1 ) === '…' ) {
235 wp.ajax.send( 'friends-get-post-counts', {
236 data: {
237 _ajax_nonce: standard_count.data( 'nonce' )
238 },
239 success( r ) {
240 for ( const i in r ) {
241 if ( ! r[ i ] ) {
242 $( '.chip.post-count-' + i ).remove();
243 continue;
244 }
245 $( '.chip.post-count-' + i ).text( r[ i ] );
246 }
247 },
248 } );
249 return false;
250
251 }
252 });
253
254
255 $( function () {
256 $( '#only_subscribe' ).on( 'change', function () {
257 $( this )
258 .closest( 'form' )
259 .find( '#submit' )
260 .text( this.checked ? '1' : '2' );
261 } );
262 $( '#post-format-switcher select' ).on( 'change', function () {
263 const re = new RegExp( '/type/[^/$]+' ),
264 v = $( this ).val();
265 let url;
266 const urlPart = v ? '/type/' + v + '/' : '/';
267 if ( window.location.href.match( re ) ) {
268 url = window.location.href.replace( re, urlPart );
269 } else {
270 url = window.location.href.replace( /\/$/, '' ) + urlPart;
271 }
272 window.location.href = url;
273 } );
274 } );
275
276 $document.on( 'click', 'a.friends-trash-post', function () {
277 const button = $( this );
278
279 wp.ajax.send( 'trash-post', {
280 data: {
281 _ajax_nonce: button.data( 'trash-nonce' ),
282 id: button.data( 'id' ),
283 },
284 success() {
285 button
286 .text( friends.text_undo )
287 .attr( 'class', 'friends-untrash-post' );
288 },
289 } );
290 return false;
291 } );
292
293 $document.on( 'click', 'a.friends-untrash-post', function () {
294 const button = $( this );
295
296 wp.ajax.send( 'untrash-post', {
297 data: {
298 _ajax_nonce: button.data( 'untrash-nonce' ),
299 id: button.data( 'id' ),
300 },
301 success() {
302 button
303 .text( friends.text_trash_post )
304 .attr( 'class', 'friends-trash-post' );
305 },
306 } );
307 return false;
308 } );
309
310 let openMenu = null;
311 $document.on( 'click', 'a.friends-dropdown-toggle', function ( e ) {
312 const $this = $( this );
313 const dropdown = $this.next( '.menu' );
314 if ( dropdown.is( ':visible' ) ) {
315 dropdown.hide();
316 openMenu = null;
317 } else {
318 $( '.menu:not(.menu-nav)' ).hide();
319 dropdown.show();
320 openMenu = dropdown;
321 }
322 e.stopPropagation();
323 return false;
324 });
325 $document.on( 'click', function ( e ) {
326 if ( e.target.closest( '.friends-dropdown' ) ) {
327 return true;
328 }
329 if ( openMenu ) {
330 openMenu.hide();
331 }
332 } );
333
334
335 function loadComments( commentsLink, callback ) {
336 const $this = $( commentsLink );
337 let content = $this.closest( 'article' ).find( '.comments-content' );
338 if ( ! content.length ) {
339 content = $this.closest( '.wp-block-friends-post-comments' ).find( '.comments-content' );
340 }
341 if ( ! content.length ) {
342 content = $this.closest( 'li' ).find( '.comments-content' );
343 }
344 if ( content.data( 'loaded' ) ) {
345 content.toggle();
346 } else {
347 content.show();
348 content.html( '<span></span> <i class="loading"></i>' );
349 content.find( 'i' ).css( 'margin-left', '1em' );
350 content.find( 'i' ).css( 'margin-right', '1em' );
351 content.find( 'span' ).text( friends.text_loading_comments );
352 let stillLoading = setTimeout( function () {
353 content.find( 'span' ).text( friends.text_still_loading );
354 }, 2000 );
355
356 wp.ajax.send( 'friends-load-comments', {
357 data: {
358 _ajax_nonce: $this.data( 'cnonce' ),
359 post_id: $this.data( 'id' ),
360 },
361 success( comments ) {
362 content.html( comments ).data( 'loaded', true );
363 clearTimeout( stillLoading );
364 callback();
365 },
366 error( message ) {
367 content.html( message ).data( 'loaded', true );
368 clearTimeout( stillLoading );
369 },
370 } );
371 }
372 }
373
374 $document.on( 'click', 'article a.comments, .wp-block-friends-post-comments a.comments', function ( e ) {
375 if ( e.metaKey || e.altKey || e.shiftKey ) {
376 return;
377 }
378
379 loadComments.call( this );
380 return false;
381 } );
382
383 $document.on( 'change', 'select.friends-change-post-format', function () {
384 const select = $( this );
385
386 wp.ajax.send( 'friends-change-post-format', {
387 data: {
388 _ajax_nonce: select.data( 'change-post-format-nonce' ),
389 id: select.data( 'id' ),
390 format: select.val(),
391 },
392 success() {
393 // TODO: add some visual indication.
394 },
395 } );
396 return false;
397 } );
398
399 $( window ).scroll( function () {
400 if (
401 $( document ).scrollTop() <
402 $( document ).height() - bottomOffsetLoadNext
403 ) {
404 return;
405 }
406 nextPage();
407 } );
408
409 $( document ).on( 'click', '.nav-previous', function () {
410 nextPage();
411 return false;
412 } );
413
414 function nextPage() {
415 if ( alreadyLoading ) {
416 return;
417 }
418
419 if ( friends.current_page >= friends.max_page ) {
420 return;
421 }
422
423 wp.ajax.send( 'friends-load-next-page', {
424 data: {
425 query_vars: friends.query_vars,
426 page: friends.current_page,
427 qv_sign: friends.qv_sign,
428 },
429 beforeSend() {
430 $( 'section.posts .posts-navigation .nav-previous' ).addClass(
431 'loading'
432 );
433 alreadyLoading = true;
434 },
435 success( newPosts ) {
436 $(
437 'section.posts .posts-navigation .nav-previous'
438 ).removeClass( 'loading' );
439 if ( newPosts ) {
440 $( 'section.posts > article' ).last().after( newPosts );
441 if (
442 ++friends.current_page < friends.max_page &&
443 /<article/.test( newPosts )
444 ) {
445 alreadyLoading = false;
446 } else {
447 $( 'section.posts .posts-navigation' ).text(
448 friends.text_no_more_posts
449 );
450 }
451 }
452 },
453 } );
454 }
455
456 /* Reactions */
457 $( function () {
458 $document.on( 'click', 'button.friends-reaction', function () {
459 const $this = $( this );
460 $this.addClass( 'loading' );
461 wp.ajax.send( 'friends-toggle-react', {
462 data: {
463 _ajax_nonce: $this.data( 'nonce' ),
464 post_id: $this.data( 'id' ),
465 reaction: $this.data( 'emoji' ),
466 },
467 success() {
468 $this.removeClass( 'loading' );
469 var isPressed = $this.hasClass( 'pressed' );
470 $this.toggleClass( 'pressed' );
471 var textNodes = $this.contents().filter( function () {
472 return this.nodeType === 3;
473 } );
474 var count = parseInt( textNodes.last().text().trim(), 10 ) || 0;
475 var newCount = isPressed ? count - 1 : count + 1;
476 if ( newCount <= 0 ) {
477 $this.remove();
478 } else {
479 textNodes.last().replaceWith( ' ' + newCount );
480 }
481 },
482 error() {
483 $this.removeClass( 'loading' );
484 },
485 } );
486 return false;
487 } );
488
489 $( '.friends-reaction-picker' ).on( 'click', 'button', function () {
490 const $this = $( this );
491 const $picker = $this.closest( '.friends-reaction-picker' );
492 $this.addClass( 'loading' );
493 wp.ajax.send( 'friends-toggle-react', {
494 data: {
495 _ajax_nonce: $picker.data( 'nonce' ),
496 post_id: $picker.data( 'id' ),
497 reaction: $this.data( 'emoji' ),
498 },
499 success() {
500 $this.removeClass( 'loading' );
501 var postId = $picker.data( 'id' );
502 var emoji = $this.data( 'emoji' );
503 var emojiChar = $this.text().trim();
504 var $footer = $picker.closest( '.card-footer, .entry-meta, .friends-post-footer, footer' );
505 var $existing = $footer.find( 'button.friends-reaction[data-emoji="' + emoji + '"]' );
506 if ( $existing.length ) {
507 var textNodes = $existing.contents().filter( function () {
508 return this.nodeType === 3;
509 } );
510 var count = parseInt( textNodes.last().text().trim(), 10 ) || 0;
511 textNodes.last().replaceWith( ' ' + ( count + 1 ) );
512 $existing.addClass( 'pressed' );
513 } else {
514 var nonce = $picker.data( 'nonce' );
515 var $btn = $( '<button class="btn btn-link ml-1 friends-action friends-reaction pressed" data-id="' + postId + '" data-emoji="' + emoji + '" data-nonce="' + nonce + '"><span>' + emojiChar + '</span> 1</button>' );
516 $picker.closest( '.friends-dropdown' ).before( $btn );
517 }
518 },
519 error() {
520 $this.removeClass( 'loading' );
521 },
522 } );
523 return false;
524 } );
525 } );
526
527 $( function () {
528 $document.on( 'click', 'a.friends-reblog', function () {
529 const $this = $( this );
530 wp.ajax.send( 'friends-reblog', {
531 data: {
532 _ajax_nonce: $this.data( 'nonce' ),
533 post_id: $this.data( 'id' ),
534 },
535 success( result ) {
536 if ( result.redirect ) {
537 location.href = result.redirect;
538 }
539 $this
540 .find( 'i.friends-reblog-status' )
541 .addClass( 'dashicons dashicons-saved' );
542 },
543 } );
544 return false;
545 } );
546 } );
547
548 $( function () {
549 $document.on( 'click', 'a.friends-boost', function () {
550 const $this = $( this );
551 $this.addClass( 'loading' );
552 wp.ajax.send( 'friends-boost', {
553 data: {
554 _ajax_nonce: $this.data( 'nonce' ),
555 post_id: $this.data( 'id' ),
556 },
557 success( result ) {
558 $this.removeClass( 'loading' );
559 if ( 'boosted' === result ) {
560 $this
561 .find( 'i.friends-boost-status' )
562 .removeClass( 'dashicons-warning' )
563 .addClass( 'dashicons dashicons-saved' );
564 } else if ( 'unboosted' === result ) {
565 $this
566 .find( 'i.friends-boost-status' )
567 .removeClass( 'dashicons dashicons-warning dashicons-saved' )
568 } else {
569 $this
570 .find( 'i.friends-boost-status' )
571 .addClass( 'dashicons dashicons-warning' )
572 .removeClass( 'dashicons-saved' )
573 .attr( 'title', result );
574 }
575 },
576 fail( result ) {
577 $this.removeClass( 'loading' );
578 $this
579 .find( 'i.friends-boost-status' )
580 .addClass( 'dashicons dashicons-warning' )
581 .removeClass( 'dashicons-saved' )
582 .attr( 'title', result );
583 }
584 } );
585 return false;
586 } );
587 } );
588
589 $document.on( 'click', 'a.send-new-message', function () {
590 $( '#friends-send-new-message' )
591 .toggle()
592 .find(
593 'textarea:visible, .block-editor-default-block-appender__content'
594 )
595 .focus();
596 return false;
597 } );
598
599 $document.on( 'click', 'button.delete-conversation', function () {
600 // eslint-disable-next-line no-alert
601 return window.confirm( friends.text_del_convers );
602 } );
603
604 function getMessageDraftKey( form, field ) {
605 const key = $( form ).data( 'friends-message-draft-key' );
606 if ( ! key ) {
607 return '';
608 }
609
610 return key + ':' + field;
611 }
612
613 function getMessageDraftStorage() {
614 try {
615 return window.localStorage;
616 } catch ( e ) {
617 return false;
618 }
619 }
620
621 function restoreMessageDrafts() {
622 const storage = getMessageDraftStorage();
623 if ( ! storage ) {
624 return;
625 }
626
627 $( 'form[data-friends-message-draft-key]' ).each( function () {
628 const form = this;
629 const messageKey = getMessageDraftKey( form, 'message' );
630 const subjectKey = getMessageDraftKey( form, 'subject' );
631 const message = storage.getItem( messageKey );
632 const subject = storage.getItem( subjectKey );
633
634 if ( message ) {
635 $( form ).find( 'textarea[name="friends_message_message"]' ).val( message );
636 }
637
638 if ( subject ) {
639 $( form ).find( 'input[name="friends_message_subject"]:not([type="hidden"])' ).val( subject );
640 }
641 } );
642 }
643
644 $document.on( 'input', 'textarea[name="friends_message_message"], input[name="friends_message_subject"]', function () {
645 const storage = getMessageDraftStorage();
646 if ( ! storage ) {
647 return;
648 }
649
650 const form = this.form;
651 const field = 'friends_message_subject' === this.name ? 'subject' : 'message';
652 const key = getMessageDraftKey( form, field );
653 if ( ! key ) {
654 return;
655 }
656
657 if ( this.value ) {
658 storage.setItem( key, this.value );
659 } else {
660 storage.removeItem( key );
661 }
662 } );
663
664 $document.on( 'submit', 'form[data-friends-message-draft-key]', function () {
665 const storage = getMessageDraftStorage();
666 if ( ! storage ) {
667 return;
668 }
669
670 storage.removeItem( getMessageDraftKey( this, 'message' ) );
671 storage.removeItem( getMessageDraftKey( this, 'subject' ) );
672 } );
673
674 $document.on( 'click', 'a.display-message', function () {
675 const $this = $( this ).closest( 'div' );
676 const conversation = $this.find( 'div.conversation' );
677 if ( conversation.is( ':visible' ) ) {
678 conversation.hide();
679 } else {
680 conversation.show();
681 const messages = conversation.find( '.messages' ).get( 0 );
682 // scroll smoothly to the bottom of the overflow div:
683 $( messages ).animate( { scrollTop: messages.scrollHeight }, 1000 );
684
685 $this.find( 'a.display-message' ).removeClass( 'unread' );
686 wp.ajax.send( 'friends-mark-read', {
687 data: {
688 _ajax_nonce: $this.data( 'nonce' ),
689 post_id: $this.data( 'id' ),
690 },
691 success() {},
692 } );
693 conversation
694 .find(
695 'textarea:visible, .block-editor-default-block-appender__content'
696 )
697 .focus();
698 }
699
700 return false;
701 } );
702
703 function getRelativeTime( timestamp ) {
704 const seconds = Math.max( 1, Math.floor( Date.now() / 1000 ) - timestamp );
705 const minute = 60;
706 const hour = 60 * minute;
707 const day = 24 * hour;
708 const week = 7 * day;
709 const month = 30 * day;
710 const year = 365 * day;
711
712 if ( seconds < minute ) {
713 return seconds + ' seconds';
714 }
715 if ( seconds < hour ) {
716 const minutes = Math.floor( seconds / minute );
717 return minutes + ' minute' + ( 1 === minutes ? '' : 's' );
718 }
719 if ( seconds < day ) {
720 const hours = Math.floor( seconds / hour );
721 return hours + ' hour' + ( 1 === hours ? '' : 's' );
722 }
723 if ( seconds < week ) {
724 const days = Math.floor( seconds / day );
725 return days + ' day' + ( 1 === days ? '' : 's' );
726 }
727 if ( seconds < month ) {
728 const weeks = Math.floor( seconds / week );
729 return weeks + ' week' + ( 1 === weeks ? '' : 's' );
730 }
731 if ( seconds < year ) {
732 const months = Math.floor( seconds / month );
733 return months + ' month' + ( 1 === months ? '' : 's' );
734 }
735
736 const years = Math.floor( seconds / year );
737 return years + ' year' + ( 1 === years ? '' : 's' );
738 }
739
740 function updateRelativeTimes( scope ) {
741 $( scope || document ).find( 'time[data-friends-relative-time]' ).each( function () {
742 const timestamp = parseInt( $( this ).data( 'friends-relative-time' ), 10 );
743 if ( ! timestamp ) {
744 return;
745 }
746
747 $( this ).text( getRelativeTime( timestamp ) + ( $( this ).data( 'friends-relative-time-suffix' ) || '' ) );
748 } );
749 }
750
751 function markCurrentDmThreadRead() {
752 const $thread = $( '.friends-dm-thread' );
753 if ( ! $thread.length ) {
754 return;
755 }
756
757 if ( '1' !== String( $thread.data( 'unread' ) ) ) {
758 return;
759 }
760
761 wp.ajax.send( 'friends-mark-read', {
762 data: {
763 _ajax_nonce: $thread.data( 'nonce' ),
764 post_id: $thread.data( 'id' ),
765 },
766 success() {
767 $( '.friends-dm-conversation.is-selected' )
768 .removeClass( 'is-unread' )
769 .find( '.friends-dm-unread-count' )
770 .remove();
771 $thread.data( 'unread', '0' ).attr( 'data-unread', '0' );
772 },
773 } );
774 }
775
776 const dmMobileQuery = window.matchMedia ? window.matchMedia( '(max-width: 840px)' ) : null;
777
778 function isMobileDmView() {
779 return dmMobileQuery && dmMobileQuery.matches;
780 }
781
782 function hasFocusedDmHash() {
783 const $thread = $( '.friends-dm-thread' );
784 return !! $thread.length && window.location.hash === '#' + $thread.attr( 'id' );
785 }
786
787 function isDmConversationVisible() {
788 return ! isMobileDmView() || hasFocusedDmHash();
789 }
790
791 function scrollCurrentDmThreadToBottom() {
792 const messages = $( '.friends-dm-thread' ).find( '.friends-dm-messages' ).get( 0 );
793 if ( messages ) {
794 messages.scrollTop = messages.scrollHeight;
795 }
796 }
797
798 function updateDmHashState() {
799 const $view = $( '.friends-dm-view' );
800 if ( ! $view.length ) {
801 return;
802 }
803
804 $view.toggleClass( 'is-focused-conversation', hasFocusedDmHash() );
805 if ( isDmConversationVisible() ) {
806 scrollCurrentDmThreadToBottom();
807 markCurrentDmThreadRead();
808 }
809 }
810
811 function updateDmViewportHeight() {
812 if ( ! $( '.friends-dm-view' ).length ) {
813 return;
814 }
815
816 const viewport = window.visualViewport || window;
817 const height = viewport.height || window.innerHeight;
818 if ( height ) {
819 document.documentElement.style.setProperty( '--friends-dm-viewport-height', height + 'px' );
820 }
821 }
822
823 function watchDmViewportHeight() {
824 if ( ! $( '.friends-dm-view' ).length ) {
825 return;
826 }
827
828 let frame = null;
829 const scheduleUpdate = function () {
830 if ( frame ) {
831 window.cancelAnimationFrame( frame );
832 }
833 frame = window.requestAnimationFrame( updateDmViewportHeight );
834 };
835
836 updateDmViewportHeight();
837 $( window ).on( 'resize orientationchange', scheduleUpdate );
838 if ( window.visualViewport ) {
839 window.visualViewport.addEventListener( 'resize', scheduleUpdate );
840 window.visualViewport.addEventListener( 'scroll', scheduleUpdate );
841 }
842 if ( dmMobileQuery && dmMobileQuery.addEventListener ) {
843 dmMobileQuery.addEventListener( 'change', updateDmHashState );
844 } else if ( dmMobileQuery && dmMobileQuery.addListener ) {
845 dmMobileQuery.addListener( updateDmHashState );
846 }
847 }
848
849 $document.on( 'focus blur', '.friends-dm-reply input, .friends-dm-reply select, .friends-dm-reply textarea', function () {
850 const reply = $( this ).closest( '.friends-dm-reply' ).get( 0 );
851 window.setTimeout( function () {
852 updateDmViewportHeight();
853 if ( reply ) {
854 reply.scrollIntoView( { block: 'end' } );
855 }
856 }, 300 );
857 } );
858
859 let dmDeliveryTooltipTimeout = null;
860
861 function hideDmDeliveryTooltip() {
862 window.clearTimeout( dmDeliveryTooltipTimeout );
863 dmDeliveryTooltipTimeout = null;
864 $( '.friends-dm-delivery-tooltip' ).remove();
865 $( '.friends-dm-delivery-status.is-showing-title' ).removeClass( 'is-showing-title' );
866 }
867
868 function showDmDeliveryTooltip( element ) {
869 const $status = $( element );
870 const title = $status.attr( 'title' );
871 if ( ! title ) {
872 return;
873 }
874
875 hideDmDeliveryTooltip();
876 $status.addClass( 'is-showing-title' );
877
878 const rect = element.getBoundingClientRect();
879 const $tooltip = $( '<div class="friends-dm-delivery-tooltip" role="tooltip"></div>' ).text( title );
880 $( document.body ).append( $tooltip );
881
882 const margin = 8;
883 const width = $tooltip.outerWidth();
884 const height = $tooltip.outerHeight();
885 const left = Math.min( Math.max( margin, rect.right - width ), window.innerWidth - width - margin );
886 let top = rect.top - height - margin;
887 if ( top < margin ) {
888 top = rect.bottom + margin;
889 }
890
891 $tooltip.css( {
892 left: left + 'px',
893 top: top + 'px',
894 } );
895
896 dmDeliveryTooltipTimeout = window.setTimeout( hideDmDeliveryTooltip, 5000 );
897 }
898
899 $document.on( 'click', '.friends-dm-delivery-status[title]', function ( event ) {
900 const $status = $( this );
901 if ( ! $status.attr( 'title' ) ) {
902 return;
903 }
904
905 event.preventDefault();
906 event.stopPropagation();
907 if ( $status.hasClass( 'is-showing-title' ) ) {
908 hideDmDeliveryTooltip();
909 } else {
910 showDmDeliveryTooltip( this );
911 }
912 } );
913
914 $document.on( 'click', hideDmDeliveryTooltip );
915 $( window ).on( 'resize scroll', hideDmDeliveryTooltip );
916
917 function updateDmDeliveryStatusElement( $status, delivery ) {
918 if ( ! delivery || ! delivery.status ) {
919 return;
920 }
921
922 const classes = ( $status.attr( 'class' ) || '' )
923 .split( /\s+/ )
924 .filter( function ( className ) {
925 return className && ! className.match( /^is-/ );
926 } );
927 classes.push( 'is-' + delivery.status );
928
929 $status
930 .attr( 'class', classes.join( ' ' ) )
931 .attr( 'title', delivery.title || '' )
932 .attr( 'aria-label', delivery.label || '' )
933 .text( $status.hasClass( 'friends-dm-delivery-icon' ) ? '' : delivery.label || '' );
934 }
935
936 let isRefreshingDmDeliveryStatuses = false;
937
938 function refreshDmDeliveryStatuses() {
939 const $statuses = $( '.friends-dm-delivery-status[data-delivery-message-id]' );
940 if ( isRefreshingDmDeliveryStatuses || ! $statuses.length || ! friends.message_delivery_nonce ) {
941 return;
942 }
943
944 const postIds = $statuses.map( function () {
945 return $( this ).data( 'delivery-message-id' );
946 } ).get().filter( function ( postId, index, postIds ) {
947 return postId && postIds.indexOf( postId ) === index;
948 } );
949
950 if ( ! postIds.length ) {
951 return;
952 }
953
954 isRefreshingDmDeliveryStatuses = true;
955
956 wp.ajax.send( 'friends-get-message-delivery-statuses', {
957 data: {
958 _ajax_nonce: friends.message_delivery_nonce,
959 post_ids: postIds,
960 },
961 success( response ) {
962 const statuses = response.statuses || {};
963 Object.keys( statuses ).forEach( function ( postId ) {
964 updateDmDeliveryStatusElement(
965 $( '.friends-dm-delivery-status[data-delivery-message-id="' + postId + '"]' ),
966 statuses[ postId ]
967 );
968 } );
969 },
970 error() {
971 isRefreshingDmDeliveryStatuses = false;
972 },
973 } ).always( function () {
974 isRefreshingDmDeliveryStatuses = false;
975 } );
976 }
977
978 $( function () {
979 restoreMessageDrafts();
980 updateRelativeTimes( document );
981 watchDmViewportHeight();
982 updateDmHashState();
983 $( window ).on( 'hashchange', updateDmHashState );
984
985 const $thread = $( '.friends-dm-thread' );
986 if ( ! $thread.length ) {
987 return;
988 }
989
990 window.setInterval( updateRelativeTimes, 60000 );
991 window.setInterval( refreshDmDeliveryStatuses, 5000 );
992 refreshDmDeliveryStatuses();
993 } );
994
995 $document.on( 'mouseenter', 'h2#page-title a.dashicons, a.wp-block-friends-author-star.dashicons', function () {
996 if ( $( this ).hasClass( 'not-starred' ) ) {
997 if ( $( this ).hasClass( 'dashicons-star-empty' ) ) {
998 $( this )
999 .removeClass( 'dashicons-star-empty' )
1000 .addClass( 'dashicons-star-filled' );
1001 }
1002 } else if ( $( this ).hasClass( 'starred' ) ) {
1003 if ( $( this ).hasClass( 'dashicons-star-filled' ) ) {
1004 $( this )
1005 .removeClass( 'dashicons-star-filled' )
1006 .addClass( 'dashicons-star-empty' );
1007 }
1008 }
1009 } );
1010
1011 $document.on( 'mouseleave', 'h2#page-title a.dashicons, a.wp-block-friends-author-star.dashicons', function () {
1012 if ( $( this ).hasClass( 'not-starred' ) ) {
1013 if ( $( this ).hasClass( 'dashicons-star-filled' ) ) {
1014 $( this )
1015 .removeClass( 'dashicons-star-filled' )
1016 .addClass( 'dashicons-star-empty' );
1017 }
1018 } else if ( $( this ).hasClass( 'starred' ) ) {
1019 if ( $( this ).hasClass( 'dashicons-star-empty' ) ) {
1020 $( this )
1021 .removeClass( 'dashicons-star-empty' )
1022 .addClass( 'dashicons-star-filled' );
1023 }
1024 }
1025 } );
1026
1027 $document.on(
1028 'click',
1029 'h2#page-title a.dashicons.starred, h2#page-title a.dashicons.not-starred, a.wp-block-friends-author-star.starred, a.wp-block-friends-author-star.not-starred',
1030 function () {
1031 let removeClass = 'dashicons-star-filled starred';
1032 let addClass = 'dashicons-star-empty not-starred';
1033 const $this = $( this );
1034 if ( $this.hasClass( 'not-starred' ) ) {
1035 const s = removeClass;
1036 removeClass = addClass;
1037 addClass = s;
1038 }
1039 wp.ajax.send( 'friends-star', {
1040 data: {
1041 friend_id: $this.data( 'id' ),
1042 _ajax_nonce: $this.data( 'nonce' ),
1043 starred: $this.hasClass( 'starred' ) ? 0 : 1,
1044 },
1045 beforeSend() {
1046 $this
1047 .removeClass(
1048 removeClass +
1049 ' dashicons-star-filled dashicons-star-empty'
1050 )
1051 .addClass( 'form-icon loading' );
1052 },
1053 success() {
1054 $this
1055 .removeClass( 'form-icon loading' )
1056 .addClass( addClass );
1057 },
1058 } );
1059 return false;
1060 }
1061 );
1062
1063 // Folder selector for subscriptions.
1064 $document.on( 'change', '.friends-move-to-folder', function () {
1065 const $select = $( this );
1066 const friendId = $select.data( 'id' );
1067 const nonce = $select.data( 'nonce' );
1068 let folderId = $select.val();
1069
1070 if ( 'new' === folderId ) {
1071 const name = prompt( friends.text_new_folder || 'Folder name:' );
1072 if ( ! name ) {
1073 $select.val( $select.data( 'current' ) || 0 );
1074 return;
1075 }
1076 wp.ajax.send( 'friends-create-folder', {
1077 data: {
1078 name: name,
1079 _ajax_nonce: nonce,
1080 },
1081 success( r ) {
1082 // Add the new option and select it.
1083 $select.find( 'option[value="new"]' ).before(
1084 '<option value="' + r.term_id + '">' + r.name + '</option>'
1085 );
1086 $select.val( r.term_id );
1087 $select.data( 'current', r.term_id );
1088 // Now move the subscription to it.
1089 wp.ajax.send( 'friends-move-to-folder', {
1090 data: {
1091 friend_id: friendId,
1092 folder_id: r.term_id,
1093 _ajax_nonce: nonce,
1094 },
1095 } );
1096 },
1097 } );
1098 return;
1099 }
1100
1101 $select.data( 'current', folderId );
1102 wp.ajax.send( 'friends-move-to-folder', {
1103 data: {
1104 friend_id: friendId,
1105 folder_id: folderId,
1106 _ajax_nonce: nonce,
1107 },
1108 } );
1109 } );
1110
1111 function getAcct( href ) {
1112 const url = new URL( href );
1113 const username = url.pathname.replace( /\/$/, '' ).split( '/' ).pop();
1114 return '@' + username.replace( /^@/, '' ) + '@' + url.hostname;
1115 }
1116
1117 function insertTextInGutenberg( text ) {
1118 let element, val, regex;
1119 if ( jQuery( '.is-root-container p' ).length ) {
1120 element = jQuery( '.is-root-container p' )[ 0 ];
1121 element.focus();
1122 window.getSelection().collapse( element.firstChild, element.textContent.length );
1123 document.execCommand( 'insertText', false, text + ' ' );
1124 } else {
1125 val = text + ' ' + jQuery( '.friends-status-content' ).val();
1126 jQuery( '.friends-status-content' ).val( val.replace( /^(@[^@]+@[^ ]+ )+/g, text + ' ' ) );
1127 }
1128 }
1129
1130 $document.on( 'click', '.quick-reply,a.comments', function () {
1131 const card = $( this ).closest( '.card' );
1132 card.click();
1133 $( this ).closest( '.friends-dropdown' ).hide();
1134 openMenu = null;
1135 let comments = $( this ).closest( '.card' ).find( '.comments' );
1136 if ( ! comments.length ) {
1137 comments = $( this ).closest( 'li' ).find( '.comments' );
1138 }
1139
1140 if ( comments.length ) {
1141 $( 'html, body' ).animate( {
1142 scrollTop: comments.offset().top - 100,
1143 }, 500 );
1144 }
1145
1146 loadComments( comments, function() {
1147 // focus #comment textarea but put the cursor at the end
1148 const comment = card.find( '#comment' );
1149 comment.focus();
1150 const val = comment.val();
1151 comment.val( '' ).val( val );
1152 } );
1153 return false;
1154 } );
1155
1156 $document.on( 'click', '.quick-post-panel-toggle', function () {
1157 $( '#quick-post-panel' ).toggleClass( 'open' );
1158 return false;
1159 } );
1160
1161 // when the ".followers details" html element is expanded
1162 $document.on( 'click', '.followers details', function () {
1163 const $this = $( this );
1164 if ( $this.find( '.loading-posts' ).length ) {
1165 wp.ajax.send( 'friends-preview-activitypub', {
1166 data: {
1167 _ajax_nonce: $this.data( 'nonce' ),
1168 url: $this.data( 'id' ),
1169 followers: $this.data( 'followers' ),
1170 following: $this.data( 'following' ),
1171 },
1172 success( result ) {
1173 $this.find( '.loading-posts' ).hide().after( result.posts );
1174 $this.find( '.their-followers' ).text( result.followers );
1175 $this.find( '.their-following' ).text( result.following );
1176 },
1177 error( result ) {
1178 $this.find( '.loading-posts' ).text( result.map(function( error ) { return error.message || error.code; } ).join( ',' ) );
1179 }
1180 } );
1181 }
1182 } );
1183
1184 $document.on( 'click', '.follower-delete', function () {
1185 const $this = $( this );
1186 if ( ! confirm( friends.text_confirm_delete_follower.replace( /%s/, $this.data( 'handle' )) ) ) {
1187 return false;
1188 }
1189 wp.ajax.send( 'friends-delete-follower', {
1190 data: {
1191 _ajax_nonce: $this.data( 'nonce' ),
1192 id: $this.data( 'id' ),
1193 },
1194 success() {
1195 $this.closest( 'details' ).remove();
1196 },
1197 } );
1198 return false;
1199 } );
1200
1201 $document.on( 'click', '.friends-widget summary', function () {
1202 const $this = $( this ).closest( 'details' );
1203 const previously_open = $this.prop( 'open' );
1204 wp.ajax.send( 'friends-set-widget-open-state', {
1205 data: {
1206 _ajax_nonce: $this.data( 'nonce' ),
1207 widget: $this.data( 'id' ),
1208 state: previously_open ? 'closed' : 'open',
1209 },
1210 success() {},
1211 } );
1212 } );
1213
1214 $document.on( 'click', '.refresh-feeds', function ( e ) {
1215 e.preventDefault();
1216 const $this = $( this );
1217 const originalText = $this.text();
1218 $this.text( friends.text_refreshing || 'Refreshing' );
1219 wp.ajax.send( 'friends-refresh-feeds', {
1220 data: {
1221 _ajax_nonce: $this.data( 'nonce' ),
1222 user: $this.data( 'user' ),
1223 },
1224 success() {
1225 $this.text( friends.text_refreshed || 'Refreshed' );
1226 setTimeout( function () {
1227 window.location.reload();
1228 }, 500 );
1229 },
1230 error( result ) {
1231 $this.text( originalText );
1232 },
1233 } );
1234 } );
1235
1236 let subscriptionPreviewId = 0;
1237 let subscriptionPasteReviewTimer = null;
1238
1239 function friendsText( key, fallback ) {
1240 return friends && friends[ key ] ? friends[ key ] : fallback;
1241 }
1242
1243 function ajaxErrorText( result ) {
1244 if ( $.isArray( result ) ) {
1245 return result.join( ', ' );
1246 }
1247 if ( result && result.message ) {
1248 return result.message;
1249 }
1250 if ( result ) {
1251 return String( result );
1252 }
1253 return friendsText( 'text_error', 'An error occurred.' );
1254 }
1255
1256 function getSubscriptionNonce() {
1257 return $( '#add-subscription-form input[name=_wpnonce]' ).val();
1258 }
1259
1260 function looksLikeOpml( text ) {
1261 if ( ! text ) {
1262 return false;
1263 }
1264 const trimmed = text.trim();
1265 if ( trimmed.charAt( 0 ) !== '<' ) {
1266 return false;
1267 }
1268 return /<\s*(opml|outline)\b/i.test( trimmed );
1269 }
1270
1271 function parsePastedPeopleList( text ) {
1272 const seen = {};
1273 const items = [];
1274 const lines = text.split( /\r?\n/ );
1275 for ( let i = 0; i < lines.length; i++ ) {
1276 const line = lines[ i ]
1277 .replace( /^\s*[-*]\s+/, '' )
1278 .replace( /^\s*\d+[.)]\s+/, '' )
1279 .trim();
1280 if ( ! line || seen[ line ] ) {
1281 continue;
1282 }
1283 seen[ line ] = true;
1284 items.push( {
1285 url: line,
1286 text: line,
1287 } );
1288 }
1289 return items.length > 1 ? items : [];
1290 }
1291
1292 function parseOpmlItems( opmlText ) {
1293 const parser = new DOMParser();
1294 const doc = parser.parseFromString( opmlText, 'application/xml' );
1295 if ( doc.getElementsByTagName( 'parsererror' ).length ) {
1296 return {
1297 error: friendsText( 'text_opml_invalid', 'Could not parse the OPML file.' ),
1298 };
1299 }
1300
1301 const outlines = doc.querySelectorAll( 'outline[xmlUrl]' );
1302 if ( ! outlines.length ) {
1303 return {
1304 error: friendsText( 'text_opml_no_feeds', 'No feeds were found in the OPML file.' ),
1305 };
1306 }
1307
1308 const items = [];
1309 $( outlines ).each( function () {
1310 const xmlUrl = this.getAttribute( 'xmlUrl' ) || '';
1311 const htmlUrl = this.getAttribute( 'htmlUrl' ) || '';
1312 items.push( {
1313 url: htmlUrl || xmlUrl,
1314 feedUrl: xmlUrl,
1315 htmlUrl: htmlUrl,
1316 text: this.getAttribute( 'text' ) || this.getAttribute( 'title' ) || xmlUrl,
1317 } );
1318 } );
1319
1320 return { items: items };
1321 }
1322
1323 function setSubscriptionPreviewError( message ) {
1324 $( '#preview-subscription' ).empty().append( $( '<p class="error"></p>' ).text( message ) );
1325 }
1326
1327 function createSelect( options, selected, className ) {
1328 const $select = $( '<select></select>' ).addClass( 'form-select ' + className );
1329 let selectedFound = false;
1330 $.each( options, function ( value, label ) {
1331 const $option = $( '<option></option>' ).attr( 'value', value ).text( label );
1332 if ( selected === value ) {
1333 $option.prop( 'selected', true );
1334 selectedFound = true;
1335 }
1336 $select.append( $option );
1337 } );
1338 if ( selected && ! selectedFound ) {
1339 $select.append( $( '<option></option>' ).attr( 'value', selected ).prop( 'selected', true ).text( selected ) );
1340 }
1341 return $select;
1342 }
1343
1344 function generateUserLogin( value ) {
1345 return String( value || '' )
1346 .toLowerCase()
1347 .replace( /[^a-z0-9.]+/g, '-' )
1348 .replace( /^-+|-+$/g, '' );
1349 }
1350
1351 function htmlToPlainText( value ) {
1352 const doc = new DOMParser().parseFromString( String( value || '' ), 'text/html' );
1353 if ( ! doc.body ) {
1354 return String( value || '' );
1355 }
1356 $( doc.body ).find( 'script, style, noscript' ).remove();
1357 return doc.body.textContent.replace( /\s+/g, ' ' ).trim();
1358 }
1359
1360 function appendMetadataText( $details, label, value ) {
1361 if ( ! value ) {
1362 return;
1363 }
1364 if ( $details.contents().length ) {
1365 $details.append( document.createTextNode( ' | ' ) );
1366 }
1367 $details.append( document.createTextNode( label + ': ' + value ) );
1368 }
1369
1370 function getDefaultParser() {
1371 const parsers = friends.registered_parsers || { simplepie: 'SimplePie' };
1372 const parserKeys = Object.keys( parsers );
1373 return parserKeys.length ? parserKeys[ 0 ] : 'simplepie';
1374 }
1375
1376 function getFeedOptionUrl( $item ) {
1377 const $input = $item.find( '.subscription-feed-url' );
1378 if ( $input.length ) {
1379 return $.trim( $input.val() );
1380 }
1381 return $.trim( $item.data( 'feedUrl' ) || '' );
1382 }
1383
1384 function resetSubscriptionFeedPreview( $item ) {
1385 const $preview = $item.find( '.subscription-feed-preview' );
1386 $preview.removeData( 'loaded' ).empty().addClass( 'hidden' );
1387 $item.find( '.subscription-feed-preview-toggle' ).text( friendsText( 'text_preview_feed', 'Preview feed' ) );
1388 }
1389
1390 function renderSubscriptionFeedOption( panelId, feedIndex, feedUrl, feed, options ) {
1391 const settings = $.extend( { hidden: false, unsupported: false, custom: false }, options || {} );
1392 const feedDetails = $.extend( { url: feedUrl }, feed || {} );
1393 const feedId = panelId + '-feed-' + feedIndex;
1394 const parser = settings.unsupported || settings.custom ? getDefaultParser() : ( feedDetails.parser || getDefaultParser() );
1395 const title = feedDetails.title || feedUrl || friendsText( 'text_custom_feed', 'Custom feed' );
1396 const $item = $( '<li class="subscription-feed-option"></li>' )
1397 .toggleClass( 'hidden rel-alternate', settings.hidden )
1398 .toggleClass( 'unsupported-feed-option', settings.unsupported )
1399 .toggleClass( 'custom-feed-option', settings.custom )
1400 .data( {
1401 feedUrl: feedUrl,
1402 feed: feedDetails,
1403 } );
1404 const $checkbox = $( '<input type="checkbox" class="subscription-feed-selected" />' )
1405 .attr( 'id', feedId )
1406 .prop( 'checked', settings.custom ? true : ( ! settings.unsupported && !! feedDetails.autoselect ) );
1407 const $label = $( '<label></label>' ).attr( 'for', feedId ).append( $checkbox ).append( ' ' );
1408 if ( feedUrl ) {
1409 $label.append(
1410 $( '<a class="subscription-feed-title-link" target="_blank" rel="noopener noreferrer"></a>' )
1411 .attr( 'href', feedUrl )
1412 .text( title )
1413 );
1414 } else {
1415 $label.append( $( '<span class="subscription-feed-title"></span>' ).text( title ) );
1416 }
1417 if ( feedDetails.type ) {
1418 $label.append( ' ' ).append( $( '<small></small>' ).text( '(' + feedDetails.type + ')' ) );
1419 }
1420 $item.append( $label );
1421
1422 const $controls = $( '<div class="subscription-feed-controls"></div>' );
1423 if ( settings.unsupported || settings.custom ) {
1424 $controls.append(
1425 $( '<label class="subscription-feed-url-label"></label>' )
1426 .text( friendsText( 'text_feed_url', 'Feed URL' ) )
1427 .append(
1428 $( '<input type="url" class="form-input subscription-feed-url" />' )
1429 .val( feedUrl )
1430 .attr( 'placeholder', 'https://example.com/feed.xml' )
1431 )
1432 );
1433 }
1434 $controls.append(
1435 $( '<label></label>' )
1436 .text( friendsText( 'text_post_format', 'Post Format' ) )
1437 .append( createSelect( friends.post_formats || { standard: 'Standard' }, feedDetails[ 'post-format' ] || 'standard', 'subscription-feed-post-format' ) )
1438 );
1439 $controls.append(
1440 $( '<label></label>' )
1441 .text( friendsText( 'text_feed_parser', 'Parser' ) )
1442 .append( createSelect( friends.registered_parsers || { simplepie: 'SimplePie' }, parser, 'subscription-feed-parser' ) )
1443 );
1444 $controls.append(
1445 $( '<button type="button" class="btn btn-link subscription-feed-preview-toggle"></button>' )
1446 .text( friendsText( 'text_preview_feed', 'Preview feed' ) )
1447 );
1448 $item.append( $controls );
1449
1450 const $details = $( '<p class="description details subscription-feed-details hidden"></p>' );
1451 appendMetadataText( $details, 'Type', feedDetails.type );
1452 appendMetadataText( $details, 'rel', feedDetails.rel );
1453 appendMetadataText( $details, 'URL', feedUrl );
1454 appendMetadataText( $details, 'Parser', feedDetails.parser );
1455 if ( feedDetails[ 'additional-info' ] ) {
1456 $details.append( $( '<br />' ) ).append( document.createTextNode( feedDetails[ 'additional-info' ] ) );
1457 }
1458 $item.append( $details );
1459 $item.append( $( '<div class="subscription-feed-preview hidden" aria-live="polite"></div>' ) );
1460 return $item;
1461 }
1462
1463 function renderSubscriptionCard( response, nonce, fallbackDisplayName ) {
1464 const feeds = response.feeds || {};
1465 const displayName = response.display_name || fallbackDisplayName || response.user_login || response.url;
1466 const userLogin = response.user_login || generateUserLogin( displayName || response.url );
1467 const panelId = 'subscription-preview-' + ( ++subscriptionPreviewId );
1468 const $panel = $( '<div class="feed-preview subscription-preview"></div>' )
1469 .attr( 'id', panelId )
1470 .data( {
1471 url: response.url,
1472 nonce: nonce,
1473 } );
1474
1475 const $header = $( '<div class="subscription-preview-header"></div>' );
1476 if ( response.avatar ) {
1477 $header.append( $( '<img class="avatar" width="48" height="48" alt="" />' ).attr( 'src', response.avatar ) );
1478 }
1479 const $title = $( '<div class="subscription-preview-title"></div>' );
1480 $title.append( $( '<strong></strong>' ).text( displayName ) );
1481 $title.append( $( '<small></small>' ).text( response.url ) );
1482 $header.append( $title );
1483 $panel.append( $header );
1484
1485 if ( response.description ) {
1486 $panel.append( $( '<p class="subscription-preview-description"></p>' ).text( htmlToPlainText( response.description ) ) );
1487 }
1488
1489 const $settings = $( '<div class="subscription-settings"></div>' );
1490 const $displayNameInput = $( '<input type="text" class="form-input subscription-display-name" required />' ).val( displayName );
1491 const $userLoginInput = $( '<input type="text" class="form-input subscription-user-login" required />' )
1492 .val( userLogin )
1493 .data( 'original', userLogin );
1494 $settings.append(
1495 $( '<label></label>' )
1496 .text( friendsText( 'text_display_name', 'Display Name' ) )
1497 .append( $displayNameInput )
1498 );
1499 $settings.append(
1500 $( '<label></label>' )
1501 .text( friendsText( 'text_username', 'Username' ) )
1502 .append( $userLoginInput )
1503 );
1504 $panel.append( $settings );
1505
1506 const $feedList = $( '<ul class="feed-list subscription-feed-list"></ul>' );
1507 const unsupportedFeeds = [];
1508 let supportedCount = 0;
1509 let hiddenFeedCount = 0;
1510
1511 $.each( feeds, function ( feedUrl, feed ) {
1512 if ( ! feed || 'unsupported' === feed.parser ) {
1513 unsupportedFeeds.push( {
1514 url: feedUrl,
1515 feed: feed || {},
1516 } );
1517 return;
1518 }
1519
1520 supportedCount++;
1521 const hidden = supportedCount > 1 && ! feed.autoselect;
1522 if ( hidden ) {
1523 hiddenFeedCount++;
1524 }
1525
1526 $feedList.append( renderSubscriptionFeedOption( panelId, supportedCount, feedUrl, feed, { hidden: hidden } ) );
1527 } );
1528
1529 if ( supportedCount ) {
1530 $panel.append( $feedList );
1531 } else {
1532 $panel.append( $( '<p class="error"></p>' ).text( friendsText( 'text_no_supported_feeds', 'No supported feeds discovered.' ) ) );
1533 }
1534
1535 const $feedActions = $( '<div class="subscription-feed-actions"></div>' );
1536 if ( hiddenFeedCount ) {
1537 $feedActions.append(
1538 $( '<button type="button" class="btn btn-link show-alternate-feeds"></button>' )
1539 .text( friendsText( 'text_show_more_feeds', 'Show more feeds' ) )
1540 );
1541 }
1542 if ( supportedCount || unsupportedFeeds.length ) {
1543 $feedActions.append(
1544 $( '<button type="button" class="btn btn-link show-feed-details"></button>' )
1545 .text( friendsText( 'text_feed_metadata', 'Display feed metadata' ) )
1546 );
1547 }
1548 if ( unsupportedFeeds.length ) {
1549 $feedActions.append(
1550 $( '<button type="button" class="btn btn-link show-unsupported-feeds"></button>' )
1551 .text( friendsText( 'text_show_unsupported_feeds', 'Show unsupported feeds' ) )
1552 );
1553 const $unsupported = $( '<ul class="feed-list subscription-feed-list unsupported-feed-list hidden"></ul>' );
1554 $.each( unsupportedFeeds, function ( index, item ) {
1555 $unsupported.append( renderSubscriptionFeedOption( panelId, supportedCount + index + 1, item.url, item.feed, { unsupported: true } ) );
1556 } );
1557 $panel.append( $unsupported );
1558 }
1559 $feedActions.append(
1560 $( '<button type="button" class="btn btn-link add-custom-feed"></button>' )
1561 .text( friendsText( 'text_add_feed_url', 'Add feed URL' ) )
1562 );
1563 if ( $feedActions.children().length ) {
1564 $panel.append( $feedActions );
1565 }
1566
1567 const $previewActions = $( '<div class="subscription-preview-actions"></div>' )
1568 .append( $( '<button type="button" class="btn btn-link bulk-skip-entry"></button>' ).text( friendsText( 'text_skip', 'Skip' ) ) )
1569 .append( $( '<button type="button" class="btn btn-primary subscribe-btn"></button>' ).text( friendsText( 'text_follow', 'Follow' ) ) )
1570 .append( $( '<span class="subscription-preview-status" aria-live="polite"></span>' ) );
1571 $panel.append( $previewActions );
1572
1573 return $panel;
1574 }
1575
1576 function collectSubscriptionFeeds( $panel ) {
1577 const feeds = [];
1578 $panel.find( '.subscription-feed-option' ).each( function () {
1579 const $item = $( this );
1580 const feed = $.extend( {}, $item.data( 'feed' ) || {} );
1581 feed.url = getFeedOptionUrl( $item );
1582 if ( ! feed.url ) {
1583 return;
1584 }
1585 feed.selected = $item.find( '.subscription-feed-selected' ).is( ':checked' );
1586 feed[ 'post-format' ] = $item.find( '.subscription-feed-post-format' ).val();
1587 feed.parser = $item.find( '.subscription-feed-parser' ).val();
1588 feeds.push( feed );
1589 } );
1590 return feeds;
1591 }
1592
1593 function renderFeedPreviewItems( $preview, items ) {
1594 $preview.empty();
1595 if ( ! items || ! items.length ) {
1596 $preview.append( $( '<p class="description"></p>' ).text( friendsText( 'text_no_preview_items', 'No preview items were found.' ) ) );
1597 return;
1598 }
1599
1600 const $list = $( '<ul></ul>' );
1601 $.each( items, function ( index, item ) {
1602 const $item = $( '<li></li>' );
1603 const $summary = $( '<div class="subscription-feed-preview-title"></div>' );
1604 if ( item.permalink ) {
1605 $summary.append(
1606 $( '<a target="_blank" rel="noopener noreferrer"></a>' )
1607 .attr( 'href', item.permalink )
1608 .text( item.title || item.permalink )
1609 );
1610 } else {
1611 $summary.text( item.title || '' );
1612 }
1613 const meta = [];
1614 if ( item.date ) {
1615 meta.push( item.date );
1616 }
1617 if ( item.author ) {
1618 meta.push( item.author );
1619 }
1620 if ( item.post_format ) {
1621 meta.push( item.post_format );
1622 }
1623 if ( meta.length ) {
1624 $summary.append( $( '<small></small>' ).text( meta.join( ' | ' ) ) );
1625 }
1626 $item.append( $summary );
1627 if ( item.excerpt ) {
1628 $item.append( $( '<p></p>' ).text( item.excerpt ) );
1629 }
1630 $list.append( $item );
1631 } );
1632 $preview.append( $list );
1633 }
1634
1635 function previewSubscriptionFeed( $item, $button ) {
1636 const $preview = $item.find( '.subscription-feed-preview' );
1637 const feedUrl = getFeedOptionUrl( $item );
1638 if ( ! feedUrl ) {
1639 $preview.removeClass( 'hidden' ).empty().append( $( '<p class="error"></p>' ).text( friendsText( 'text_feed_url_required', 'Please enter a feed URL.' ) ) );
1640 return;
1641 }
1642 if ( $preview.data( 'loaded' ) ) {
1643 const isHidden = $preview.hasClass( 'hidden' );
1644 $preview.toggleClass( 'hidden', ! isHidden );
1645 $button.text( isHidden ? friendsText( 'text_hide_preview', 'Hide preview' ) : friendsText( 'text_preview_feed', 'Preview feed' ) );
1646 return;
1647 }
1648
1649 $button.prop( 'disabled', true );
1650 $preview.removeClass( 'hidden' ).text( friendsText( 'text_loading', 'Loading...' ) );
1651 wp.ajax.send( 'friends-preview-subscription-feed', {
1652 data: {
1653 _ajax_nonce: $item.closest( '.subscription-preview' ).data( 'nonce' ),
1654 url: feedUrl,
1655 parser: $item.find( '.subscription-feed-parser' ).val(),
1656 },
1657 success( response ) {
1658 renderFeedPreviewItems( $preview, response.items );
1659 $preview.data( 'loaded', true );
1660 $button.prop( 'disabled', false ).text( friendsText( 'text_hide_preview', 'Hide preview' ) );
1661 },
1662 error( result ) {
1663 $preview.empty().append( $( '<p class="error"></p>' ).text( ajaxErrorText( result ) ) );
1664 $button.prop( 'disabled', false );
1665 },
1666 } );
1667 }
1668
1669 function subscribeSubscriptionPanel( $panel, $button, done ) {
1670 const feeds = collectSubscriptionFeeds( $panel );
1671 let selectedFeedCount = 0;
1672 for ( let i = 0; i < feeds.length; i++ ) {
1673 if ( feeds[ i ].selected ) {
1674 selectedFeedCount++;
1675 }
1676 }
1677
1678 const $status = $panel.find( '.subscription-preview-status' );
1679 if ( ! selectedFeedCount ) {
1680 $status.addClass( 'error' ).text( friendsText( 'text_no_feed_selected', 'Please select at least one feed.' ) );
1681 if ( done ) {
1682 done( false );
1683 }
1684 return;
1685 }
1686
1687 $button.prop( 'disabled', true );
1688 $status.removeClass( 'error' ).text( friendsText( 'text_loading', 'Loading...' ) );
1689 wp.ajax.send( 'friends-subscribe-frontend', {
1690 data: {
1691 _ajax_nonce: $panel.data( 'nonce' ),
1692 url: $panel.data( 'url' ),
1693 display_name: $panel.find( '.subscription-display-name' ).val(),
1694 user_login: $panel.find( '.subscription-user-login' ).val(),
1695 feeds: feeds,
1696 },
1697 success( response ) {
1698 const avatar = $panel.find( '.avatar' ).attr( 'src' );
1699 const $success = $( '<div class="subscribe-success"></div>' );
1700 if ( avatar ) {
1701 $success.append( $( '<img class="avatar" width="48" height="48" alt="" />' ).attr( 'src', avatar ) );
1702 }
1703 $success.append( $( '<a></a>' ).attr( 'href', response.url ).text( response.message ) );
1704 $panel.addClass( 'is-subscribed' ).empty().append( $success );
1705 if ( done ) {
1706 done( true );
1707 }
1708 },
1709 error( result ) {
1710 $status.addClass( 'error' ).text( ajaxErrorText( result ) );
1711 $button.prop( 'disabled', false );
1712 if ( done ) {
1713 done( false );
1714 }
1715 },
1716 } );
1717 }
1718
1719 function discoverSubscription( url, nonce, fallbackDisplayName, $target, done ) {
1720 wp.ajax.send( 'friends-preview-subscription', {
1721 data: {
1722 _ajax_nonce: nonce,
1723 url: url,
1724 },
1725 success( response ) {
1726 $target.append( renderSubscriptionCard( response, nonce, fallbackDisplayName ) );
1727 if ( done ) {
1728 done( true );
1729 }
1730 },
1731 error( result ) {
1732 if ( done ) {
1733 done( false, ajaxErrorText( result ) );
1734 } else {
1735 setSubscriptionPreviewError( ajaxErrorText( result ) );
1736 }
1737 },
1738 } );
1739 }
1740
1741 function renderSingleSubscriptionPreview( url ) {
1742 const $preview = $( '#preview-subscription' ).empty();
1743 const $results = $( '<div class="subscription-preview-results"></div>' );
1744 $preview.append( $( '<p class="loading"></p>' ).text( friendsText( 'text_loading', 'Loading...' ) ) );
1745 $preview.append( $results );
1746 discoverSubscription( url, getSubscriptionNonce(), '', $results, function ( success, message ) {
1747 $preview.find( '.loading' ).remove();
1748 if ( ! success ) {
1749 setSubscriptionPreviewError( message || friendsText( 'text_error', 'An error occurred.' ) );
1750 }
1751 } );
1752 }
1753
1754 function renderBulkPicker( items ) {
1755 const $preview = $( '#preview-subscription' ).empty();
1756 const $controls = $( '<div class="bulk-controls"></div>' );
1757 $controls.append( $( '<button type="button" class="btn btn-link bulk-toggle-all"></button>' ).text( friendsText( 'text_opml_deselect_all', 'Deselect all' ) ) );
1758 $controls.append( ' ' );
1759 $controls.append( $( '<button type="button" class="btn btn-primary bulk-preview-selected"></button>' ).text( friendsText( 'text_review_selected', 'Review selected' ) ) );
1760 $controls.append( ' ' );
1761 $controls.append( $( '<button type="button" class="btn btn-primary bulk-follow-selected hidden"></button>' ).text( friendsText( 'text_follow_selected', 'Follow selected' ) ) );
1762
1763 const $list = $( '<ul class="feed-list bulk-feed-list"></ul>' );
1764 $.each( items, function ( index, item ) {
1765 const itemId = 'bulk-feed-' + index;
1766 const $checkbox = $( '<input type="checkbox" class="bulk-feed-cb" checked />' )
1767 .attr( 'id', itemId )
1768 .data( item );
1769 const $label = $( '<label class="label-for-bulkfeed"></label>' )
1770 .attr( 'for', itemId )
1771 .append( $checkbox )
1772 .append( ' ' )
1773 .append( document.createTextNode( item.text || item.url ) );
1774 if ( item.htmlUrl ) {
1775 $label.append( ' ' ).append(
1776 $( '<small></small>' ).append(
1777 $( '<a target="_blank" rel="noopener noreferrer"></a>' )
1778 .attr( 'href', item.htmlUrl )
1779 .text( item.htmlUrl )
1780 )
1781 );
1782 }
1783 $list.append(
1784 $( '<li class="bulk-feed-row"></li>' )
1785 .append( $label )
1786 .append( ' ' )
1787 .append( $( '<span class="bulk-feed-status"></span>' ) )
1788 .append( $( '<div class="bulk-feed-review"></div>' ) )
1789 );
1790 } );
1791
1792 $preview.append( $controls ).append( $list );
1793 }
1794
1795 function updateBulkFeedRowState( $row ) {
1796 const isChecked = $row.find( '.bulk-feed-cb' ).is( ':checked' );
1797 const hasPreview = !! $row.find( '.bulk-feed-review .subscription-preview' ).length;
1798 const previewIsVisible = ! $row.find( '.bulk-feed-review' ).hasClass( 'hidden' );
1799 const isReviewing = isChecked && hasPreview && previewIsVisible;
1800 $row.toggleClass( 'is-reviewing', isReviewing );
1801 $row.find( '.label-for-bulkfeed, .bulk-feed-status' ).toggleClass( 'hidden', isReviewing );
1802 }
1803
1804 function reviewBulkSubscription( $item, nonce, done ) {
1805 const data = $item.find( '.bulk-feed-cb' ).data();
1806 const $status = $item.find( '.bulk-feed-status' );
1807 const $review = $item.find( '.bulk-feed-review' ).empty();
1808 $status.removeClass( 'error' ).text( friendsText( 'text_discovering', 'Discovering feeds...' ) );
1809 updateBulkFeedRowState( $item );
1810 discoverSubscription( data.url, nonce, data.text, $review, function ( success, message ) {
1811 if ( success ) {
1812 $status.text( friendsText( 'text_ready_to_follow', 'Ready' ) );
1813 } else {
1814 $status.addClass( 'error' ).text( message || friendsText( 'text_error', 'An error occurred.' ) );
1815 }
1816 updateBulkFeedRowState( $item );
1817 done();
1818 } );
1819 }
1820
1821 $document.on( 'submit', '#add-subscription-form', function ( e ) {
1822 e.preventDefault();
1823 const value = $( this ).find( '[name="url"]' ).val();
1824 if ( ! value ) {
1825 return;
1826 }
1827
1828 if ( looksLikeOpml( value ) ) {
1829 const parsedOpml = parseOpmlItems( value );
1830 if ( parsedOpml.error ) {
1831 setSubscriptionPreviewError( parsedOpml.error );
1832 return;
1833 }
1834 renderBulkPicker( parsedOpml.items );
1835 return;
1836 }
1837
1838 const pastedItems = parsePastedPeopleList( value );
1839 if ( pastedItems.length ) {
1840 renderBulkPicker( pastedItems );
1841 return;
1842 }
1843
1844 renderSingleSubscriptionPreview( value.trim() );
1845 } );
1846
1847 $document.on( 'paste', '#subscription-url', function () {
1848 const input = this;
1849 clearTimeout( subscriptionPasteReviewTimer );
1850 subscriptionPasteReviewTimer = setTimeout( function () {
1851 if ( $( input ).val().trim() ) {
1852 $( input ).closest( 'form' ).trigger( 'submit' );
1853 }
1854 }, 25 );
1855 } );
1856
1857 $document.on( 'change', '#opml-file', function () {
1858 const file = this.files && this.files[ 0 ];
1859 if ( ! file ) {
1860 return;
1861 }
1862 const reader = new FileReader();
1863 reader.onload = function ( e ) {
1864 const parsedOpml = parseOpmlItems( e.target.result );
1865 if ( parsedOpml.error ) {
1866 setSubscriptionPreviewError( parsedOpml.error );
1867 return;
1868 }
1869 renderBulkPicker( parsedOpml.items );
1870 };
1871 reader.readAsText( file );
1872 } );
1873
1874 $document.on( 'keyup', '.subscription-display-name', function () {
1875 const $login = $( this ).closest( '.subscription-preview' ).find( '.subscription-user-login' );
1876 if ( this.value && $login.data( 'original' ) === $login.val() ) {
1877 const generatedLogin = generateUserLogin( this.value );
1878 $login.val( generatedLogin );
1879 $login.data( 'original', generatedLogin );
1880 }
1881 } );
1882
1883 $document.on( 'click', '.show-alternate-feeds', function () {
1884 $( this ).closest( '.subscription-preview' ).find( '.rel-alternate' ).toggleClass( 'hidden' );
1885 return false;
1886 } );
1887
1888 $document.on( 'click', '.show-feed-details', function () {
1889 const $this = $( this );
1890 const $details = $this.closest( '.subscription-preview' ).find( '.subscription-feed-details' );
1891 const isHidden = $details.first().hasClass( 'hidden' );
1892 $details.toggleClass( 'hidden', ! isHidden );
1893 $this.text( isHidden ? friendsText( 'text_hide_feed_metadata', 'Hide feed metadata' ) : friendsText( 'text_feed_metadata', 'Display feed metadata' ) );
1894 return false;
1895 } );
1896
1897 $document.on( 'click', '.show-unsupported-feeds', function () {
1898 $( this ).closest( '.subscription-preview' ).find( '.unsupported-feed-list' ).toggleClass( 'hidden' );
1899 return false;
1900 } );
1901
1902 $document.on( 'change', '.subscription-feed-parser', function () {
1903 resetSubscriptionFeedPreview( $( this ).closest( '.subscription-feed-option' ) );
1904 } );
1905
1906 $document.on( 'input change', '.subscription-feed-url', function () {
1907 const $item = $( this ).closest( '.subscription-feed-option' );
1908 const feedUrl = getFeedOptionUrl( $item );
1909 const feed = $.extend( {}, $item.data( 'feed' ) || {} );
1910 feed.url = feedUrl;
1911 $item.data( 'feedUrl', feedUrl ).data( 'feed', feed );
1912 $item.find( '.subscription-feed-title-link' ).attr( 'href', feedUrl || '#' );
1913 $item.find( '.subscription-feed-selected' ).prop( 'checked', !! feedUrl );
1914 resetSubscriptionFeedPreview( $item );
1915 } );
1916
1917 $document.on( 'click', '.add-custom-feed', function () {
1918 const $panel = $( this ).closest( '.subscription-preview' );
1919 let $list = $panel.find( '.custom-feed-list' );
1920 if ( ! $list.length ) {
1921 $list = $( '<ul class="feed-list subscription-feed-list custom-feed-list"></ul>' );
1922 $list.insertBefore( $panel.find( '.subscription-feed-actions' ) );
1923 }
1924 const feedIndex = $panel.find( '.subscription-feed-option' ).length + 1;
1925 const panelId = $panel.attr( 'id' ) || 'subscription-preview';
1926 const $item = renderSubscriptionFeedOption(
1927 panelId,
1928 feedIndex,
1929 '',
1930 {
1931 title: friendsText( 'text_custom_feed', 'Custom feed' ),
1932 parser: getDefaultParser(),
1933 rel: 'manual',
1934 type: 'custom',
1935 },
1936 { custom: true }
1937 );
1938 $list.append( $item );
1939 $item.find( '.subscription-feed-url' ).trigger( 'focus' );
1940 return false;
1941 } );
1942
1943 $document.on( 'click', '.subscription-feed-preview-toggle', function () {
1944 previewSubscriptionFeed( $( this ).closest( '.subscription-feed-option' ), $( this ) );
1945 return false;
1946 } );
1947
1948 $document.on( 'click', '.subscribe-btn', function () {
1949 subscribeSubscriptionPanel( $( this ).closest( '.subscription-preview' ), $( this ) );
1950 return false;
1951 } );
1952
1953 $document.on( 'click', '.bulk-toggle-all', function () {
1954 const $this = $( this );
1955 const $checkboxes = $( '.bulk-feed-cb' );
1956 const allChecked = $checkboxes.length === $checkboxes.filter( ':checked' ).length;
1957 $checkboxes.prop( 'checked', ! allChecked ).trigger( 'change' );
1958 $this.text( allChecked ? friendsText( 'text_opml_select_all', 'Select all' ) : friendsText( 'text_opml_deselect_all', 'Deselect all' ) );
1959 return false;
1960 } );
1961
1962 $document.on( 'change', '.bulk-feed-cb', function () {
1963 const $row = $( this ).closest( '.bulk-feed-row' );
1964 $row.find( '.bulk-feed-review' ).toggleClass( 'hidden', ! this.checked );
1965 if ( ! this.checked ) {
1966 $row.find( '.bulk-feed-status' ).text( '' ).removeClass( 'error' );
1967 }
1968 updateBulkFeedRowState( $row );
1969 } );
1970
1971 $document.on( 'click', '.bulk-skip-entry', function () {
1972 const $panel = $( this ).closest( '.subscription-preview' );
1973 const $row = $panel.closest( '.bulk-feed-row' );
1974 if ( $row.length ) {
1975 $row.find( '.bulk-feed-cb' ).prop( 'checked', false ).trigger( 'change' );
1976 } else {
1977 $panel.addClass( 'hidden' );
1978 }
1979 return false;
1980 } );
1981
1982 $document.on( 'click', '.bulk-preview-selected', function () {
1983 const $button = $( this ).prop( 'disabled', true );
1984 const nonce = getSubscriptionNonce();
1985 const queue = [];
1986 $( '.bulk-feed-list > .bulk-feed-row' ).each( function () {
1987 const $item = $( this );
1988 if ( $item.find( '.bulk-feed-cb' ).is( ':checked' ) ) {
1989 queue.push( $item );
1990 }
1991 } );
1992 if ( ! queue.length ) {
1993 alert( friendsText( 'text_opml_no_selected', 'Please select at least one feed.' ) );
1994 $button.prop( 'disabled', false );
1995 return false;
1996 }
1997
1998 let index = 0;
1999 function next() {
2000 if ( index >= queue.length ) {
2001 if ( $( '.bulk-feed-review .subscription-preview .subscribe-btn' ).length ) {
2002 $( '.bulk-follow-selected' ).removeClass( 'hidden' );
2003 }
2004 $button.prop( 'disabled', false );
2005 return;
2006 }
2007 reviewBulkSubscription( queue[ index++ ], nonce, next );
2008 }
2009 next();
2010 return false;
2011 } );
2012
2013 $document.on( 'click', '.bulk-follow-selected', function () {
2014 const $button = $( this ).prop( 'disabled', true );
2015 const queue = [];
2016 $( '.bulk-feed-list > .bulk-feed-row' ).each( function () {
2017 if ( $( this ).find( '.bulk-feed-cb' ).is( ':checked' ) ) {
2018 $( this ).find( '.bulk-feed-review .subscription-preview' ).has( '.subscribe-btn' ).not( '.is-subscribed' ).each( function () {
2019 queue.push( this );
2020 } );
2021 }
2022 } );
2023 let index = 0;
2024 function next() {
2025 if ( index >= queue.length ) {
2026 $button.prop( 'disabled', false );
2027 return;
2028 }
2029 const $panel = $( queue[ index++ ] );
2030 subscribeSubscriptionPanel( $panel, $panel.find( '.subscribe-btn' ), next );
2031 }
2032 next();
2033 return false;
2034 } );
2035
2036 } )( jQuery, window.wp, window.friends );
2037