Roles no longer carry a list of channels to grant or revoke. Instead, each channel says which roles open it: right-click the channel, Channel Functions, Required roles. Anyone who holds those roles is put in the channel the moment they get them and taken out the moment they lose them.
+
Nothing you had set up was lost. Any channel a role used to grant has been converted into Required roles on that channel, so the same people still get in. People you added to a channel by hand keep their place.
+
Only admins see this, and only once.
+
+
+
+
+
+
diff --git a/public/js/modules/app-socket.js b/public/js/modules/app-socket.js
index 0cb9f411..5c209103 100644
--- a/public/js/modules/app-socket.js
+++ b/public/js/modules/app-socket.js
@@ -2408,6 +2408,20 @@ _setupSocketListeners() {
// No GIF provider on this server: the button would only open an empty
// picker, so it goes (#5654).
document.documentElement.toggleAttribute('data-no-gif', settings && settings.gif_search_available === 'false');
+ // TEMPORARY (#5649): a one-time notice to admins that channel access moved
+ // from roles to the channel's Required roles. Remove after the 4.8.x cycle.
+ if (settings && settings.role_gate_notice === '1' && !this._roleGateNoticeShown &&
+ (this.user?.isAdmin || this._hasPerm?.('manage_roles') || this._hasPerm?.('manage_server'))) {
+ this._roleGateNoticeShown = true;
+ const modal = document.getElementById('role-gate-notice-modal');
+ if (modal) {
+ modal.style.display = 'flex';
+ document.getElementById('role-gate-notice-ok')?.addEventListener('click', () => {
+ modal.style.display = 'none';
+ this.socket.emit('update-server-setting', { key: 'role_gate_notice', value: '0' });
+ }, { once: true });
+ }
+ }
this._applyServerSettings();
this._renderChannelTemplates();
this._maybeShowSetupWizard();
diff --git a/public/locales/en.json b/public/locales/en.json
index f7705f9c..0bba7bde 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -2785,6 +2785,13 @@
"title": "Rename Channel",
"prompt": "Rename #{{name}}:\nEnter new name:"
},
+ "role_gate_notice": {
+ "title": "Channel access works differently now",
+ "p1": "Roles no longer carry a list of channels to grant or revoke. Instead, each channel says which roles open it: right-click the channel, Channel Functions, Required roles. Anyone who holds those roles is put in the channel the moment they get them and taken out the moment they lose them.",
+ "p2": "Nothing you had set up was lost. Any channel a role used to grant has been converted into Required roles on that channel, so the same people still get in. People you added to a channel by hand keep their place.",
+ "p3": "Only admins see this, and only once.",
+ "ok": "Got it"
+ },
"schedule": {
"title": "Send later",
"hint": "The message waits on the server and goes out at the time you pick, whether or not you are online. Not available in direct messages.",
diff --git a/src/database.js b/src/database.js
index cb8cbc77..32fe6e9d 100644
--- a/src/database.js
+++ b/src/database.js
@@ -1464,6 +1464,11 @@ function initDatabase() {
}
db.prepare('UPDATE roles SET link_channel_access = 0').run();
db.prepare("INSERT OR REPLACE INTO server_settings (key, value) VALUES ('role_links_migrated', '1')").run();
+ // An existing server gets a one-time notice for admins about the change
+ // in how channel access works. Nothing to explain on a fresh install.
+ // TEMPORARY: remove this flag and the notice modal after the 4.8.x cycle.
+ const existing = db.prepare('SELECT COUNT(*) AS c FROM channels WHERE is_dm = 0').get().c;
+ if (existing) db.prepare("INSERT OR REPLACE INTO server_settings (key, value) VALUES ('role_gate_notice', '1')").run();
if (converted) console.log(`[migration] Role channel access lists became Required roles on ${converted} channel(s) (#5649)`);
}
} catch (err) {
diff --git a/src/socketHandlers/admin.js b/src/socketHandlers/admin.js
index 69dddf21..0e404a92 100644
--- a/src/socketHandlers/admin.js
+++ b/src/socketHandlers/admin.js
@@ -145,6 +145,7 @@ module.exports = function register(socket, ctx) {
'automod_scan_dms', 'automod_block_ip_urls', 'automod_block_punycode',
'automod_block_obfuscated', 'automod_preview_allowlist_only', 'automod_escalation',
'automod_ban_ip', 'automod_log_channel', 'automod_words',
+ 'role_gate_notice', // TEMPORARY (#5649): one-time admin notice, remove after the 4.8.x cycle
'voice_force_relay',
'media_proxy_enabled', // (v3.43.0) server-side fetch + cache for remote images
'fcm_enabled', // admin gate for Google FCM mobile push; off = FCM sends skipped (web-push unaffected)
@@ -171,6 +172,7 @@ module.exports = function register(socket, ctx) {
if (key === 'automod_link_exempt_level') { const n = parseInt(value); if (isNaN(n) || n < 0 || n > 100) return; }
if (key === 'automod_link_min_account_hours') { const n = parseInt(value); if (isNaN(n) || n < 0 || n > 8760) return; }
if (key === 'automod_log_channel' && value && !/^[a-f0-9]{8}$/i.test(value)) return;
+ if (key === 'role_gate_notice' && !['0', '1'].includes(value)) return;
// Word groups (#5614): stored normalised, so a hand-edited or oversized
// payload never reaches the matcher.
if (key === 'automod_words') {