PageSourceSearch

https://hub.farthinghalearms.com/view/js/main.js?v=11.4.1

js farthinghalearms.com collected 2026-10-07 21:29:49 UTC 69,988 bytes, 2,368 lines download raw bytes

1var src = null;
2var prev = null;
3var livetime = null;
4var msie = false;
5var stopped = false;
6var totStopped = false;
7var timer = null;
8var pr = 0;
9var liking = 0;
10var in_progress = false;
11var langSelect = false;
12var commentBusy = false;
13var last_popup_menu = null;
14var last_popup_button = null;
15var scroll_next = false;
16var next_page = 1;
17var page_load = true;
18var loadingPage = true;
19var pageHasMoreContent = true;
20var divmore_height = 400;
21var last_filestorage_id = null;
22var mediaPlaying = false;
23var liveRecurse = 0;
24var savedTitle = '';
25var followUpPageLoad = false;
26var window_needs_alert = true;
27var expanded_items = [];
28var updateTimeout = [];
29const singlethread_modules = ['display', 'hq'];
30const redirect_modules = ['display', 'notify'];
31let b64mids = [];
32
33
34var page_cache = {};
35
36// take care of tab/window reloads on channel change
37if(localStorage.getItem('uid') !== localUser.toString()) {
38	localStorage.clear();
39	sessionStorage.clear();
40	localStorage.setItem('uid', localUser.toString());
41}
42
43window.onstorage = function(e) {
44	if(e.key === 'uid' && parseInt(e.newValue) !== localUser) {
45		if(window_needs_alert) {
46			window_needs_alert = false;
47			alert("Your identity has changed. Page reload required!");
48			window.location.reload();
49			return;
50		}
51	}
52}
53
54if ('serviceWorker' in navigator) {
55	navigator.serviceWorker.register('/ServiceWorker.js', { scope: '/' }).then(function(registration) {
56		console.log('Service worker registered. scope is', registration.scope);
57	}).catch(function(error) {
58		console.log('Service worker registration failed because ' + error);
59	});
60}
61
62$.ajaxSetup({cache: false});
63
64var tf = new Function('n', 's', 'var k = s.split("/")['+aStr['plural_func']+']; return (k ? k : s);');
65
66$(document).ready(function() {
67
68	$(document).on('click focus', '.comment-edit-form', handle_comment_form);
69	$(document).on('click', '.conversation-settings-link', getConversationSettings);
70	$(document).on('click', '#settings_module_ajax_submit', postConversationSettings);
71
72	$(document).on('click', '#expand-aside', toggleAside);
73
74	$(document).on('click focus', '.comment-edit-form  textarea', function(e) {
75		if(! this.autocomplete_handled) {
76			/* autocomplete @nicknames */
77			$(this).editor_autocomplete(baseurl+"/acl?n=1");
78			/* autocomplete bbcode */
79			$(this).bbco_autocomplete('bbcode');
80
81			this.autocomplete_handled = true;
82		}
83
84	});
85
86	$(document).on('keydown', '.comment-edit-form  textarea.expanded', function (e) {
87		if (e.ctrlKey && e.keyCode === 13) {
88			post_comment(this.id.replace('comment-edit-text-',''));
89		}
90	});
91
92	document.addEventListener('click', function(event) {
93		// Only handle clicks on .wall-item-reaction or its children
94		const target = event.target.closest('.wall-item-reaction');
95		if (!target) return;
96
97		let doRequest = true;
98		const isUserClick = event.isTrusted;
99
100		// Destructure relevant data attributes
101		const { itemId: id, itemMid: mid, itemParent: parentId, itemUuid: uuid, itemVerb: verb } = target.dataset;
102		const isCommentBtn = target.classList.contains('wall-item-comment');
103
104		if (isCommentBtn) {
105			if (id === parentId) {
106				// Handle blog mode
107				target.classList.add('disabled');
108				document.getElementById(`load-more-progress-wrapper-${id}`).classList.remove('d-none');
109				document.getElementById(`load-more-${id}`).classList.remove('d-none')
110				request(id, mid, 'load', parentId, uuid, isUserClick);
111				return;
112			}
113
114			// Get relevant DOM elements
115			const threadWrapper = document.getElementById(`thread-wrapper-${id}`);
116			const parentWrapper = document.getElementById(`thread-wrapper-${parentId}`);
117			const subThreadWrapper = document.getElementById(`wall-item-sub-thread-wrapper-${id}`);
118			const parentSubThreadWrapper = document.getElementById(`wall-item-sub-thread-wrapper-${parentId}`);
119
120			// Query related sub-thread and highlight elements
121			const parentIndentedThreads = document.querySelectorAll(`#wall-item-sub-thread-wrapper-${parentId} .wall-item-sub-thread-wrapper.item-indent`);
122
123			let ancestorIds = [];
124
125			doRequest = !subThreadWrapper.children.length;
126
127			// Set visual styles using UUID
128			subThreadWrapper.style.setProperty('--hz-item-indent', stringToHslColor(uuid));
129			threadWrapper.style.setProperty('--hz-item-highlight', stringToHslColor(uuid));
130			threadWrapper.style.setProperty('--hz-wall-item-expanded-before-content', '"' + aStr.dblclick_to_exit_zoom + '"');
131
132			// Clear previous highlights
133			parentSubThreadWrapper.querySelectorAll('.thread-wrapper.item-highlight').forEach(el => el.classList.remove('item-highlight'));
134
135			if (isUserClick && parentIndentedThreads.length === 0 && !subThreadWrapper.children.length) {
136				// Handle first-time expansion and highlighting but not for toplevels (blog mode)
137				threadWrapper.classList.add('item-highlight');
138			} else {
139				// Handle indentation and zooming
140				let ancestor = subThreadWrapper.parentElement;
141				ancestorIds.push(ancestor.id.slice(15)); // thread-wrapper-1234
142
143				while (ancestor) {
144					if (ancestor.classList.contains('item-indent') && ancestor.classList.contains('wall-item-sub-thread-wrapper')) {
145						ancestorIds.push(ancestor.parentElement.id.slice(15));
146					}
147					ancestor = ancestor.parentElement;
148				}
149
150				ancestorIds.reverse();
151
152				if (ancestorIds.length > 3) {
153					// Handle zooming in
154					let firstWrapper = document.getElementById('thread-wrapper-' + ancestorIds[0]);
155					let firstSubWrapper = document.getElementById('wall-item-sub-thread-wrapper-' + ancestorIds[0]);
156
157					firstWrapper.querySelector('.wall-item-comment').classList.remove('indented');
158					firstWrapper.classList.remove('wall-item-expanded', 'shadow');
159					firstSubWrapper.classList.remove('item-indent');
160
161					let newFirstWrapper = document.getElementById('thread-wrapper-' + ancestorIds[1])
162					let newFirstSubWrapper = document.getElementById('wall-item-sub-thread-wrapper-' + ancestorIds[1])
163
164					newFirstWrapper.classList.add('wall-item-expanded', 'shadow');
165					parentWrapper.classList.add('wall-item-backdrop');
166
167					// Exit zoom on double-click
168					newFirstWrapper.addEventListener('dblclick', function() {
169						parentWrapper.querySelectorAll('.wall-item-comment.indented').forEach(el => el.classList.remove('indented'));
170						parentWrapper.querySelectorAll('.wall-item-comment.collapsed').forEach(el => el.classList.remove('collapsed'));
171						parentWrapper.classList.remove('wall-item-backdrop');
172						parentWrapper.querySelectorAll('.wall-item-sub-thread-wrapper.item-indent').forEach(el => el.classList.remove('item-indent'));
173						parentWrapper.querySelectorAll('.wall-item-sub-thread-wrapper.d-none').forEach(el => el.classList.remove('d-none'));
174						parentWrapper.querySelectorAll('.thread-wrapper.wall-item-expanded').forEach(el => el.classList.remove('wall-item-expanded', 'shadow'));
175					}, { once: true });
176				}
177
178				// Toggle sub-thread visibility if indented
179				if (isUserClick && target.classList.contains('indented')) {
180					doRequest = false;
181					subThreadWrapper.classList.toggle('d-none');
182					target.classList.toggle('collapsed');
183				}
184
185				// Indenting of already expanded but flattened items
186				if (isUserClick && subThreadWrapper.classList.contains('item-expanded') && !subThreadWrapper.classList.contains('item-indent')) {
187					doRequest = false;
188
189					threadWrapper.querySelectorAll('.wall-item-sub-thread-wrapper.item-expanded').forEach(function (el, i) {
190						el.classList.add('item-indent');
191
192						el.querySelectorAll('.wall-item-comment.expanded').forEach(function (el, i) {
193							el.classList.add('collapsed', 'indented');
194						});
195
196						// Collapse everything below the first level
197						if (i > 0) {
198							el.classList.add('d-none');
199						}
200					});
201				}
202
203				// Indent the subthread
204				subThreadWrapper.classList.add('item-indent', 'item-expanded');
205
206				// Mark as indented after visibility toggle
207				target.classList.add('indented');
208			}
209
210			// Mark as expanded
211			target.classList.add('expanded');
212		}
213
214		if (doRequest) {
215			request(id, mid, verb, parentId, uuid, isUserClick);
216		}
217	});
218
219	document.addEventListener('click', function(event) {
220		const targetElement = event.target.closest('.dropdown-item-expand');
221		if (!targetElement) return;
222
223		event.preventDefault();
224
225		const id = targetElement.dataset.itemId;
226		const subWrapper = document.getElementById(`wall-item-sub-thread-wrapper-${id}`);
227
228		subWrapper.innerHTML = '';
229		autoExpand(id);
230	});
231
232	// @hilmar |->
233	if ( typeof(window.tao) == 'undefined' ) {
234		window.tao = {};
235	}
236	if ( typeof(window.tao.zin) == 'undefined' ) {
237		tao.zin = { syslc: '', syslcs: {}, htm: '', me: '', debug: '' };
238		tao.zin.axim = '<i class="zinpax bi bi-arrow-repeat"></i>';
239		$('.navbar-app[href*="/lang"]').attr({"aria-expand": "true", "id": "zintog"})
240			.removeAttr('href').addClass('zinlcx zinlcxp dropdown dropdown-toggle').css('cursor','pointer');
241		$('.nav-link[href*="/lang"]').addClass('zinlcxmi zinlcx').removeAttr('href').css('cursor','pointer');
242	}
243	$('.zinlcx').on('click',  function(e) {
244		if (tao.zin.syslc == '') {
245			$('.zinlcx').append(tao.zin.axim);
246			$.ajax({
247    			type: 'POST', url: 'lang',
248      			data: { zinlc: '??' }
249   			}).done( function(re) {
250   				tao.zin.re = JSON.parse(re);
251   			 	tao.zin.syslc = tao.zin.re.lc;
252   			 	tao.zin.syslcs = tao.zin.re.lcs;
253				tao.zin.htm = '<ul class="zinlcs">';
254				$.each( tao.zin.syslcs, function( k, v ) {
255					tao.zin.htm += '<li><a id="zinlc' + k + '" class="zinlc fakelink">' + k + ' ' + v + '</a></li>';
256				});
257				tao.zin.htm += '</ul>';
258				$('.zinpax').remove();
259				$('.zinlcx').append(tao.zin.htm);
260				$('.zinlcxp > ul').addClass('dropdown dropdown-menu dropdown-menu-end mt-2 show').css('right', 0);
261   			});
262			return false;
263		} else {
264			if (e.target.id == '' || e.target.id == 'zintog') {
265				// noop click on lang again
266				return false;
267			}
268			tao.zin.me = e.target.id.substr(5);
269			$('#right_aside_wrapper').append(tao.zin.axim);
270			$.ajax({
271	   			type: 'POST', url: 'lang',
272      			data: { zinlc: tao.zin.me }
273    		}).done( function(re) {
274     			tao.zin.re = JSON.parse(re);
275     			location.reload(true);
276    		});
277		}
278	});
279	$('#zintog').on('click', function() {
280		$('.zinlcs').toggle();
281	});
282	$('#dropdown-menu').on('shown.bs.dropdown', function() {
283		tao.zin.debug += 'e,';
284		//$('.zinlcs').removeAttr('display');
285	})
286		.on('mouseleave', function() {
287		$(this).trigger('click');
288	});
289	// @hilmar <-|
290
291	$(document).on('click', '.notification, .message', function(e) {
292		let b64mid = this.dataset.b64mid;
293		let notify_id = this.dataset.notify_id;
294		let path = $(this)[0].pathname.split('/')[1];
295		let stateObj = { b64mid: b64mid };
296
297		if (!b64mid && !notify_id) {
298			return;
299		}
300
301		if (document.querySelector('main').classList.contains('region_1-on')) {
302			toggleAside();
303		}
304
305		if (localUser && redirect_modules.indexOf(path) !== -1) {
306			path = 'hq';
307		}
308
309		if (notify_id) {
310			$.ajax({
311				type: 'post',
312				url: 'notify',
313				data: {
314					'notify_id' : notify_id
315				},
316				async: ((module !== path) ? false : true)
317			});
318		}
319
320		if (module !== path) {
321			e.preventDefault();
322			window.location.href = path + '/' + b64mid;
323		}
324		else {
325			if (singlethread_modules.indexOf(module) !== -1) {
326				history.pushState(stateObj, '', module + '/' + b64mid);
327			}
328
329			if (b64mid) {
330				e.preventDefault();
331				if (!page_load) {
332					prepareLiveUpdate(b64mid, notify_id);
333					$('.message').removeClass('active');
334					$('[data-b64mid="' + b64mid + '"].message').addClass('active');
335					$('[data-b64mid="' + b64mid + '"].message .unseen_count').remove();
336				}
337			}
338		}
339	});
340
341	window.onpopstate = function(e) {
342		if(e.state !== null && e.state.b64mid !== bParam_mid) {
343			prepareLiveUpdate(e.state.b64mid, '');
344			$('.message').removeClass('active');
345			$('[data-b64mid="' + e.state.b64mid + '"].message').addClass('active');
346		}
347
348	};
349
350	savedTitle = document.title;
351
352	updateInit();
353
354	if (document.getElementById('content-complete')) {
355		pageHasMoreContent = false;
356	}
357
358	document.addEventListener('hz:updateConvItems', function(e) {
359		if (!bParam_mid) {
360			cache_next_page();
361		}
362	});
363
364	document.addEventListener('hz:handleNetworkNotificationsItems', function(e) {
365		push_notification(
366			e.detail.name,
367			$('<p>' + e.detail.message + '</p>').text(),
368			e.detail.b64mid
369		);
370	});
371});
372
373function getConversationSettings() {
374	$.get('settings/conversation/?aj=1',function(data) {
375		$('#conversation_settings_body').html(data);
376	});
377}
378
379function postConversationSettings() {
380	if(next_page === 1) {
381		page_load = true;
382	}
383
384	$.post(
385		'settings/conversation',
386		$('#settings_module_ajax_form').serialize() + "&auto_update=" + next_page
387	).done(function() {
388		$('#conversation_settings').modal('hide');
389		toast('Conversation features updated', 'info');
390		updateInit();
391	});
392
393	return false;
394}
395
396function datasrc2src(selector) {
397	$(selector).each(function(i, el) {
398		$(el).attr("src", $(el).data("src"));
399		$(el).removeAttr("data-src");
400	});
401}
402
403function confirmDelete() {
404	return confirm(aStr.delitem);
405}
406
407function handle_comment_form(e) {
408	e.stopPropagation();
409
410	//handle eventual expanded forms
411	var expanded = $('.comment-edit-text.expanded');
412	var i = 0;
413
414	if(expanded.length) {
415		expanded.each(function() {
416			var ex_form = $(expanded[i].form);
417			var ex_fields = ex_form.find(':input[type=text], textarea');
418			var ex_fields_empty = true;
419
420			ex_fields.each(function() {
421				if($(this).val() != '')
422					ex_fields_empty = false;
423			});
424			if(ex_fields_empty) {
425				ex_form.find('.comment-edit-text').removeClass('expanded').attr('placeholder', aStr.comment);
426				ex_form.find(':not(.comment-edit-text)').hide();
427			}
428			i++;
429		});
430	}
431
432	// handle clicked form
433	var form = $(this);
434
435	var fields = form.find(':input[type=text], textarea');
436	var fields_empty = true;
437
438	if(form.find('.comment-edit-text').length) {
439		var commentElm = form.find('.comment-edit-text').attr('id');
440		var submitElm = commentElm.replace(/text/,'submit');
441
442		$('#' + commentElm).addClass('expanded').removeAttr('placeholder');
443		$('#' + commentElm).attr('tabindex','9');
444		$('#' + submitElm).attr('tabindex','10');
445
446		form.find(':not(:visible)').show();
447	}
448
449
450	// handle click outside of form (close empty forms)
451	$(document).one('click', function(e) {
452		fields.each(function() {
453			if($(this).val() != '')
454				fields_empty = false;
455		});
456		if(fields_empty) {
457			var emptyCommentElm = form.find('.comment-edit-text').attr('id');
458        	var emptySubmitElm = commentElm.replace(/text/,'submit');
459
460			$('#' + emptyCommentElm).removeClass('expanded').attr('placeholder', aStr.comment);
461			$('#' + emptyCommentElm).removeAttr('tabindex');
462			$('#' + emptySubmitElm).removeAttr('tabindex');
463			form.find(':not(.comment-edit-text)').hide();
464			form.find(':input[name=parent]').val(emptyCommentElm.replace(/\D/g,''));
465			var btn = form.find(':button[type=submit]').html();
466			form.find(':button[type=submit]').html(btn.replace(/<[^>]*>/g, '').trim());
467			form.find(':button[type=submit]').prop('title', '');
468		}
469	});
470
471	var commentSaveTimer = null;
472	var emptyCommentElm = form.find('.comment-edit-text').attr('id');
473	var convId = emptyCommentElm.replace('comment-edit-text-','');
474
475	// in case parent input is set use it as convId
476	const parentInputVal = form.find(':input[name=parent]').val();
477	if (parentInputVal) {
478		convId = parentInputVal;
479	}
480
481	$('#' + emptyCommentElm).on('focusout',function(e){
482		if(commentSaveTimer)
483			clearTimeout(commentSaveTimer);
484		commentSaveChanges(convId,true);
485		commentSaveTimer = null;
486		$('#' + emptyCommentElm).off();
487	});
488
489	$('#' + emptyCommentElm).on('focusin', function (e){
490		commentSaveTimer = setTimeout(function () {
491			commentSaveChanges(convId,false);
492		},10000);
493	});
494
495	function commentSaveChanges(convId, isFinal) {
496
497		if(typeof isFinal === 'undefined')
498			isFinal = false;
499
500		if(auto_save_draft) {
501			tmp = $('#' + emptyCommentElm).val();
502			if(tmp) {
503				localStorage.setItem("comment_body-" + convId, tmp);
504			}
505			else {
506				localStorage.removeItem("comment_body-" + convId);
507			}
508			if( !isFinal) {
509				commentSaveTimer = setTimeout(commentSaveChanges,10000,convId);
510			}
511		}
512	}
513
514}
515
516
517
518function commentClose(obj, id) {
519	if(obj.value === '') {
520		$("#comment-edit-text-" + id).removeClass("expanded");
521		$("#mod-cmnt-wrap-" + id).hide();
522		$("#comment-tools-" + id).hide();
523		$("#comment-edit-anon-" + id).hide();
524		return true;
525	}
526	return false;
527}
528
529function showHideCommentBox(id) {
530	if( $('#comment-edit-form-' + id).is(':visible')) {
531		$('#comment-edit-form-' + id).hide();
532	} else {
533		$('#comment-edit-form-' + id).show();
534	}
535}
536
537function commentInsert(obj, id) {
538	var tmpStr = $("#comment-edit-text-" + id).val();
539	if(tmpStr == '$comment') {
540		tmpStr = '';
541		$("#comment-edit-text-" + id).addClass("expanded");
542		openMenu("comment-tools-" + id);
543	}
544	var ins = $(obj).html();
545	ins = ins.replace('&lt;','<');
546	ins = ins.replace('&gt;','>');
547	ins = ins.replace('&amp;','&');
548	ins = ins.replace('&quot;','"');
549	$("#comment-edit-text-" + id).val(tmpStr + ins);
550}
551
552function insertbbcomment(comment, BBcode, id) {
553	// allow themes to override this
554	if(typeof(insertFormatting) != 'undefined')
555		return(insertFormatting(comment, BBcode, id));
556
557	var urlprefix = ((BBcode == 'url') ? '#^' : '');
558
559	var tmpStr = $("#comment-edit-text-" + id).val();
560	if(tmpStr == comment) {
561		tmpStr = "";
562		$("#comment-edit-text-" + id).addClass("expanded");
563		openMenu("comment-tools-" + id);
564		$("#comment-edit-text-" + id).val(tmpStr);
565	}
566
567	textarea = document.getElementById("comment-edit-text-" +id);
568	if (document.selection) {
569		textarea.focus();
570		selected = document.selection.createRange();
571		selected.text = urlprefix+"["+BBcode+"]" + selected.text + "[/"+BBcode+"]";
572	} else if (textarea.selectionStart || textarea.selectionStart == "0") {
573		var start = textarea.selectionStart;
574		var end = textarea.selectionEnd;
575		textarea.value = textarea.value.substring(0, start) + urlprefix+"["+BBcode+"]" + textarea.value.substring(start, end) + "[/"+BBcode+"]" + textarea.value.substring(end, textarea.value.length);
576	}
577	return true;
578}
579
580function inserteditortag(BBcode, id) {
581	// allow themes to override this
582	if(typeof(insertEditorFormatting) != 'undefined')
583		return(insertEditorFormatting(BBcode));
584
585	textarea = document.getElementById(id);
586	if (document.selection) {
587		textarea.focus();
588		selected = document.selection.createRange();
589		selected.text = urlprefix+"["+BBcode+"]" + selected.text + "[/"+BBcode+"]";
590	} else if (textarea.selectionStart || textarea.selectionStart == "0") {
591		var start = textarea.selectionStart;
592		var end = textarea.selectionEnd;
593		textarea.value = textarea.value.substring(0, start) + "["+BBcode+"]" + textarea.value.substring(start, end) + "[/"+BBcode+"]" + textarea.value.substring(end, textarea.value.length);
594	}
595	return true;
596}
597
598function insertCommentAttach(comment,id) {
599	activeCommentID = id;
600	activeCommentText = comment;
601	$('body').css('cursor', 'wait');
602	$('#invisible-comment-upload').trigger('click');
603	return false;
604}
605
606function insertCommentEmbed(comment,id) {
607	activeCommentID = id;
608	activeCommentText = comment;
609	initializeEmbedPhotoDialog();
610	return false;
611}
612
613function insertCommentURL(comment, id) {
614	reply = prompt(aStr.linkurl);
615	if(reply && reply.length) {
616		reply = bin2hex(reply);
617		$('body').css('cursor', 'wait');
618		$.get('linkinfo?binurl=' + reply, function(data) {
619			var tmpStr = $("#comment-edit-text-" + id).val();
620			if(tmpStr == comment) {
621				tmpStr = "";
622				$("#comment-edit-text-" + id).addClass("expanded");
623				openMenu("comment-tools-" + id);
624				$("#comment-edit-text-" + id).val(tmpStr);
625			}
626
627			textarea = document.getElementById("comment-edit-text-" +id);
628			textarea.value = textarea.value + data;
629			preview_comment(id);
630			$('body').css('cursor', 'auto');
631		});
632	}
633	return true;
634}
635
636function doFollowAuthor(url) {
637	$.get(url, function(data) { notificationsUpdate(); });
638	return true;
639}
640
641
642function viewsrc(id) {
643	$.colorbox({href: 'viewsrc/' + id, maxWidth: '80%', maxHeight: '80%' });
644}
645
646function showHideComments(id) {
647	let collapsedComments = document.getElementById('collapsed-comments-' + id);
648	let hideCommentsLabel = document.getElementById('hide-comments-label-' + id);
649	let hideCommentsTotal = document.getElementById('hide-comments-total-' + id);
650	let hideCommentsIcon = document.getElementById('hide-comments-icon-' + id);
651	let isCollapsed = collapsedComments.style.display === 'none';
652
653	collapsedComments.style.display = isCollapsed ? '' : 'none';
654	hideCommentsLabel.textContent = isCollapsed ? hideCommentsLabel.dataset.expanded : hideCommentsLabel.dataset.collapsed;
655
656	if (hideCommentsTotal) {
657		hideCommentsTotal.style.display = isCollapsed ? 'none' : '';
658	}
659
660	let oldClass = isCollapsed ? 'bi-chevron-down' : 'bi-chevron-up';
661	let newClass = isCollapsed ? 'bi-chevron-up' : 'bi-chevron-down';
662	hideCommentsIcon.classList.replace(oldClass, newClass);
663}
664
665function openClose(theID, display) {
666	if (typeof display == typeof undefined)
667		display = 'block';
668
669	if(document.getElementById(theID).style.display == display) {
670		document.getElementById(theID).style.display = "none";
671	} else {
672		document.getElementById(theID).style.display = display;
673	}
674}
675
676function openCloseTR(theID) {
677	if(document.getElementById(theID).style.display == "table-row") {
678		document.getElementById(theID).style.display = "none";
679	} else {
680		document.getElementById(theID).style.display = "table-row";
681	}
682}
683
684function closeOpen(theID, display) {
685	if (typeof display == typeof undefined)
686		display = 'block';
687	if(document.getElementById(theID).style.display == "none") {
688		document.getElementById(theID).style.display = display;
689	} else {
690		document.getElementById(theID).style.display = "none";
691	}
692}
693
694function openMenu(theID) {
695	document.getElementById(theID).style.display = "block";
696}
697
698function closeMenu(theID) {
699	document.getElementById(theID).style.display = "none";
700}
701
702function markRead(notifType) {
703	$.get('notifications?markRead='+notifType);
704	$('.' + notifType + '-button').fadeOut(function() {
705		$("." + notifType + "-update").html('0');
706		$('#nav-' + notifType + '-menu').html('');
707		$('#nav-' + notifType + '-sub').removeClass('show');
708		sessionStorage.removeItem('notification_open');
709		$(document).trigger('hz:sse_setNotificationsStatus');
710	});
711
712
713}
714
715function markItemRead(itemId) {
716	$.get('notifications?markItemRead='+itemId);
717	$('.unseen-wall-indicator-'+itemId).remove();
718}
719
720function contextualHelp() {
721	var container = $("#contextual-help-content");
722
723	if(container.hasClass('contextual-help-content-open')) {
724		container.removeClass('contextual-help-content-open');
725		$('main').css('margin-top', '')
726	}
727	else {
728		container.addClass('contextual-help-content-open');
729		var mainTop = container.outerHeight(true);
730		$('main').css('margin-top', mainTop + 'px');
731	}
732}
733
734function contextualHelpFocus(target, openSidePanel) {
735        if($(target).length) {
736            if (openSidePanel) {
737                    $("main").addClass('region_1-on');  // Open the side panel to highlight element
738            }
739            else {
740                    $("main").removeClass('region_1-on');
741            }
742
743	    var css_position = $(target).parent().css('position');
744	    if (css_position === 'fixed') {
745	            $(target).parent().css('position', 'static');
746	    }
747
748            $('html,body').animate({ scrollTop: $(target).offset().top - $('nav').outerHeight(true) - $('#contextual-help-content').outerHeight(true)}, 'slow');
749            for (i = 0; i < 3; i++) {
750                    $(target).fadeTo('slow', 0.1).fadeTo('slow', 1.0);
751            }
752
753	    $(target).parent().css('position', css_position);
754        }
755}
756
757function updatePageItems(mode, data) {
758
759	$(document).trigger('hz:updatePageItems');
760
761	if(mode === 'append') {
762		newitemcount = 0;
763		$(data).each(function() {
764			$('#page-end').before($(this));
765			newitemcount++;
766		});
767
768		if(loadingPage) {
769			loadingPage = false;
770		}
771	}
772
773	var e = document.getElementById('content-complete');
774	if(e) {
775		pageHasMoreContent = false;
776	} else {
777		if (newitemcount < 1) {
778			pageUpdate();
779		}
780	}
781
782	collapseHeight();
783}
784
785function updateConvItems(mode, data) {
786	let scroll_position = window.scrollY;
787	let b64mids = [];
788
789	// Parse the data string into a DOM object
790	let parser = new DOMParser();
791	let doc = parser.parseFromString(data, 'text/html');
792
793	if (mode !== 'update') {
794		document.dispatchEvent(new Event('hz:updateConvItems'));
795	}
796
797	let prev, next;
798	if (mode === 'update' || mode === 'replace') {
799		prev = document.getElementById('threads-begin');
800	}
801	if (mode === 'append') {
802		next = document.getElementById('threads-end');
803	}
804
805	doc.querySelectorAll('.thread-wrapper').forEach(function (elem) {
806		if (elem.classList.contains('toplevel_item')) {
807			let ident = elem.id;
808			let convId = ident.replace('thread-wrapper-', '');
809			let commentWrap = elem.querySelector('.collapsed-comments')?.id;
810
811			let itmId = 0;
812			let isVisible = false;
813
814			// figure out the comment state
815			if (commentWrap !== undefined) {
816				itmId = commentWrap.replace('collapsed-comments-', '');
817			}
818
819			let collapsedComment = document.getElementById('collapsed-comments-' + itmId);
820			if (collapsedComment && collapsedComment.style.display !== 'none') {
821				isVisible = true;
822			}
823
824			// insert the content according to the mode and first_page
825			// and whether or not the content exists already (overwrite it)
826			let existingElem = document.getElementById(ident);
827			if (!existingElem) {
828				if ((mode === 'update' || mode === 'replace') && profile_page == 1) {
829					if (prev) {
830						prev.after(elem);
831						prev = elem;
832					}
833				}
834				if (mode === 'append') {
835					if (next) {
836						next.before(elem);
837					}
838				}
839			} else {
840				existingElem.replaceWith(elem);
841			}
842
843			// DOMParser will prevent scripts from execution for security reasons.
844			// We remove all scripts but possibly injected some from
845			// addons like for example gallery later.
846			// TODO: make the script run from the addon itself.
847			let scripts = elem.querySelectorAll('script');
848			scripts.forEach(script => {
849				let scriptContent = script.textContent || script.innerText;
850				eval(scriptContent);  // Execute the script
851			});
852
853			// set the comment state to the state we discovered earlier
854			if (isVisible) {
855				showHideComments(itmId);
856			}
857
858			let commentBody = localStorage.getItem("comment_body-" + convId);
859			if (commentBody) {
860				let commentElm = document.getElementById('comment-edit-text-' + convId);
861				if (auto_save_draft && commentElm) {
862					if (commentElm.value === '') {
863						let commentForm = document.getElementById('comment-edit-form-' + convId);
864						if (commentForm) {
865							commentForm.style.display = 'block';
866						}
867						commentElm.classList.add("expanded");
868						openMenu("comment-tools-" + convId);
869						commentElm.value = commentBody;
870					}
871				} else {
872					localStorage.removeItem("comment_body-" + convId);
873				}
874			}
875		}
876
877		if ((mode === 'append' || mode === 'replace') && loadingPage) {
878			loadingPage = false;
879		}
880
881		// if single thread view and the item has a title, display it in the title bar
882		if (mode === 'replace') {
883			if (window.location.search.includes("mid=") || window.location.pathname.includes("display")) {
884				let titleElem = document.querySelector(".wall-item-title");
885				if (titleElem) {
886					let title = titleElem.textContent.trim();
887					if (title) {
888						savedTitle = title + ' ' + savedTitle;
889						document.title = title;
890					}
891				}
892			}
893		}
894
895		let data_json = JSON.parse(elem.dataset.b64mids);
896
897		if (elem.parentNode.classList.contains('wall-item-sub-thread-wrapper') && elem.parentNode.children.length) {
898			// Set the highlight state
899			if (data_json.includes(bParam_mid) && !elem.parentNode.parentNode.classList.contains('toplevel_item')) {
900				elem.parentNode.parentNode.classList.add('item-highlight');
901				document.documentElement.style.setProperty('--hz-item-highlight', stringToHslColor(JSON.parse(elem.parentNode.parentNode.dataset.b64mids)[0]));
902			}
903
904			let elemSubThreadWrapper = elem.querySelector('.wall-item-sub-thread-wrapper');
905			let elemCommentButton = elem.querySelector('.wall-item-comment');
906
907			// Set the button and sub-thread-wrapper state
908			if (elemCommentButton && elemSubThreadWrapper.children.length) {
909				elemCommentButton.classList.add('expanded');
910			}
911
912			elem.parentNode.classList.add('item-expanded');
913		}
914
915		b64mids.push(...data_json);
916	});
917
918	document.dispatchEvent(new CustomEvent('hz:sse_setNotificationsStatus', { detail: b64mids }));
919
920	window.scrollTo(0, scroll_position);
921
922	if (followUpPageLoad) {
923		document.dispatchEvent(new Event('hz:sse_bs_counts'));
924	} else {
925		document.dispatchEvent(new Event('hz:sse_bs_init'));
926	}
927
928	if (commentBusy) {
929		commentBusy = false;
930		document.body.style.cursor = 'auto';
931	}
932
933	// Setup to determine if the media player is playing. This affects some content loading decisions.
934	['playing', 'pause'].forEach(event => {
935		document.querySelectorAll('video, audio').forEach(media => {
936			media.removeEventListener(event, mediaHandler);
937			media.addEventListener(event, mediaHandler);
938		});
939	});
940
941	function mediaHandler(event) {
942		mediaPlaying = event.type === 'playing';
943	}
944
945	if (bParam_mid && mode === 'replace') {
946		scrollToItem();
947	}
948
949	// A slight delay to give the browser time to render images.
950	// Otherwise height calculation might not be accurate.
951	setTimeout(collapseHeight, 10);
952
953	// Reset rotators and cursors we may have set before reaching this place
954	let pageSpinner = document.getElementById("page-spinner");
955	if (pageSpinner) {
956		pageSpinner.style.display = 'none';
957	}
958
959	let profileJotTextLoading = document.getElementById("profile-jot-text-loading");
960	if (profileJotTextLoading) {
961		profileJotTextLoading.style.display = 'none';
962	}
963
964	followUpPageLoad = true;
965
966
967	updateRelativeTime('.autotime');
968}
969
970function imagesLoaded(elements, callback) {
971	let loadedCount = 0;
972	let totalImages = 0;
973	let timeoutId;
974	let timedOut = false;
975	const timeout = 10000;
976	const processed = new Set(); // Use a Set for efficient lookup
977
978	// Helper function to extract img elements from an HTML string
979	function extractImagesFromHtml(htmlString) {
980		const tempDiv = document.createElement('div');
981		tempDiv.innerHTML = htmlString;
982		return tempDiv.querySelectorAll('.wall-item-body img, .wall-photo-item img');
983	}
984
985	function checkComplete(src) {
986		// If preloading timed out make sure to not call the callback again
987		// in case a load event listener fires later.
988		if (timedOut) return;
989
990		// Skip processing if image has already been processed
991		if (processed.has(src)) return;
992
993		processed.add(src);
994		loadedCount++;
995
996		// Update progress
997		const progress = Math.round((loadedCount * 100) / totalImages);
998		document.getElementById('image_counter').innerText = `${progress}%`;
999
1000		// If all images are loaded, trigger the callback
1001		if (loadedCount === totalImages) {
1002			document.getElementById('image_counter').innerText = '';
1003			clearTimeout(timeoutId);
1004			callback();
1005		}
1006	}
1007
1008	// Convert HTML string to img elements if necessary
1009	if (typeof elements === 'string') {
1010		elements = extractImagesFromHtml(elements);
1011	}
1012
1013	// Exit early if there are no images to load
1014	if (!elements || elements.length === 0) {
1015		callback();
1016		return;
1017	}
1018
1019	// Filter valid image elements (only img with src attribute)
1020	const images = Array.from(elements)
1021		.filter((element) => element.tagName.toLowerCase() === 'img' && element.src)
1022		.filter((element, index, self) =>
1023			index === self.findIndex(e => e.src === element.src) // Avoid duplicates
1024		);
1025
1026	// If no images are found, call the callback immediately
1027	if (images.length === 0) {
1028		callback();
1029		return;
1030	}
1031
1032	totalImages = images.length;
1033
1034	// Set timeout for the loading process
1035	timeoutId = setTimeout(() => {
1036		console.warn(`Image loading timed out after ${timeout}ms`);
1037		document.getElementById('image_counter').innerText = '';
1038		callback(false);
1039		timedOut = true;
1040	}, timeout);
1041
1042	// Iterate through images to add load and error event listeners
1043	images.forEach((img) => {
1044		let new_img = new Image();
1045		new_img.src = img.src;
1046
1047		if (new_img.complete && new_img.naturalHeight > 0) {
1048			// Image is already loaded, handle immediately
1049			// console.log(`Image cached: ${new_img.src}`);
1050			checkComplete(new_img.src);
1051		} else {
1052			// Add event listeners for load and error events
1053			new_img.addEventListener('load', () => {
1054				// console.log(`Image loaded: ${new_img.src}`);
1055				checkComplete(new_img.src)
1056			});
1057			new_img.addEventListener('error', () => {
1058				console.log(`Image failed to load: ${new_img.src}`);
1059				checkComplete(new_img.src);
1060			});
1061		}
1062
1063	});
1064}
1065
1066function updateRelativeTime(selector) {
1067	// Get all elements with the given selector
1068	const timeElements = document.querySelectorAll(selector);
1069	if (timeElements.length === 0) return;
1070
1071	// Default time style and map for supported options
1072	const styleMap = ['narrow', 'short', 'long'];
1073	const style = styleMap.find(s => selector.includes(s)) || 'long';
1074
1075	// Create an instance of RelativeTimeFormat
1076	const rtf = new Intl.RelativeTimeFormat(lang, {
1077		localeMatcher: 'best fit', // 'best fit' or 'lookup'
1078		numeric: 'always', // 'always' or 'auto'
1079		style: style // 'long', 'short', or 'narrow'
1080	});
1081
1082	const now = Date.now(); // Get the current time only once
1083
1084	// Helper function to calculate the time difference in appropriate units
1085	function getRelativeTime(diffInSeconds) {
1086		const isFuture = diffInSeconds > 0;
1087		const absDiffInSeconds = Math.abs(diffInSeconds);
1088
1089		if (absDiffInSeconds < 60) return { value: absDiffInSeconds, unit: 'second' };
1090		if (absDiffInSeconds < 3600) return { value: Math.floor(absDiffInSeconds / 60), unit: 'minute' };
1091		if (absDiffInSeconds < 86400) return { value: Math.floor(absDiffInSeconds / 3600), unit: 'hour' };
1092		if (absDiffInSeconds < 2592000) return { value: Math.floor(absDiffInSeconds / 86400), unit: 'day' };
1093		if (absDiffInSeconds < 31536000) return { value: Math.floor(absDiffInSeconds / 2592000), unit: 'month' };
1094		return { value: Math.floor(absDiffInSeconds / 31536000), unit: 'year' };
1095	}
1096
1097	// Process each element
1098	timeElements.forEach(element => {
1099		const timestamp = new Date(element.title).getTime();
1100		if (isNaN(timestamp)) return; // Skip invalid timestamps
1101
1102		const diffInSeconds = Math.floor((timestamp - now) / 1000); // Time difference in seconds
1103		const { value, unit } = getRelativeTime(diffInSeconds);
1104
1105		// Format the relative time and set it as the element's text
1106		const formattedTime = rtf.format(diffInSeconds > 0 ? value : -value, unit);
1107		element.textContent = formattedTime;
1108	});
1109
1110	// Avoid duplicate timeout registrations for the same selector
1111	if (!updateTimeout.includes(selector)) {
1112		updateTimeout.push(selector);
1113		setTimeout(() => updateRelativeTime(selector), 60000); // Re-run the update every 60 seconds
1114	}
1115}
1116
1117function scrollToItem() {
1118	// auto-scroll to a particular comment in a thread (designated by mid) when in single-thread mode
1119
1120	if (justifiedGalleryActive) return;
1121
1122	let submid = ((bParam_mid.length) ? bParam_mid : 'abcdefg');
1123
1124	// Select all thread wrappers
1125	let threadWrappers = document.querySelectorAll('.thread-wrapper');
1126
1127	threadWrappers.forEach(thread => {
1128		// Get the 'data-b64mids' attribute and check if it contains submid
1129		let b64mids = thread.dataset.b64mids;
1130
1131		if (b64mids && b64mids.includes(submid)) {
1132			// Handle collapsed comments if any
1133			let collapsedComments = document.querySelectorAll('.collapsed-comments');
1134			if (collapsedComments.length) {
1135				let scrollToId = collapsedComments[0].id.substring(19);
1136				showHideComments(scrollToId);
1137			}
1138
1139			collapseHeight();
1140
1141			if (!thread.classList.contains('toplevel_item')) {
1142				// Scroll to the target element
1143				let navHeight = document.getElementById('navbar-top') ? document.getElementById('navbar-top').offsetHeight : 0;
1144				window.scrollTo({
1145					top: getOffsetTopRelativeToBody(thread) - navHeight,
1146					behavior: 'smooth'
1147				});
1148			}
1149
1150			let id = thread.id.replace('thread-wrapper-', '');
1151			let content = document.getElementById('wall-item-content-wrapper-' + id);
1152			content.classList.add('item-highlight-fade');
1153		}
1154	});
1155}
1156
1157function getOffsetTopRelativeToBody(element) {
1158	let offsetTop = 0;
1159	while (element) {
1160		offsetTop += element.offsetTop;
1161		element = element.offsetParent;
1162	}
1163	return offsetTop;
1164}
1165
1166function collapseHeight() {
1167	$(".wall-item-content:not('.divmore_checked'), .directory-collapse:not('.divmore_checked')").each(function(i) {
1168		let orgHeight = $(this).outerHeight(true);
1169		let id = (($(this).attr('id')) ? $(this).attr('id').split('wall-item-content-').pop() : 0);
1170		let b64mid = ((typeof bParam_mid !== 'undefined') ? bParam_mid : '');
1171
1172		if (b64mid) {
1173			// Display the selected mid in an open state
1174			let b64mids = $('#thread-wrapper-' + id).data('b64mids');
1175
1176			if (b64mids.length && b64mids.indexOf(b64mid) !== -1) {;
1177				expanded_items.push(id);
1178			}
1179		}
1180
1181		let open = ((expanded_items.indexOf(id) === -1) ? false : true);
1182
1183		if(orgHeight > divmore_height) {
1184			if(! $(this).hasClass('divmore') && $(this).has('div.no-collapse').length == 0) {
1185				$(this).readmore({
1186					speed: 0,
1187					startOpen: open,
1188					heightMargin: 50,
1189					collapsedHeight: divmore_height,
1190					moreLink: '<a href="#" class="divgrow-showmore fakelink"><i class="bi bi-chevron-down align-middle divgrow-showmore-icon"></i>&nbsp;<span class="divgrow-showmore-label align-middle">' + aStr.divgrowmore + '</span></a>',
1191					lessLink: '<a href="#" class="divgrow-showmore fakelink"><i class="bi bi-chevron-up align-middle divgrow-showmore-icon"></i>&nbsp;<span class="divgrow-showmore-label align-middle">' + aStr.divgrowless + '</span></a>',
1192					beforeToggle: function(trigger, element, expanded) {
1193						if(expanded) {
1194							if((($(element).offset().top + divmore_height) - $(window).scrollTop()) < 65 ) {
1195								$(window).scrollTop($(window).scrollTop() - ($(element).outerHeight(true) - divmore_height));
1196							}
1197							expanded_items = expanded_items.filter(expanded_items => expanded_items !== id);
1198						}
1199						else {
1200							expanded_items.push(id);
1201						}
1202					}
1203				});
1204				$(this).addClass('divmore');
1205			}
1206		}
1207		$(this).addClass('divmore_checked');
1208	});
1209
1210}
1211
1212function updateInit() {
1213
1214	if($('#live-network').length)    { src = 'network'; }
1215	if($('#live-channel').length)    { src = 'channel'; }
1216	if($('#live-pubstream').length)  { src = 'pubstream'; }
1217	if($('#live-display').length)    { src = 'display'; }
1218	//if($('#live-hq').length)         { src = 'hq'; }
1219	if($('#live-search').length)     { src = 'search'; }
1220	// if($('#live-cards').length)      { src = 'cards'; }
1221	// if($('#live-articles').length)   { src = 'articles'; }
1222
1223	if(src) {
1224		liveUpdate();
1225	}
1226	else {
1227		document.dispatchEvent(new Event('hz:sse_bs_init'));
1228	}
1229
1230	if($('#live-photos').length || $('#live-cards').length || $('#live-articles').length ) {
1231		if(liking) {
1232			liking = 0;
1233			window.location.href=window.location.href;
1234		}
1235	}
1236}
1237
1238function prepareLiveUpdate(b64mid, notify_id) {
1239	$(document).scrollTop(0);
1240	$('.thread-wrapper').remove();
1241	bParam_mid = b64mid;
1242	mode = 'replace';
1243	page_load = true;
1244	if (module == 'hq') {
1245		liveUpdate(notify_id);
1246	}
1247	if (module == 'display') {
1248		liveUpdate();
1249	}
1250}
1251
1252function liveUpdate(notify_id) {
1253
1254	if(typeof profile_uid === 'undefined') profile_uid = false; /* Should probably be unified with channelId defined in head.tpl */
1255
1256	if((src === null) || (stopped) || (! profile_uid)) { $('.like-rotator').hide(); return; }
1257
1258	if(in_progress || mediaPlaying) {
1259		if(livetime) {
1260			clearTimeout(livetime);
1261		}
1262		livetime = setTimeout(liveUpdate, 10000);
1263		return;
1264	}
1265
1266	if(timer)
1267		clearTimeout(timer);
1268
1269	if(livetime !== null)
1270		livetime = null;
1271
1272	prev = 'live-' + src;
1273
1274	in_progress = true;
1275
1276	var update_url;
1277	var update_mode;
1278
1279	if(scroll_next) {
1280		bParam_page = next_page;
1281		page_load = true;
1282	}
1283	else {
1284		bParam_page = 1;
1285	}
1286
1287	update_url = buildCmd();
1288
1289	console.log('displaying: ' + update_url);
1290
1291	if(page_load) {
1292
1293		$("#page-spinner").show();
1294
1295		if(bParam_page == 1) {
1296			update_mode = 'replace';
1297			$('.thread-wrapper').remove();
1298		}
1299		else
1300			update_mode = 'append';
1301	}
1302	else {
1303		update_mode = 'update';
1304		var orgHeight = $("#region_2").height();
1305	}
1306
1307	if(page_cache.data && bParam_page == page_cache.page) {
1308		page_load = false;
1309		scroll_next = false;
1310		updateConvItems(update_mode,page_cache.data);
1311		in_progress = false;
1312		return;
1313	}
1314
1315	var dstart = new Date();
1316	console.log('LOADING data...');
1317	$.get(update_url, function(data) {
1318
1319		// on shared hosts occasionally the live update process will be killed
1320		// leaving an incomplete HTML structure, which leads to conversations getting
1321		// truncated and the page messed up if all the divs aren't closed. We will try
1322		// again and give up if we can't get a valid HTML response after 10 tries.
1323
1324		if((data.indexOf("<html>") != (-1)) && (data.indexOf("</html>") == (-1))) {
1325			console.log('Incomplete data. Reloading');
1326			in_progress = false;
1327			liveRecurse ++;
1328			if(liveRecurse < 10) {
1329				liveUpdate(notify_id);
1330				return;
1331			}
1332			else {
1333				console.log('Incomplete data. Too many attempts. Giving up.');
1334				return;
1335			}
1336		}
1337
1338		// else data was valid - reset the recursion counter
1339		liveRecurse = 0;
1340
1341		if(notify_id) {
1342			$.post(
1343				"notify",
1344				{
1345					"notify_id" : notify_id
1346				}
1347			);
1348		}
1349
1350		var dready = new Date();
1351		console.log('DATA ready in: ' + (dready - dstart)/1000 + ' seconds.');
1352
1353		console.log('LOADING images...');
1354		imagesLoaded(data, function () {
1355			var iready = new Date();
1356			console.log('IMAGES ready in: ' + (iready - dready)/1000 + ' seconds.');
1357
1358			page_load = false;
1359			scroll_next = false;
1360			updateConvItems(update_mode,data);
1361			in_progress = false;
1362		});
1363	});
1364}
1365
1366function cache_next_page() {
1367	page_load = true;
1368	bParam_page++;
1369	update_url = buildCmd();
1370
1371	$.get(update_url, function(data) {
1372
1373		// on shared hosts occasionally the live update process will be killed
1374		// leaving an incomplete HTML structure, which leads to conversations getting
1375		// truncated and the page messed up if all the divs aren't closed. We will try
1376		// again and give up if we can't get a valid HTML response after 10 tries.
1377
1378		if((data.indexOf("<html>") != (-1)) && (data.indexOf("</html>") == (-1))) {
1379			console.log('Incomplete data. Reloading');
1380			in_progress = false;
1381			bParam_page--;
1382			liveRecurse++;
1383			if(liveRecurse < 10) {
1384				liveUpdate();
1385				return;
1386			}
1387			else {
1388				console.log('Incomplete data. Too many attempts. Giving up.');
1389				return;
1390			}
1391		}
1392
1393		// else data was valid - reset the recursion counter
1394		liveRecurse = 0;
1395
1396		console.log('cached: ' + update_url);
1397
1398		imagesLoaded(data, function () {
1399			console.log('page_cache: images loaded');
1400
1401			page_cache.data = data;
1402			page_cache.page = bParam_page;
1403			page_cache.time = Date.now();
1404
1405			bParam_page--;
1406			page_load = false;
1407		});
1408	});
1409
1410}
1411
1412function pageUpdate() {
1413
1414	in_progress = true;
1415
1416	var update_url;
1417	var update_mode;
1418
1419	if(scroll_next) {
1420		bParam_page = next_page;
1421		page_load = true;
1422	}
1423	else {
1424		bParam_page = 1;
1425	}
1426
1427	update_url = baseurl + '/' + decodeURIComponent(page_query) + '?aj=1&page=' + bParam_page + extra_args ;
1428
1429	$("#page-spinner").show();
1430	update_mode = 'append';
1431
1432	$.get(update_url,function(data) {
1433		page_load = false;
1434		scroll_next = false;
1435		updatePageItems(update_mode,data);
1436		$("#page-spinner").hide();
1437		updateRelativeTime('.autotime');
1438		in_progress = false;
1439	});
1440}
1441
1442function justifyPhotos(id) {
1443	justifiedGalleryActive = true;
1444	$('#' + id).show();
1445	$('#' + id).justifiedGallery({
1446		rowHeight: 150,
1447		selector: 'a',
1448		margins: 3,
1449		border: 0
1450	}).on('jg.complete', function(e){ justifiedGalleryActive = false; });
1451}
1452
1453function justifyPhotosAjax(id) {
1454	justifiedGalleryActive = true;
1455	$('#' + id).justifiedGallery('norewind').on('jg.complete', function(e){ justifiedGalleryActive = false; });
1456}
1457
1458function request(id, mid, verb, parent, uuid, userClick) {
1459
1460	if (verb === 'load') {
1461		const dots = document.getElementById('load-more-dots-' + parent);
1462		dots.classList.add('jumping-dots');
1463
1464		const parent_sub = document.getElementById('wall-item-sub-thread-wrapper-' + parent);
1465		const offset = parent_sub.children.length;
1466
1467		fetch('/request?offset=' + offset + '&verb=' + verb + '&mid=' + mid + '&parent=' + parent + '&module=' + module)
1468		.then(response => response.json())
1469		.then(obj => {
1470			let parser = new DOMParser();
1471			let doc = parser.parseFromString(obj.html, 'text/html');
1472			let b64mids = [];
1473
1474			doc.querySelectorAll('.thread-wrapper').forEach(function (e) {
1475				let data = JSON.parse(e.dataset.b64mids);
1476				b64mids.push(...data);
1477			});
1478
1479			imagesLoaded(doc.querySelectorAll('.wall-item-body img'), function () {
1480				injectWithAnimation('wall-item-sub-thread-wrapper-' + parent, doc);
1481				dots.classList.remove('jumping-dots');
1482
1483				const loadmore_progress = document.getElementById('load-more-progress-' + parent);
1484				loadmore_progress.style.width = Math.round(100 * parent_sub.children.length / loadmore_progress.dataset.commentsTotal) + '%';
1485
1486				if (Number(parent_sub.children.length) === Number(loadmore_progress.dataset.commentsTotal)) {
1487					const loadmore = document.getElementById('load-more-' + parent);
1488					loadmore.remove();
1489				}
1490
1491				updateRelativeTime('.autotime');
1492				collapseHeight();
1493
1494				document.dispatchEvent(new CustomEvent('hz:sse_setNotificationsStatus', { detail: b64mids }));
1495				document.dispatchEvent(new Event('hz:sse_bs_counts'));
1496			});
1497
1498		})
1499		.catch(error => {
1500			console.error('Error fetching data:', error);
1501		});
1502
1503		return;
1504	}
1505
1506	const loading = document.getElementById('like-rotator-' + id);
1507
1508	if (userClick) {
1509		loading.style.display = 'block';
1510	}
1511
1512	if (verb === 'comment') {
1513		if (userClick && singlethread_modules.indexOf(module) !== -1) {
1514			let stateObj = { b64mid: uuid };
1515			history.pushState(stateObj, '', module + '/' + uuid);
1516		}
1517
1518		fetch('/request?verb=' + verb + '&mid=' + mid + '&parent=' + parent + '&module=' + module)
1519		.then(response => response.json())
1520		.then(obj => {
1521			let parser = new DOMParser();
1522			let doc = parser.parseFromString(obj.html, 'text/html');
1523
1524			doc.querySelectorAll('.thread-wrapper').forEach(function (e) {
1525				let data = JSON.parse(e.dataset.b64mids);
1526				b64mids.push(...data);
1527			});
1528
1529			imagesLoaded(doc.querySelectorAll('.wall-item-body img'), function () {
1530				injectWithAnimation('wall-item-sub-thread-wrapper-' + id, doc, true);
1531				updateRelativeTime('.autotime');
1532				collapseHeight();
1533
1534				if (userClick) {
1535					loading.style.display = 'none';
1536					document.dispatchEvent(new CustomEvent('hz:sse_setNotificationsStatus', { detail: b64mids }));
1537					document.dispatchEvent(new Event('hz:sse_bs_counts'));
1538				}
1539			});
1540
1541		})
1542		.catch(error => {
1543			console.error('Error fetching data:', error);
1544		});
1545	}
1546	else {
1547		fetch('/request?verb=' + verb + '&mid=' + mid + '&parent=' + parent)
1548		.then(response => response.json())
1549		.then(obj => {
1550			const modal = new bootstrap.Modal('#reactions');
1551			const modal_content = document.getElementById('reactions_body');
1552			const modal_title = document.getElementById('reactions_title');
1553			const modal_action = document.getElementById('reactions_action');
1554			modal_action.style.display = 'none';
1555			modal_title.innerHTML = obj.title;
1556			modal_content.innerHTML = '';
1557			if (obj.action) {
1558				modal_action.innerHTML = '<a href="#" onclick="' + obj.action + '(' + id + ',\'' + verb + '\'); return false;">' + obj.action_label + '</a>';
1559				modal_action.style.display = 'block';
1560			}
1561			console.log(obj)
1562			obj.result.forEach(e => {
1563				let mod = '';
1564				if (e.item_blocked === 4) {
1565					mod = '<span onclick="moderate_approve(' + e.id + '); return false;" class="text-success pe-4 d-inline-block"><i class="bi bi-check-lg" ></i></span><span onclick="moderate_drop(' + e.id + '); return false;" class="text-danger pe-4 d-inline-block"><i class="bi bi-trash" ></i></span>';
1566				}
1567				modal_content.innerHTML += '<a href="' + e.url + '" class="list-group-item list-group-item-action border-0">' + mod + '<img src="' + e.photo + '" class="menu-img-1" loading="lazy">&nbsp;' + e.name + '</a>';
1568
1569			});
1570
1571			modal.show();
1572			loading.style.display = 'none';
1573		})
1574		.catch(error => {
1575			console.error('Error fetching data:', error);
1576		});
1577	}
1578
1579}
1580
1581function injectWithAnimation(containerId, parsedDoc, overwrite = false) {
1582	const container = document.getElementById(containerId);
1583	if (!container) return;
1584	if (overwrite) container.innerHTML = '';
1585
1586	const newElements = Array.from(parsedDoc.body.children);
1587
1588	for (let i = newElements.length - 1; i >= 0; i--) {
1589		const el = newElements[i].cloneNode(true);
1590		el.classList.add('item-fade-in');
1591		container.insertBefore(el, container.firstChild);
1592
1593		// Remove classes after transition ends
1594		const onTransitionEnd = (event) => {
1595			el.classList.remove('item-fade-in', 'show');
1596			el.removeEventListener('transitionend', onTransitionEnd);
1597		};
1598		el.addEventListener('transitionend', onTransitionEnd);
1599
1600		setTimeout(() => {
1601			el.classList.add('show');
1602		}, (newElements.length - 1 - i) * 30);
1603	}
1604}
1605
1606const autoExpand = (function () {
1607	const clickedElements = new Set(); // Stores clicked button references
1608
1609	// We wait 10 seconds for images. Set the timeout here slightly higher.
1610	function waitForElement(selector, timeout = 11000) {
1611		return new Promise((resolve, reject) => {
1612			// Check if the element already exists
1613			const element = document.querySelector(selector);
1614
1615			if (element) {
1616				resolve(element);
1617				return;
1618			}
1619
1620			// Set up a timeout to reject the promise if the element doesn't appear in time
1621			const timer = setTimeout(() => {
1622				observer.disconnect();
1623				reject(new Error(`Element "${selector}" not found within ${timeout}ms`));
1624			}, timeout);
1625
1626			// Create a MutationObserver to watch for DOM changes
1627			const observer = new MutationObserver(() => {
1628				const el = document.querySelector(selector);
1629				if (el) {
1630					clearTimeout(timer);
1631					observer.disconnect();
1632					resolve(el);
1633				}
1634			});
1635
1636			// Start observing the document for changes
1637			observer.observe(document.documentElement, {
1638				childList: true,
1639				subtree: true
1640			});
1641		});
1642	}
1643
1644	async function autoExpand(id) {
1645		const loading = document.getElementById('like-rotator-' + id);
1646		let iteration = 0;
1647		const maxIterations = 3;
1648		clickedElements.clear();
1649
1650		try {
1651			// Step 1: Ensure initial button is clicked
1652			const initBtnSelector = '#wall-item-comment-' + id;
1653			const initBtn = await waitForElement(initBtnSelector);
1654
1655			if (!clickedElements.has(initBtn)) {
1656				initBtn.click();
1657				clickedElements.add(initBtn);
1658				iteration++;
1659			}
1660
1661			// Step 2: Loop until no new buttons are found
1662			let newButtonsFound;
1663
1664			const commentSelector = `#wall-item-sub-thread-wrapper-${id} .thread-wrapper`;
1665			const commentBtnSelector = `#wall-item-sub-thread-wrapper-${id} .wall-item-comment`;
1666
1667			do {
1668				newButtonsFound = false;
1669
1670				// Wait for any comment to appear
1671				await waitForElement(commentSelector);
1672
1673				const expandButtons = document.querySelectorAll(commentBtnSelector);
1674
1675				for (const btn of expandButtons) {
1676					if (!clickedElements.has(btn)) {
1677						btn.click();
1678						clickedElements.add(btn);
1679						newButtonsFound = true;
1680						// Optional: await waitForElement(...) to wait for new content
1681					}
1682				}
1683
1684				// Wait between iterations to allow UI to update
1685				if (newButtonsFound) {
1686					iteration++;
1687					await new Promise(res => setTimeout(res, 700));
1688				}
1689
1690			} while (newButtonsFound && iteration < maxIterations);
1691
1692			console.log('Replies loaded!');
1693
1694			loading.style.display = 'none';
1695
1696			document.dispatchEvent(new CustomEvent('hz:sse_setNotificationsStatus', { detail: b64mids }));
1697			document.dispatchEvent(new Event('hz:sse_bs_counts'));
1698
1699		} catch (error) {
1700			loading.style.display = 'none';
1701			console.error("autoExpand failed:", error.message);
1702		}
1703	}
1704
1705	return autoExpand;
1706})();
1707
1708
1709function stringToHexColor(str) {
1710	let hash = 0;
1711	for (let i = 0; i < str.length; i++) {
1712		hash = str.charCodeAt(i) + ((hash << 5) - hash);
1713	}
1714	let color = "#";
1715	for (let i = 0; i < 3; i++) {
1716		const value = (hash >> (i * 8)) & 0xFF;
1717		color += value.toString(16).padStart(2, '0');
1718	}
1719	return color;
1720}
1721
1722function stringToHslColor(str) {
1723	let stringUniqueHash = [...str].reduce((acc, char) => {
1724		return char.charCodeAt(0) + ((acc << 5) - acc);
1725	}, 0);
1726	return `hsl(${stringUniqueHash % 360}, 65%, 65%)`;
1727}
1728
1729function dolike(ident, verb) {
1730	$('#like-rotator-' + ident).show();
1731
1732	if (typeof conv_mode == typeof undefined) {
1733		conv_mode = '';
1734	}
1735
1736	if (typeof page_mode == typeof undefined) {
1737		page_mode = '';
1738	}
1739
1740	let reload = 0;
1741
1742	if (module == 'photos') {
1743		reload = 1;
1744	}
1745
1746
1747	$.get('like/' + ident + '?verb=' + verb + '&conv_mode=' + conv_mode + '&page_mode=' + page_mode + '&reload=' + reload, function (data) {
1748		if (data.success) {
1749
1750			close_modal();
1751
1752			// mod photos
1753			if (data.reload) {
1754				window.location.href = window.location.href;
1755			}
1756
1757			// this is a bit tricky since the top level thread wrapper wraps the whole thread
1758			if($('#thread-wrapper-' + data.orig_id).hasClass('toplevel_item')) {
1759				var wrapper = $('<div></div>').html( data.html ).find('#wall-item-outside-wrapper-' + data.id);
1760				$('#wall-item-outside-wrapper-' + data.orig_id).html(wrapper[0].innerHTML);
1761				// those were not replaced - swap the id
1762				$('#thread-wrapper-' + data.orig_id).attr('id', 'thread-wrapper-' + data.id);
1763				$('#wall-item-outside-wrapper-' + data.orig_id).attr('id', 'wall-item-outside-wrapper-' + data.id);
1764			}
1765			else {
1766				$('#thread-wrapper-' + data.orig_id).replaceWith(data.html);
1767			}
1768
1769			updateRelativeTime('.autotime');
1770			collapseHeight();
1771			liking = 0;
1772		}
1773	});
1774
1775	liking = 1;
1776}
1777
1778function doprofilelike(ident, verb) {
1779	$.get('like/' + ident + '?verb=' + verb, function() { window.location.href=window.location.href; });
1780}
1781
1782
1783function doreply(parent, ident, owner, hint) {
1784	const modal = new bootstrap.Modal('#reactions');
1785	const modal_container = document.getElementById('reactions')
1786	const modal_content = document.getElementById('reactions_body');
1787	const modal_title = document.getElementById('reactions_title');
1788	const modal_action = document.getElementById('reactions_action');
1789
1790	modal_action.style.display = 'none';
1791	modal_title.innerHTML = hint;
1792
1793	const preview = document.getElementById('comment-edit-preview-' + parent.toString());
1794	if (preview) preview.innerHTML = '';
1795
1796	const form_container = document.getElementById('comment-edit-wrapper-' + parent.toString());
1797
1798	// Get the form element by ID
1799	const form = document.getElementById('comment-edit-form-' + parent.toString());
1800	if (!form) return;
1801
1802	modal_content.innerHTML = '';
1803	modal_content.append(form);
1804	modal_content.append(preview);
1805
1806	// Set the value of the input named 'parent'
1807	const parentInput = form.querySelector('input[name=parent]');
1808
1809	if (parentInput) {
1810		parentInput.value = ident;
1811	}
1812
1813	// Find the submit button and update its HTML
1814	const submitBtn = form.querySelector('button[type=submit]');
1815
1816	if (submitBtn) {
1817		const btnText = submitBtn.innerHTML.replace(/<[^>]*>/g, '').trim();
1818		submitBtn.innerHTML = '<i class="bi bi-arrow-90deg-left"></i> ' + btnText;
1819	}
1820
1821	// Prepare the quote logic
1822	const sel = 'wall-item-body-' + ident.toString();
1823	const quote = window.getSelection().toString().trim();
1824
1825	// Check if the selection is inside the correct element
1826	let isInSel = false;
1827	const anchorNode = window.getSelection().anchorNode;
1828
1829	if (anchorNode) {
1830		let node = anchorNode.nodeType === 3 ? anchorNode.parentNode : anchorNode;
1831		while (node) {
1832			if (node.id === sel) {
1833				isInSel = true;
1834				break;
1835			}
1836			node = node.parentNode;
1837		}
1838	}
1839
1840	modal.show();
1841
1842	modal_container.addEventListener('hide.bs.modal', event => {
1843		// move form back to where it was
1844		form_container.append(form);
1845		form_container.append(preview);
1846	});
1847
1848	// Set the textarea value
1849	const textarea = form.querySelector('textarea');
1850
1851	if (textarea) {
1852		let commentBody = localStorage.getItem('comment_body-' + ident);
1853		if (commentBody) {
1854			textarea.value = commentBody;
1855		}
1856		else {
1857			textarea.value = '@{' + owner + '} ';
1858
1859			if (quote && isInSel) {
1860				textarea.value += "\n[quote]" + quote + "[/quote]\n";
1861			}
1862		}
1863
1864		textarea.focus();
1865	}
1866}
1867
1868
1869function doscroll(parent, hidden) {
1870	var id;
1871	var x = '#hide-comments-outer-' + hidden.toString();
1872	var back = $('#back-to-reply');
1873	if(back.length == 0)
1874		var pos = $(window).scrollTop();
1875	else
1876		var pos = back.attr('href').replace(/[^\d|\.]/g,'');
1877	if($(x).length !== 0) {
1878		x = $(x).attr("onclick").replace(/\D/g,'');
1879		var c = '#collapsed-comments-' + x;
1880		if($(c).length !== 0 && (! $(c).is(':visible'))) {
1881			showHideComments(x);
1882			collapseHeight();
1883			pos += $(c).height();
1884		}
1885	}
1886	back.remove();
1887
1888	$('.thread-wrapper').filter(function() {
1889		if($(this).data('b64mids').indexOf(parent) > -1) {
1890			id = $(this);
1891		}
1892	});
1893
1894	$('html, body').animate({scrollTop:(id.offset().top) - 50}, 'slow');
1895	$('<a href="javascript:doscrollback(' + pos + ');" id="back-to-reply" title="' + aStr['to_reply'] + '"><i class="bi bi-chevron-double-down"></i></a>').insertAfter('#wall-item-ago-' + id.attr('id').replace(/\D/g,''));
1896}
1897
1898function doscrollback(pos) {
1899	$('#back-to-reply').remove();
1900	$(window).scrollTop(pos);
1901}
1902
1903function dopin(id) {
1904        id = id.toString();
1905        $('#like-rotator-' + id).show();
1906        $.post('pin/pin', { 'id' : id })
1907                .done(function() {
1908                        var i = $('#wall-item-pinned-' + id);
1909                        var me = $('#item-pinnable-' + id);
1910                        var pin = $('.pinned-item');
1911                        if(pin.length) {
1912                                $('html, body').animate({ scrollTop: $('#region_2').offset().top }, 'slow', function() {
1913                                        pin.fadeTo('fast', 0.33, function() { this.remove(); });
1914                                });
1915                        };
1916                        pin.remove();
1917                        $('.dropdown-item-pinnable').html($('.dropdown-item-pinnable').html().replace(aStr['unpin_item'],aStr['pin_item']));
1918                        $('.wall-item-pinned').remove()
1919                        if(i.length == 0) {
1920                                $('<span class="wall-item-pinned" title="' + aStr['pinned'] + '" id="wall-item-pinned-' + id + '"><i class="bi bi-pin">&nbsp;</i></span>').insertAfter('#wall-item-ago-' + id);
1921                                me.html(me.html().replace(aStr['pin_item'],aStr['unpin_item']));
1922                        };
1923                })
1924                .fail(function() {
1925                        location.reload();
1926                })
1927                .always(function() {
1928                        $('#like-rotator-' + id).hide();
1929                });
1930}
1931
1932function dropItem(url, object, b64mid) {
1933	var confirm = confirmDelete();
1934	if(confirm) {
1935		var id = url.split('/')[2];
1936		$('body').css('cursor', 'wait');
1937		$(object + ', #pinned-wrapper-' + id).css('opacity', 0.33);
1938
1939		$.get(url, function() {
1940			$(object + ', #pinned-wrapper-' + id).remove();
1941			$('body').css('cursor', 'auto');
1942
1943			toast(aStr.itemdel, 'info');
1944			//$.jGrowl(aStr.itemdel, { sticky: false, theme: 'info', life: 3000 });
1945
1946			if (typeof b64mid !== typeof undefined) {
1947				$('[data-b64mid=\'' + b64mid + '\']').fadeOut(function() {
1948					this.remove();
1949				});
1950			}
1951		});
1952
1953		if($('#wall-item-pinned-' + id).length) {
1954			$.post('pin/pin', { 'id' : id });
1955		}
1956
1957		return true;
1958	}
1959	else {
1960		return false;
1961	}
1962}
1963
1964function dosubthread(ident) {
1965	$('#like-rotator-' + ident).show();
1966	$.get('subthread/sub/' + ident, updateInit );
1967	liking = 1;
1968}
1969
1970function dounsubthread(ident) {
1971	$('#like-rotator-' + ident).show();
1972	$.get('subthread/unsub/' + ident, updateInit );
1973	liking = 1;
1974}
1975
1976function moderate_approve(ident, verb) {
1977	$('#like-rotator-' + ident).show();
1978	close_modal();
1979	$.get('moderate/' + ident + '/approve', updateInit );
1980	liking = 1;
1981}
1982
1983function moderate_drop(ident) {
1984	$('#like-rotator-' + ident).show();
1985	close_modal();
1986	$.get('moderate/' + ident + '/drop', $('#thread-wrapper-' + ident).fadeOut(function() { this.remove(); }));
1987	liking = 1;
1988}
1989
1990function dostar(ident) {
1991	ident = ident.toString();
1992	$('#like-rotator-' + ident).show();
1993	$.get('starred/' + ident, function(data) {
1994		if(data.result == 1) {
1995			$('#starred-' + ident).addClass('starred');
1996			$('#starred-' + ident).removeClass('unstarred');
1997			$('#starred-' + ident).addClass('bi-star-fill');
1998			$('#starred-' + ident).removeClass('bi-star');
1999			$('#star-' + ident).addClass('hidden');
2000			$('#unstar-' + ident).removeClass('hidden');
2001			var btn_tpl = '<div class="" id="star-button-' + ident + '"><button type="button" class="btn btn-sm btn-link link-secondary wall-item-star" onclick="dostar(' + ident + ');"><i class="bi bi-star generic-icons"></i></button></div>'
2002			$('#wall-item-tools-right-' + ident).prepend(btn_tpl);
2003		}
2004		else {
2005			$('#starred-' + ident).addClass('unstarred');
2006			$('#starred-' + ident).removeClass('starred');
2007			$('#starred-' + ident).addClass('bi-star');
2008			$('#starred-' + ident).removeClass('bi-star-fill');
2009			$('#star-' + ident).removeClass('hidden');
2010			$('#unstar-' + ident).addClass('hidden');
2011			$('#star-button-' + ident).remove();
2012		}
2013		$('#like-rotator-' + ident).hide();
2014	});
2015}
2016
2017function getPosition(e) {
2018	var cursor = {x:0, y:0};
2019	if ( e.pageX || e.pageY  ) {
2020		cursor.x = e.pageX;
2021		cursor.y = e.pageY;
2022	}
2023	else {
2024		if( e.clientX || e.clientY ) {
2025			cursor.x = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft) - document.documentElement.clientLeft;
2026			cursor.y = e.clientY + (document.documentElement.scrollTop  || document.body.scrollTop)  - document.documentElement.clientTop;
2027		}
2028		else {
2029			if( e.x || e.y ) {
2030				cursor.x = e.x;
2031				cursor.y = e.y;
2032			}
2033		}
2034	}
2035	return cursor;
2036}
2037
2038function lockview(type, id) {
2039	$.get('lockview/' + type + '/' + id, function(data) {
2040		$('#panel-' + id).html(data);
2041	});
2042}
2043
2044function submitPoll(id) {
2045
2046	$.post('vote/' + id,
2047		$('#question-form-' + id).serialize(),
2048		function(data) {
2049			toast(data.message, ((data.success) ? 'info' : 'danger'));
2050			//$.jGrowl(data.message, { sticky: false, theme: ((data.success) ? 'info' : 'notice'), life: 10000 });
2051			if(timer) clearTimeout(timer);
2052			timer = setTimeout(updateInit, 500);
2053		}
2054	);
2055
2056}
2057
2058function post_comment(id) {
2059
2060	commentBusy = true;
2061	$('body').css('cursor', 'wait');
2062	$("#comment-preview-inp-" + id).val("0");
2063
2064	if (typeof conv_mode == typeof undefined) {
2065		conv_mode = '';
2066	}
2067
2068	const form_data = $("#comment-edit-form-" + id).serialize();
2069
2070	$.post(
2071		"item",
2072		form_data  + '&conv_mode=' + conv_mode,
2073		function(data) {
2074			if (data.success) {
2075
2076				//mod photos
2077				if (data.reload) {
2078					window.location.href = data.reload;
2079				}
2080
2081				close_modal();
2082
2083				localStorage.removeItem("comment_body-" + id);
2084				$("#comment-edit-preview-" + id).hide();
2085				$("#comment-edit-text-" + id).val('').blur().attr('placeholder', aStr.comment);
2086				$('#wall-item-sub-thread-wrapper-' + data.thr_parent_id).append(data.html);
2087
2088				const comment = document.getElementById('wall-item-content-wrapper-' + data.id);
2089				comment.classList.add('item-highlight-fade');
2090				comment.scrollIntoView({
2091					behavior: 'smooth',
2092					block: 'center'
2093				});
2094
2095				updateRelativeTime('.autotime');
2096				$('body').css('cursor', 'unset');
2097				collapseHeight();
2098				commentBusy = false;
2099
2100				const tarea = document.getElementById("comment-edit-text-" + id);
2101				if (tarea) {
2102					commentClose(tarea, id);
2103					$(document).off( "click.commentOpen");
2104				}
2105			}
2106		},
2107		"json"
2108	);
2109
2110	return false;
2111
2112}
2113
2114
2115
2116function preview_comment(id) {
2117	$("#comment-preview-inp-" + id).val("1");
2118	$("#comment-edit-preview-" + id).show();
2119	$.post(
2120		"item",
2121		$("#comment-edit-form-" + id).serialize(),
2122		function(data) {
2123			if(data.preview) {
2124				$("#comment-edit-preview-" + id).html(data.preview);
2125				updateRelativeTime('.autotime');
2126				$("#comment-edit-preview-" + id + " a").click(function() { return false; });
2127			}
2128		},
2129		"json"
2130	);
2131	return true;
2132}
2133
2134function importElement(elem) {
2135	$.post(
2136		"impel",
2137		{ "element" : elem },
2138		function(data) {
2139			if(timer) clearTimeout(timer);
2140			timer = setTimeout(updateInit,10);
2141		}
2142	);
2143	return false;
2144}
2145
2146function preview_post() {
2147	$("#jot-preview").val("1");
2148	$("#jot-preview-content").show();
2149	$.post(
2150		"item",
2151		$("#profile-jot-form").serialize(),
2152		function(data) {
2153			if(data.preview) {
2154				$("#jot-preview-content").html(data.preview);
2155				updateRelativeTime('.autotime');
2156				$("#jot-preview-content" + " a").click(function() { return false; });
2157			}
2158		},
2159		"json"
2160	);
2161	$("#jot-preview").val("0");
2162	return true;
2163}
2164
2165function bin2hex(s) {
2166	// UTF-8 encoding to hex is supported
2167	var bytes = new TextEncoder().encode(s);
2168	return Array.from(
2169		bytes,
2170		byte => byte.toString(16).padStart(2, "0")
2171	).join("");
2172}
2173
2174function hex2bin(hex) {
2175	// UTF-8 decoding from hex is supported
2176	var bytes = new Uint8Array(hex.length / 2);
2177	for (i = 0; i !== bytes.length; i++) {
2178		bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
2179	}
2180	return new TextDecoder().decode(bytes);
2181}
2182
2183function groupChangeMember(gid, cid, sec_token) {
2184	$('body .fakelink').css('cursor', 'wait');
2185	$.get('group/' + gid + '/' + cid + "?t=" + sec_token, function(data) {
2186		$('#group-update-wrapper').html(data);
2187		$('body .fakelink').css('cursor', 'auto');
2188	});
2189}
2190
2191function profChangeMember(gid, cid) {
2192	$('body .fakelink').css('cursor', 'wait');
2193	$.get('profperm/' + gid + '/' + cid, function(data) {
2194		$('#prof-update-wrapper').html(data);
2195		$('body .fakelink').css('cursor', 'auto');
2196	});
2197}
2198
2199function contactgroupChangeMember(gid, cid) {
2200	$('body').css('cursor', 'wait');
2201	$.get('contactgroup/' + gid + '/' + cid, function(data) {
2202		$('body').css('cursor', 'auto');
2203		$('#group-' + gid).toggleClass('bi-check-square bi-square');
2204	});
2205}
2206
2207function checkboxhighlight(box) {
2208	if($(box).is(':checked')) {
2209		$(box).addClass('checkeditem');
2210	} else {
2211		$(box).removeClass('checkeditem');
2212	}
2213}
2214
2215/**
2216 * sprintf in javascript
2217 *  "{0} and {1}".format('zero','uno');
2218 */
2219String.prototype.format = function() {
2220	var formatted = this;
2221	for (var i = 0; i < arguments.length; i++) {
2222		var regexp = new RegExp('\\{'+i+'\\}', 'gi');
2223		formatted = formatted.replace(regexp, ((typeof arguments[i] !== 'undefined') ? arguments[i] : ''));
2224	}
2225	return formatted;
2226};
2227
2228// Array Remove
2229Array.prototype.remove = function(item) {
2230	to = undefined;
2231	from = this.indexOf(item);
2232	var rest = this.slice((to || from) + 1 || this.length);
2233	this.length = from < 0 ? this.length + from : from;
2234	return this.push.apply(this, rest);
2235};
2236
2237function zFormError(elm,x) {
2238	if(x) {
2239		$(elm).addClass("zform-error");
2240		$(elm).removeClass("zform-ok");
2241	} else {
2242		$(elm).addClass("zform-ok");
2243		$(elm).removeClass("zform-error");
2244	}
2245}
2246
2247$(window).scroll(function () {
2248	if(typeof buildCmd == 'function') {
2249		// This is a content page with items and/or conversations
2250		if($('#conversation-end').length && ($(window).scrollTop() + $(window).height()) > $('#conversation-end').position().top) {
2251			if((pageHasMoreContent) && (! loadingPage)) {
2252				next_page++;
2253				scroll_next = true;
2254				loadingPage = true;
2255				liveUpdate();
2256			}
2257		}
2258	}
2259	else {
2260		// This is some other kind of page - perhaps a directory
2261		if($('#page-end').length && ($(window).scrollTop() + $(window).height()) > $('#page-end').position().top) {
2262			if((pageHasMoreContent) && (! loadingPage) && (! justifiedGalleryActive)) {
2263				next_page++;
2264				scroll_next = true;
2265				loadingPage = true;
2266				pageUpdate();
2267			}
2268		}
2269	}
2270});
2271
2272function loadText(textRegion,data) {
2273	var currentText = $(textRegion).val();
2274	$(textRegion).val(currentText + data);
2275}
2276
2277function makeid(length) {
2278	var result = '';
2279	var characters = 'abcdef0123456789';
2280	var charactersLength = characters.length;
2281	for ( var i = 0; i < length; i++ ) {
2282		result += characters.charAt(Math.floor(Math.random() * charactersLength));
2283	}
2284	return result;
2285}
2286
2287function push_notification_request(e) {
2288    if (!('Notification' in window)) {
2289        alert('This browser does not support push notifications');
2290    }
2291    else if (Notification.permission !== 'granted') {
2292        Notification.requestPermission(function(permission) {
2293			if(permission === 'granted') {
2294				$(e.target).closest('div').hide();
2295			}
2296		});
2297   }
2298}
2299
2300
2301function push_notification(title, body, b64mid) {
2302	let options = {
2303		body: body,
2304		data: b64mid,
2305		icon: '/images/app/hz-96.png',
2306		silent: false
2307	}
2308
2309	let n = new Notification(title, options);
2310	n.onclick = function (e) {
2311		if(module === 'hq') {
2312			prepareLiveUpdate(e.target.data);
2313		}
2314		else {
2315			window.location.href = baseurl + '/hq/' + e.target.data;
2316		}
2317	}
2318}
2319
2320function toggleAside() {
2321	if ($('main.region_1-on').length) {
2322		$('#expand-aside-icon').addClass('bi-arrow-right-circle').removeClass('bi-arrow-left-circle');
2323		$('html, body').css({ 'position': '', 'left': '' });
2324		$('main').removeClass('region_1-on');
2325		$('#region_1').addClass('d-none');
2326		$('#region_2').css({ 'min-width': '' });
2327		$('#overlay').remove();
2328	}
2329	else {
2330		$('#expand-aside-icon').removeClass('bi-arrow-right-circle').addClass('bi-arrow-left-circle');
2331		$('html, body').css({ 'position': 'sticky',  'left': '0px'});
2332		$('main').addClass('region_1-on');
2333		$('#region_1').removeClass('d-none');
2334		$('#region_2').css({ 'min-width': window.outerWidth });
2335		$('<div id="overlay"></div>').appendTo('body').one('click', function() { toggleAside(); });
2336	}
2337}
2338
2339function toast(string, severity) {
2340	let id = bin2hex(string);
2341	let container = document.getElementById('toast-container');
2342	let toast = document.getElementById(id);
2343
2344	if (!toast) {
2345		toast = document.createElement('div');
2346		toast.setAttribute('id', id);
2347		toast.innerHTML = '<div class="d-flex"><div class="toast-body">' + string + '</div><button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button></div>';
2348		toast.classList.add('toast', 'p-2', 'bg-' + severity + '-subtle', 'text-' + severity + '-emphasis', 'border-' + severity);
2349	}
2350
2351	container.prepend(toast);
2352
2353	let toastInstance = bootstrap.Toast.getOrCreateInstance(toast);
2354
2355	if (severity === 'danger') {
2356		toastInstance._config.autohide = false;
2357	}
2358
2359	toastInstance.show();
2360}
2361
2362function close_modal() {
2363	let modal = bootstrap.Modal.getInstance(document.querySelector('.modal.show'));
2364
2365	if (modal) {
2366		modal.hide();
2367	}
2368}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.