{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.chat_rooms.page_title', {}, locale, ['common']) }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ t('ui.admin.chat_rooms.heading', {}, locale, ['common']) }}</h1>
<button type="button" class="btn btn-primary" onclick="showCreateRoomModal()">
<i class="fas fa-plus me-2"></i>{{ t('ui.admin.chat_rooms.add_room', {}, locale, ['common']) }}
</button>
</div>
<div class="card shadow mb-3">
<div class="card-header">
<i class="fas fa-bridge-water me-2"></i>{{ t('ui.admin.matterbridge_settings.heading', {}, locale, ['common']) }}
</div>
<div class="card-body">
<div id="matterbridgeAlert"></div>
<form id="matterbridgeSettingsForm">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="matterbridgeEnabled">
<label class="form-check-label" for="matterbridgeEnabled">{{ t('ui.admin.matterbridge_settings.enabled', {}, locale, ['common']) }}</label>
</div>
<div class="row g-3">
<div class="col-md-6">
<label for="matterbridgeBaseUrl" class="form-label">{{ t('ui.admin.matterbridge_settings.base_url', {}, locale, ['common']) }}</label>
<input type="url" class="form-control" id="matterbridgeBaseUrl" placeholder="http://127.0.0.1:4240">
</div>
<div class="col-md-3">
<label for="matterbridgeTimeout" class="form-label">{{ t('ui.admin.matterbridge_settings.timeout_seconds', {}, locale, ['common']) }}</label>
<input type="number" min="1" max="120" class="form-control" id="matterbridgeTimeout" value="10">
</div>
<div class="col-md-3">
<label for="matterbridgeUsernameSuffix" class="form-label">{{ t('ui.admin.matterbridge_settings.username_suffix', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="matterbridgeUsernameSuffix" placeholder=" @ BinktermPHP">
</div>
<div class="col-md-6">
<label for="matterbridgeToken" class="form-label">{{ t('ui.admin.matterbridge_settings.token', {}, locale, ['common']) }}</label>
<div class="input-group">
<input type="password" class="form-control" id="matterbridgeToken" autocomplete="off">
<button class="btn btn-outline-secondary" type="button" onclick="toggleMatterbridgeTokenVisibility(this)" title="{{ t('ui.common.show_password', {}, locale, ['common']) }}" aria-label="{{ t('ui.common.show_password', {}, locale, ['common']) }}">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="col-md-3">
<label for="matterbridgeBridgeUserSearch" class="form-label">{{ t('ui.admin.matterbridge_settings.bridge_user_id', {}, locale, ['common']) }}</label>
<input type="hidden" id="matterbridgeBridgeUserId">
<div class="position-relative">
<input type="text" class="form-control" id="matterbridgeBridgeUserSearch" autocomplete="off" placeholder="{{ t('ui.admin.matterbridge_settings.bridge_user_search_placeholder', {}, locale, ['common']) }}">
<div id="matterbridgeBridgeUserResults" class="list-group position-absolute w-100 shadow-sm d-none" style="z-index: 1060;"></div>
</div>
<div class="form-text">{{ t('ui.admin.matterbridge_settings.bridge_user_id_help', {}, locale, ['common']) }}</div>
</div>
<div class="col-md-3 d-flex align-items-start pt-4">
<div class="form-check mt-1">
<input class="form-check-input" type="checkbox" id="matterbridgeSkipTlsVerify">
<label class="form-check-label" for="matterbridgeSkipTlsVerify">{{ t('ui.admin.matterbridge_settings.skip_tls_verify', {}, locale, ['common']) }}</label>
</div>
</div>
</div>
<div class="form-text mt-2">{{ t('ui.admin.matterbridge_settings.help', {}, locale, ['common']) }}</div>
<div class="mt-3">
<button type="button" class="btn btn-primary" onclick="saveMatterbridgeSettings()">
{{ t('ui.admin.matterbridge_settings.save', {}, locale, ['common']) }}
</button>
</div>
</form>
</div>
</div>
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="chatRoomsTable">
<thead class="table-dark">
<tr>
<th>{{ t('ui.common.id', {}, locale, ['common']) }}</th>
<th>{{ t('ui.common.name', {}, locale, ['common']) }}</th>
<th>{{ t('ui.common.description', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.chat_rooms.bridge', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.chat_rooms.status', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.chat_rooms.created', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.chat_rooms.actions', {}, locale, ['common']) }}</th>
</tr>
</thead>
<tbody id="chatRoomsTableBody">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="chatRoomModal" tabindex="-1" aria-labelledby="chatRoomModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="chatRoomModalLabel">{{ t('ui.admin.chat_rooms.add_chat_room', {}, locale, ['common']) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t('ui.common.close', {}, locale, ['common']) }}"></button>
</div>
<div class="modal-body">
<form id="chatRoomForm">
<input type="hidden" id="chatRoomId" name="chatRoomId">
<div class="mb-3">
<label for="chatRoomName" class="form-label">{{ t('ui.common.name', {}, locale, ['common']) }} <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="chatRoomName" name="name" required>
</div>
<div class="mb-3">
<label for="chatRoomDescription" class="form-label">{{ t('ui.common.description', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="chatRoomDescription" name="description">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="chatRoomActive" name="is_active" checked>
<label class="form-check-label" for="chatRoomActive">
{{ t('ui.admin.chat_rooms.active_room', {}, locale, ['common']) }}
</label>
</div>
<hr>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="chatRoomMatterbridgeEnabled" name="matterbridge_enabled">
<label class="form-check-label" for="chatRoomMatterbridgeEnabled">
{{ t('ui.admin.chat_rooms.matterbridge_enabled', {}, locale, ['common']) }}
</label>
</div>
<div class="mb-3">
<label for="chatRoomMatterbridgeGateway" class="form-label">{{ t('ui.admin.chat_rooms.matterbridge_gateway', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="chatRoomMatterbridgeGateway" name="matterbridge_gateway">
<div class="form-text">{{ t('ui.admin.chat_rooms.matterbridge_gateway_help', {}, locale, ['common']) }}</div>
</div>
<div class="mb-3">
<label for="chatRoomMatterbridgeUsernameTemplate" class="form-label">{{ t('ui.admin.chat_rooms.matterbridge_username_template', {}, locale, ['common']) }}</label>
<input type="text" class="form-control" id="chatRoomMatterbridgeUsernameTemplate" name="matterbridge_username_template" placeholder="{username} @ BinktermPHP">
<div class="form-text">{{ t('ui.admin.chat_rooms.matterbridge_username_template_help', {}, locale, ['common']) }}</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button>
<button type="button" class="btn btn-primary" onclick="saveChatRoom()">{{ t('ui.admin.chat_rooms.save_room', {}, locale, ['common']) }}</button>
</div>
</div>
</div>
</div>
<script>
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
if (payload && payload.error) {
return String(payload.error);
}
return fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
function escapeHtml(text) {
return String(text).replace(/[&<>"']/g, function(m) {
return ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m];
});
}
// ---- Bridge user search ----
let selectedBridgeUser = null;
let bridgeUserSearchTimer = null;
function initBridgeUserSearch() {
const searchEl = document.getElementById('matterbridgeBridgeUserSearch');
if (!searchEl) return;
searchEl.addEventListener('input', function() {
selectedBridgeUser = null;
document.getElementById('matterbridgeBridgeUserId').value = '';
clearTimeout(bridgeUserSearchTimer);
bridgeUserSearchTimer = setTimeout(function() {
searchBridgeUsers(searchEl.value);
}, 200);
});
searchEl.addEventListener('focus', function() {
clearTimeout(bridgeUserSearchTimer);
bridgeUserSearchTimer = setTimeout(function() {
searchBridgeUsers(searchEl.value);
}, 200);
});
searchEl.addEventListener('blur', function() {
setTimeout(function() {
document.getElementById('matterbridgeBridgeUserResults').classList.add('d-none');
}, 150);
});
}
function searchBridgeUsers(query) {
const search = (query || '').trim();
const results = document.getElementById('matterbridgeBridgeUserResults');
if (search.length < 2) {
results.classList.add('d-none');
results.innerHTML = '';
return;
}
results.classList.remove('d-none');
results.innerHTML = `<div class="list-group-item text-muted">${uiT('ui.admin.auto_feed.searching_users', 'Searching users...')}</div>`;
fetch(`/admin/api/users?search=${encodeURIComponent(search)}&limit=10`)
.then(r => r.json())
.then(data => displayBridgeUserResults(data.users || []))
.catch(() => {
results.classList.remove('d-none');
results.innerHTML = `<div class="list-group-item text-danger">${uiT('ui.admin.auto_feed.user_search_failed', 'User search failed')}</div>`;
});
}
function displayBridgeUserResults(users) {
const results = document.getElementById('matterbridgeBridgeUserResults');
results.innerHTML = '';
if (!users.length) {
results.classList.remove('d-none');
results.innerHTML = `<div class="list-group-item text-muted">${uiT('ui.admin.auto_feed.no_users_found', 'No users found')}</div>`;
return;
}
users.forEach(function(user) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'list-group-item list-group-item-action';
btn.innerHTML = `<div class="fw-semibold">${escapeHtml(user.username || '')}</div><small class="text-muted">${escapeHtml(user.real_name || '')}</small>`;
btn.addEventListener('mousedown', function(e) {
e.preventDefault();
setSelectedBridgeUser(user);
results.classList.add('d-none');
});
results.appendChild(btn);
});
results.classList.remove('d-none');
}
function setSelectedBridgeUser(user) {
selectedBridgeUser = (user && user.id) ? user : null;
const hiddenEl = document.getElementById('matterbridgeBridgeUserId');
const searchEl = document.getElementById('matterbridgeBridgeUserSearch');
if (!hiddenEl || !searchEl) return;
if (selectedBridgeUser) {
hiddenEl.value = selectedBridgeUser.id;
const label = selectedBridgeUser.real_name
? `${selectedBridgeUser.username} (${selectedBridgeUser.real_name})`
: selectedBridgeUser.username;
searchEl.value = label;
} else {
hiddenEl.value = '';
searchEl.value = '';
}
}
// ---- End bridge user search ----
document.addEventListener('DOMContentLoaded', function() {
loadI18nNamespaces(['common', 'errors']).then(function() {
initBridgeUserSearch();
loadMatterbridgeSettings();
loadChatRooms();
});
});
function toggleMatterbridgeTokenVisibility(btn) {
const input = document.getElementById('matterbridgeToken');
const icon = btn.querySelector('i');
const isHidden = input.type === 'password';
input.type = isHidden ? 'text' : 'password';
icon.classList.toggle('fa-eye', !isHidden);
icon.classList.toggle('fa-eye-slash', isHidden);
}
function loadMatterbridgeSettings() {
fetch('/admin/api/matterbridge-settings')
.then(response => response.json())
.then(data => {
const config = data.config || {};
document.getElementById('matterbridgeEnabled').checked = !!config.enabled;
document.getElementById('matterbridgeBaseUrl').value = config.api?.base_url || 'http://127.0.0.1:4240';
document.getElementById('matterbridgeToken').value = config.api?.token || '';
document.getElementById('matterbridgeTimeout').value = config.api?.timeout_seconds || 10;
document.getElementById('matterbridgeSkipTlsVerify').checked = !!config.api?.skip_tls_verify;
document.getElementById('matterbridgeUsernameSuffix').value = config.defaults?.username_suffix || ' @ BinktermPHP';
const bridgeId = parseInt(config.bridge_user_id || 0, 10);
if (bridgeId > 0) {
fetch(`/admin/api/users/${bridgeId}`)
.then(r => r.json())
.then(u => { if (u && u.user && u.user.id) setSelectedBridgeUser(u.user); })
.catch(() => {});
} else {
setSelectedBridgeUser(null);
}
})
.catch(error => {
console.error(uiT('ui.admin.matterbridge_settings.load_failed', 'Error loading Matterbridge settings') + ':', error);
showInlineAlert('matterbridgeAlert', uiT('ui.admin.matterbridge_settings.load_failed', 'Error loading Matterbridge settings'), 'danger');
});
}
function saveMatterbridgeSettings() {
const payload = {
config: {
enabled: document.getElementById('matterbridgeEnabled').checked,
bridge_user_id: parseInt(document.getElementById('matterbridgeBridgeUserId').value || '0', 10),
api: {
base_url: document.getElementById('matterbridgeBaseUrl').value.trim(),
token: document.getElementById('matterbridgeToken').value,
timeout_seconds: parseInt(document.getElementById('matterbridgeTimeout').value || '10', 10),
skip_tls_verify: document.getElementById('matterbridgeSkipTlsVerify').checked
},
defaults: {
username_suffix: document.getElementById('matterbridgeUsernameSuffix').value
}
}
};
fetch('/admin/api/matterbridge-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
if (data.success) {
const message = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.matterbridge_settings.saved_success', 'Matterbridge settings saved successfully'))
: uiT('ui.admin.matterbridge_settings.saved_success', 'Matterbridge settings saved successfully');
showInlineAlert('matterbridgeAlert', message, 'success');
} else {
showInlineAlert('matterbridgeAlert', apiError(data, uiT('ui.admin.matterbridge_settings.save_failed', 'Error saving Matterbridge settings')), 'danger');
}
})
.catch(error => {
console.error(uiT('ui.admin.matterbridge_settings.save_failed', 'Error saving Matterbridge settings') + ':', error);
showInlineAlert('matterbridgeAlert', uiT('ui.admin.matterbridge_settings.save_failed', 'Error saving Matterbridge settings'), 'danger');
});
}
function loadChatRooms() {
fetch('/admin/api/chat-rooms')
.then(response => response.json())
.then(data => {
displayChatRooms(data.rooms || []);
})
.catch(error => {
console.error(uiT('ui.admin.chat_rooms.load_failed', 'Error loading chat rooms') + ':', error);
showAlert(uiT('ui.admin.chat_rooms.load_failed', 'Error loading chat rooms'), 'danger');
});
}
function displayChatRooms(rooms) {
const tbody = document.getElementById('chatRoomsTableBody');
tbody.innerHTML = '';
rooms.forEach(room => {
const row = document.createElement('tr');
const isLobby = room.name === 'Lobby';
row.innerHTML = `
<td>${room.id}</td>
<td>${room.name}</td>
<td>${room.description || uiT('ui.admin.chat_rooms.no_description', '-')}</td>
<td>
${room.matterbridge_enabled
? `<span class="badge bg-info text-dark">${escapeHtml(room.matterbridge_gateway || uiT('ui.admin.chat_rooms.bridge_enabled', 'Enabled'))}</span>`
: `<span class="badge bg-secondary">${uiT('ui.admin.chat_rooms.bridge_disabled', 'Disabled')}</span>`}
</td>
<td>
<span class="badge bg-${room.is_active ? 'success' : 'secondary'}">
${room.is_active ? uiT('ui.admin.chat_rooms.active', 'Active') : uiT('ui.admin.chat_rooms.inactive', 'Inactive')}
</span>
</td>
<td>${new Date(room.created_at).toLocaleDateString()}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="editChatRoom(${room.id})" title="${uiT('ui.common.edit', 'Edit')}">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deleteChatRoom(${room.id})" title="${uiT('ui.common.delete', 'Delete')}" ${isLobby ? 'disabled' : ''}>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
tbody.appendChild(row);
});
}
function showCreateRoomModal() {
document.getElementById('chatRoomModalLabel').textContent = uiT('ui.admin.chat_rooms.add_chat_room', 'Add Chat Room');
document.getElementById('chatRoomForm').reset();
document.getElementById('chatRoomId').value = '';
document.getElementById('chatRoomName').disabled = false;
document.getElementById('chatRoomActive').checked = true;
document.getElementById('chatRoomMatterbridgeEnabled').checked = false;
document.getElementById('chatRoomMatterbridgeGateway').value = '';
document.getElementById('chatRoomMatterbridgeUsernameTemplate').value = '';
new bootstrap.Modal(document.getElementById('chatRoomModal')).show();
}
function editChatRoom(roomId) {
fetch('/admin/api/chat-rooms')
.then(response => response.json())
.then(data => {
const room = (data.rooms || []).find(item => item.id === roomId);
if (!room) {
showAlert(uiT('ui.admin.chat_rooms.not_found', 'Chat room not found'), 'danger');
return;
}
document.getElementById('chatRoomModalLabel').textContent = uiT('ui.admin.chat_rooms.edit_chat_room', 'Edit Chat Room');
document.getElementById('chatRoomId').value = room.id;
document.getElementById('chatRoomName').value = room.name;
document.getElementById('chatRoomName').disabled = room.name === 'Lobby';
document.getElementById('chatRoomDescription').value = room.description || '';
document.getElementById('chatRoomActive').checked = !!room.is_active;
document.getElementById('chatRoomMatterbridgeEnabled').checked = !!room.matterbridge_enabled;
document.getElementById('chatRoomMatterbridgeGateway').value = room.matterbridge_gateway || '';
document.getElementById('chatRoomMatterbridgeUsernameTemplate').value = room.matterbridge_options?.username_template || '';
new bootstrap.Modal(document.getElementById('chatRoomModal')).show();
})
.catch(error => {
console.error(uiT('ui.admin.chat_rooms.load_single_failed', 'Error loading chat room') + ':', error);
showAlert(uiT('ui.admin.chat_rooms.load_single_failed', 'Error loading chat room'), 'danger');
});
}
function saveChatRoom() {
const form = document.getElementById('chatRoomForm');
const formData = new FormData(form);
const roomId = formData.get('chatRoomId');
const payload = {
name: formData.get('name'),
description: formData.get('description'),
is_active: formData.has('is_active'),
matterbridge_enabled: formData.has('matterbridge_enabled'),
matterbridge_gateway: formData.get('matterbridge_gateway'),
matterbridge_options: {
username_template: formData.get('matterbridge_username_template')
}
};
const url = roomId ? `/admin/api/chat-rooms/${roomId}` : '/admin/api/chat-rooms';
const method = roomId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
if (data.success || data.id) {
const successMessage = window.getApiMessage
? window.getApiMessage(
data,
roomId
? uiT('ui.admin.chat_rooms.updated_success', 'Room updated successfully')
: uiT('ui.admin.chat_rooms.created_success', 'Room created successfully')
)
: (roomId
? uiT('ui.admin.chat_rooms.updated_success', 'Room updated successfully')
: uiT('ui.admin.chat_rooms.created_success', 'Room created successfully'));
showAlert(successMessage, 'success');
bootstrap.Modal.getInstance(document.getElementById('chatRoomModal')).hide();
loadChatRooms();
} else {
showAlert(apiError(data, uiT('ui.admin.chat_rooms.save_failed', 'Error saving room')), 'danger');
}
})
.catch(error => {
console.error(uiT('ui.admin.chat_rooms.save_failed', 'Error saving room') + ':', error);
showAlert(uiT('ui.admin.chat_rooms.save_failed', 'Error saving room'), 'danger');
});
}
function deleteChatRoom(roomId) {
if (!confirm(uiT('ui.admin.chat_rooms.delete_confirm', 'Delete this chat room? This cannot be undone.'))) {
return;
}
fetch(`/admin/api/chat-rooms/${roomId}`, { method: 'DELETE' })
.then(response => response.json())
.then(data => {
if (data.success) {
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.chat_rooms.deleted_success', 'Room deleted successfully'))
: uiT('ui.admin.chat_rooms.deleted_success', 'Room deleted successfully');
showAlert(successMessage, 'success');
loadChatRooms();
} else {
showAlert(apiError(data, uiT('ui.admin.chat_rooms.delete_failed', 'Error deleting room')), 'danger');
}
})
.catch(error => {
console.error(uiT('ui.admin.chat_rooms.delete_failed', 'Error deleting room') + ':', error);
showAlert(uiT('ui.admin.chat_rooms.delete_failed', 'Error deleting room'), 'danger');
});
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container-fluid');
container.insertBefore(alertDiv, container.firstChild);
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
function showInlineAlert(elementId, message, type) {
const container = document.getElementById(elementId);
if (!container) {
return;
}
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
</script>
{% endblock %}
|