diff --git a/README.md b/README.md index b94924b..a8b95bb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ CyberChat is a framework-free PHP, SQLite, JavaScript, and HTML5 chat applicatio - Stripe Checkout, Billing Portal, webhook synchronization, and cancellation - Configurable plans, prices, Stripe Price IDs, limits, and feature assignments - Subscription display switch that hides new sales while preserving billing management for existing customers -- Private groups with plan-based member limits, including the owner +- Globally configurable private groups with plan-based member limits, including the owner - Premium-configurable recording presence for both sending and viewing - Premium-configurable automatic voice sending - Tier-configurable automatic playback of newly received voice clips @@ -77,6 +77,8 @@ Each tier can independently configure its price, currency, billing interval, Str Administrators can assign a tier directly from **Admin > Users > Edit > Tier Override**. The override grants that tier's features without changing Stripe billing data and remains in effect until it is changed back to **Follow Stripe / billing status**. +Private groups can be enabled or disabled globally under **Admin > Plans & Platform > Group Availability**. Disabling groups hides their navigation and channels and blocks group API access without deleting existing groups or messages. + ## Mailgun Configure these fields under **Admin > Plans & Platform**: diff --git a/api/config.php b/api/config.php index cd665ef..6d5f2bc 100644 --- a/api/config.php +++ b/api/config.php @@ -19,5 +19,6 @@ jsonResponse([ 'max_upload_bytes' => $config['voice']['max_upload_bytes'] ?? 12582912, 'auto_play_default' => $config['voice']['auto_play_default'] ?? true, ], + 'groups' => ['enabled' => $config['groups']['enabled'] ?? true], 'subscriptions' => ['enabled' => $config['subscriptions']['enabled'] ?? true], ]); diff --git a/api/groups.php b/api/groups.php index 8e89c77..04bfdf3 100644 --- a/api/groups.php +++ b/api/groups.php @@ -6,9 +6,20 @@ sendCorsHeaders(); $user = authRequired(); $action = $_POST['action'] ?? $_GET['action'] ?? 'list'; +if (!groupsEnabled()) { + if ($action === 'list') { + jsonResponse(['groups' => [], 'member_limit' => 0, 'enabled' => false]); + } + jsonResponse(['error' => 'Private groups are currently disabled', 'groups_enabled' => false], 403); +} + switch ($action) { case 'list': - jsonResponse(['groups' => userGroups((int)$user['id']), 'member_limit' => (int)featureValue($user, 'group_member_limit', 2)]); + jsonResponse([ + 'groups' => userGroups((int)$user['id']), + 'member_limit' => (int)featureValue($user, 'group_member_limit', 2), + 'enabled' => true, + ]); case 'create': $name = trim((string)($_POST['name'] ?? '')); diff --git a/api/messages.php b/api/messages.php index 2f57490..48766ab 100644 --- a/api/messages.php +++ b/api/messages.php @@ -33,6 +33,9 @@ function messagePayload(array $row): array { function requestedGroup(array $user): ?int { $groupId = normalizeGroupId($_POST['group_id'] ?? $_GET['group_id'] ?? null); + if ($groupId !== null && !groupsEnabled()) { + jsonResponse(['error' => 'Private groups are currently disabled', 'groups_enabled' => false], 403); + } requireGroupAccess($user, $groupId); return $groupId; } @@ -171,7 +174,8 @@ function pollMessages(): never { 'messages' => array_map('messagePayload', $rows), 'online' => (int)$onlineStmt->fetchColumn(), 'recording' => $recording, - 'groups' => userGroups((int)$user['id']), + 'groups' => groupsEnabled() ? userGroups((int)$user['id']) : [], + 'groups_enabled' => groupsEnabled(), 'group_id' => $groupId, 'server_time' => time(), ]); diff --git a/assets/js/cyberchat-v4.js b/assets/js/cyberchat-v4.js index e2b8608..9ee01fa 100644 --- a/assets/js/cyberchat-v4.js +++ b/assets/js/cyberchat-v4.js @@ -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 = ``; $$('[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 = `
+ ${groupsAvailable() ? `` : ''} ${voice ? `Up to ${limit} people per group, including you.