]> git.wh0rd.org Git - tt-rss.git/blob - js/viewfeed.js
8f1605faca6faa0618cf06bc06f6d36b5491144b
[tt-rss.git] / js / viewfeed.js
1 var _active_article_id = 0;
2
3 var vgroup_last_feed = false;
4 var post_under_pointer = false;
5
6 var last_requested_article = false;
7
8 var catchup_id_batch = [];
9 var catchup_timeout_id = false;
10
11 var cids_requested = [];
12 var loaded_article_ids = [];
13 var _last_headlines_update = 0;
14 var _headlines_scroll_offset = 0;
15 var current_first_id = 0;
16 var last_search_query;
17
18 var _catchup_request_sent = false;
19
20 var has_storage = 'sessionStorage' in window && window['sessionStorage'] !== null;
21
22 function headlines_callback2(transport, offset, background, infscroll_req) {
23         handle_rpc_json(transport);
24
25         console.log("headlines_callback2 [offset=" + offset + "] B:" + background + " I:" + infscroll_req);
26
27         let is_cat = false;
28         let feed_id = false;
29
30         let reply = false;
31
32         try {
33                 reply = JSON.parse(transport.responseText);
34         } catch (e) {
35                 console.error(e);
36         }
37
38         if (reply) {
39
40                 is_cat = reply['headlines']['is_cat'];
41                 feed_id = reply['headlines']['id'];
42                 last_search_query = reply['headlines']['search_query'];
43
44                 if (background) {
45                         let content = reply['headlines']['content'];
46
47                         content = content + "<div id='headlines-spacer'></div>";
48                         return;
49                 }
50
51                 if (feed_id != -7 && (feed_id != getActiveFeedId() || is_cat != activeFeedIsCat()))
52                         return;
53
54                 /* dijit.getEnclosingWidget(
55                         document.forms["main_toolbar_form"].update).attr('disabled',
56                                 is_cat || feed_id <= 0); */
57
58                 try {
59                         if (infscroll_req == false) {
60                                 $("headlines-frame").scrollTop = 0;
61
62                                 $("floatingTitle").style.visibility = "hidden";
63                                 $("floatingTitle").setAttribute("data-article-id", 0);
64                                 $("floatingTitle").innerHTML = "";
65                         }
66                 } catch (e) { }
67
68                 $("headlines-frame").removeClassName("cdm");
69                 $("headlines-frame").removeClassName("normal");
70
71                 $("headlines-frame").addClassName(isCdmMode() ? "cdm" : "normal");
72
73                 const headlines_count = reply['headlines-info']['count'];
74
75                 vgroup_last_feed = reply['headlines-info']['vgroup_last_feed'];
76
77                 if (parseInt(headlines_count) < 30) {
78                         _infscroll_disable = 1;
79                 } else {
80                         _infscroll_disable = 0;
81                 }
82
83                 current_first_id = reply['headlines']['first_id'];
84                 const counters = reply['counters'];
85                 const articles = reply['articles'];
86                 //var runtime_info = reply['runtime-info'];
87
88                 if (infscroll_req == false) {
89                         loaded_article_ids = [];
90
91                         dojo.html.set($("headlines-toolbar"),
92                                         reply['headlines']['toolbar'],
93                                         {parseContent: true});
94
95                         /*dojo.html.set($("headlines-frame"),
96                                 reply['headlines']['content'],
97                                 {parseContent: true});
98
99                         $$("#headlines-frame div[id*='RROW']").each(function(row) {
100                                 loaded_article_ids.push(row.id);
101                         });*/
102
103                         $("headlines-frame").innerHTML = '';
104
105                         var tmp = new Element("div");
106                         tmp.innerHTML = reply['headlines']['content'];
107                         dojo.parser.parse(tmp);
108
109                         while (tmp.hasChildNodes()) {
110                                 var row = tmp.removeChild(tmp.firstChild);
111
112                                 if (loaded_article_ids.indexOf(row.id) == -1 || row.hasClassName("cdmFeedTitle")) {
113                                         dijit.byId("headlines-frame").domNode.appendChild(row);
114
115                                         loaded_article_ids.push(row.id);
116                                 }
117                         }
118
119                         var hsp = $("headlines-spacer");
120                         if (!hsp) hsp = new Element("DIV", {"id": "headlines-spacer"});
121                         dijit.byId('headlines-frame').domNode.appendChild(hsp);
122
123                         initHeadlinesMenu();
124
125                         if (_infscroll_disable)
126                                 hsp.innerHTML = "<a href='#' onclick='openNextUnreadFeed()'>" +
127                                         __("Click to open next unread feed.") + "</a>";
128
129                         if (_search_query) {
130                                 $("feed_title").innerHTML += "<span id='cancel_search'>" +
131                                         " (<a href='#' onclick='cancelSearch()'>" + __("Cancel search") + "</a>)" +
132                                         "</span>";
133                         }
134
135                 } else if (headlines_count > 0 && feed_id == getActiveFeedId() && is_cat == activeFeedIsCat()) {
136                                 console.log("adding some more headlines: " + headlines_count);
137
138                                 const c = dijit.byId("headlines-frame");
139                                 const ids = getSelectedArticleIds2();
140
141                                 var hsp = $("headlines-spacer");
142
143                                 if (hsp)
144                                         c.domNode.removeChild(hsp);
145
146                                 var tmp = new Element("div");
147                                 tmp.innerHTML = reply['headlines']['content'];
148                                 dojo.parser.parse(tmp);
149
150                                 while (tmp.hasChildNodes()) {
151                                         var row = tmp.removeChild(tmp.firstChild);
152
153                                         if (loaded_article_ids.indexOf(row.id) == -1 || row.hasClassName("cdmFeedTitle")) {
154                                                 dijit.byId("headlines-frame").domNode.appendChild(row);
155
156                                                 loaded_article_ids.push(row.id);
157                                         }
158                                 }
159
160                                 if (!hsp) hsp = new Element("DIV", {"id": "headlines-spacer"});
161                                 c.domNode.appendChild(hsp);
162
163                                 if (headlines_count < 30) _infscroll_disable = true;
164
165                                 console.log("restore selected ids: " + ids);
166
167                                 for (var i = 0; i < ids.length; i++) {
168                                         markHeadline(ids[i]);
169                                 }
170
171                                 initHeadlinesMenu();
172
173                                 if (_infscroll_disable) {
174                                         hsp.innerHTML = "<a href='#' onclick='openNextUnreadFeed()'>" +
175                                         __("Click to open next unread feed.") + "</a>";
176                                 }
177
178                         } else {
179                                 console.log("no new headlines received");
180
181                                 const first_id_changed = reply['headlines']['first_id_changed'];
182                                 console.log("first id changed:" + first_id_changed);
183
184                                 var hsp = $("headlines-spacer");
185
186                                 if (hsp) {
187                                         if (first_id_changed) {
188                                                 hsp.innerHTML = "<a href='#' onclick='viewCurrentFeed()'>" +
189                                                 __("New articles found, reload feed to continue.") + "</a>";
190                                         } else {
191                                                 hsp.innerHTML = "<a href='#' onclick='openNextUnreadFeed()'>" +
192                                                 __("Click to open next unread feed.") + "</a>";
193                                         }
194
195                                 }
196
197                         }
198
199                 if (articles) {
200                         for (var i = 0; i < articles.length; i++) {
201                                 const a_id = articles[i]['id'];
202                                 cache_set("article:" + a_id, articles[i]['content']);
203                         }
204                 } else {
205                         console.log("no cached articles received");
206                 }
207
208                 if (counters)
209                         parse_counters(counters);
210                 else
211                         request_counters();
212
213         } else {
214                 console.error("Invalid object received: " + transport.responseText);
215                 dijit.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
216                                 __('Could not update headlines (invalid object received - see error console for details)') +
217                                 "</div>");
218         }
219
220         _infscroll_request_sent = 0;
221         _last_headlines_update = new Date().getTime();
222
223         unpackVisibleHeadlines();
224
225         // if we have some more space in the buffer, why not try to fill it
226
227         if (!_infscroll_disable && $("headlines-spacer") &&
228                         $("headlines-spacer").offsetTop < $("headlines-frame").offsetHeight) {
229
230                 window.setTimeout(function() {
231                         loadMoreHeadlines();
232                 }, 250);
233         }
234
235         notify("");
236 }
237
238 function render_article(article) {
239         cleanup_memory("content-insert");
240
241         dijit.byId("headlines-wrap-inner").addChild(
242                         dijit.byId("content-insert"));
243
244         const c = dijit.byId("content-insert");
245
246         try {
247                 c.domNode.scrollTop = 0;
248         } catch (e) { }
249
250         c.attr('content', article);
251         PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED, c.domNode);
252
253         correctHeadlinesOffset(getActiveArticleId());
254
255         try {
256                 c.focus();
257         } catch (e) { }
258 }
259
260 function showArticleInHeadlines(id, noexpand) {
261         const row = $("RROW-" + id);
262         if (!row) return;
263
264         if (!noexpand)
265                 row.removeClassName("Unread");
266
267         row.addClassName("active");
268
269         selectArticles('none');
270
271         markHeadline(id);
272 }
273
274 function article_callback2(transport, id) {
275         console.log("article_callback2 " + id);
276
277         handle_rpc_json(transport);
278
279         let reply = false;
280
281         try {
282                 reply = JSON.parse(transport.responseText);
283         } catch (e) {
284                 console.error(e);
285         }
286
287         if (reply) {
288
289                 reply.each(function(article) {
290                         if (getActiveArticleId() == article['id']) {
291                                 render_article(article['content']);
292                         }
293                         cids_requested.remove(article['id']);
294
295                         cache_set("article:" + article['id'], article['content']);
296                 });
297
298 //                      if (id != last_requested_article) {
299 //                              console.log("requested article id is out of sequence, aborting");
300 //                              return;
301 //                      }
302
303         } else {
304                 console.error("Invalid object received: " + transport.responseText);
305
306                 render_article("<div class='whiteBox'>" +
307                                 __('Could not display article (invalid object received - see error console for details)') + "</div>");
308         }
309
310         const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length
311         request_counters(unread_in_buffer == 0);
312
313         notify("");
314 }
315
316 function view(id, activefeed, noexpand) {
317         const oldrow = $("RROW-" + getActiveArticleId());
318         if (oldrow) oldrow.removeClassName("active");
319
320         const crow = $("RROW-" + id);
321
322         if (!crow) return;
323         if (noexpand) {
324                 setActiveArticleId(id);
325                 showArticleInHeadlines(id, noexpand);
326                 return;
327         }
328
329         console.log("loading article: " + id);
330
331         const cached_article = cache_get("article:" + id);
332
333         console.log("cache check result: " + (cached_article != false));
334
335         let query = "?op=article&method=view&id=" + param_escape(id);
336
337         const neighbor_ids = getRelativePostIds(id);
338
339         /* only request uncached articles */
340
341         const cids_to_request = [];
342
343         for (let i = 0; i < neighbor_ids.length; i++) {
344                 if (cids_requested.indexOf(neighbor_ids[i]) == -1)
345                         if (!cache_get("article:" + neighbor_ids[i])) {
346                                 cids_to_request.push(neighbor_ids[i]);
347                                 cids_requested.push(neighbor_ids[i]);
348                         }
349         }
350
351         console.log("additional ids: " + cids_to_request.toString());
352
353         query = query + "&cids=" + cids_to_request.toString();
354
355         const article_is_unread = crow.hasClassName("Unread");
356
357         setActiveArticleId(id);
358         showArticleInHeadlines(id);
359
360         if (cached_article && article_is_unread) {
361
362                 query = query + "&mode=prefetch";
363
364                 render_article(cached_article);
365
366         } else if (cached_article) {
367
368                 query = query + "&mode=prefetch_old";
369                 render_article(cached_article);
370
371                 // if we don't need to request any relative ids, we might as well skip
372                 // the server roundtrip altogether
373                 if (cids_to_request.length == 0) {
374                         return;
375                 }
376         }
377
378         last_requested_article = id;
379
380         console.log(query);
381
382         if (article_is_unread) {
383                 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
384         }
385
386         new Ajax.Request("backend.php", {
387                 parameters: query,
388                 onComplete: function(transport) {
389                         article_callback2(transport, id);
390                 } });
391
392         return false;
393
394 }
395
396 function toggleMark(id, client_only) {
397         let query = "?op=rpc&id=" + id + "&method=mark";
398
399         const row = $("RROW-" + id);
400         if (!row) return;
401
402         const imgs = [];
403
404         const row_imgs = row.getElementsByClassName("markedPic");
405
406         for (var i = 0; i < row_imgs.length; i++)
407                 imgs.push(row_imgs[i]);
408
409         const ft = $("floatingTitle");
410
411         if (ft && ft.getAttribute("data-article-id") == id) {
412                 const fte = ft.getElementsByClassName("markedPic");
413
414                 for (var i = 0; i < fte.length; i++)
415                         imgs.push(fte[i]);
416         }
417
418         for (i = 0; i < imgs.length; i++) {
419                 const img = imgs[i];
420
421                 if (!row.hasClassName("marked")) {
422                         img.src = img.src.replace("mark_unset", "mark_set");
423                         query = query + "&mark=1";
424                 } else {
425                         img.src = img.src.replace("mark_set", "mark_unset");
426                         query = query + "&mark=0";
427                 }
428         }
429
430         row.toggleClassName("marked");
431
432         if (!client_only) {
433                 new Ajax.Request("backend.php", {
434                         parameters: query,
435                         onComplete: function (transport) {
436                                 handle_rpc_json(transport);
437                         }
438                 });
439         }
440 }
441
442 function togglePub(id, client_only, no_effects, note) {
443         let query = "?op=rpc&id=" + id + "&method=publ";
444
445         if (note != undefined) {
446                 query = query + "&note=" + param_escape(note);
447         } else {
448                 query = query + "&note=undefined";
449         }
450
451         const row = $("RROW-" + id);
452         if (!row) return;
453
454         const imgs = [];
455
456         const row_imgs = row.getElementsByClassName("pubPic");
457
458         for (var i = 0; i < row_imgs.length; i++)
459                 imgs.push(row_imgs[i]);
460
461         const ft = $("floatingTitle");
462
463         if (ft && ft.getAttribute("data-article-id") == id) {
464                 const fte = ft.getElementsByClassName("pubPic");
465
466                 for (var i = 0; i < fte.length; i++)
467                         imgs.push(fte[i]);
468         }
469
470         for (var i = 0; i < imgs.length; i++) {
471                 const img = imgs[i];
472
473                 if (!row.hasClassName("published") || note != undefined) {
474                         img.src = img.src.replace("pub_unset", "pub_set");
475                         query = query + "&pub=1";
476                 } else {
477                         img.src = img.src.replace("pub_set", "pub_unset");
478                         query = query + "&pub=0";
479                 }
480         }
481
482         if (note != undefined)
483                 row.addClassName("published");
484         else
485                 row.toggleClassName("published");
486
487         if (!client_only) {
488                 new Ajax.Request("backend.php", {
489                         parameters: query,
490                         onComplete: function(transport) {
491                                 handle_rpc_json(transport);
492                         } });
493         }
494
495 }
496
497 function moveToPost(mode, noscroll, noexpand) {
498         const rows = getLoadedArticleIds();
499
500         let prev_id = false;
501         let next_id = false;
502
503         if (!$('RROW-' + getActiveArticleId())) {
504                 setActiveArticleId(0);
505         }
506
507         if (!getActiveArticleId()) {
508                 next_id = rows[0];
509                 prev_id = rows[rows.length-1]
510         } else {
511                 for (let i = 0; i < rows.length; i++) {
512                         if (rows[i] == getActiveArticleId()) {
513
514                                 // Account for adjacent identical article ids.
515                                 if (i > 0) prev_id = rows[i-1];
516
517                                 for (let j = i+1; j < rows.length; j++) {
518                                         if (rows[j] != getActiveArticleId()) {
519                                                 next_id = rows[j];
520                                                 break;
521                                         }
522                                 }
523                                 break;
524                         }
525                 }
526         }
527
528         console.log("cur: " + getActiveArticleId() + " next: " + next_id);
529
530         if (mode == "next") {
531                 if (next_id || getActiveArticleId()) {
532                         if (isCdmMode()) {
533
534                                 var article = $("RROW-" + getActiveArticleId());
535                                 var ctr = $("headlines-frame");
536
537                                 if (!noscroll && article && article.offsetTop + article.offsetHeight >
538                                                 ctr.scrollTop + ctr.offsetHeight) {
539
540                                         scrollArticle(ctr.offsetHeight/4);
541
542                                 } else if (next_id) {
543                                         cdmExpandArticle(next_id, noexpand);
544                                         cdmScrollToArticleId(next_id, true);
545                                 }
546
547                         } else if (next_id) {
548                                 correctHeadlinesOffset(next_id);
549                                 view(next_id, getActiveFeedId(), noexpand);
550                         }
551                 }
552         }
553
554         if (mode == "prev") {
555                 if (prev_id || getActiveArticleId()) {
556                         if (isCdmMode()) {
557
558                                 var article = $("RROW-" + getActiveArticleId());
559                                 const prev_article = $("RROW-" + prev_id);
560                                 var ctr = $("headlines-frame");
561
562                                 if (!getInitParam("cdm_expanded")) {
563
564                                         if (!noscroll && article && article.offsetTop < ctr.scrollTop) {
565                                                 scrollArticle(-ctr.offsetHeight/4);
566                                         } else {
567                                                 cdmExpandArticle(prev_id, noexpand);
568                                                 cdmScrollToArticleId(prev_id, true);
569                                         }
570                                 } else if (!noscroll && article && article.offsetTop < ctr.scrollTop) {
571                                                 scrollArticle(-ctr.offsetHeight/3);
572                                         } else if (!noscroll && prev_article &&
573                                                         prev_article.offsetTop < ctr.scrollTop) {
574                                                 cdmExpandArticle(prev_id, noexpand);
575                                                 scrollArticle(-ctr.offsetHeight/4);
576                                         } else if (prev_id) {
577                                                 cdmExpandArticle(prev_id, noexpand);
578                                                 cdmScrollToArticleId(prev_id, noscroll);
579                                         }
580
581                         } else if (prev_id) {
582                                 correctHeadlinesOffset(prev_id);
583                                 view(prev_id, getActiveFeedId(), noexpand);
584                         }
585                 }
586         }
587
588 }
589
590 function toggleSelected(id, force_on) {
591         const row = $("RROW-" + id);
592
593         if (row) {
594                 const cb = dijit.getEnclosingWidget(
595                                 row.getElementsByClassName("rchk")[0]);
596
597                 if (row.hasClassName('Selected') && !force_on) {
598                         row.removeClassName('Selected');
599                         if (cb) cb.attr("checked", false);
600                 } else {
601                         row.addClassName('Selected');
602                         if (cb) cb.attr("checked", true);
603                 }
604         }
605
606         updateSelectedPrompt();
607 }
608
609 function updateSelectedPrompt() {
610         const count = getSelectedArticleIds2().size();
611         const elem = $("selected_prompt");
612
613         if (elem) {
614                 elem.innerHTML = ngettext("%d article selected",
615                                 "%d articles selected", count).replace("%d", count);
616
617                 if (count > 0)
618                         Element.show(elem);
619                 else
620                         Element.hide(elem);
621         }
622
623 }
624
625 function toggleUnread(id, cmode, effect) {
626         const row = $("RROW-" + id);
627         if (row) {
628                 const tmpClassName = row.className;
629
630                 if (cmode == undefined || cmode == 2) {
631                         if (row.hasClassName("Unread")) {
632                                 row.removeClassName("Unread");
633
634                         } else {
635                                 row.addClassName("Unread");
636                         }
637
638                 } else if (cmode == 0) {
639
640                         row.removeClassName("Unread");
641
642                 } else if (cmode == 1) {
643                         row.addClassName("Unread");
644                 }
645
646                 if (cmode == undefined) cmode = 2;
647
648                 const query = "?op=rpc&method=catchupSelected" +
649                         "&cmode=" + param_escape(cmode) + "&ids=" + param_escape(id);
650
651 //                      notify_progress("Loading, please wait...");
652
653                 if (tmpClassName != row.className) {
654                         new Ajax.Request("backend.php", {
655                                 parameters: query,
656                                 onComplete: function (transport) {
657                                         handle_rpc_json(transport);
658                                 }
659                         });
660                 }
661
662         }
663 }
664
665 function selectionRemoveLabel(id, ids) {
666         if (!ids) ids = getSelectedArticleIds2();
667
668         if (ids.length == 0) {
669                 alert(__("No articles are selected."));
670                 return;
671         }
672
673         const query = "?op=article&method=removeFromLabel&ids=" +
674                 param_escape(ids.toString()) + "&lid=" + param_escape(id);
675
676         console.log(query);
677
678         new Ajax.Request("backend.php", {
679                 parameters: query,
680                 onComplete: function(transport) {
681                         handle_rpc_json(transport);
682                         show_labels_in_headlines(transport);
683                 } });
684
685 }
686
687 function selectionAssignLabel(id, ids) {
688         if (!ids) ids = getSelectedArticleIds2();
689
690         if (ids.length == 0) {
691                 alert(__("No articles are selected."));
692                 return;
693         }
694
695         const query = "?op=article&method=assignToLabel&ids=" +
696                 param_escape(ids.toString()) + "&lid=" + param_escape(id);
697
698         console.log(query);
699
700         new Ajax.Request("backend.php", {
701                 parameters: query,
702                 onComplete: function(transport) {
703                         handle_rpc_json(transport);
704                         show_labels_in_headlines(transport);
705                 } });
706 }
707
708 function selectionToggleUnread(set_state, callback, no_error, ids) {
709         const rows = ids ? ids : getSelectedArticleIds2();
710
711         if (rows.length == 0 && !no_error) {
712                 alert(__("No articles are selected."));
713                 return;
714         }
715
716         for (let i = 0; i < rows.length; i++) {
717                 const row = $("RROW-" + rows[i]);
718                 if (row) {
719                         if (set_state == undefined) {
720                                 if (row.hasClassName("Unread")) {
721                                         row.removeClassName("Unread");
722                                 } else {
723                                         row.addClassName("Unread");
724                                 }
725                         }
726
727                         if (set_state == false) {
728                                 row.removeClassName("Unread");
729                         }
730
731                         if (set_state == true) {
732                                 row.addClassName("Unread");
733                         }
734                 }
735         }
736
737         updateFloatingTitle(true);
738
739         if (rows.length > 0) {
740
741                 let cmode = "";
742
743                 if (set_state == undefined) {
744                         cmode = "2";
745                 } else if (set_state == true) {
746                         cmode = "1";
747                 } else if (set_state == false) {
748                         cmode = "0";
749                 }
750
751                 const query = "?op=rpc&method=catchupSelected" +
752                         "&cmode=" + cmode + "&ids=" + param_escape(rows.toString());
753
754                 notify_progress("Loading, please wait...");
755
756                 new Ajax.Request("backend.php", {
757                         parameters: query,
758                         onComplete: function(transport) {
759                                 handle_rpc_json(transport);
760                                 if (callback) callback(transport);
761                         } });
762
763         }
764 }
765
766 // sel_state ignored
767 function selectionToggleMarked(sel_state, callback, no_error, ids) {
768         const rows = ids ? ids : getSelectedArticleIds2();
769
770         if (rows.length == 0 && !no_error) {
771                 alert(__("No articles are selected."));
772                 return;
773         }
774
775         for (let i = 0; i < rows.length; i++) {
776                 toggleMark(rows[i], true, true);
777         }
778
779         if (rows.length > 0) {
780
781                 const query = "?op=rpc&method=markSelected&ids=" +
782                         param_escape(rows.toString()) + "&cmode=2";
783
784                 new Ajax.Request("backend.php", {
785                         parameters: query,
786                         onComplete: function(transport) {
787                                 handle_rpc_json(transport);
788                                 if (callback) callback(transport);
789                         } });
790
791         }
792 }
793
794 // sel_state ignored
795 function selectionTogglePublished(sel_state, callback, no_error, ids) {
796         const rows = ids ? ids : getSelectedArticleIds2();
797
798         if (rows.length == 0 && !no_error) {
799                 alert(__("No articles are selected."));
800                 return;
801         }
802
803         for (let i = 0; i < rows.length; i++) {
804                 togglePub(rows[i], true, true);
805         }
806
807         if (rows.length > 0) {
808
809                 const query = "?op=rpc&method=publishSelected&ids=" +
810                         param_escape(rows.toString()) + "&cmode=2";
811
812                 new Ajax.Request("backend.php", {
813                         parameters: query,
814                         onComplete: function(transport) {
815                                 handle_rpc_json(transport);
816                         } });
817
818         }
819 }
820
821 function getSelectedArticleIds2() {
822
823         const rv = [];
824
825         $$("#headlines-frame > div[id*=RROW][class*=Selected]").each(
826                 function(child) {
827                         rv.push(child.getAttribute("data-article-id"));
828                 });
829
830         return rv;
831 }
832
833 function getLoadedArticleIds() {
834         const rv = [];
835
836         const children = $$("#headlines-frame > div[id*=RROW-]");
837
838         children.each(function(child) {
839                 if (Element.visible(child)) {
840                         rv.push(child.getAttribute("data-article-id"));
841                 }
842         });
843
844         return rv;
845
846 }
847
848 // mode = all,none,unread,invert,marked,published
849 function selectArticles(mode, query) {
850         if (!query) query = "#headlines-frame > div[id*=RROW]";
851
852         const children = $$(query);
853
854         children.each(function(child) {
855                 const id = child.getAttribute("data-article-id");
856
857                 const cb = dijit.getEnclosingWidget(
858                                 child.getElementsByClassName("rchk")[0]);
859
860                 if (mode == "all") {
861                         child.addClassName("Selected");
862                         if (cb) cb.attr("checked", true);
863                 } else if (mode == "unread") {
864                         if (child.hasClassName("Unread")) {
865                                 child.addClassName("Selected");
866                                 if (cb) cb.attr("checked", true);
867                         } else {
868                                 child.removeClassName("Selected");
869                                 if (cb) cb.attr("checked", false);
870                         }
871                 } else if (mode == "marked") {
872                         if (child.hasClassName("marked")) {
873                                 child.addClassName("Selected");
874                                 if (cb) cb.attr("checked", true);
875                         } else {
876                                 child.removeClassName("Selected");
877                                 if (cb) cb.attr("checked", false);
878                         }
879                 } else if (mode == "published") {
880                         if (child.hasClassName("published")) {
881                                 child.addClassName("Selected");
882                                 if (cb) cb.attr("checked", true);
883                         } else {
884                                 child.removeClassName("Selected");
885                                 if (cb) cb.attr("checked", false);
886                         }
887
888                 } else if (mode == "invert") {
889                         if (child.hasClassName("Selected")) {
890                                 child.removeClassName("Selected");
891                                 if (cb) cb.attr("checked", false);
892                         } else {
893                                 child.addClassName("Selected");
894                                 if (cb) cb.attr("checked", true);
895                         }
896
897                 } else {
898                         child.removeClassName("Selected");
899                         if (cb) cb.attr("checked", false);
900                 }
901         });
902
903         updateSelectedPrompt();
904 }
905
906 function deleteSelection() {
907
908         const rows = getSelectedArticleIds2();
909
910         if (rows.length == 0) {
911                 alert(__("No articles are selected."));
912                 return;
913         }
914
915         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
916         let str;
917
918         if (getActiveFeedId() != 0) {
919                 str = ngettext("Delete %d selected article in %s?", "Delete %d selected articles in %s?", rows.length);
920         } else {
921                 str = ngettext("Delete %d selected article?", "Delete %d selected articles?", rows.length);
922         }
923
924         str = str.replace("%d", rows.length);
925         str = str.replace("%s", fn);
926
927         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
928                 return;
929         }
930
931         const query = "?op=rpc&method=delete&ids=" + param_escape(rows);
932
933         console.log(query);
934
935         new Ajax.Request("backend.php", {
936                 parameters: query,
937                 onComplete: function (transport) {
938                         handle_rpc_json(transport);
939                         viewCurrentFeed();
940                 }
941         });
942 }
943
944 function archiveSelection() {
945
946         const rows = getSelectedArticleIds2();
947
948         if (rows.length == 0) {
949                 alert(__("No articles are selected."));
950                 return;
951         }
952
953         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
954         let str;
955         let op;
956
957         if (getActiveFeedId() != 0) {
958                 str = ngettext("Archive %d selected article in %s?", "Archive %d selected articles in %s?", rows.length);
959                 op = "archive";
960         } else {
961                 str = ngettext("Move %d archived article back?", "Move %d archived articles back?", rows.length);
962
963                 str += " " + __("Please note that unstarred articles might get purged on next feed update.");
964
965                 op = "unarchive";
966         }
967
968         str = str.replace("%d", rows.length);
969         str = str.replace("%s", fn);
970
971         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
972                 return;
973         }
974
975         const query = "?op=rpc&method="+op+"&ids=" + param_escape(rows);
976
977         console.log(query);
978
979         for (let i = 0; i < rows.length; i++) {
980                 cache_delete("article:" + rows[i]);
981         }
982
983         new Ajax.Request("backend.php", {
984                 parameters: query,
985                 onComplete: function(transport) {
986                                 handle_rpc_json(transport);
987                                 viewCurrentFeed();
988                         } });
989
990 }
991
992 function catchupSelection() {
993
994         const rows = getSelectedArticleIds2();
995
996         if (rows.length == 0) {
997                 alert(__("No articles are selected."));
998                 return;
999         }
1000
1001         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
1002
1003         let str = ngettext("Mark %d selected article in %s as read?", "Mark %d selected articles in %s as read?", rows.length);
1004
1005         str = str.replace("%d", rows.length);
1006         str = str.replace("%s", fn);
1007
1008         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
1009                 return;
1010         }
1011
1012         selectionToggleUnread(false, 'viewCurrentFeed()', true);
1013 }
1014
1015 function editArticleTags(id) {
1016         const query = "backend.php?op=article&method=editArticleTags&param=" + param_escape(id);
1017
1018         if (dijit.byId("editTagsDlg"))
1019                 dijit.byId("editTagsDlg").destroyRecursive();
1020
1021         var dialog = new dijit.Dialog({
1022                 id: "editTagsDlg",
1023                 title: __("Edit article Tags"),
1024                 style: "width: 600px",
1025                 execute: function() {
1026                         if (this.validate()) {
1027                                 const query = dojo.objectToQuery(this.attr('value'));
1028
1029                                 notify_progress("Saving article tags...", true);
1030
1031                                 new Ajax.Request("backend.php", {
1032                                 parameters: query,
1033                                 onComplete: function(transport) {
1034                                         try {
1035                                                 notify('');
1036                                                 dialog.hide();
1037
1038                                                 const data = JSON.parse(transport.responseText);
1039
1040                                                 if (data) {
1041                                                         const id = data.id;
1042
1043                                                         console.log(id);
1044
1045                                                         const tags = $("ATSTR-" + id);
1046                                                         const tooltip = dijit.byId("ATSTRTIP-" + id);
1047
1048                                                         if (tags) tags.innerHTML = data.content;
1049                                                         if (tooltip) tooltip.attr('label', data.content_full);
1050                                                 }
1051                                         } catch (e) {
1052                                                 exception_error(e);
1053                                         }
1054
1055                                 }});
1056                         }
1057                 },
1058                 href: query
1059         });
1060
1061         var tmph = dojo.connect(dialog, 'onLoad', function() {
1062                 dojo.disconnect(tmph);
1063
1064                 new Ajax.Autocompleter('tags_str', 'tags_choices',
1065                    "backend.php?op=article&method=completeTags",
1066                    { tokens: ',', paramName: "search" });
1067         });
1068
1069         dialog.show();
1070
1071 }
1072
1073 function cdmScrollToArticleId(id, force) {
1074         const ctr = $("headlines-frame");
1075         const e = $("RROW-" + id);
1076
1077         if (!e || !ctr) return;
1078
1079         if (force || e.offsetTop+e.offsetHeight > (ctr.scrollTop+ctr.offsetHeight) ||
1080                         e.offsetTop < ctr.scrollTop) {
1081
1082                 // expanded cdm has a 4px margin now
1083                 ctr.scrollTop = parseInt(e.offsetTop) - 4;
1084         }
1085 }
1086
1087 function setActiveArticleId(id) {
1088         console.log("setActiveArticleId:" + id);
1089
1090         _active_article_id = id;
1091         PluginHost.run(PluginHost.HOOK_ARTICLE_SET_ACTIVE, _active_article_id);
1092 }
1093
1094 function getActiveArticleId() {
1095         return _active_article_id;
1096 }
1097
1098 function postMouseIn(e, id) {
1099         post_under_pointer = id;
1100 }
1101
1102 function postMouseOut(id) {
1103         post_under_pointer = false;
1104 }
1105
1106 function unpackVisibleHeadlines() {
1107         if (!isCdmMode() || !getInitParam("cdm_expanded")) return;
1108
1109         $$("#headlines-frame span.cencw[id]").each(
1110                 function (child) {
1111                         const row = $("RROW-" + child.id.replace("CENCW-", ""));
1112
1113                         if (row && row.offsetTop <= $("headlines-frame").scrollTop +
1114                                 $("headlines-frame").offsetHeight) {
1115
1116                                 //console.log("unpacking: " + child.id);
1117
1118                                 child.innerHTML = htmlspecialchars_decode(child.innerHTML);
1119                                 child.removeAttribute('id');
1120
1121                                 PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED_CDM, row);
1122
1123                                 Element.show(child);
1124                         }
1125                 }
1126         );
1127 }
1128
1129 function headlines_scroll_handler(e) {
1130         try {
1131
1132                 // rate-limit in case of smooth scrolling and similar abominations
1133                 if (Math.max(e.scrollTop, _headlines_scroll_offset) - Math.min(e.scrollTop, _headlines_scroll_offset) < 25) {
1134                         return;
1135                 }
1136
1137                 _headlines_scroll_offset = e.scrollTop;
1138
1139                 const hsp = $("headlines-spacer");
1140
1141                 unpackVisibleHeadlines();
1142
1143                 // set topmost child in the buffer as active
1144                 if (isCdmMode() && getInitParam("cdm_auto_catchup") == 1 &&
1145                                 getSelectedArticleIds2().length <= 1 &&
1146                                 getInitParam("cdm_expanded")) {
1147
1148                         const rows = $$("#headlines-frame > div[id*=RROW]");
1149
1150                         for (let i = 0; i < rows.length; i++) {
1151                                 var child = rows[i];
1152
1153                                 if ($("headlines-frame").scrollTop <= child.offsetTop &&
1154                                         child.offsetTop - $("headlines-frame").scrollTop < 100 &&
1155                                         child.getAttribute("data-article-id") != _active_article_id) {
1156
1157                                         if (_active_article_id) {
1158                                                 const row = $("RROW-" + _active_article_id);
1159                                                 if (row) row.removeClassName("active");
1160                                         }
1161
1162                                         _active_article_id = child.getAttribute("data-article-id");
1163                                         showArticleInHeadlines(_active_article_id, true);
1164                                         updateSelectedPrompt();
1165                                         break;
1166                                 }
1167                         }
1168                 }
1169
1170                 if (!_infscroll_disable) {
1171                         if (hsp && hsp.offsetTop - 250 <= e.scrollTop + e.offsetHeight) {
1172
1173                                 hsp.innerHTML = "<span class='loading'><img src='images/indicator_tiny.gif'> " +
1174                                         __("Loading, please wait...") + "</span>";
1175
1176                                 loadMoreHeadlines();
1177                                 return;
1178
1179                         }
1180                 }
1181
1182                 if (isCdmMode()) {
1183                         updateFloatingTitle();
1184                 }
1185
1186                 catchupCurrentBatchIfNeeded();
1187
1188                 if (getInitParam("cdm_auto_catchup") == 1) {
1189
1190                         // let's get DOM some time to settle down
1191                         const ts = new Date().getTime();
1192                         if (ts - _last_headlines_update < 100) return;
1193
1194                         $$("#headlines-frame > div[id*=RROW][class*=Unread]").each(
1195                                 function(child) {
1196                                         if (child.hasClassName("Unread") && $("headlines-frame").scrollTop >
1197                                                         (child.offsetTop + child.offsetHeight/2)) {
1198
1199                                                 const id = child.getAttribute("data-article-id")
1200
1201                                                 if (catchup_id_batch.indexOf(id) == -1)
1202                                                         catchup_id_batch.push(id);
1203
1204                                                 //console.log("auto_catchup_batch: " + catchup_id_batch.toString());
1205                                         }
1206
1207                                 });
1208
1209                         if (_infscroll_disable) {
1210                                 var child = $$("#headlines-frame div[id*=RROW]").last();
1211
1212                                 if (child && $("headlines-frame").scrollTop >
1213                                                 (child.offsetTop + child.offsetHeight - 50)) {
1214
1215                                         console.log("we seem to be at an end");
1216
1217                                         if (getInitParam("on_catchup_show_next_feed") == "1") {
1218                                                 openNextUnreadFeed();
1219                                         }
1220                                 }
1221                         }
1222                 }
1223
1224         } catch (e) {
1225                 console.warn("headlines_scroll_handler: " + e);
1226         }
1227 }
1228
1229 function openNextUnreadFeed() {
1230         const is_cat = activeFeedIsCat();
1231         const nuf = getNextUnreadFeed(getActiveFeedId(), is_cat);
1232         if (nuf) viewfeed({feed: nuf, is_cat: is_cat});
1233 }
1234
1235 function catchupBatchedArticles() {
1236         if (catchup_id_batch.length > 0 && !_infscroll_request_sent && !_catchup_request_sent) {
1237
1238                 console.log("catchupBatchedArticles: working");
1239
1240                 // make a copy of the array
1241                 const batch = catchup_id_batch.slice();
1242                 const query = "?op=rpc&method=catchupSelected" +
1243                         "&cmode=0&ids=" + param_escape(batch.toString());
1244
1245                 console.log(query);
1246
1247                 _catchup_request_sent = true;
1248
1249                 new Ajax.Request("backend.php", {
1250                         parameters: query,
1251                         onComplete: function (transport) {
1252                                 handle_rpc_json(transport);
1253
1254                                 _catchup_request_sent = false;
1255
1256                                 const reply = JSON.parse(transport.responseText);
1257                                 const batch = reply.ids;
1258
1259                                 batch.each(function (id) {
1260                                         console.log(id);
1261                                         const elem = $("RROW-" + id);
1262                                         if (elem) elem.removeClassName("Unread");
1263                                         catchup_id_batch.remove(id);
1264                                 });
1265
1266                                 updateFloatingTitle(true);
1267
1268                         }
1269                 });
1270         }
1271 }
1272
1273 function catchupRelativeToArticle(below, id) {
1274
1275         if (!id) id = getActiveArticleId();
1276
1277         if (!id) {
1278                 alert(__("No article is selected."));
1279                 return;
1280         }
1281
1282         const visible_ids = getLoadedArticleIds();
1283
1284         const ids_to_mark = new Array();
1285
1286         if (!below) {
1287                 for (var i = 0; i < visible_ids.length; i++) {
1288                         if (visible_ids[i] != id) {
1289                                 var e = $("RROW-" + visible_ids[i]);
1290
1291                                 if (e && e.hasClassName("Unread")) {
1292                                         ids_to_mark.push(visible_ids[i]);
1293                                 }
1294                         } else {
1295                                 break;
1296                         }
1297                 }
1298         } else {
1299                 for (var i = visible_ids.length - 1; i >= 0; i--) {
1300                         if (visible_ids[i] != id) {
1301                                 var e = $("RROW-" + visible_ids[i]);
1302
1303                                 if (e && e.hasClassName("Unread")) {
1304                                         ids_to_mark.push(visible_ids[i]);
1305                                 }
1306                         } else {
1307                                 break;
1308                         }
1309                 }
1310         }
1311
1312         if (ids_to_mark.length == 0) {
1313                 alert(__("No articles found to mark"));
1314         } else {
1315                 const msg = ngettext("Mark %d article as read?", "Mark %d articles as read?", ids_to_mark.length).replace("%d", ids_to_mark.length);
1316
1317                 if (getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
1318
1319                         for (var i = 0; i < ids_to_mark.length; i++) {
1320                                 var e = $("RROW-" + ids_to_mark[i]);
1321                                 e.removeClassName("Unread");
1322                         }
1323
1324                         const query = "?op=rpc&method=catchupSelected" +
1325                                 "&cmode=0" + "&ids=" + param_escape(ids_to_mark.toString());
1326
1327                         new Ajax.Request("backend.php", {
1328                                 parameters: query,
1329                                 onComplete: function (transport) {
1330                                         handle_rpc_json(transport);
1331                                 }
1332                         });
1333
1334                 }
1335         }
1336 }
1337
1338 function cdmCollapseArticle(event, id, unmark) {
1339         if (unmark == undefined) unmark = true;
1340
1341         const row = $("RROW-" + id);
1342         const elem = $("CICD-" + id);
1343
1344         if (elem && row) {
1345                 const collapse = row.select("span[class='collapseBtn']")[0];
1346
1347                 Element.hide(elem);
1348                 Element.show("CEXC-" + id);
1349                 Element.hide(collapse);
1350
1351                 if (unmark) {
1352                         row.removeClassName("active");
1353
1354                         markHeadline(id, false);
1355
1356                         if (id == getActiveArticleId()) {
1357                                 setActiveArticleId(0);
1358                         }
1359
1360                         updateSelectedPrompt();
1361                 }
1362
1363                 if (event) Event.stop(event);
1364
1365                 PluginHost.run(PluginHost.HOOK_ARTICLE_COLLAPSED, id);
1366
1367                 if (row.offsetTop < $("headlines-frame").scrollTop)
1368                         scrollToRowId(row.id);
1369
1370                 $("floatingTitle").style.visibility = "hidden";
1371                 $("floatingTitle").setAttribute("data-article-id", 0);
1372         }
1373 }
1374
1375 function cdmExpandArticle(id, noexpand) {
1376         console.log("cdmExpandArticle " + id);
1377
1378         const row = $("RROW-" + id);
1379
1380         if (!row) return false;
1381
1382         const oldrow = $("RROW-" + getActiveArticleId());
1383
1384         let elem = $("CICD-" + getActiveArticleId());
1385
1386         if (id == getActiveArticleId() && Element.visible(elem))
1387                 return true;
1388
1389         selectArticles("none");
1390
1391         const old_offset = row.offsetTop;
1392
1393         if (getActiveArticleId() && elem && !getInitParam("cdm_expanded")) {
1394                 var collapse = oldrow.select("span[class='collapseBtn']")[0];
1395
1396                 Element.hide(elem);
1397                 Element.show("CEXC-" + getActiveArticleId());
1398                 Element.hide(collapse);
1399         }
1400
1401         if (oldrow) oldrow.removeClassName("active");
1402
1403         setActiveArticleId(id);
1404
1405         elem = $("CICD-" + id);
1406
1407         var collapse = row.select("span[class='collapseBtn']")[0];
1408
1409         const cencw = $("CENCW-" + id);
1410
1411         if (!Element.visible(elem) && !noexpand) {
1412                 if (cencw) {
1413                         cencw.innerHTML = htmlspecialchars_decode(cencw.innerHTML);
1414                         cencw.setAttribute('id', '');
1415                         Element.show(cencw);
1416                 }
1417
1418                 Element.show(elem);
1419                 Element.hide("CEXC-" + id);
1420                 Element.show(collapse);
1421         }
1422
1423         const new_offset = row.offsetTop;
1424
1425         if (old_offset > new_offset)
1426                 $("headlines-frame").scrollTop -= (old_offset - new_offset);
1427
1428         if (!noexpand) {
1429                 if (catchup_id_batch.indexOf(id) == -1)
1430                         catchup_id_batch.push(id);
1431
1432                 catchupCurrentBatchIfNeeded();
1433         }
1434
1435         toggleSelected(id);
1436         row.addClassName("active");
1437
1438         PluginHost.run(PluginHost.HOOK_ARTICLE_EXPANDED, id);
1439
1440         return false;
1441 }
1442
1443 function getArticleUnderPointer() {
1444         return post_under_pointer;
1445 }
1446
1447 function scrollArticle(offset) {
1448         if (!isCdmMode()) {
1449                 const ci = $("content-insert");
1450                 if (ci) {
1451                         ci.scrollTop += offset;
1452                 }
1453         } else {
1454                 const hi = $("headlines-frame");
1455                 if (hi) {
1456                         hi.scrollTop += offset;
1457                 }
1458
1459         }
1460 }
1461
1462 function show_labels_in_headlines(transport) {
1463         const data = JSON.parse(transport.responseText);
1464
1465         if (data) {
1466                 data['info-for-headlines'].each(function (elem) {
1467                         $$(".HLLCTR-" + elem.id).each(function (ctr) {
1468                                 ctr.innerHTML = elem.labels;
1469                         });
1470                 });
1471         }
1472 }
1473
1474 function cdmClicked(event, id, in_body) {
1475         //var shift_key = event.shiftKey;
1476
1477         if (!event.ctrlKey && !event.metaKey) {
1478
1479                 if (!getInitParam("cdm_expanded")) {
1480                         return cdmExpandArticle(id);
1481                 } else {
1482
1483                         var elem = $("RROW-" + getActiveArticleId());
1484
1485                         if (elem) elem.removeClassName("active");
1486
1487                         selectArticles("none");
1488                         toggleSelected(id);
1489
1490                         var elem = $("RROW-" + id);
1491                         var article_is_unread = elem.hasClassName("Unread");
1492
1493                         elem.removeClassName("Unread");
1494                         elem.addClassName("active");
1495
1496                         setActiveArticleId(id);
1497
1498                         if (article_is_unread) {
1499                                 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1500                                 updateFloatingTitle(true);
1501                         }
1502
1503                         const query = "?op=rpc&method=catchupSelected" +
1504                                 "&cmode=0&ids=" + param_escape(id);
1505
1506                         new Ajax.Request("backend.php", {
1507                                 parameters: query,
1508                                 onComplete: function (transport) {
1509                                         handle_rpc_json(transport);
1510                                 }
1511                         });
1512
1513                         return !event.shiftKey;
1514                 }
1515
1516         } else if (!in_body) {
1517
1518                 toggleSelected(id, true);
1519
1520                 var elem = $("RROW-" + id);
1521                 var article_is_unread = elem.hasClassName("Unread");
1522
1523                 if (article_is_unread) {
1524                         decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1525                 }
1526
1527                 toggleUnread(id, 0, false);
1528
1529                 openArticleInNewWindow(id);
1530         } else {
1531                 return true;
1532         }
1533
1534         const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length
1535         request_counters(unread_in_buffer == 0);
1536
1537         return false;
1538 }
1539
1540 function hlClicked(event, id) {
1541         if (event.which == 2) {
1542                 view(id);
1543                 return true;
1544         } else if (event.ctrlKey || event.metaKey) {
1545                 openArticleInNewWindow(id);
1546                 return false;
1547         } else {
1548                 view(id);
1549                 return false;
1550         }
1551 }
1552
1553 function openArticleInNewWindow(id) {
1554         toggleUnread(id, 0, false);
1555
1556         const w = window.open("");
1557         w.opener = null;
1558         w.location = "backend.php?op=article&method=redirect&id=" + id;
1559 }
1560
1561 function isCdmMode() {
1562         return getInitParam("combined_display_mode");
1563 }
1564
1565 function markHeadline(id, marked) {
1566         if (marked == undefined) marked = true;
1567
1568         const row = $("RROW-" + id);
1569         if (row) {
1570                 const check = dijit.getEnclosingWidget(
1571                                 row.getElementsByClassName("rchk")[0]);
1572
1573                 if (check) {
1574                         check.attr("checked", marked);
1575                 }
1576
1577                 if (marked)
1578                         row.addClassName("Selected");
1579                 else
1580                         row.removeClassName("Selected");
1581         }
1582 }
1583
1584 function getRelativePostIds(id, limit) {
1585
1586         const tmp = [];
1587
1588         if (!limit) limit = 6; //3
1589
1590         const ids = getLoadedArticleIds();
1591
1592         for (let i = 0; i < ids.length; i++) {
1593                 if (ids[i] == id) {
1594                         for (let k = 1; k <= limit; k++) {
1595                                 //if (i > k-1) tmp.push(ids[i-k]);
1596                                 if (i < ids.length - k) tmp.push(ids[i + k]);
1597                         }
1598                         break;
1599                 }
1600         }
1601
1602         return tmp;
1603 }
1604
1605 function correctHeadlinesOffset(id) {
1606
1607         const container = $("headlines-frame");
1608         const row = $("RROW-" + id);
1609
1610         if (!container || !row) return;
1611
1612         const viewport = container.offsetHeight;
1613
1614         const rel_offset_top = row.offsetTop - container.scrollTop;
1615         const rel_offset_bottom = row.offsetTop + row.offsetHeight - container.scrollTop;
1616
1617         //console.log("Rtop: " + rel_offset_top + " Rbtm: " + rel_offset_bottom);
1618         //console.log("Vport: " + viewport);
1619
1620         if (rel_offset_top <= 0 || rel_offset_top > viewport) {
1621                 container.scrollTop = row.offsetTop;
1622         } else if (rel_offset_bottom > viewport) {
1623
1624                 /* doesn't properly work with Opera in some cases because
1625                  Opera fucks up element scrolling */
1626
1627                 container.scrollTop = row.offsetTop + row.offsetHeight - viewport;
1628         }
1629 }
1630
1631 function headlineActionsChange(elem) {
1632         eval(elem.value);
1633         elem.attr('value', 'false');
1634 }
1635
1636 function closeArticlePanel() {
1637
1638         if (dijit.byId("content-insert"))
1639                 dijit.byId("headlines-wrap-inner").removeChild(
1640                         dijit.byId("content-insert"));
1641 }
1642
1643 function initFloatingMenu() {
1644         if (!dijit.byId("floatingMenu")) {
1645
1646                 const menu = new dijit.Menu({
1647                         id: "floatingMenu",
1648                         targetNodeIds: ["floatingTitle"]
1649                 });
1650
1651                 headlinesMenuCommon(menu);
1652
1653                 menu.startup();
1654         }
1655 }
1656
1657 function headlinesMenuCommon(menu) {
1658
1659         menu.addChild(new dijit.MenuItem({
1660                 label: __("Open original article"),
1661                 onClick: function (event) {
1662                         openArticleInNewWindow(this.getParent().currentTarget.getAttribute("data-article-id"));
1663                 }
1664         }));
1665
1666         menu.addChild(new dijit.MenuItem({
1667                 label: __("Display article URL"),
1668                 onClick: function (event) {
1669                         displayArticleUrl(this.getParent().currentTarget.getAttribute("data-article-id"));
1670                 }
1671         }));
1672
1673         menu.addChild(new dijit.MenuSeparator());
1674
1675         menu.addChild(new dijit.MenuItem({
1676                 label: __("Toggle unread"),
1677                 onClick: function (event) {
1678
1679                         let ids = getSelectedArticleIds2();
1680                         // cast to string
1681                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1682                         ids = ids.size() != 0 && ids.indexOf(id) != -1 ? ids : [id];
1683
1684                         selectionToggleUnread(undefined, false, true, ids);
1685                 }
1686         }));
1687
1688         menu.addChild(new dijit.MenuItem({
1689                 label: __("Toggle starred"),
1690                 onClick: function (event) {
1691                         let ids = getSelectedArticleIds2();
1692                         // cast to string
1693                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1694                         ids = ids.size() != 0 && ids.indexOf(id) != -1 ? ids : [id];
1695
1696                         selectionToggleMarked(undefined, false, true, ids);
1697                 }
1698         }));
1699
1700         menu.addChild(new dijit.MenuItem({
1701                 label: __("Toggle published"),
1702                 onClick: function (event) {
1703                         let ids = getSelectedArticleIds2();
1704                         // cast to string
1705                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1706                         ids = ids.size() != 0 && ids.indexOf(id) != -1 ? ids : [id];
1707
1708                         selectionTogglePublished(undefined, false, true, ids);
1709                 }
1710         }));
1711
1712         menu.addChild(new dijit.MenuSeparator());
1713
1714         menu.addChild(new dijit.MenuItem({
1715                 label: __("Mark above as read"),
1716                 onClick: function (event) {
1717                         catchupRelativeToArticle(0, this.getParent().currentTarget.getAttribute("data-article-id"));
1718                 }
1719         }));
1720
1721         menu.addChild(new dijit.MenuItem({
1722                 label: __("Mark below as read"),
1723                 onClick: function (event) {
1724                         catchupRelativeToArticle(1, this.getParent().currentTarget.getAttribute("data-article-id"));
1725                 }
1726         }));
1727
1728
1729         const labels = getInitParam("labels");
1730
1731         if (labels && labels.length) {
1732
1733                 menu.addChild(new dijit.MenuSeparator());
1734
1735                 const labelAddMenu = new dijit.Menu({ownerMenu: menu});
1736                 const labelDelMenu = new dijit.Menu({ownerMenu: menu});
1737
1738                 labels.each(function (label) {
1739                         const bare_id = label.id;
1740                         const name = label.caption;
1741
1742                         labelAddMenu.addChild(new dijit.MenuItem({
1743                                 label: name,
1744                                 labelId: bare_id,
1745                                 onClick: function (event) {
1746
1747                                         let ids = getSelectedArticleIds2();
1748                                         // cast to string
1749                                         const id = (this.getParent().ownerMenu.currentTarget.getAttribute("data-article-id")) + "";
1750
1751                                         ids = ids.size() != 0 && ids.indexOf(id) != -1 ? ids : [id];
1752
1753                                         selectionAssignLabel(this.labelId, ids);
1754                                 }
1755                         }));
1756
1757                         labelDelMenu.addChild(new dijit.MenuItem({
1758                                 label: name,
1759                                 labelId: bare_id,
1760                                 onClick: function (event) {
1761                                         let ids = getSelectedArticleIds2();
1762                                         // cast to string
1763                                         const id = (this.getParent().ownerMenu.currentTarget.getAttribute("data-article-id")) + "";
1764
1765                                         ids = ids.size() != 0 && ids.indexOf(id) != -1 ? ids : [id];
1766
1767                                         selectionRemoveLabel(this.labelId, ids);
1768                                 }
1769                         }));
1770
1771                 });
1772
1773                 menu.addChild(new dijit.PopupMenuItem({
1774                         label: __("Assign label"),
1775                         popup: labelAddMenu
1776                 }));
1777
1778                 menu.addChild(new dijit.PopupMenuItem({
1779                         label: __("Remove label"),
1780                         popup: labelDelMenu
1781                 }));
1782
1783         }
1784 }
1785
1786 function initHeadlinesMenu() {
1787         if (!dijit.byId("headlinesMenu")) {
1788
1789                 var menu = new dijit.Menu({
1790                         id: "headlinesMenu",
1791                         targetNodeIds: ["headlines-frame"],
1792                         selector: ".hlMenuAttach"
1793                 });
1794
1795                 headlinesMenuCommon(menu);
1796
1797                 menu.startup();
1798         }
1799
1800         /* vgroup feed title menu */
1801
1802         if (!dijit.byId("headlinesFeedTitleMenu")) {
1803
1804                 var menu = new dijit.Menu({
1805                         id: "headlinesFeedTitleMenu",
1806                         targetNodeIds: ["headlines-frame"],
1807                         selector: "div.cdmFeedTitle"
1808                 });
1809
1810                 menu.addChild(new dijit.MenuItem({
1811                         label: __("Select articles in group"),
1812                         onClick: function (event) {
1813                                 selectArticles("all",
1814                                         "#headlines-frame > div[id*=RROW]" +
1815                                         "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute("data-feed-id") + "']");
1816
1817                         }
1818                 }));
1819
1820                 menu.addChild(new dijit.MenuItem({
1821                         label: __("Mark group as read"),
1822                         onClick: function (event) {
1823                                 selectArticles("none");
1824                                 selectArticles("all",
1825                                         "#headlines-frame > div[id*=RROW]" +
1826                                         "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute("data-feed-id") + "']");
1827
1828                                 catchupSelection();
1829                         }
1830                 }));
1831
1832                 menu.addChild(new dijit.MenuItem({
1833                         label: __("Mark feed as read"),
1834                         onClick: function (event) {
1835                                 catchupFeedInGroup(this.getParent().currentTarget.getAttribute("data-feed-id"));
1836                         }
1837                 }));
1838
1839                 menu.addChild(new dijit.MenuItem({
1840                         label: __("Edit feed"),
1841                         onClick: function (event) {
1842                                 editFeed(this.getParent().currentTarget.getAttribute("data-feed-id"));
1843                         }
1844                 }));
1845
1846                 menu.startup();
1847         }
1848 }
1849
1850 function cache_set(id, obj) {
1851         //console.log("cache_set: " + id);
1852         if (has_storage)
1853                 try {
1854                         sessionStorage[id] = obj;
1855                 } catch (e) {
1856                         sessionStorage.clear();
1857                 }
1858 }
1859
1860 function cache_get(id) {
1861         if (has_storage)
1862                 return sessionStorage[id];
1863 }
1864
1865 function cache_clear() {
1866         if (has_storage)
1867                 sessionStorage.clear();
1868 }
1869
1870 function cache_delete(id) {
1871         if (has_storage)
1872                 sessionStorage.removeItem(id);
1873 }
1874
1875 function cancelSearch() {
1876         _search_query = "";
1877         viewCurrentFeed();
1878 }
1879
1880 function setSelectionScore() {
1881         const ids = getSelectedArticleIds2();
1882
1883         if (ids.length > 0) {
1884                 console.log(ids);
1885
1886                 var score = prompt(__("Please enter new score for selected articles:"), score);
1887
1888                 if (score != undefined) {
1889                         const query = "op=article&method=setScore&id=" + param_escape(ids.toString()) +
1890                                 "&score=" + param_escape(score);
1891
1892                         new Ajax.Request("backend.php", {
1893                                 parameters: query,
1894                                 onComplete: function (transport) {
1895                                         const reply = JSON.parse(transport.responseText);
1896                                         if (reply) {
1897                                                 console.log(ids);
1898
1899                                                 ids.each(function (id) {
1900                                                         const row = $("RROW-" + id);
1901
1902                                                         if (row) {
1903                                                                 const pic = row.getElementsByClassName("hlScorePic")[0];
1904
1905                                                                 if (pic) {
1906                                                                         pic.src = pic.src.replace(/score_.*?\.png/,
1907                                                                                 reply["score_pic"]);
1908                                                                         pic.setAttribute("score", score);
1909                                                                 }
1910                                                         }
1911                                                 });
1912                                         }
1913                                 }
1914                         });
1915                 }
1916
1917         } else {
1918                 alert(__("No articles are selected."));
1919         }
1920 }
1921
1922 function updateScore(id) {
1923         const pic = $$("#RROW-" + id + " .hlScorePic")[0];
1924
1925         if (pic) {
1926
1927                 const query = "op=article&method=getScore&id=" + param_escape(id);
1928
1929                 new Ajax.Request("backend.php", {
1930                         parameters: query,
1931                         onComplete: function (transport) {
1932                                 console.log(transport.responseText);
1933
1934                                 const reply = JSON.parse(transport.responseText);
1935
1936                                 if (reply) {
1937                                         pic.src = pic.src.replace(/score_.*?\.png/, reply["score_pic"]);
1938                                         pic.setAttribute("score", reply["score"]);
1939                                         pic.setAttribute("title", reply["score"]);
1940                                 }
1941                         }
1942                 });
1943         }
1944 }
1945
1946 function changeScore(id, pic) {
1947         const score = pic.getAttribute("score");
1948
1949         const new_score = prompt(__("Please enter new score for this article:"), score);
1950
1951         if (new_score != undefined) {
1952
1953                 const query = "op=article&method=setScore&id=" + param_escape(id) +
1954                         "&score=" + param_escape(new_score);
1955
1956                 new Ajax.Request("backend.php", {
1957                         parameters: query,
1958                         onComplete: function (transport) {
1959                                 const reply = JSON.parse(transport.responseText);
1960
1961                                 if (reply) {
1962                                         pic.src = pic.src.replace(/score_.*?\.png/, reply["score_pic"]);
1963                                         pic.setAttribute("score", new_score);
1964                                         pic.setAttribute("title", new_score);
1965                                 }
1966                         }
1967                 });
1968         }
1969 }
1970
1971 function displayArticleUrl(id) {
1972         const query = "op=rpc&method=getlinktitlebyid&id=" + param_escape(id);
1973
1974         new Ajax.Request("backend.php", {
1975                 parameters: query,
1976                 onComplete: function (transport) {
1977                         const reply = JSON.parse(transport.responseText);
1978
1979                         if (reply && reply.link) {
1980                                 prompt(__("Article URL:"), reply.link);
1981                         }
1982                 }
1983         });
1984 }
1985
1986 function scrollToRowId(id) {
1987         const row = $(id);
1988
1989         if (row)
1990                 $("headlines-frame").scrollTop = row.offsetTop - 4;
1991 }
1992
1993 function updateFloatingTitle(unread_only) {
1994         if (!isCdmMode()) return;
1995
1996         const hf = $("headlines-frame");
1997
1998         const elems = $$("#headlines-frame > div[id*=RROW]");
1999
2000         for (let i = 0; i < elems.length; i++) {
2001
2002                 const child = elems[i];
2003
2004                 if (child && child.offsetTop + child.offsetHeight > hf.scrollTop) {
2005
2006                         const header = child.getElementsByClassName("cdmHeader")[0];
2007
2008                         if (unread_only || child.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
2009                                 if (child.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
2010
2011                                         $("floatingTitle").setAttribute("data-article-id", child.getAttribute("data-article-id"));
2012                                         $("floatingTitle").innerHTML = header.innerHTML;
2013                                         $("floatingTitle").firstChild.innerHTML = "<img class='anchor markedPic' src='images/page_white_go.png' onclick=\"scrollToRowId('" + child.id + "')\">" + $("floatingTitle").firstChild.innerHTML;
2014
2015                                         initFloatingMenu();
2016
2017                                         const cb = $$("#floatingTitle .dijitCheckBox")[0];
2018
2019                                         if (cb)
2020                                                 cb.parentNode.removeChild(cb);
2021                                 }
2022
2023                                 if (child.hasClassName("Unread"))
2024                                         $("floatingTitle").addClassName("Unread");
2025                                 else
2026                                         $("floatingTitle").removeClassName("Unread");
2027
2028                                 PluginHost.run(PluginHost.HOOK_FLOATING_TITLE, child);
2029                         }
2030
2031                         $("floatingTitle").style.marginRight = hf.offsetWidth - child.offsetWidth + "px";
2032                         if (header.offsetTop + header.offsetHeight < hf.scrollTop + $("floatingTitle").offsetHeight - 5 &&
2033                                 child.offsetTop + child.offsetHeight >= hf.scrollTop + $("floatingTitle").offsetHeight - 5)
2034                                 $("floatingTitle").style.visibility = "visible";
2035                         else
2036                                 $("floatingTitle").style.visibility = "hidden";
2037
2038                         return;
2039
2040                 }
2041         }
2042 }
2043
2044 function catchupCurrentBatchIfNeeded() {
2045         if (catchup_id_batch.length > 0) {
2046                 window.clearTimeout(catchup_timeout_id);
2047                 catchup_timeout_id = window.setTimeout(catchupBatchedArticles, 1000);
2048
2049                 if (catchup_id_batch.length >= 10) {
2050                         catchupBatchedArticles();
2051                 }
2052         }
2053 }
2054
2055 function cdmFooterClick(event) {
2056         event.stopPropagation();
2057 }