- Groups are now globally configurable under Admin > Plans & Platform > Group Availability.
When disabled: Group navigation and channel selector are hidden. Group API operations are blocked. Active sessions update automatically. Existing groups and messages remain preserved. Static and browser tests passed with no console errors.
This commit is contained in:
@@ -46,6 +46,29 @@
|
||||
return state.user?.features && Object.prototype.hasOwnProperty.call(state.user.features, key)
|
||||
? state.user.features[key] : fallback;
|
||||
}
|
||||
function groupsAvailable() {
|
||||
return state.config.groups?.enabled !== false;
|
||||
}
|
||||
function applyGroupsAvailability(enabled, groups = []) {
|
||||
const next = enabled !== false;
|
||||
const changed = groupsAvailable() !== next;
|
||||
state.config.groups = { ...(state.config.groups || {}), enabled: next };
|
||||
if (next) {
|
||||
if (changed) syncGroups(groups);
|
||||
} else {
|
||||
state.groups = [];
|
||||
state.groupSeen = {};
|
||||
state.groupUnread = {};
|
||||
state.groupId = null;
|
||||
if (state.view === 'groups') state.view = 'chat';
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
function disableGroups(message = '') {
|
||||
applyGroupsAvailability(false);
|
||||
if (message) toast(message);
|
||||
app();
|
||||
}
|
||||
function toast(message, type = 'error') {
|
||||
const area = $('#cc-toasts');
|
||||
if (!area) return;
|
||||
@@ -238,10 +261,16 @@
|
||||
|
||||
async function enter() {
|
||||
const [groups, billing] = await Promise.all([
|
||||
api('groups.php', null, 'action=list').catch(() => ({ groups: [] })),
|
||||
groupsAvailable()
|
||||
? api('groups.php', null, 'action=list').catch(() => ({ groups: [] }))
|
||||
: Promise.resolve({ groups: [], enabled: false }),
|
||||
api('billing.php', null, 'action=status').catch(() => null)
|
||||
]);
|
||||
state.groups = groups.groups || [];
|
||||
state.config.groups = {
|
||||
...(state.config.groups || {}),
|
||||
enabled: groups.enabled !== false && groupsAvailable(),
|
||||
};
|
||||
state.groups = groupsAvailable() ? (groups.groups || []) : [];
|
||||
state.groupSeen = {};
|
||||
state.groupUnread = {};
|
||||
state.groups.forEach(group => { state.groupSeen[group.id] = Number(group.latest_message_id || 0); });
|
||||
@@ -266,8 +295,9 @@
|
||||
}
|
||||
}
|
||||
function app() {
|
||||
if (!groupsAvailable() && state.view === 'groups') state.view = 'chat';
|
||||
$('#cc-body').innerHTML = `<nav class="cc-app-nav">
|
||||
<button data-view="chat">CHAT</button><button data-view="groups">GROUPS</button>
|
||||
<button data-view="chat">CHAT</button>${groupsAvailable() ? '<button data-view="groups">GROUPS</button>' : ''}
|
||||
<button data-view="archive">MY ARCHIVE</button><button data-view="account">ACCOUNT</button>
|
||||
</nav><main class="cc-view" id="cc-view"></main>`;
|
||||
$$('[data-view]').forEach(button => button.addEventListener('click', () => {
|
||||
@@ -281,7 +311,7 @@
|
||||
stopVoice(true);
|
||||
$$('[data-view]').forEach(button => button.classList.toggle('active', button.dataset.view === state.view));
|
||||
if (state.view === 'chat') chat();
|
||||
else if (state.view === 'groups') groupsView();
|
||||
else if (state.view === 'groups' && groupsAvailable()) groupsView();
|
||||
else if (state.view === 'archive') archiveView();
|
||||
else accountView();
|
||||
}
|
||||
@@ -311,7 +341,7 @@
|
||||
$('#cc-view').innerHTML = `<div class="cc-userbar"><div class="cc-userbar-left">
|
||||
<span class="cc-you-label">YOU:</span><span class="cc-you-name" style="color:${esc(state.user.color)};border-color:${esc(state.user.color)}">${esc(state.user.username)}</span>
|
||||
<span class="cc-plan-badge">${esc(state.user.plan.name)}</span></div>
|
||||
<select class="cc-compact-select" id="group-select">${groupOptions()}</select></div>
|
||||
${groupsAvailable() ? `<select class="cc-compact-select" id="group-select">${groupOptions()}</select>` : ''}</div>
|
||||
<div class="cc-recording-indicator" id="recording" hidden></div>
|
||||
<div class="cc-messages" id="messages"><div class="cc-loading-msg"><div class="cc-spin"></div><span>LOADING CHANNEL</span></div></div>
|
||||
${voice ? `<div class="cc-voice-compose"><button class="cc-voice-record" id="record"><span class="cc-voice-dot"></span><span id="record-label">RECORD</span></button>
|
||||
@@ -322,7 +352,7 @@
|
||||
<button class="cc-voice-action send" id="voice-send">SEND</button><button class="cc-voice-action" id="voice-discard">DISCARD</button></div></div>` : ''}
|
||||
<div class="cc-compose"><input class="cc-compose-input" id="message" maxlength="${max}" placeholder="transmit message...">
|
||||
<span class="cc-compose-counter" id="count">${max}</span><button class="cc-compose-send" id="send">SEND</button></div>`;
|
||||
$('#group-select').addEventListener('change', event => {
|
||||
$('#group-select')?.addEventListener('change', event => {
|
||||
state.groupId = event.target.value ? Number(event.target.value) : null;
|
||||
if (state.groupId !== null) {
|
||||
state.groupUnread[state.groupId] = false;
|
||||
@@ -355,6 +385,10 @@
|
||||
if (!message) return;
|
||||
input.value = '';
|
||||
const result = await api('messages.php', form({ action: 'send', message, group_id: state.groupId || '' }));
|
||||
if (result.groups_enabled === false) {
|
||||
input.value = message;
|
||||
return disableGroups(result.error);
|
||||
}
|
||||
if (result.error) { input.value = message; return toast(result.error); }
|
||||
appendMessage(result.message);
|
||||
state.lastId = Math.max(state.lastId, result.message.id);
|
||||
@@ -381,9 +415,15 @@
|
||||
const result = await api('messages.php', null, query.toString()).catch(() => null);
|
||||
if (generation !== state.pollGeneration || Number(state.groupId || 0) !== Number(requestedGroup || 0)) return;
|
||||
if (result && !result.error && Number(result.group_id || 0) === Number(requestedGroup || 0)) {
|
||||
const availabilityChanged = applyGroupsAvailability(result.groups_enabled !== false, result.groups || []);
|
||||
if (availabilityChanged) {
|
||||
toast(groupsAvailable() ? 'Private groups are now available' : 'Private groups have been disabled', 'success');
|
||||
app();
|
||||
return;
|
||||
}
|
||||
$('#cc-conn-dot').className = 'cc-conn-dot online';
|
||||
$('#cc-online-count').textContent = result.online;
|
||||
syncGroups(result.groups, true);
|
||||
if (groupsAvailable()) syncGroups(result.groups, true);
|
||||
if (requestedSince === 0) renderMessages(result.messages || []);
|
||||
else (result.messages || []).forEach(message => appendMessage(message, true));
|
||||
if (result.messages?.length) state.lastId = result.messages[result.messages.length - 1].id;
|
||||
@@ -408,6 +448,10 @@
|
||||
async function recoverGroupAccess(groupId, generation) {
|
||||
const result = await api('groups.php', null, 'action=list').catch(() => null);
|
||||
if (!result || result.error || generation !== state.pollGeneration) return;
|
||||
if (result.enabled === false) {
|
||||
disableGroups('Private groups have been disabled');
|
||||
return;
|
||||
}
|
||||
syncGroups(result.groups || []);
|
||||
if (!state.groups.some(group => Number(group.id) === Number(groupId))) {
|
||||
state.groupId = null;
|
||||
@@ -560,6 +604,7 @@
|
||||
body.append('voice', state.voice.blob, 'voice.webm');
|
||||
const result = await api('messages.php', { method: 'POST', body });
|
||||
state.voice.sending = false;
|
||||
if (result.groups_enabled === false) return disableGroups(result.error);
|
||||
if (result.error) return toast(result.error);
|
||||
appendMessage(result.message);
|
||||
state.lastId = Math.max(state.lastId, result.message.id);
|
||||
@@ -567,6 +612,11 @@
|
||||
}
|
||||
|
||||
function groupsView() {
|
||||
if (!groupsAvailable()) {
|
||||
state.view = 'chat';
|
||||
renderView();
|
||||
return;
|
||||
}
|
||||
const limit = Number(feature('group_member_limit', 2));
|
||||
$('#cc-view').innerHTML = `<div class="cc-page"><div class="cc-page-head"><div><h2>GROUPS</h2>
|
||||
<p>Up to ${limit} people per group, including you.</p></div></div>
|
||||
@@ -592,6 +642,10 @@
|
||||
}));
|
||||
api('groups.php', null, 'action=list').then(result => {
|
||||
if (result.error || state.view !== 'groups') return;
|
||||
if (result.enabled === false) {
|
||||
disableGroups('Private groups have been disabled');
|
||||
return;
|
||||
}
|
||||
const before = JSON.stringify(state.groups);
|
||||
syncGroups(result.groups || []);
|
||||
if (JSON.stringify(state.groups) !== before) groupsView();
|
||||
@@ -608,18 +662,21 @@
|
||||
}
|
||||
async function createGroup() {
|
||||
const result = await api('groups.php', form({ action: 'create', name: $('#group-name').value.trim(), members: $('#group-members').value.trim() }));
|
||||
if (result.groups_enabled === false) return disableGroups(result.error);
|
||||
if (result.error) return toast(result.error);
|
||||
state.groups = result.groups;
|
||||
groupsView();
|
||||
}
|
||||
async function addMember(groupId) {
|
||||
const result = await api('groups.php', form({ action: 'add_member', group_id: groupId, username: $('#add-' + groupId).value.trim() }));
|
||||
if (result.groups_enabled === false) return disableGroups(result.error);
|
||||
if (result.error) return toast(result.error);
|
||||
state.groups = result.groups;
|
||||
groupsView();
|
||||
}
|
||||
async function removeMember(groupId, userId) {
|
||||
const result = await api('groups.php', form({ action: 'remove_member', group_id: groupId, user_id: userId }));
|
||||
if (result.groups_enabled === false) return disableGroups(result.error);
|
||||
if (result.error) return toast(result.error);
|
||||
state.groups = result.groups;
|
||||
groupsView();
|
||||
@@ -627,6 +684,7 @@
|
||||
async function deleteGroup(groupId) {
|
||||
if (!confirm('Delete this group and its messages?')) return;
|
||||
const result = await api('groups.php', form({ action: 'delete', group_id: groupId }));
|
||||
if (result.groups_enabled === false) return disableGroups(result.error);
|
||||
if (result.error) return toast(result.error);
|
||||
state.groups = result.groups;
|
||||
if (state.groupId === groupId) state.groupId = null;
|
||||
|
||||
Reference in New Issue
Block a user