Skip to content

Commit 6e97a7a

Browse files
committed
v2.9.8 — read-only channels, server-relayed mic illumination, role permission row highlight
1 parent 6e756b6 commit 6e97a7a

22 files changed

Lines changed: 409 additions & 41 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Haven uses [Sema
1111
1212
---
1313

14+
## [2.9.8] — 2026-04-11
15+
16+
### Added
17+
- **Read-only channels** — admins can now mark any text channel as read-only. Members without the new `Read-Only Override` role permission can still read and react, but the message input is hidden. Useful for announcement-style channels. (#5231)
18+
- **`Read-Only Override` role permission** — grants specific roles the ability to post in read-only channels.
19+
- **Server-relayed mic illumination** — the speaking indicator now reflects what the server actually received rather than local mic detection. If your audio isn't making it to the server, the indicator won't light up, giving a more accurate picture of what others are hearing.
20+
21+
### Changed
22+
- **Role permission row highlight** — checking a permission in the role editor now lights up that entire row with an accent background, making it easier to see which permissions are enabled at a glance.
23+
24+
---
25+
1426
## [2.9.7] — 2026-04-09
1527

1628
### Changed

docs/index.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,12 +1415,12 @@ <h2>Download Haven</h2>
14151415
</div>
14161416

14171417
<div class="download-card fade-in">
1418-
<h2>&#x2B21; Haven Server &mdash; v2.9.7</h2>
1418+
<h2>&#x2B21; Haven Server &mdash; v2.9.8</h2>
14191419
<p class="download-version">Latest stable release &middot; Windows, macOS &amp; Linux &middot; ~5 MB</p>
14201420

14211421
<div class="download-btn-group">
1422-
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.7.zip" class="btn btn-primary download-main">
1423-
<span class="icon">&#x2B07;</span> Download v2.9.7 (.zip)
1422+
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.8.zip" class="btn btn-primary download-main">
1423+
<span class="icon">&#x2B07;</span> Download v2.9.8 (.zip)
14241424
</a>
14251425
<div class="download-alt-links">
14261426
<a href="https://github.com/ancsemi/Haven" target="_blank">&#9965; View on GitHub</a>
@@ -1437,7 +1437,11 @@ <h2>&#x2B21; Haven Server &mdash; v2.9.7</h2>
14371437
<div class="version-list">
14381438
<div class="version-list-inner">
14391439
<div class="version-item">
1440-
<div><span class="v-name">v2.9.7</span><span class="v-tag latest">Latest</span></div>
1440+
<div><span class="v-name">v2.9.8</span><span class="v-tag latest">Latest</span></div>
1441+
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.8.zip">Download &rarr;</a>
1442+
</div>
1443+
<div class="version-item">
1444+
<div><span class="v-name">v2.9.7</span> &mdash; Open-source STUN servers, STUN_URLS env var</div>
14411445
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.9.7.zip">Download &rarr;</a>
14421446
</div>
14431447
<div class="version-item">

donors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"Orange Lantern",
2626
"lataxd9",
2727
"HoppyGamers",
28-
"deNully"
28+
"deNully",
29+
"Morgan"
2930
]
3031
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "haven",
3-
"version": "2.9.7",
3+
"version": "2.9.8",
44
"description": "Haven — self-hosted private chat for your server, your rules",
55
"license": "AGPL-3.0",
66
"main": "server.js",

public/app.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,22 @@ <h5 class="settings-section-title">😀 Reaction Size</h5>
11351135
</div>
11361136
</div>
11371137

1138+
<!-- Role Color Display -->
1139+
<div class="settings-section" id="section-role-display" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
1140+
<h5 class="settings-section-title">🎨 Role Display</h5>
1141+
<div class="density-picker" id="role-display-picker">
1142+
<button type="button" class="density-btn active" data-roledisplay="colored-name" title="Color the username with the role color">
1143+
<span class="density-icon" style="color:#5865f2;font-weight:bold">A</span>
1144+
<span class="density-label">Colored Name</span>
1145+
</button>
1146+
<button type="button" class="density-btn" data-roledisplay="dot" title="Show a colored dot next to the name (legacy)">
1147+
<span class="density-icon"></span>
1148+
<span class="density-label">Dot</span>
1149+
</button>
1150+
</div>
1151+
<small class="settings-hint" style="margin-top:4px;display:block">How role colors are shown next to usernames in chat and the member list.</small>
1152+
</div>
1153+
11381154
<!-- Image Display Mode -->
11391155
<div class="settings-section" id="section-density" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
11401156
<h5 class="settings-section-title">🖼️ <span data-i18n="settings.image_display.title">Image Display</span></h5>
@@ -1439,6 +1455,11 @@ <h5 class="settings-section-subtitle">🏠 <span data-i18n="settings.admin.brand
14391455
</select>
14401456
</label>
14411457
<small class="settings-hint" style="margin-top:2px;display:block" data-i18n="settings.admin.default_theme_hint">New users see this theme on first visit. They can still pick their own.</small>
1458+
<label class="select-row" style="margin-top:12px">
1459+
<span>Welcome Message</span>
1460+
<input type="text" id="welcome-message-input" maxlength="500" class="settings-text-input" style="max-width:280px" placeholder="e.g. Welcome {user}!">
1461+
</label>
1462+
<small class="settings-hint" style="margin-top:2px;display:block">Shown when a new user joins a channel. Use <code>{user}</code> for the username. Leave blank to disable.</small>
14421463
</div>
14431464

14441465
<div class="admin-settings" id="section-members" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
@@ -2118,6 +2139,10 @@ <h3>⚙ <span data-i18n="modals.manage_servers.title">Manage Servers</span></h3>
21182139
<span class="cfn-label">🖼️ <span data-i18n="channel_functions.media">Media</span></span>
21192140
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
21202141
</div>
2142+
<div class="cfn-row" data-fn="read-only" data-i18n-title="channel_functions.read_only_hint" title="Make this channel read-only — only users with the Read-Only Override permission can post">
2143+
<span class="cfn-label">🔒 <span data-i18n="channel_functions.read_only">Read Only</span></span>
2144+
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
2145+
</div>
21212146
<div class="cfn-divider"></div>
21222147
<div class="cfn-row" data-fn="slow-mode" data-i18n-title="channel_functions.slow_mode_hint" title="Require a cooldown between messages (0 = off)">
21232148
<span class="cfn-label">🐢 <span data-i18n="channel_functions.slow_mode">Slow Mode</span></span>

public/css/style.css

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4170,6 +4170,19 @@ select.cfn-select.cfn-input {
41704170
text-align: center;
41714171
}
41724172

4173+
.welcome-message {
4174+
padding: 10px 16px;
4175+
margin: 8px auto;
4176+
max-width: 480px;
4177+
font-size: 14px;
4178+
font-weight: 600;
4179+
color: var(--accent, #5865f2);
4180+
text-align: center;
4181+
background: var(--bg-secondary, #2b2d31);
4182+
border: 1px solid var(--border-light, #3a3c42);
4183+
border-radius: 8px;
4184+
}
4185+
41734186
.typing-indicator {
41744187
padding: 0 20px;
41754188
height: 20px;
@@ -10950,6 +10963,15 @@ ul.chat-list { list-style-type: disc; }
1095010963
}
1095110964
.role-detail-form .toggle-row {
1095210965
font-size: 13px;
10966+
padding: 5px 8px;
10967+
border-radius: var(--radius-sm, 4px);
10968+
transition: background 0.15s;
10969+
}
10970+
.role-detail-form .toggle-row:hover {
10971+
background: rgba(255,255,255,0.04);
10972+
}
10973+
.role-detail-form .toggle-row:has(input[type="checkbox"]:checked) {
10974+
background: color-mix(in srgb, var(--accent, #6b4fdb) 12%, transparent);
1095310975
}
1095410976

1095510977
/* ── Role preview in admin settings ──────────────────── */
@@ -12914,8 +12936,17 @@ ul.chat-list { list-style-type: disc; }
1291412936
align-items: center;
1291512937
gap: 6px;
1291612938
font-size: 0.8rem;
12917-
padding: 3px 0;
12939+
padding: 3px 6px;
1291812940
color: var(--text-secondary, #ccc);
12941+
border-radius: var(--radius-sm, 4px);
12942+
transition: background 0.15s;
12943+
}
12944+
.rac-perm-item:hover {
12945+
background: rgba(255,255,255,0.04);
12946+
}
12947+
.rac-perm-item.checked,
12948+
.rac-perm-item:has(input[type="checkbox"]:checked) {
12949+
background: color-mix(in srgb, var(--accent, #6b4fdb) 12%, transparent);
1291912950
}
1292012951
.rac-perm-item input[type="checkbox"] {
1292112952
accent-color: var(--accent, #6b4fdb);

public/js/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class HavenApp {
176176
this._setupFontSizePicker();
177177
this._setupEmojiSizePicker();
178178
this._setupImageModePicker();
179+
this._setupRoleDisplayPicker();
179180
this._setupLightbox();
180181
this._setupOnlineOverlay();
181182
this._setupModalExpand();

public/js/modules/app-admin.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const ALL_PERMS = [
55
'rename_channel', 'rename_sub_channel', 'set_channel_topic', 'manage_sub_channels',
66
'create_channel', 'create_temp_channel', 'upload_files', 'use_voice', 'use_tts', 'manage_webhooks', 'mention_everyone', 'view_history',
77
'view_all_members', 'view_channel_members', 'manage_emojis', 'manage_soundboard', 'manage_music_queue', 'promote_user', 'transfer_admin',
8-
'manage_roles', 'manage_server', 'delete_channel'
8+
'manage_roles', 'manage_server', 'delete_channel', 'read_only_override'
99
];
1010
//Similarly flavored solution to perm labels
1111
const PERM_LABELS = {
@@ -39,7 +39,8 @@ const PERM_LABELS = {
3939
get transfer_admin() { return t('permissions.transfer_admin'); },
4040
get manage_roles() { return t('permissions.manage_roles'); },
4141
get manage_server() { return t('permissions.manage_server'); },
42-
get delete_channel() { return t('permissions.delete_channel'); }
42+
get delete_channel() { return t('permissions.delete_channel'); },
43+
get read_only_override() { return t('permissions.read_only_override'); }
4344
};
4445

4546
export default {
@@ -318,6 +319,10 @@ _applyServerSettings() {
318319
if (titleInput && this.serverSettings.server_title !== undefined) {
319320
titleInput.value = this.serverSettings.server_title || '';
320321
}
322+
const welcomeInput = document.getElementById('welcome-message-input');
323+
if (welcomeInput) {
324+
welcomeInput.value = this.serverSettings.welcome_message || '';
325+
}
321326
const cleanupEnabled = document.getElementById('cleanup-enabled');
322327
if (cleanupEnabled) {
323328
cleanupEnabled.checked = this.serverSettings.cleanup_enabled === 'true';
@@ -442,6 +447,7 @@ _snapshotAdminSettings() {
442447
this._adminSnapshot = {
443448
server_name: this.serverSettings.server_name || 'HAVEN',
444449
server_title: this.serverSettings.server_title || '',
450+
welcome_message: this.serverSettings.welcome_message || '',
445451
member_visibility: this.serverSettings.member_visibility || 'online',
446452
cleanup_enabled: this.serverSettings.cleanup_enabled || 'false',
447453
cleanup_max_age_days: this.serverSettings.cleanup_max_age_days || '0',
@@ -483,6 +489,12 @@ _saveAdminSettings() {
483489
changed = true;
484490
}
485491

492+
const welcomeMsg = document.getElementById('welcome-message-input')?.value.trim() || '';
493+
if (welcomeMsg !== (snap.welcome_message || '')) {
494+
this.socket.emit('update-server-setting', { key: 'welcome_message', value: welcomeMsg });
495+
changed = true;
496+
}
497+
486498
const vis = document.getElementById('member-visibility-select')?.value;
487499
if (vis && vis !== snap.member_visibility) {
488500
this.socket.emit('update-server-setting', { key: 'member_visibility', value: vis });

public/js/modules/app-channels.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ async switchChannel(code) {
6363
}
6464
document.getElementById('search-toggle-btn').style.display = '';
6565
document.getElementById('pinned-toggle-btn').style.display = '';
66+
// Auto-close pinned panel on channel switch so stale pins don't linger
67+
document.getElementById('pinned-panel').style.display = 'none';
6668

6769
// Show "Select messages" button for admins/mods on non-DM channels
6870
const moveSelectBtn = document.getElementById('move-select-btn');
@@ -80,7 +82,9 @@ async switchChannel(code) {
8082
const msgInputArea = document.getElementById('message-input-area');
8183
const _textOff = channel && channel.text_enabled === 0;
8284
const _mediaOff = channel && channel.media_enabled === 0;
83-
if (msgInputArea) msgInputArea.style.display = (_textOff && _mediaOff) ? 'none' : '';
85+
// Read-only: hide input unless user is admin or has read_only_override permission
86+
const _isReadOnly = channel && channel.read_only === 1 && !this.user?.isAdmin && !this._hasPerm('read_only_override');
87+
if (msgInputArea) msgInputArea.style.display = (_isReadOnly || (_textOff && _mediaOff)) ? 'none' : '';
8488
// Text-only elements
8589
const _msgInput = document.getElementById('message-input');
8690
const _sendBtn = document.getElementById('send-btn');
@@ -337,6 +341,9 @@ _updateChannelFunctionsPanel(ch) {
337341
this._setCfnBadge('streams', ch.streams_enabled !== 0, ch.streams_enabled !== 0 ? 'ON' : 'OFF');
338342
this._setCfnBadge('music', ch.music_enabled !== 0, ch.music_enabled !== 0 ? 'ON' : 'OFF');
339343
this._setCfnBadge('media', ch.media_enabled !== 0, ch.media_enabled !== 0 ? 'ON' : 'OFF');
344+
// Read-only toggle
345+
const isReadOnly = ch.read_only === 1;
346+
this._setCfnBadge('read-only', isReadOnly, isReadOnly ? 'ON' : 'OFF');
340347
const interval = ch.slow_mode_interval || 0;
341348
this._setCfnBadge('slow-mode', interval > 0, interval > 0 ? `${interval}s` : 'OFF');
342349
this._setCfnBadge('cleanup-exempt', ch.cleanup_exempt === 1, ch.cleanup_exempt === 1 ? 'ON' : 'OFF');

public/js/modules/app-media.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,31 @@ _applyImageMode(mode) {
15711571
document.body.classList.toggle('image-mode-full', mode === 'full');
15721572
},
15731573

1574+
// ── Role Display Picker ──
1575+
1576+
_setupRoleDisplayPicker() {
1577+
const picker = document.getElementById('role-display-picker');
1578+
if (!picker) return;
1579+
1580+
const saved = localStorage.getItem('haven-role-display') || 'colored-name';
1581+
document.documentElement.dataset.roleDisplay = saved;
1582+
picker.querySelectorAll('[data-roledisplay]').forEach(btn => {
1583+
btn.classList.toggle('active', btn.dataset.roledisplay === saved);
1584+
});
1585+
1586+
picker.addEventListener('click', (e) => {
1587+
const btn = e.target.closest('[data-roledisplay]');
1588+
if (!btn) return;
1589+
const mode = btn.dataset.roledisplay;
1590+
document.documentElement.dataset.roleDisplay = mode;
1591+
localStorage.setItem('haven-role-display', mode);
1592+
picker.querySelectorAll('[data-roledisplay]').forEach(b => b.classList.remove('active'));
1593+
btn.classList.add('active');
1594+
// Re-render member list to reflect the change
1595+
if (this._updateUsers) this._updateUsers();
1596+
});
1597+
},
1598+
15741599
// ── Image Lightbox ──
15751600

15761601
_setupLightbox() {

0 commit comments

Comments
 (0)