- 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:
@@ -10,7 +10,7 @@ CyberChat is a framework-free PHP, SQLite, JavaScript, and HTML5 chat applicatio
|
|||||||
- Stripe Checkout, Billing Portal, webhook synchronization, and cancellation
|
- Stripe Checkout, Billing Portal, webhook synchronization, and cancellation
|
||||||
- Configurable plans, prices, Stripe Price IDs, limits, and feature assignments
|
- Configurable plans, prices, Stripe Price IDs, limits, and feature assignments
|
||||||
- Subscription display switch that hides new sales while preserving billing management for existing customers
|
- 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 recording presence for both sending and viewing
|
||||||
- Premium-configurable automatic voice sending
|
- Premium-configurable automatic voice sending
|
||||||
- Tier-configurable automatic playback of newly received voice clips
|
- 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**.
|
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
|
## Mailgun
|
||||||
|
|
||||||
Configure these fields under **Admin > Plans & Platform**:
|
Configure these fields under **Admin > Plans & Platform**:
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ jsonResponse([
|
|||||||
'max_upload_bytes' => $config['voice']['max_upload_bytes'] ?? 12582912,
|
'max_upload_bytes' => $config['voice']['max_upload_bytes'] ?? 12582912,
|
||||||
'auto_play_default' => $config['voice']['auto_play_default'] ?? true,
|
'auto_play_default' => $config['voice']['auto_play_default'] ?? true,
|
||||||
],
|
],
|
||||||
|
'groups' => ['enabled' => $config['groups']['enabled'] ?? true],
|
||||||
'subscriptions' => ['enabled' => $config['subscriptions']['enabled'] ?? true],
|
'subscriptions' => ['enabled' => $config['subscriptions']['enabled'] ?? true],
|
||||||
]);
|
]);
|
||||||
|
|||||||
+12
-1
@@ -6,9 +6,20 @@ sendCorsHeaders();
|
|||||||
$user = authRequired();
|
$user = authRequired();
|
||||||
$action = $_POST['action'] ?? $_GET['action'] ?? 'list';
|
$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) {
|
switch ($action) {
|
||||||
case 'list':
|
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':
|
case 'create':
|
||||||
$name = trim((string)($_POST['name'] ?? ''));
|
$name = trim((string)($_POST['name'] ?? ''));
|
||||||
|
|||||||
+5
-1
@@ -33,6 +33,9 @@ function messagePayload(array $row): array {
|
|||||||
|
|
||||||
function requestedGroup(array $user): ?int {
|
function requestedGroup(array $user): ?int {
|
||||||
$groupId = normalizeGroupId($_POST['group_id'] ?? $_GET['group_id'] ?? null);
|
$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);
|
requireGroupAccess($user, $groupId);
|
||||||
return $groupId;
|
return $groupId;
|
||||||
}
|
}
|
||||||
@@ -171,7 +174,8 @@ function pollMessages(): never {
|
|||||||
'messages' => array_map('messagePayload', $rows),
|
'messages' => array_map('messagePayload', $rows),
|
||||||
'online' => (int)$onlineStmt->fetchColumn(),
|
'online' => (int)$onlineStmt->fetchColumn(),
|
||||||
'recording' => $recording,
|
'recording' => $recording,
|
||||||
'groups' => userGroups((int)$user['id']),
|
'groups' => groupsEnabled() ? userGroups((int)$user['id']) : [],
|
||||||
|
'groups_enabled' => groupsEnabled(),
|
||||||
'group_id' => $groupId,
|
'group_id' => $groupId,
|
||||||
'server_time' => time(),
|
'server_time' => time(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -46,6 +46,29 @@
|
|||||||
return state.user?.features && Object.prototype.hasOwnProperty.call(state.user.features, key)
|
return state.user?.features && Object.prototype.hasOwnProperty.call(state.user.features, key)
|
||||||
? state.user.features[key] : fallback;
|
? 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') {
|
function toast(message, type = 'error') {
|
||||||
const area = $('#cc-toasts');
|
const area = $('#cc-toasts');
|
||||||
if (!area) return;
|
if (!area) return;
|
||||||
@@ -238,10 +261,16 @@
|
|||||||
|
|
||||||
async function enter() {
|
async function enter() {
|
||||||
const [groups, billing] = await Promise.all([
|
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)
|
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.groupSeen = {};
|
||||||
state.groupUnread = {};
|
state.groupUnread = {};
|
||||||
state.groups.forEach(group => { state.groupSeen[group.id] = Number(group.latest_message_id || 0); });
|
state.groups.forEach(group => { state.groupSeen[group.id] = Number(group.latest_message_id || 0); });
|
||||||
@@ -266,8 +295,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function app() {
|
function app() {
|
||||||
|
if (!groupsAvailable() && state.view === 'groups') state.view = 'chat';
|
||||||
$('#cc-body').innerHTML = `<nav class="cc-app-nav">
|
$('#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>
|
<button data-view="archive">MY ARCHIVE</button><button data-view="account">ACCOUNT</button>
|
||||||
</nav><main class="cc-view" id="cc-view"></main>`;
|
</nav><main class="cc-view" id="cc-view"></main>`;
|
||||||
$$('[data-view]').forEach(button => button.addEventListener('click', () => {
|
$$('[data-view]').forEach(button => button.addEventListener('click', () => {
|
||||||
@@ -281,7 +311,7 @@
|
|||||||
stopVoice(true);
|
stopVoice(true);
|
||||||
$$('[data-view]').forEach(button => button.classList.toggle('active', button.dataset.view === state.view));
|
$$('[data-view]').forEach(button => button.classList.toggle('active', button.dataset.view === state.view));
|
||||||
if (state.view === 'chat') chat();
|
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 if (state.view === 'archive') archiveView();
|
||||||
else accountView();
|
else accountView();
|
||||||
}
|
}
|
||||||
@@ -311,7 +341,7 @@
|
|||||||
$('#cc-view').innerHTML = `<div class="cc-userbar"><div class="cc-userbar-left">
|
$('#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-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>
|
<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-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>
|
<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>
|
${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>` : ''}
|
<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...">
|
<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>`;
|
<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;
|
state.groupId = event.target.value ? Number(event.target.value) : null;
|
||||||
if (state.groupId !== null) {
|
if (state.groupId !== null) {
|
||||||
state.groupUnread[state.groupId] = false;
|
state.groupUnread[state.groupId] = false;
|
||||||
@@ -355,6 +385,10 @@
|
|||||||
if (!message) return;
|
if (!message) return;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
const result = await api('messages.php', form({ action: 'send', message, group_id: state.groupId || '' }));
|
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); }
|
if (result.error) { input.value = message; return toast(result.error); }
|
||||||
appendMessage(result.message);
|
appendMessage(result.message);
|
||||||
state.lastId = Math.max(state.lastId, result.message.id);
|
state.lastId = Math.max(state.lastId, result.message.id);
|
||||||
@@ -381,9 +415,15 @@
|
|||||||
const result = await api('messages.php', null, query.toString()).catch(() => null);
|
const result = await api('messages.php', null, query.toString()).catch(() => null);
|
||||||
if (generation !== state.pollGeneration || Number(state.groupId || 0) !== Number(requestedGroup || 0)) return;
|
if (generation !== state.pollGeneration || Number(state.groupId || 0) !== Number(requestedGroup || 0)) return;
|
||||||
if (result && !result.error && Number(result.group_id || 0) === Number(requestedGroup || 0)) {
|
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-conn-dot').className = 'cc-conn-dot online';
|
||||||
$('#cc-online-count').textContent = result.online;
|
$('#cc-online-count').textContent = result.online;
|
||||||
syncGroups(result.groups, true);
|
if (groupsAvailable()) syncGroups(result.groups, true);
|
||||||
if (requestedSince === 0) renderMessages(result.messages || []);
|
if (requestedSince === 0) renderMessages(result.messages || []);
|
||||||
else (result.messages || []).forEach(message => appendMessage(message, true));
|
else (result.messages || []).forEach(message => appendMessage(message, true));
|
||||||
if (result.messages?.length) state.lastId = result.messages[result.messages.length - 1].id;
|
if (result.messages?.length) state.lastId = result.messages[result.messages.length - 1].id;
|
||||||
@@ -408,6 +448,10 @@
|
|||||||
async function recoverGroupAccess(groupId, generation) {
|
async function recoverGroupAccess(groupId, generation) {
|
||||||
const result = await api('groups.php', null, 'action=list').catch(() => null);
|
const result = await api('groups.php', null, 'action=list').catch(() => null);
|
||||||
if (!result || result.error || generation !== state.pollGeneration) return;
|
if (!result || result.error || generation !== state.pollGeneration) return;
|
||||||
|
if (result.enabled === false) {
|
||||||
|
disableGroups('Private groups have been disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
syncGroups(result.groups || []);
|
syncGroups(result.groups || []);
|
||||||
if (!state.groups.some(group => Number(group.id) === Number(groupId))) {
|
if (!state.groups.some(group => Number(group.id) === Number(groupId))) {
|
||||||
state.groupId = null;
|
state.groupId = null;
|
||||||
@@ -560,6 +604,7 @@
|
|||||||
body.append('voice', state.voice.blob, 'voice.webm');
|
body.append('voice', state.voice.blob, 'voice.webm');
|
||||||
const result = await api('messages.php', { method: 'POST', body });
|
const result = await api('messages.php', { method: 'POST', body });
|
||||||
state.voice.sending = false;
|
state.voice.sending = false;
|
||||||
|
if (result.groups_enabled === false) return disableGroups(result.error);
|
||||||
if (result.error) return toast(result.error);
|
if (result.error) return toast(result.error);
|
||||||
appendMessage(result.message);
|
appendMessage(result.message);
|
||||||
state.lastId = Math.max(state.lastId, result.message.id);
|
state.lastId = Math.max(state.lastId, result.message.id);
|
||||||
@@ -567,6 +612,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupsView() {
|
function groupsView() {
|
||||||
|
if (!groupsAvailable()) {
|
||||||
|
state.view = 'chat';
|
||||||
|
renderView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const limit = Number(feature('group_member_limit', 2));
|
const limit = Number(feature('group_member_limit', 2));
|
||||||
$('#cc-view').innerHTML = `<div class="cc-page"><div class="cc-page-head"><div><h2>GROUPS</h2>
|
$('#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>
|
<p>Up to ${limit} people per group, including you.</p></div></div>
|
||||||
@@ -592,6 +642,10 @@
|
|||||||
}));
|
}));
|
||||||
api('groups.php', null, 'action=list').then(result => {
|
api('groups.php', null, 'action=list').then(result => {
|
||||||
if (result.error || state.view !== 'groups') return;
|
if (result.error || state.view !== 'groups') return;
|
||||||
|
if (result.enabled === false) {
|
||||||
|
disableGroups('Private groups have been disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const before = JSON.stringify(state.groups);
|
const before = JSON.stringify(state.groups);
|
||||||
syncGroups(result.groups || []);
|
syncGroups(result.groups || []);
|
||||||
if (JSON.stringify(state.groups) !== before) groupsView();
|
if (JSON.stringify(state.groups) !== before) groupsView();
|
||||||
@@ -608,18 +662,21 @@
|
|||||||
}
|
}
|
||||||
async function createGroup() {
|
async function createGroup() {
|
||||||
const result = await api('groups.php', form({ action: 'create', name: $('#group-name').value.trim(), members: $('#group-members').value.trim() }));
|
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);
|
if (result.error) return toast(result.error);
|
||||||
state.groups = result.groups;
|
state.groups = result.groups;
|
||||||
groupsView();
|
groupsView();
|
||||||
}
|
}
|
||||||
async function addMember(groupId) {
|
async function addMember(groupId) {
|
||||||
const result = await api('groups.php', form({ action: 'add_member', group_id: groupId, username: $('#add-' + groupId).value.trim() }));
|
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);
|
if (result.error) return toast(result.error);
|
||||||
state.groups = result.groups;
|
state.groups = result.groups;
|
||||||
groupsView();
|
groupsView();
|
||||||
}
|
}
|
||||||
async function removeMember(groupId, userId) {
|
async function removeMember(groupId, userId) {
|
||||||
const result = await api('groups.php', form({ action: 'remove_member', group_id: groupId, user_id: 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);
|
if (result.error) return toast(result.error);
|
||||||
state.groups = result.groups;
|
state.groups = result.groups;
|
||||||
groupsView();
|
groupsView();
|
||||||
@@ -627,6 +684,7 @@
|
|||||||
async function deleteGroup(groupId) {
|
async function deleteGroup(groupId) {
|
||||||
if (!confirm('Delete this group and its messages?')) return;
|
if (!confirm('Delete this group and its messages?')) return;
|
||||||
const result = await api('groups.php', form({ action: 'delete', group_id: groupId }));
|
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);
|
if (result.error) return toast(result.error);
|
||||||
state.groups = result.groups;
|
state.groups = result.groups;
|
||||||
if (state.groupId === groupId) state.groupId = null;
|
if (state.groupId === groupId) state.groupId = null;
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ function getConfigVal(string $path, mixed $default = null): mixed {
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupsEnabled(): bool {
|
||||||
|
return (bool)getConfigVal('groups.enabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
function setConfigValues(array $changes): void {
|
function setConfigValues(array $changes): void {
|
||||||
$config = getConfig();
|
$config = getConfig();
|
||||||
foreach ($changes as $path => $value) {
|
foreach ($changes as $path => $value) {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
"auto_play_default": true,
|
"auto_play_default": true,
|
||||||
"upload_dir": "uploads/voice"
|
"upload_dir": "uploads/voice"
|
||||||
},
|
},
|
||||||
|
"groups": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"database": {
|
"database": {
|
||||||
"main_db": "db/chat.sqlite",
|
"main_db": "db/chat.sqlite",
|
||||||
"archive_path": "archive/{year}/{month}/{day}.sqlite",
|
"archive_path": "archive/{year}/{month}/{day}.sqlite",
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,7 @@
|
|||||||
<title>CyberChat — Embed Example</title>
|
<title>CyberChat — Embed Example</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260608.2">
|
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260608.3">
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
<span class="kw"><link</span> href=<span class="str">"https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@700;900&display=swap"</span> rel=<span class="str">"stylesheet"</span><span class="kw">></span>
|
<span class="kw"><link</span> href=<span class="str">"https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@700;900&display=swap"</span> rel=<span class="str">"stylesheet"</span><span class="kw">></span>
|
||||||
|
|
||||||
<span class="cm"><!-- 2. CyberChat stylesheet --></span>
|
<span class="cm"><!-- 2. CyberChat stylesheet --></span>
|
||||||
<span class="kw"><link</span> rel=<span class="str">"stylesheet"</span> href=<span class="str">"assets/css/cyberchat.css?v=20260608.2"</span><span class="kw">></span>
|
<span class="kw"><link</span> rel=<span class="str">"stylesheet"</span> href=<span class="str">"assets/css/cyberchat.css?v=20260608.3"</span><span class="kw">></span>
|
||||||
|
|
||||||
<span class="cm"><!-- 3. Container with any dimensions --></span>
|
<span class="cm"><!-- 3. Container with any dimensions --></span>
|
||||||
<span class="kw"><div</span> id=<span class="str">"my-chat"</span> style=<span class="str">"width:100%; height:600px"</span><span class="kw">></div></span>
|
<span class="kw"><div</span> id=<span class="str">"my-chat"</span> style=<span class="str">"width:100%; height:600px"</span><span class="kw">></div></span>
|
||||||
|
|
||||||
<span class="cm"><!-- 4. Script + init --></span>
|
<span class="cm"><!-- 4. Script + init --></span>
|
||||||
<span class="kw"><script</span> src=<span class="str">"assets/js/cyberchat-v4.js?v=20260608.2"</span><span class="kw">></script></span>
|
<span class="kw"><script</span> src=<span class="str">"assets/js/cyberchat-v4.js?v=20260608.3"</span><span class="kw">></script></span>
|
||||||
<span class="kw"><script></span>
|
<span class="kw"><script></span>
|
||||||
window.CYBERCHAT_BASE = <span class="str">'/path/to/cyberchat'</span>; <span class="cm">// adjust to your install path</span>
|
window.CYBERCHAT_BASE = <span class="str">'/path/to/cyberchat'</span>; <span class="cm">// adjust to your install path</span>
|
||||||
CyberChat.init(<span class="str">'#my-chat'</span>);
|
CyberChat.init(<span class="str">'#my-chat'</span>);
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '';
|
window.CYBERCHAT_BASE = '';
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/cyberchat-v4.js?v=20260608.2"></script>
|
<script src="assets/js/cyberchat-v4.js?v=20260608.3"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
CyberChat.init('#chat-here');
|
CyberChat.init('#chat-here');
|
||||||
|
|||||||
+2
-2
@@ -7,14 +7,14 @@
|
|||||||
<title>Chat @ TyClifford.com</title>
|
<title>Chat @ TyClifford.com</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260608.2">
|
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260608.3">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '';
|
window.CYBERCHAT_BASE = '';
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/cyberchat-v4.js?v=20260608.2"></script>
|
<script src="assets/js/cyberchat-v4.js?v=20260608.3"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
CyberChat.init('#app');
|
CyberChat.init('#app');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ function handleAdminPlatformAction(string $action): ?string {
|
|||||||
if ($action === 'save_platform_services') {
|
if ($action === 'save_platform_services') {
|
||||||
setConfigValues([
|
setConfigValues([
|
||||||
'subscriptions.enabled' => !empty($_POST['subscriptions_enabled']),
|
'subscriptions.enabled' => !empty($_POST['subscriptions_enabled']),
|
||||||
|
'groups.enabled' => !empty($_POST['groups_enabled']),
|
||||||
'mailgun.api_key' => trim((string)($_POST['mailgun_api_key'] ?? '')),
|
'mailgun.api_key' => trim((string)($_POST['mailgun_api_key'] ?? '')),
|
||||||
'mailgun.domain' => trim((string)($_POST['mailgun_domain'] ?? '')),
|
'mailgun.domain' => trim((string)($_POST['mailgun_domain'] ?? '')),
|
||||||
'mailgun.from' => trim((string)($_POST['mailgun_from'] ?? '')),
|
'mailgun.from' => trim((string)($_POST['mailgun_from'] ?? '')),
|
||||||
@@ -141,6 +142,10 @@ function renderAdminPlatformTab(): void {
|
|||||||
<div class="panel-body"><label class="cb-row"><input type="checkbox" name="subscriptions_enabled" <?= getConfigVal('subscriptions.enabled', true) ? 'checked' : '' ?>>
|
<div class="panel-body"><label class="cb-row"><input type="checkbox" name="subscriptions_enabled" <?= getConfigVal('subscriptions.enabled', true) ? 'checked' : '' ?>>
|
||||||
<span>Allow and display new subscriptions. When off, existing subscribers can still manage billing.</span></label></div></div>
|
<span>Allow and display new subscriptions. When off, existing subscribers can still manage billing.</span></label></div></div>
|
||||||
|
|
||||||
|
<div class="panel"><div class="panel-head"><span class="panel-title">// GROUP AVAILABILITY</span></div>
|
||||||
|
<div class="panel-body"><label class="cb-row"><input type="checkbox" name="groups_enabled" <?= groupsEnabled() ? 'checked' : '' ?>>
|
||||||
|
<span>Enable private groups. When off, group navigation and channels are hidden while existing group data is preserved.</span></label></div></div>
|
||||||
|
|
||||||
<div class="panel"><div class="panel-head"><span class="panel-title">// MAILGUN</span></div><div class="panel-body">
|
<div class="panel"><div class="panel-head"><span class="panel-title">// MAILGUN</span></div><div class="panel-body">
|
||||||
<div class="form-row3">
|
<div class="form-row3">
|
||||||
<div class="field"><label>API Key</label><input type="password" name="mailgun_api_key" value="<?= esc(getConfigVal('mailgun.api_key', '')) ?>"></div>
|
<div class="field"><label>API Key</label><input type="password" name="mailgun_api_key" value="<?= esc(getConfigVal('mailgun.api_key', '')) ?>"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user