PluginProbe
Friends / 4.3.2
Friends v4.3.2
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.2, at friends.js

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