v2.9.6: custom ToS, unpin bug fix, Android popup fixes

This commit is contained in:
ancsemi 2026-04-07 22:47:02 -04:00
parent 508c42ef5c
commit 4b9e16d7b6
14 changed files with 90 additions and 24 deletions

View file

@ -11,6 +11,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Haven uses [Sema
---
## [2.9.6] — 2026-04-07
### Added
- **Custom Terms of Service** — admins can now add custom terms that appear above the default Haven ToS on the login page. Set via a new textarea in Admin Settings. Supports plain text with paragraph breaks, max 50,000 characters. Leave empty to show only the default ToS. (#5229)
### Fixed
- **Unpin message visual bug** — unpinning a message while viewing the pinned messages panel no longer leaves the pin border on the message. The pinned panel item is also removed in real time and the count updates. (#5228)
- **Android app popup "Don't show this again"** — the checkbox now persists correctly across sessions. Previously the v3 migration flag used sessionStorage, causing dismissals to reset on every new session.
- **Android app popup layout** — moved the "NOW AVAILABLE" badge above the title instead of inline, and centered the title text.
---
## [2.9.5] — 2026-04-07
### Changed

View file

@ -1415,12 +1415,12 @@
</div>
<div class="download-card fade-in">
<h2>&#x2B21; Haven Server &mdash; v2.9.5</h2>
<h2>&#x2B21; Haven Server &mdash; v2.9.6</h2>
<p class="download-version">Latest stable release &middot; Windows, macOS &amp; Linux &middot; ~5 MB</p>
<div class="download-btn-group">
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.5.zip" class="btn btn-primary download-main">
<span class="icon">&#x2B07;</span> Download v2.9.5 (.zip)
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.6.zip" class="btn btn-primary download-main">
<span class="icon">&#x2B07;</span> Download v2.9.6 (.zip)
</a>
<div class="download-alt-links">
<a href="https://github.com/ancsemi/Haven" target="_blank">&#9965; View on GitHub</a>
@ -1437,7 +1437,11 @@
<div class="version-list">
<div class="version-list-inner">
<div class="version-item">
<div><span class="v-name">v2.9.5</span><span class="v-tag latest">Latest</span></div>
<div><span class="v-name">v2.9.6</span><span class="v-tag latest">Latest</span></div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.6.zip">Download &rarr;</a>
</div>
<div class="version-item">
<div><span class="v-name">v2.9.5</span> &mdash; License changed to AGPL-3.0</div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.5.zip">Download &rarr;</a>
</div>
<div class="version-item">

View file

@ -1,6 +1,6 @@
{
"name": "haven",
"version": "2.9.5",
"version": "2.9.6",
"description": "Haven — self-hosted private chat for your server, your rules",
"license": "AGPL-3.0",
"main": "server.js",

View file

@ -1574,6 +1574,12 @@
<p class="muted-text" data-i18n="settings.admin.no_bots">No bots configured</p>
</div>
</div>
<!-- Custom Terms of Service (Admin only) -->
<div class="admin-settings" id="section-custom-tos" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">⚖️ <span data-i18n="settings.admin.custom_tos_title">Custom Terms of Service</span></h5>
<small class="settings-hint" data-i18n="settings.admin.custom_tos_hint">Add custom terms that appear before the default ToS on the login page. Supports plain text. Leave empty to show only the default Haven ToS.</small>
<textarea id="custom-tos-input" class="settings-textarea" rows="6" maxlength="50000" placeholder="Enter your custom terms of service..." style="margin-top:8px;width:100%;resize:vertical;min-height:80px;font-size:12px;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-family:inherit;"></textarea>
</div>
<!-- Discord Import (Admin only) -->
<div class="admin-settings" id="section-import" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
<h5 class="settings-section-subtitle">📦 <span data-i18n="settings.admin.import_title">Discord Import</span></h5>
@ -2464,7 +2470,8 @@
<div class="modal-overlay" id="android-beta-modal" style="display:none">
<div class="modal android-beta-promo">
<div class="android-beta-promo-icon">📱</div>
<h3 class="android-beta-promo-title"><span data-i18n="modals.android_beta.title">Amni-Haven Android</span> <span class="android-beta-promo-badge" data-i18n="modals.android_beta.badge" style="background:var(--accent-bright,#4caf50)">NOW AVAILABLE</span></h3>
<span class="android-beta-promo-badge" data-i18n="modals.android_beta.badge" style="background:var(--accent-bright,#4caf50)">NOW AVAILABLE</span>
<h3 class="android-beta-promo-title"><span data-i18n="modals.android_beta.title">Amni-Haven Android</span></h3>
<p class="android-beta-promo-subtitle" data-i18n="modals.android_beta.subtitle">A native Android app for Haven, built from the ground up by Amnibro!</p>
<ul class="android-beta-promo-features">
<li><span class="promo-check"></span> <span data-i18n="modals.android_beta.feature_native">Native Android experience</span></li>

View file

@ -3389,12 +3389,10 @@ html.rgb-cycling *::after {
font-weight: 700;
color: var(--text-primary);
margin: 0 0 4px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
text-align: center;
}
.android-beta-promo-badge {
display: inline-block;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
@ -3403,6 +3401,7 @@ html.rgb-cycling *::after {
color: #fff;
padding: 2px 8px;
border-radius: 10px;
margin-bottom: 6px;
}
.android-beta-promo-subtitle {
font-size: 14px;

View file

@ -178,8 +178,10 @@
<!-- EULA Modal -->
<div class="modal-overlay" id="eula-modal" style="display:none">
<div class="modal eula-modal">
<h3>⚖️ Terms of Service &amp; Release of Liability Agreement</h3>
<div class="eula-content">
<h3>⚖️ Terms of Service &amp; Release of Liability Agreement</h3> <div id="custom-tos-section" style="display:none">
<div class="eula-content" id="custom-tos-content" style="margin-bottom:0;border-bottom:1px solid var(--border,#444);padding-bottom:16px;"></div>
<p style="text-align:center;font-size:12px;color:var(--text-muted,#888);margin:12px 0;">── Default Haven Terms of Service ──</p>
</div> <div class="eula-content">
<p><strong>HAVEN — TERMS OF SERVICE, END USER LICENSE AGREEMENT &amp; RELEASE OF LIABILITY</strong></p>
<p><em>Effective Date: February 12, 2026 &nbsp;|&nbsp; Version 2.0</em></p>

View file

@ -50,6 +50,17 @@
const titleEl = document.getElementById('server-title');
if (titleEl) titleEl.textContent = d.server_title;
}
if (d.custom_tos) {
const section = document.getElementById('custom-tos-section');
const content = document.getElementById('custom-tos-content');
if (section && content) {
// Render as plain text with paragraph breaks
content.innerHTML = d.custom_tos.split(/\n\n+/).map(p =>
'<p>' + p.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>') + '</p>'
).join('');
section.style.display = 'block';
}
}
}).catch(() => {});
// ── EULA ─────────────────────────────────────────────

View file

@ -451,8 +451,11 @@ _snapshotAdminSettings() {
max_emoji_kb: this.serverSettings.max_emoji_kb || '256',
max_poll_options: this.serverSettings.max_poll_options || '10',
update_banner_admin_only: this.serverSettings.update_banner_admin_only || 'false',
default_theme: this.serverSettings.default_theme || ''
default_theme: this.serverSettings.default_theme || '',
custom_tos: this.serverSettings.custom_tos || ''
};
const tosEl = document.getElementById('custom-tos-input');
if (tosEl) tosEl.value = this._adminSnapshot.custom_tos;
// Load webhooks list for admin preview
if (this.user?.isAdmin) {
this.socket.emit('get-webhooks');
@ -546,6 +549,12 @@ _saveAdminSettings() {
changed = true;
}
const customTos = document.getElementById('custom-tos-input')?.value.trim() || '';
if (customTos !== (snap.custom_tos || '')) {
this.socket.emit('update-server-setting', { key: 'custom_tos', value: customTos });
changed = true;
}
if (changed) {
this._showToast(t('settings.admin.settings_saved'), 'success');
} else {
@ -583,6 +592,8 @@ _cancelAdminSettings() {
if (uba) uba.checked = snap.update_banner_admin_only === 'true';
const dt = document.getElementById('default-theme-select');
if (dt) dt.value = snap.default_theme || '';
const ct = document.getElementById('custom-tos-input');
if (ct) ct.value = snap.custom_tos || '';
}
document.getElementById('settings-modal').style.display = 'none';
},

View file

@ -679,7 +679,7 @@ _renderPinnedPanel(pins) {
list.querySelectorAll('.pinned-item').forEach(item => {
item.addEventListener('click', () => {
const msgId = item.dataset.msgId;
const msgEl = document.querySelector(`[data-msg-id="${msgId}"]`);
const msgEl = document.querySelector(`#messages [data-msg-id="${msgId}"]`);
if (msgEl) {
msgEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
msgEl.classList.add('highlight-flash');

View file

@ -175,12 +175,12 @@ _initDesktopAppBanner() {
_initAndroidBetaBanner() {
// ── v3 migration: Android app is now a full release; reset dismissals so
// users who dismissed the old closed-beta popup see the new announcement ──
if (!sessionStorage.getItem('_ab_v3_migrated')) {
if (!localStorage.getItem('_ab_v3_migrated')) {
localStorage.removeItem('haven_android_beta_banner_dismissed');
localStorage.removeItem('haven_android_beta_promo_dismissed');
localStorage.removeItem('haven_ab_banner_nodisplay');
localStorage.removeItem('haven_ab_promo_nodisplay');
sessionStorage.setItem('_ab_v3_migrated', '1');
localStorage.setItem('_ab_v3_migrated', '1');
}
// ── Top-bar banner ──

View file

@ -922,7 +922,7 @@ _setupSocketListeners() {
// ── Pin / Unpin ──────────────────────────────────
this.socket.on('message-pinned', (data) => {
if (data.channelCode === this.currentChannel) {
const msgEl = document.querySelector(`[data-msg-id="${data.messageId}"]`);
const msgEl = document.querySelector(`#messages [data-msg-id="${data.messageId}"]`);
if (msgEl) {
msgEl.classList.add('pinned');
msgEl.dataset.pinned = '1';
@ -941,7 +941,7 @@ _setupSocketListeners() {
this.socket.on('message-unpinned', (data) => {
if (data.channelCode === this.currentChannel) {
const msgEl = document.querySelector(`[data-msg-id="${data.messageId}"]`);
const msgEl = document.querySelector(`#messages [data-msg-id="${data.messageId}"]`);
if (msgEl) {
msgEl.classList.remove('pinned');
delete msgEl.dataset.pinned;
@ -951,6 +951,17 @@ _setupSocketListeners() {
const unpinBtn = msgEl.querySelector('[data-action="unpin"]');
if (unpinBtn) { unpinBtn.dataset.action = 'pin'; unpinBtn.title = 'Pin'; }
}
// Remove from pinned panel if it's open
const pinnedItem = document.querySelector(`#pinned-panel .pinned-item[data-msg-id="${data.messageId}"]`);
if (pinnedItem) {
pinnedItem.remove();
const count = document.getElementById('pinned-count');
const remaining = document.querySelectorAll('#pinned-list .pinned-item').length;
count.textContent = `📌 ${t(remaining !== 1 ? 'pinned_panel.count_other' : 'pinned_panel.count_one', { count: remaining })}`;
if (remaining === 0) {
document.getElementById('pinned-list').innerHTML = `<p class="muted-text" style="padding:12px">${t('pinned_panel.no_messages')}</p>`;
}
}
this._appendSystemMessage(`📌 ${t('header.messages.message_unpinned')}`);
}
});

View file

@ -597,9 +597,11 @@ app.get('/api/public-config', (req, res) => {
const db = getDb();
const themeRow = db.prepare("SELECT value FROM server_settings WHERE key = 'default_theme'").get();
const titleRow = db.prepare("SELECT value FROM server_settings WHERE key = 'server_title'").get();
const tosRow = db.prepare("SELECT value FROM server_settings WHERE key = 'custom_tos'").get();
res.json({
default_theme: themeRow?.value || '',
server_title: titleRow?.value || ''
server_title: titleRow?.value || '',
custom_tos: tosRow?.value || ''
});
} catch {
res.json({ default_theme: '', server_title: '' });

View file

@ -4424,7 +4424,7 @@ function setupSocketHandlers(io, db) {
const key = typeof data.key === 'string' ? data.key.trim() : '';
const value = typeof data.value === 'string' ? data.value.trim() : '';
const allowedKeys = ['member_visibility', 'cleanup_enabled', 'cleanup_max_age_days', 'cleanup_max_size_mb', 'giphy_api_key', 'server_name', 'server_title', 'server_icon', 'permission_thresholds', 'tunnel_enabled', 'tunnel_provider', 'server_code', 'max_upload_mb', 'max_poll_options', 'max_sound_kb', 'max_emoji_kb', 'setup_wizard_complete', 'update_banner_admin_only', 'default_theme', 'channel_sort_mode', 'channel_cat_order', 'channel_cat_sort', 'channel_tag_sorts'];
const allowedKeys = ['member_visibility', 'cleanup_enabled', 'cleanup_max_age_days', 'cleanup_max_size_mb', 'giphy_api_key', 'server_name', 'server_title', 'server_icon', 'permission_thresholds', 'tunnel_enabled', 'tunnel_provider', 'server_code', 'max_upload_mb', 'max_poll_options', 'max_sound_kb', 'max_emoji_kb', 'setup_wizard_complete', 'update_banner_admin_only', 'default_theme', 'channel_sort_mode', 'channel_cat_order', 'channel_cat_sort', 'channel_tag_sorts', 'custom_tos'];
if (!allowedKeys.includes(key)) return;
if (key === 'member_visibility' && !['all', 'online', 'none'].includes(value)) return;
@ -4487,6 +4487,9 @@ function setupSocketHandlers(io, db) {
const validThemes = ['', 'haven', 'discord', 'matrix', 'fallout', 'ffx', 'ice', 'nord', 'darksouls', 'eldenring', 'bloodborne', 'cyberpunk', 'lotr', 'abyss', 'scripture', 'chapel', 'gospel', 'tron', 'halo', 'dracula', 'win95'];
if (!validThemes.includes(value)) return;
}
if (key === 'custom_tos') {
if (value.length > 50000) return;
}
if (key === 'server_code') {
// Server code is managed via generate/rotate events, not directly
return;

View file

@ -1415,12 +1415,12 @@
</div>
<div class="download-card fade-in">
<h2>&#x2B21; Haven Server &mdash; v2.9.5</h2>
<h2>&#x2B21; Haven Server &mdash; v2.9.6</h2>
<p class="download-version">Latest stable release &middot; Windows, macOS &amp; Linux &middot; ~5 MB</p>
<div class="download-btn-group">
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.5.zip" class="btn btn-primary download-main">
<span class="icon">&#x2B07;</span> Download v2.9.5 (.zip)
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.6.zip" class="btn btn-primary download-main">
<span class="icon">&#x2B07;</span> Download v2.9.6 (.zip)
</a>
<div class="download-alt-links">
<a href="https://github.com/ancsemi/Haven" target="_blank">&#9965; View on GitHub</a>
@ -1437,7 +1437,11 @@
<div class="version-list">
<div class="version-list-inner">
<div class="version-item">
<div><span class="v-name">v2.9.5</span><span class="v-tag latest">Latest</span></div>
<div><span class="v-name">v2.9.6</span><span class="v-tag latest">Latest</span></div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.6.zip">Download &rarr;</a>
</div>
<div class="version-item">
<div><span class="v-name">v2.9.5</span> &mdash; License changed to AGPL-3.0</div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.5.zip">Download &rarr;</a>
</div>
<div class="version-item">