[automated] update groups.io wiki

This commit is contained in:
github-actions
2026-04-29 01:30:12 +00:00
parent 93ca409bcd
commit b90e1cd934
241 changed files with 80200 additions and 4482 deletions
+312 -15
View File
@@ -6,8 +6,9 @@
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#2f6fa7">
<meta name="pinterest" content="nopin" />
<meta name="pinterest" content="nohover" />
@@ -86,6 +87,45 @@
var pushSubToken;
var ignoreErrors = false;
var lastError = "";
// Auto-hide the fixed app top navbar on reading-heavy pages. The
// navbar tracks scroll delta 1:1 in both directions: scrolling down
// slides it off at the same pixel rate as the page; scrolling up —
// even one pixel — starts bringing it back at the same rate. When
// the user is at the very top, it's always fully visible. Opt-in
// per page; xs only. Bottom tab bar is untouched.
function EnableTopbarAutoHide() {
if (document.documentElement.clientWidth > 767) return;
var body = document.body;
body.classList.add('auto-hide-topbar');
// Measure the actual headerbar height so notched devices (iOS)
// hide the full bar including the safe-area-inset-top padding,
// not just the 50px content row. Fall back to 50 if the
// element isn't found for any reason.
var headerEl = document.getElementById('headerbar');
var navHeight = headerEl ? headerEl.offsetHeight : 50;
var offset = 0;
var lastY = window.scrollY;
function onScroll() {
var y = window.scrollY;
var dy = y - lastY;
lastY = y;
if (y <= 0) {
offset = 0;
} else {
offset -= dy;
if (offset > 0) offset = 0;
if (offset < -navHeight) offset = -navHeight;
}
body.style.setProperty('--topbar-offset', offset + 'px');
}
window.addEventListener('scroll', onScroll, { passive: true });
gioDestroy(function() {
window.removeEventListener('scroll', onScroll);
body.classList.remove('auto-hide-topbar');
body.style.removeProperty('--topbar-offset');
});
}
window.onerror = function(errorMessage, errorUrl, errorLine, errorColumn, errorObj) {
let column;
@@ -102,7 +142,7 @@
console.log("stack:", errorObj.stack);
stack = errorObj.stack;
}
console.log("client_id:", "web.app02-g2.8459966.1776820350345234217");
console.log("client_id:", "web.app02-g2.789189.1777426185360995598");
console.log("last_error:", lastError);
console.log("stack:", stack);
}
@@ -151,7 +191,7 @@
column: column,
stack: stack,
last_error: lastError,
client_id: "web.app02-g2.8459966.1776820350345234217"
client_id: "web.app02-g2.789189.1777426185360995598"
},
success: function() {
if (console && console.log) {
@@ -174,7 +214,7 @@
<script src="../../../../tinymce-5.10.9/tinymce.min.js"></script>
<script src="../../../../js/browser-image-compression-2.0.2.min.js"></script>
<link href="../../../../css/application-f04f83f5851c7a0844717a3b496877d3.css" rel="stylesheet" id="groupsio-css">
<link href="../../../../css/application-24dcf155f8cf9ca30214ea64f62961b4.css" rel="stylesheet" id="groupsio-css">
<script src="../../../../js/application-7800e976860b357df405a3ea22aa72b1.js" id="groupsio-js"></script>
<script src="../../../../js/run_prettify.js"></script>
<title>MTFHRT@groups.io | Wiki</title>
@@ -220,9 +260,51 @@
htmx.config.historyCacheSize = 0;
htmx.config.defaultSettleDelay = 0;
console.log("FULL PAGE LOAD");
// Short fade-in on #maincontent-refresh after every HTMX swap — adds zero latency
// because it runs AFTER the new content has arrived. We listen to afterRequest
// (fires once per HTMX request) rather than afterSwap (fires once per swapped
// element, including every hx-swap-oob badge — which would cancel the animation).
// Listen on document because this script runs in <head> before <body> exists.
document.addEventListener('htmx:afterRequest', function(evt) {
if (!evt.detail || !evt.detail.successful) return;
var refresh = document.getElementById('maincontent-refresh');
if (!refresh) return;
refresh.classList.remove('gio-fade-in');
// force reflow so removing then adding re-starts the animation each time
void refresh.offsetWidth;
refresh.classList.add('gio-fade-in');
});
// Top-of-viewport progress bar shown while an HTMX request is in flight.
// Provides immediate visual feedback for taps even before content arrives.
document.addEventListener('htmx:beforeRequest', function() {
var bar = document.getElementById('gio-progress-bar');
if (bar) bar.classList.add('active');
});
document.addEventListener('htmx:afterRequest', function() {
var bar = document.getElementById('gio-progress-bar');
if (bar) bar.classList.remove('active');
});
// Haptic feedback on mobile-native chrome taps: bottom-nav tabs, back
// chevron, and the navbar overflow action slot. Uses the shared helpers
// from qtutils/appfunctions.qtpl — they no-op on non-Capacitor runtimes,
// so this is safe on the plain web too.
document.addEventListener('click', function(ev) {
if (typeof hapticsSelectionChanged !== 'function') return;
var target = ev.target;
if (!target || !target.closest) return;
if (target.closest('.mobilesidebar-item') ||
target.closest('#logo a') ||
target.closest('#header-action-slot a')) {
hapticsSelectionChanged();
}
}, true);
</script>
</head>
<body id="body" class="" hx-headers='{"Accept-Version": "b6bf5273f75920ed951f70f381c6d7e3177d7469"}' hx-ext="preload" hx-boost="false">
<body id="body" class="" hx-headers='{"Accept-Version": "ca86cf933a8828e8bf670199572b45bc66bd1319"}' hx-ext="preload" hx-boost="false">
<div id="gio-progress-bar" aria-hidden="true"></div>
<script>
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
@@ -582,18 +664,128 @@ function showAutocompleteModal(modalName) {
location.reload();
});
function ShowBackButton(label) {
function ShowBackButton(label, url, subtitle) {
var logoElement = document.getElementById("logo");
if (logoElement) {
html = `
<span style="color:#fff;margin-top:14px;margin-left:12px;float:left !important; height:36px;">
<a onclick="goBack()" style="color:#fff">
<i class="fa-fw fa-solid fa-chevron-left fa-lg"></i>
</a></span>`;
if (label != "") {
html += `<div style="position:absolute;left:50%;transform:translateX(-50%);text-align:center;color:#ecf0f1;margin-top:5px;"><h4>` + label + `</h4></div>`;
if (!logoElement) {
return;
}
logoElement.innerHTML = '';
var span = document.createElement('span');
span.style.cssText = "color:#fff;margin-top:14px;margin-left:12px;float:left !important;height:36px;";
var a = document.createElement('a');
a.style.color = '#fff';
if (url) {
a.href = url;
// HTMX-boost only when the target is still inside the same group — then the
// bottom nav (which sits outside #maincontent-refresh) is the same layout and
// can stay put. Cross-context targets (e.g. /groups) need a full page load so
// the bottom nav actually switches.
if (/\/g\//.test(url)) {
a.setAttribute('hx-boost', 'true');
a.setAttribute('hx-target', '#maincontent-refresh');
a.setAttribute('hx-push-url', 'true');
a.setAttribute('hx-history', 'false');
// Reset window scroll to the top after the swap, otherwise the
// user lands on the previous page at whatever Y they were at on
// the current one — typically mid-page, behind the fixed
// navbar/notch in the iOS app.
a.setAttribute('hx-on::after-request', 'window.scrollTo({top:0, behavior:"instant"})');
if (window.htmx) {
htmx.process(a);
}
}
logoElement.innerHTML = html;
} else {
a.onclick = goBack;
a.style.cursor = 'pointer';
}
var icon = document.createElement('i');
icon.className = 'fa-fw fa-solid fa-chevron-left fa-lg';
a.appendChild(icon);
span.appendChild(a);
logoElement.appendChild(span);
if (label) {
var titleDiv = document.createElement('div');
// The navbar in the iOS app gets padding-top equal to the safe-area
// inset, so the outer box can be taller than the visible content
// strip. Centering at 50% of the outer box lands the title inside
// the notch — offset the center down by half the inset so the
// title sits in the visible strip. On non-app contexts
// env(safe-area-inset-top) is 0 and this is a no-op.
titleDiv.style.cssText = "position:absolute;left:50%;top:calc(50% + env(safe-area-inset-top) / 2);transform:translate(-50%,-50%);text-align:center;color:#ecf0f1;max-width:60%;overflow:hidden;";
var h4 = document.createElement('h4');
h4.style.cssText = "margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.1;";
h4.textContent = label;
titleDiv.appendChild(h4);
if (subtitle) {
var sub = document.createElement('div');
sub.style.cssText = "font-size:11px;line-height:1.2;margin-top:1px;opacity:0.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
sub.textContent = subtitle;
titleDiv.appendChild(sub);
}
logoElement.appendChild(titleDiv);
}
}
// ShowNavbarTitle sets a centered title (and optional subtitle) in the
// mobile navbar without any back arrow — used on top-level tab pages
// (/feed, /groups) where there's nowhere to go back to.
function ShowNavbarTitle(label, subtitle) {
var logoElement = document.getElementById("logo");
if (!logoElement) {
return;
}
logoElement.innerHTML = '';
if (label) {
var titleDiv = document.createElement('div');
titleDiv.style.cssText = "position:absolute;left:50%;top:calc(50% + env(safe-area-inset-top) / 2);transform:translate(-50%,-50%);text-align:center;color:#ecf0f1;max-width:70%;overflow:hidden;";
var h4 = document.createElement('h4');
h4.style.cssText = "margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.1;";
h4.textContent = label;
titleDiv.appendChild(h4);
if (subtitle) {
var sub = document.createElement('div');
sub.style.cssText = "font-size:11px;line-height:1.2;margin-top:1px;opacity:0.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
sub.textContent = subtitle;
titleDiv.appendChild(sub);
}
logoElement.appendChild(titleDiv);
}
}
// ShowHeaderAction adds an icon button (e.g. settings cog) to the mobile navbar,
// to the left of the profile icon. modalTarget is optional — if given, clicking
// opens that Bootstrap modal. ariaLabel is for accessibility.
function ShowHeaderAction(iconClass, modalTarget, ariaLabel) {
var slot = document.getElementById("header-action-slot");
if (!slot) {
return;
}
slot.innerHTML = '';
var a = document.createElement('a');
a.href = '#';
a.className = 'navbar-action-xs';
if (ariaLabel) {
a.setAttribute('aria-label', ariaLabel);
}
if (modalTarget) {
a.setAttribute('data-toggle', 'modal');
a.setAttribute('data-target', modalTarget);
}
var icon = document.createElement('i');
icon.className = iconClass;
a.appendChild(icon);
slot.appendChild(a);
slot.style.display = '';
}
function HideHeaderAction() {
var slot = document.getElementById("header-action-slot");
if (slot) {
slot.innerHTML = '';
slot.style.display = 'none';
slot.style.float = 'left';
}
}
@@ -611,6 +803,90 @@ function showAutocompleteModal(modalName) {
history.back();
}
// --- Native date/time picker helpers -----------------------------------
// On touch devices we swap flatpickr-wrapped <input type="text"> fields
// for native <input type="date"> / type="time" so iOS / Android bring up
// their OS pickers (wheel, spinner, Material dialog). On pointer
// devices we leave the flatpickr popover in place, since Chrome's
// default date popover looks out of place. Both helpers are global so
// every page with a date/time input can call them the same way.
window.__isTouchPicker = function() {
return window.matchMedia && window.matchMedia('(any-pointer: coarse)').matches;
};
// Fallback formats if data.User is nil on this page (logged out, etc).
window.__userDateFmt = "M/D/YYYY";
window.__userTimeFmt = "h:mmA";
// Hybrid native picker: the field is shown to the user as a plain
// type=text input holding the date/time formatted per the user's
// Groups.io preference (e.g., "19/04/2026" for an International
// user). When the user taps the field, we briefly swap the input's
// type to 'date' or 'time' so the OS brings up its native picker
// (iOS wheel, Android calendar/spinner). When the user finishes
// picking and blurs the field, we swap back to text and reformat
// the value in the user's chosen format. The server sees the
// user-format value on submit, same as with flatpickr.
//
// `kind` is 'date' or 'time'. `onHumanChange(humanStr, momentObj)`
// fires whenever the user picks a new value, with the value already
// in the user's format.
window.useNativePicker = function(id, kind, onHumanChange) {
var input = document.getElementById(id);
if (!input) return null;
var userFmt = kind === 'time' ? window.__userTimeFmt : window.__userDateFmt;
var isoFmt = kind === 'time' ? 'HH:mm' : 'YYYY-MM-DD';
// Page loads with input type=text holding a user-format value —
// leave it as is until the user actually taps.
function toPicker() {
if (input.type !== kind) {
var m = moment(input.value, userFmt);
input.type = kind;
input.value = m.isValid() ? m.format(isoFmt) : '';
}
}
function toDisplay() {
if (input.type === kind) {
var m = moment(input.value, isoFmt);
input.type = 'text';
input.value = m.isValid() ? m.format(userFmt) : '';
}
}
// pointerdown fires before focus/click so we can swap the type
// in time for the browser's default tap-handling to open the
// correct native picker. Fall back to 'focus' for keyboard users.
input.addEventListener('pointerdown', toPicker);
input.addEventListener('focus', toPicker);
input.addEventListener('change', function() {
if (input.type === kind) {
var m = moment(input.value, isoFmt);
if (m.isValid() && onHumanChange) {
onHumanChange(m.format(userFmt), m);
}
}
});
input.addEventListener('blur', toDisplay);
// Make sure the server always sees the user-format value.
var form = input.closest('form');
if (form) form.addEventListener('submit', toDisplay);
return input;
};
// Set a field's value as a user-format string, whether the field is a
// flatpickr-wrapped text input (desktop) or a native type=date/time
// input (touch). Used by onChange handlers that update sibling fields.
window.setFieldHuman = function(id, humanStr, kind) {
var input = document.getElementById(id);
if (!input) return;
if (input._flatpickr) {
input._flatpickr.setDate(humanStr);
} else if (input.type === 'date' || input.type === 'time') {
var userFmt = kind === 'time' ? window.__userTimeFmt : window.__userDateFmt;
var isoFmt = kind === 'time' ? 'HH:mm' : 'YYYY-MM-DD';
var m = moment(humanStr, userFmt);
if (m.isValid()) input.value = m.format(isoFmt);
} else {
input.value = humanStr;
}
};
// submitEvent acts like .submit(), except it is compatible with the app, by using events instead.
function submitEvent(ele) {
console.log("in submitEvent");
@@ -1444,6 +1720,19 @@ function countChecked(id) {
<div class="navbar navbar-default navbar-fixed-bottom">
<div class="container" style="display: flex; justify-content: space-around;">
<div id="mobilesidebar-groups" class="mobilesidebar-item" style="margin-top:10px; padding-bottom:20px; flex: 1; text-align: center;" hx-get="https://groups.io/groups" hx-boost="true" hx-target="#maincontent-refresh" hx-push-url="true" hx-history="false" hx-on::after-request="document.getElementById('maincontent').scrollTo({top: 0, behavior: 'instant'})" >
<div style="display: inline-block; position: relative;">
<div style="position: relative;display: inline-block;">
<i class="fa-fw fa-light fa-users fa-2x"></i>
</div><br>
<span style="font-size:12px;">Groups</span>
</div>
</div>
@@ -1693,6 +1982,14 @@ function countChecked(id) {
});
function UpdateSidebar(id) {
console.log(id + " was clicked");
if (typeof HideHeaderAction === 'function') {
HideHeaderAction();
}
// restore mobile bottom nav in case a previous page (e.g. /post) hid it
var mobSidebar = document.getElementById("mobileSidebar");
if (mobSidebar) mobSidebar.style.display = '';
var mainContent = document.getElementById("maincontent");
if (mainContent) mainContent.style.bottom = '';
var sidebarItems = document.querySelectorAll('.mobilesidebar-item');
sidebarItems.forEach(function(item) {
var iElement = item.getElementsByTagName('div')[0].getElementsByTagName('i')[0];