feat: comment pagination, Matrix integration, Synapse mention resolution
Comment pagination: - CommentModel: add getCommentCount(), paginated getCommentsByTicketId() with getThreadedCommentsPaged() for threading + LIMIT/OFFSET - TicketController: load first 50 root comments + total count on page load - api/get_comments.php: new AJAX endpoint for Load More (index.php routed) - TicketView: Load More button + buildCommentEl() JS renderer for AJAX comments; passes totalComments/commentOffset/isAdmin to window.ticketData Matrix integration: - NotificationHelper: add sendStatusChangeNotification(), sendCommentNotification(), sendMentionNotification(), sendAssignmentNotification() alongside existing sendTicketNotification(); internal fire() helper replaces duplicated cURL logic - SynapseHelper: new helper that resolves SSO usernames → Matrix IDs by querying Synapse Admin REST API directly (no caching, no stale data) - config.php: add SYNAPSE_ADMIN_URL, SYNAPSE_ADMIN_TOKEN, MATRIX_NOTIFY_COMMENTS, MATRIX_NOTIFY_ASSIGNMENTS config keys (all from .env) - api/update_ticket.php: fire status-change notification after successful save - api/add_comment.php: resolve @mentioned usernames via SynapseHelper and fire mention notification; fire general comment notification when MATRIX_NOTIFY_COMMENTS=1 - api/assign_ticket.php: fire assignment notification (resolves assignee via Synapse) when MATRIX_NOTIFY_ASSIGNMENTS=1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+102
-8
@@ -50,10 +50,35 @@ class CommentModel {
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function getCommentsByTicketId($ticketId, $threaded = true) {
|
||||
// Check if threading columns exist
|
||||
/**
|
||||
* Get total comment count for a ticket
|
||||
*/
|
||||
public function getCommentCount(int $ticketId): int {
|
||||
$stmt = $this->conn->prepare(
|
||||
"SELECT COUNT(*) as total FROM ticket_comments WHERE ticket_id = ?"
|
||||
);
|
||||
$stmt->bind_param("i", $ticketId);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
return (int)($row['total'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $ticketId
|
||||
* @param bool $threaded Build nested reply structure (threading)
|
||||
* @param int $limit Max root-level comments to return (0 = all)
|
||||
* @param int $offset Root-level comment offset for pagination
|
||||
*/
|
||||
public function getCommentsByTicketId($ticketId, $threaded = true, int $limit = 0, int $offset = 0) {
|
||||
$hasThreading = $this->hasThreadingSupport();
|
||||
|
||||
// When paginating with threading we fetch root comments page first,
|
||||
// then pull all their replies in a second query.
|
||||
if ($hasThreading && $threaded && $limit > 0) {
|
||||
return $this->getThreadedCommentsPaged($ticketId, $limit, $offset);
|
||||
}
|
||||
|
||||
if ($hasThreading) {
|
||||
$sql = "SELECT tc.*, u.display_name, u.username, tc.parent_comment_id, tc.thread_depth
|
||||
FROM ticket_comments tc
|
||||
@@ -70,16 +95,21 @@ class CommentModel {
|
||||
ORDER BY tc.created_at DESC";
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$sql .= " LIMIT ? OFFSET ?";
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("i", $ticketId);
|
||||
if ($limit > 0) {
|
||||
$stmt->bind_param("iii", $ticketId, $limit, $offset);
|
||||
} else {
|
||||
$stmt->bind_param("i", $ticketId);
|
||||
}
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$comments = [];
|
||||
$commentMap = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
// Use display_name from users table if available, fallback to user_name field
|
||||
if (!empty($row['display_name'])) {
|
||||
$row['display_name_formatted'] = $row['display_name'];
|
||||
} else {
|
||||
@@ -90,8 +120,9 @@ class CommentModel {
|
||||
$row['thread_depth'] = $row['thread_depth'] ?? 0;
|
||||
$commentMap[$row['comment_id']] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Build threaded structure if threading is enabled
|
||||
// Build threaded structure if threading is enabled (no pagination — all loaded)
|
||||
if ($hasThreading && $threaded) {
|
||||
$rootComments = [];
|
||||
foreach ($commentMap as $id => $comment) {
|
||||
@@ -102,10 +133,73 @@ class CommentModel {
|
||||
return $rootComments;
|
||||
}
|
||||
|
||||
// Flat list
|
||||
return array_values($commentMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated threaded comments: fetch one page of root comments + all their replies.
|
||||
*/
|
||||
private function getThreadedCommentsPaged(int $ticketId, int $limit, int $offset): array {
|
||||
// Page of root comments
|
||||
$rootSql = "SELECT tc.*, u.display_name, u.username
|
||||
FROM ticket_comments tc
|
||||
LEFT JOIN users u ON tc.user_id = u.user_id
|
||||
WHERE tc.ticket_id = ? AND tc.parent_comment_id IS NULL
|
||||
ORDER BY tc.created_at DESC
|
||||
LIMIT ? OFFSET ?";
|
||||
$stmt = $this->conn->prepare($rootSql);
|
||||
$stmt->bind_param("iii", $ticketId, $limit, $offset);
|
||||
$stmt->execute();
|
||||
$rootResult = $stmt->get_result();
|
||||
$stmt->close();
|
||||
|
||||
$commentMap = [];
|
||||
$rootIds = [];
|
||||
while ($row = $rootResult->fetch_assoc()) {
|
||||
$row['display_name_formatted'] = $row['display_name'] ?: ($row['user_name'] ?? 'Unknown User');
|
||||
$row['replies'] = [];
|
||||
$row['parent_comment_id'] = null;
|
||||
$row['thread_depth'] = 0;
|
||||
$commentMap[$row['comment_id']] = $row;
|
||||
$rootIds[] = $row['comment_id'];
|
||||
}
|
||||
|
||||
if (empty($rootIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// All replies for these root comments (up to 3 levels deep)
|
||||
$placeholders = implode(',', array_fill(0, count($rootIds), '?'));
|
||||
$replySql = "SELECT tc.*, u.display_name, u.username
|
||||
FROM ticket_comments tc
|
||||
LEFT JOIN users u ON tc.user_id = u.user_id
|
||||
WHERE tc.ticket_id = ?
|
||||
AND tc.parent_comment_id IN ($placeholders)
|
||||
AND tc.parent_comment_id IS NOT NULL
|
||||
ORDER BY tc.created_at ASC";
|
||||
$replyStmt = $this->conn->prepare($replySql);
|
||||
$types = 'i' . str_repeat('i', count($rootIds));
|
||||
$replyStmt->bind_param($types, $ticketId, ...$rootIds);
|
||||
$replyStmt->execute();
|
||||
$replyResult = $replyStmt->get_result();
|
||||
$replyStmt->close();
|
||||
|
||||
while ($row = $replyResult->fetch_assoc()) {
|
||||
$row['display_name_formatted'] = $row['display_name'] ?: ($row['user_name'] ?? 'Unknown User');
|
||||
$row['replies'] = [];
|
||||
$row['thread_depth'] = $row['thread_depth'] ?? 1;
|
||||
$commentMap[$row['comment_id']] = $row;
|
||||
}
|
||||
|
||||
$rootComments = [];
|
||||
foreach ($rootIds as $rid) {
|
||||
if (isset($commentMap[$rid])) {
|
||||
$rootComments[] = $this->buildCommentThread($commentMap[$rid], $commentMap);
|
||||
}
|
||||
}
|
||||
return $rootComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if threading columns exist
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user