ReleaseV11+AnnonceManuelle

This commit is contained in:
Flatlogic Bot 2026-02-20 21:21:48 +00:00
parent d761c251bf
commit 88493bedcd
5 changed files with 377 additions and 40 deletions

View File

@ -135,31 +135,70 @@ if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$action = $data['action'] ?? 'edit';
try {
$stmt = db()->prepare("SELECT user_id, channel_id FROM messages WHERE id = ?");
$stmt->execute([$message_id]);
$msg_data = $stmt->fetch();
if (!$msg_data) {
echo json_encode(['success' => false, 'error' => 'Message not found']);
exit;
}
if ($action === 'pin') {
if (!Permissions::canDoInChannel($user_id, $msg_data['channel_id'], Permissions::MANAGE_MESSAGES)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$stmt = db()->prepare("UPDATE messages SET is_pinned = 1 WHERE id = ?");
$stmt->execute([$message_id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'unpin') {
if (!Permissions::canDoInChannel($user_id, $msg_data['channel_id'], Permissions::MANAGE_MESSAGES)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$stmt = db()->prepare("UPDATE messages SET is_pinned = 0 WHERE id = ?");
$stmt->execute([$message_id]);
echo json_encode(['success' => true]);
exit;
}
if ($msg_data['user_id'] != $user_id && !Permissions::canDoInChannel($user_id, $msg_data['channel_id'], Permissions::MANAGE_MESSAGES)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if (empty($content)) {
echo json_encode(['success' => false, 'error' => 'Content cannot be empty']);
exit;
}
$stmt = db()->prepare("UPDATE messages SET content = ? WHERE id = ? AND user_id = ?");
$stmt->execute([$content, $message_id, $user_id]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Message not found or unauthorized']);
$metadata = null;
if (isset($data['is_announcement']) && $data['is_announcement'] == '1') {
if (!Permissions::canDoInChannel($user_id, $msg_data['channel_id'], Permissions::CREATE_ANNOUNCEMENT)) {
echo json_encode(['success' => false, 'error' => 'You do not have permission to manage announcements in this channel.']);
exit;
}
$metadata = json_encode([
'is_manual_announcement' => true,
'title' => $data['ann_title'] ?? '',
'color' => $data['ann_color'] ?? '#5865f2',
'description' => $content,
'url' => $data['ann_link'] ?? ''
]);
}
if ($metadata) {
$stmt = db()->prepare("UPDATE messages SET content = ?, metadata = ? WHERE id = ?");
$stmt->execute([$content, $metadata, $message_id]);
} else {
$stmt = db()->prepare("UPDATE messages SET content = ? WHERE id = ?");
$stmt->execute([$content, $message_id]);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
@ -171,14 +210,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$message_id = $data['id'] ?? 0;
try {
$stmt = db()->prepare("DELETE FROM messages WHERE id = ? AND user_id = ?");
$stmt->execute([$message_id, $user_id]);
$stmt = db()->prepare("SELECT user_id, channel_id FROM messages WHERE id = ?");
$stmt->execute([$message_id]);
$msg_data = $stmt->fetch();
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Message not found or unauthorized']);
if (!$msg_data) {
echo json_encode(['success' => false, 'error' => 'Message not found']);
exit;
}
if ($msg_data['user_id'] != $user_id && !Permissions::canDoInChannel($user_id, $msg_data['channel_id'], Permissions::MANAGE_MESSAGES)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$stmt = db()->prepare("DELETE FROM messages WHERE id = ?");
$stmt->execute([$message_id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
@ -261,7 +310,21 @@ if (!empty($content)) {
}
$metadata = null;
if (!empty($content)) {
if (isset($_POST['is_announcement']) && $_POST['is_announcement'] == '1') {
if (!Permissions::canDoInChannel($user_id, $channel_id, Permissions::CREATE_ANNOUNCEMENT)) {
echo json_encode(['success' => false, 'error' => 'You do not have permission to create announcements in this channel.']);
exit;
}
$metadata = json_encode([
'is_manual_announcement' => true,
'title' => $_POST['ann_title'] ?? '',
'color' => $_POST['ann_color'] ?? '#5865f2',
'description' => $content,
'url' => $_POST['ann_link'] ?? ''
]);
// Clear content for the message text itself if we want it only in the embed
// But keeping it in content might be good for search/fallback
} elseif (!empty($content)) {
$urls = extractUrls($content);
if (!empty($urls)) {
// Fetch OG data for the first URL

View File

@ -693,6 +693,98 @@ document.addEventListener('DOMContentLoaded', () => {
xhr.send(formData);
});
// Announcement News Button
document.addEventListener('click', (e) => {
const annBtn = e.target.closest('#announcement-news-btn');
if (annBtn) {
const idField = document.getElementById('announcement-id');
if (idField) idField.value = '';
const form = document.getElementById('announcement-form');
if (form) form.reset();
const submitBtn = document.getElementById('announcement-submit-btn');
if (submitBtn) submitBtn.innerText = "Publier l'annonce";
const modal = new bootstrap.Modal(document.getElementById('createAnnouncementModal'));
modal.show();
return;
}
});
// Announcement Form Submission
const annForm = document.getElementById('announcement-form');
annForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const annId = document.getElementById('announcement-id').value;
const title = document.getElementById('announcement-title').value.trim();
const color = document.getElementById('announcement-color').value;
const content = document.getElementById('announcement-content').value.trim();
const link = document.getElementById('announcement-link').value.trim();
if (!title || !content) return;
const modalEl = document.getElementById('createAnnouncementModal');
const modal = bootstrap.Modal.getInstance(modalEl);
let resp;
if (annId) {
resp = await fetch('api_v1_messages.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: annId,
content: content,
is_announcement: '1',
ann_title: title,
ann_color: color,
ann_link: link
})
});
} else {
const formData = new FormData();
formData.append('channel_id', currentChannel);
formData.append('content', content);
formData.append('is_announcement', '1');
formData.append('ann_title', title);
formData.append('ann_color', color);
formData.append('ann_link', link);
resp = await fetch('api_v1_messages.php', {
method: 'POST',
body: formData
});
}
try {
const result = await resp.json();
if (result.success) {
if (annId) {
// Update via WS or reload
ws?.send(JSON.stringify({ type: 'message_edit', message_id: annId, content: content }));
location.reload();
} else {
appendMessage(result.message);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
data: JSON.stringify({
...result.message,
channel_id: currentChannel
})
}));
}
}
modal.hide();
annForm.reset();
} else {
alert(result.error || 'Erreur lors de la publication');
}
} catch (err) {
console.error(err);
alert('Une erreur est survenue');
}
});
// Handle Click Events
document.addEventListener('click', (e) => {
console.log('Global click at:', e.target);
@ -805,6 +897,40 @@ document.addEventListener('DOMContentLoaded', () => {
if (editBtn) {
const msgId = editBtn.dataset.id;
const msgItem = editBtn.closest('.message-item');
if (editBtn.classList.contains('edit-announcement')) {
const modalEl = document.getElementById('createAnnouncementModal');
const modal = new bootstrap.Modal(modalEl);
const titleEl = msgItem.querySelector('.embed-title');
const title = titleEl ? titleEl.innerText : '';
const description = msgItem.dataset.rawContent;
const embedEl = msgItem.querySelector('.rich-embed');
const color = embedEl ? embedEl.style.borderLeftColor : '#5865f2';
const rgbToHex = (rgb) => {
if (!rgb || !rgb.startsWith('rgb')) return '#5865f2';
const parts = rgb.match(/\d+/g);
return "#" + parts.map(x => {
const hex = parseInt(x).toString(16);
return hex.length === 1 ? "0" + hex : hex;
}).join("");
};
const hexColor = rgbToHex(color);
const url = titleEl ? (titleEl.getAttribute('href') || '') : '';
document.getElementById('announcement-id').value = msgId;
document.getElementById('announcement-title').value = title;
document.getElementById('announcement-color').value = hexColor;
document.getElementById('announcement-content').value = description;
document.getElementById('announcement-link').value = url;
document.getElementById('announcement-submit-btn').innerText = "Modifier l'annonce";
modal.show();
return;
}
const textEl = msgItem.querySelector('.message-text');
const originalContent = msgItem.dataset.rawContent || textEl.innerText;
@ -1462,6 +1588,12 @@ document.addEventListener('DOMContentLoaded', () => {
eventPerms.forEach(p => {
p.style.setProperty('display', channelType === 'event' ? 'block' : 'none', channelType === 'event' ? '' : 'important');
});
// Show/Hide announcement permissions
const annPerms = document.querySelectorAll('.announcement-permission-only');
annPerms.forEach(p => {
p.style.setProperty('display', channelType === 'announcement' ? 'block' : 'none', channelType === 'announcement' ? '' : 'important');
});
});
});
@ -1487,6 +1619,12 @@ document.addEventListener('DOMContentLoaded', () => {
p.style.setProperty('display', type === 'event' ? 'block' : 'none', type === 'event' ? '' : 'important');
});
// Show/Hide announcement permissions
const annPerms = document.querySelectorAll('.announcement-permission-only');
annPerms.forEach(p => {
p.style.setProperty('display', type === 'announcement' ? 'block' : 'none', type === 'announcement' ? '' : 'important');
});
// Rules specific visibility
const rulesRoleContainer = document.getElementById('edit-channel-rules-role-container');
if (rulesRoleContainer) {
@ -3092,12 +3230,41 @@ document.addEventListener('DOMContentLoaded', () => {
let embedHtml = '';
if (msg.metadata) {
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
const borderColor = meta.color || 'var(--blurple)';
let metaHtml = '';
if (meta.is_rss) {
const parts = [];
if (meta.category) parts.push(escapeHTML(meta.category));
if (meta.date) parts.push(escapeHTML(meta.date));
if (meta.author) parts.push(escapeHTML(meta.author));
metaHtml = `<div class="embed-meta mb-2" style="font-size: 0.8em; color: var(--text-muted);">${parts.join(' · ')}</div>`;
} else if (meta.is_manual_announcement) {
metaHtml = `<div class="embed-meta mb-2" style="font-size: 0.8em; color: var(--text-muted);">${msg.timestamp || 'Just now'}</div>`;
}
let titleHtml = '';
if (meta.title) {
if (meta.url) {
titleHtml = `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc; font-size: 1.1em;">${escapeHTML(meta.title)}</a>`;
} else {
titleHtml = `<div class="embed-title d-block mb-1" style="font-weight: 600; color: #00a8fc; font-size: 1.1em;">${escapeHTML(meta.title)}</div>`;
}
}
let footerHtml = '';
if (meta.is_manual_announcement && meta.url) {
footerHtml = `<div class="embed-footer mt-2"><a href="${meta.url}" target="_blank" class="btn btn-sm btn-outline-info" style="font-size: 0.8em;">Source / En savoir plus</a></div>`;
}
embedHtml = `
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
${meta.site_name ? `<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;">${escapeHTML(meta.site_name)}</div>` : ''}
${meta.title ? `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;">${escapeHTML(meta.title)}</a>` : ''}
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${escapeHTML(meta.description)}</div>` : ''}
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid ${borderColor}; max-width: 520px;">
${(meta.site_name && !meta.is_rss && !meta.is_manual_announcement) ? `<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;">${escapeHTML(meta.site_name)}</div>` : ''}
${titleHtml}
${metaHtml}
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${parseMarkdown(meta.description)}</div>` : ''}
${meta.image ? `<div class="embed-image"><img src="${meta.image}" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;"></div>` : ''}
${footerHtml}
</div>
`;
}
@ -3114,8 +3281,8 @@ document.addEventListener('DOMContentLoaded', () => {
const actionsHtml = (isMe || hasManageRights) ? `
<div class="message-actions-menu">
${pinHtml}
${isMe ? `
<span class="action-btn edit" title="Edit" data-id="${msg.id}">
${(isMe || (isManualAnn && hasManageRights)) ? `
<span class="action-btn edit ${isManualAnn ? 'edit-announcement' : ''}" title="Edit" data-id="${msg.id}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</span>
<span class="action-btn delete" title="Delete" data-id="${msg.id}">
@ -3132,21 +3299,12 @@ document.addEventListener('DOMContentLoaded', () => {
</span>
` : '';
let userBadgesHtml = '';
const bData = msg.badge_data || '';
if (bData) {
bData.split(':::').forEach(d => {
const parts = d.split('|');
const name = parts[0];
const url = parts[1];
userBadgesHtml += `<img src="${url}" class="ms-1" style="width: 32px; height: 32px; object-fit: contain; vertical-align: middle;" title="${escapeHTML(name)}">`;
});
}
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
const mentionHtml = `<span class="mention">@${window.currentUsername}</span>`;
const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml);
const isManualAnn = msg.metadata && (typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata).is_manual_announcement;
div.innerHTML = `
<div class="message-avatar" style="${avatarStyle}"></div>
<div class="message-content">
@ -3154,12 +3312,11 @@ document.addEventListener('DOMContentLoaded', () => {
<span class="message-username" style="color: ${msg.role_color || 'inherit'};">
${escapeHTML(msg.username)}
${renderRoleIconJS(msg.role_icon, '14px')}
<span class="ms-1 d-inline-flex gap-1">${userBadgesHtml}</span>
</span>
<span class="message-timestamp">${msg.timestamp || 'Just now'}</span>
${pinnedBadge}
</div>
<div class="message-text">${contentWithMentions}</div>
${!isManualAnn ? `<div class="message-text">${contentWithMentions}</div>` : ''}
${attachmentHtml}
${embedHtml}
<div class="message-reactions mt-1" data-message-id="${msg.id}"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -16,6 +16,7 @@ class Permissions {
const CREATE_EVENT = 4096;
const EDIT_EVENT = 8192;
const DELETE_EVENT = 16384;
const CREATE_ANNOUNCEMENT = 32768;
public static function hasPermission($user_id, $server_id, $permission) {
$stmt = db()->prepare("SELECT is_admin FROM users WHERE id = ?");

130
index.php
View File

@ -239,6 +239,7 @@ if ($is_dm_view) {
$can_create_event = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::CREATE_EVENT);
$can_edit_event = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::EDIT_EVENT);
$can_delete_event = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::DELETE_EVENT);
$can_create_announcement = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::CREATE_ANNOUNCEMENT);
break;
}
@ -1259,7 +1260,18 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php endif; ?>
</div>
<div class="message-text">
<?php echo parse_emotes($m['content'], $user['username']); ?>
<?php
$is_manual_ann = false;
if (!empty($m['metadata'])) {
$meta_check = json_decode($m['metadata'], true);
if (isset($meta_check['is_manual_announcement']) && $meta_check['is_manual_announcement']) {
$is_manual_ann = true;
}
}
if (!$is_manual_ann) {
echo parse_emotes($m['content'], $user['username']);
}
?>
<?php if ($m['attachment_url']): ?>
<div class="message-attachment mt-2">
<?php
@ -1279,12 +1291,16 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php if (!empty($m['metadata'])):
$meta = json_decode($m['metadata'], true);
if ($meta): ?>
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
<?php if (!empty($meta['site_name']) && empty($meta['is_rss'])): ?>
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid <?php echo htmlspecialchars($meta['color'] ?? 'var(--blurple)'); ?>; max-width: 520px;">
<?php if (!empty($meta['site_name']) && empty($meta['is_rss']) && empty($meta['is_manual_announcement'])): ?>
<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;"><?php echo htmlspecialchars($meta['site_name']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['title'])): ?>
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc; font-size: 1.1em;"><?php echo htmlspecialchars($meta['title']); ?></a>
<?php if (!empty($meta['url'])): ?>
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc; font-size: 1.1em;"><?php echo htmlspecialchars($meta['title']); ?></a>
<?php else: ?>
<div class="embed-title d-block mb-1" style="font-weight: 600; color: #00a8fc; font-size: 1.1em;"><?php echo htmlspecialchars($meta['title']); ?></div>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($meta['is_rss'])): ?>
<div class="embed-meta mb-2" style="font-size: 0.8em; color: var(--text-muted);">
@ -1297,8 +1313,18 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
</div>
<?php endif; ?>
<?php if (!empty($meta['is_manual_announcement'])): ?>
<div class="embed-meta mb-2" style="font-size: 0.8em; color: var(--text-muted);">
<?php echo date('d/m/Y H:i', strtotime($m['created_at'])); ?>
</div>
<?php endif; ?>
<?php if (!empty($meta['description'])): ?>
<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);"><?php echo htmlspecialchars($meta['description']); ?></div>
<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);"><?php echo parse_markdown($meta['description']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['is_manual_announcement']) && !empty($meta['url'])): ?>
<div class="embed-footer mt-2">
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="btn btn-sm btn-outline-info" style="font-size: 0.8em;">Source / En savoir plus</a>
</div>
<?php endif; ?>
<?php if (!empty($meta['image'])): ?>
<div class="embed-image">
@ -1330,8 +1356,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<span class="action-btn pin <?php echo $m['is_pinned'] ? 'active' : ''; ?>" title="<?php echo $m['is_pinned'] ? 'Désépingler' : 'Épingler'; ?>" data-id="<?php echo $m['id']; ?>" data-pinned="<?php echo $m['is_pinned'] ? '1' : '0'; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</span>
<?php if ($m['user_id'] == $current_user_id): ?>
<span class="action-btn edit" title="Modifier" data-id="<?php echo $m['id']; ?>">
<?php if ($m['user_id'] == $current_user_id || ($is_manual_ann && $can_manage_channels)): ?>
<span class="action-btn edit <?php echo $is_manual_ann ? 'edit-announcement' : ''; ?>" title="Modifier" data-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</span>
<span class="action-btn delete" title="Supprimer" data-id="<?php echo $m['id']; ?>">
@ -1393,6 +1419,12 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="8" x2="16" y2="16"></line><line x1="16" y1="8" x2="8" y2="16"></line></svg>
</div>
<?php endif; ?>
<?php if ($channel_type === 'announcement' && $can_create_announcement): ?>
<button type="button" class="btn border-0 text-muted p-2" id="announcement-news-btn" title="Créer une annonce manuelle">
<span style="font-size: 1.2rem;">📰</span>
</button>
<?php endif; ?>
<?php if (isset($active_thread) && $active_thread['is_locked']): ?>
<textarea id="chat-input" class="chat-input" placeholder="Cette discussion est verrouillée." autocomplete="off" rows="1" disabled style="background-color: rgba(0,0,0,0.1); cursor: not-allowed;"></textarea>
<?php else: ?>
@ -1432,6 +1464,52 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- Modal Créer une Annonce -->
<div class="modal fade" id="createAnnouncementModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg bg-dark text-white">
<div class="modal-header border-0">
<h5 class="modal-title"><span class="me-2">📰</span> Nouvelle Annonce</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="announcement-form">
<input type="hidden" id="announcement-id" value="">
<div class="mb-3">
<label class="form-label text-muted small text-uppercase fw-bold">Titre de l'annonce</label>
<input type="text" id="announcement-title" class="form-control bg-black text-white border-secondary" placeholder="Titre accrocheur..." required>
</div>
<div class="mb-3">
<label class="form-label small text-muted text-uppercase fw-bold">Couleur de la bordure</label>
<div class="d-flex gap-2 flex-wrap">
<input type="color" id="announcement-color" class="form-control form-control-color bg-transparent border-0" value="#5865f2" title="Choisir une couleur">
<div class="color-presets d-flex gap-2 align-items-center">
<span class="rounded-circle" style="width: 20px; height: 20px; background: #5865f2; cursor: pointer;" onclick="document.getElementById('announcement-color').value='#5865f2'"></span>
<span class="rounded-circle" style="width: 20px; height: 20px; background: #23a559; cursor: pointer;" onclick="document.getElementById('announcement-color').value='#23a559'"></span>
<span class="rounded-circle" style="width: 20px; height: 20px; background: #f04747; cursor: pointer;" onclick="document.getElementById('announcement-color').value='#f04747'"></span>
<span class="rounded-circle" style="width: 20px; height: 20px; background: #faa61a; cursor: pointer;" onclick="document.getElementById('announcement-color').value='#faa61a'"></span>
<span class="rounded-circle" style="width: 20px; height: 20px; background: #eb459e; cursor: pointer;" onclick="document.getElementById('announcement-color').value='#eb459e'"></span>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label small text-muted text-uppercase fw-bold">Texte de présentation</label>
<textarea id="announcement-content" class="form-control bg-black text-white border-secondary" rows="5" placeholder="Contenu de votre annonce..." required></textarea>
</div>
<div class="mb-3">
<label class="form-label small text-muted text-uppercase fw-bold">Lien source (optionnel)</label>
<input type="url" id="announcement-link" class="form-control bg-black text-white border-secondary" placeholder="https://...">
</div>
</form>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-link text-muted text-decoration-none" data-bs-dismiss="modal">Annuler</button>
<button type="submit" id="announcement-submit-btn" form="announcement-form" class="btn btn-primary px-4">Publier l'annonce</button>
</div>
</div>
</div>
</div>
<!-- Paramètres utilisateur Modal -->
<div class="modal fade" id="userParamètresModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
@ -2794,6 +2872,44 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Gérer les messages</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet de supprimer ou d'épingler les messages des autres membres.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="4">
<input type="radio" class="btn-check" name="perm_4" id="perm_4_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_4_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_4" id="perm_4_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_4_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_4" id="perm_4_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_4_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item announcement-permission-only mb-3 p-2 rounded" style="background: var(--separator-soft); display: none;">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Créer des annonces</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet de créer des annonces manuelles stylisées dans ce salon.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="32768">
<input type="radio" class="btn-check" name="perm_32768" id="perm_32768_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_32768_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_32768" id="perm_32768_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_32768_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_32768" id="perm_32768_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_32768_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">