Add comment threading and fix fetch authentication

- Add comment threading/reply functionality with nested display
  - Database migration for parent_comment_id and thread_depth columns
  - Recursive comment rendering with depth-based indentation
  - Reply form with inline UI and smooth animations
  - Thread collapse/expand capability
  - Max thread depth of 3 levels

- Fix 401 authentication errors on API calls
  - Add credentials: 'same-origin' to all fetch calls
  - Affects settings.js, ticket.js, dashboard.js, advanced-search.js
  - Ensures session cookies are sent with requests

- Enhanced comment styling
  - Thread connector lines for visual hierarchy
  - Reply button on comments (up to depth 3)
  - Quote block styling for replies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 23:43:36 -05:00
parent 1c1eb19876
commit a8738fdf57
9 changed files with 1081 additions and 46 deletions

View File

@@ -331,42 +331,76 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$currentUserId = $GLOBALS['currentUser']['user_id'] ?? null;
$isAdmin = $GLOBALS['currentUser']['is_admin'] ?? false;
foreach ($comments as $comment) {
// Use display_name_formatted which falls back appropriately
// Recursive function to render threaded comments
function renderComment($comment, $currentUserId, $isAdmin, $depth = 0) {
$displayName = $comment['display_name_formatted'] ?? $comment['user_name'] ?? 'Unknown User';
$commentId = $comment['comment_id'];
$isOwner = ($comment['user_id'] == $currentUserId);
$canModify = $isOwner || $isAdmin;
$markdownEnabled = $comment['markdown_enabled'] ? 1 : 0;
$threadDepth = $comment['thread_depth'] ?? $depth;
$parentId = $comment['parent_comment_id'] ?? null;
echo "<div class='comment' data-comment-id='{$commentId}' data-markdown-enabled='{$markdownEnabled}'>";
$depthClass = 'thread-depth-' . min($threadDepth, 3);
$threadClass = $parentId ? 'comment-reply' : 'comment-root';
echo "<div class='comment {$depthClass} {$threadClass}' data-comment-id='{$commentId}' data-markdown-enabled='{$markdownEnabled}' data-thread-depth='{$threadDepth}' data-parent-id='" . ($parentId ?? '') . "'>";
// Thread connector line for replies
if ($parentId) {
echo "<div class='thread-line'></div>";
}
echo "<div class='comment-content'>";
echo "<div class='comment-header'>";
echo "<span class='comment-user'>" . htmlspecialchars($displayName) . "</span>";
$dateStr = date('M d, Y H:i', strtotime($comment['created_at']));
$editedIndicator = !empty($comment['updated_at']) ? ' <span class="comment-edited">(edited)</span>' : '';
echo "<span class='comment-date'>{$dateStr}{$editedIndicator}</span>";
// Action buttons
echo "<div class='comment-actions'>";
// Reply button (max depth of 3)
if ($threadDepth < 3) {
echo "<button type='button' class='comment-action-btn reply-btn' data-action='reply-comment' data-comment-id='{$commentId}' data-user='" . htmlspecialchars($displayName) . "' title='Reply'>↩</button>";
}
// Edit/Delete buttons for owner or admin
if ($canModify) {
echo "<div class='comment-actions'>";
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit comment'></button>";
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete comment'>🗑️</button>";
echo "</div>";
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit'>✏️</button>";
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete'>🗑</button>";
}
echo "</div>";
echo "</div>"; // .comment-header
echo "<div class='comment-text' id='comment-text-{$commentId}' " . ($comment['markdown_enabled'] ? "data-markdown" : "") . ">";
if ($comment['markdown_enabled']) {
// Markdown will be rendered by JavaScript
echo htmlspecialchars($comment['comment_text']);
} else {
// For non-markdown comments, convert line breaks to <br> and escape HTML
echo nl2br(htmlspecialchars($comment['comment_text']));
}
echo "</div>";
// Hidden raw text for editing
echo "<textarea class='comment-edit-raw' id='comment-raw-{$commentId}' style='display:none;'>" . htmlspecialchars($comment['comment_text']) . "</textarea>";
echo "</div>";
echo "</div>"; // .comment-content
// Render replies recursively
if (!empty($comment['replies'])) {
echo "<div class='comment-replies'>";
foreach ($comment['replies'] as $reply) {
renderComment($reply, $currentUserId, $isAdmin, $threadDepth + 1);
}
echo "</div>";
}
echo "</div>"; // .comment
}
// Render all comments
foreach ($comments as $comment) {
renderComment($comment, $currentUserId, $isAdmin);
}
?>
</div>