feat: ticket watchers, fulltext search, single-query pagination, watcher notifications
Ticket watchers: - api/watch_ticket.php: GET (watch state) + POST (watch/unwatch toggle) - index.php: route for /api/watch_ticket.php - TicketView: WATCH/UNWATCH button with live state fetch and toggle - NotificationHelper::notifyWatchers(): fetches watchers from DB, resolves Matrix IDs via Synapse, fires notification to watchers + global list - add_comment.php, update_ticket.php: call notifyWatchers on comment and status-change events respectively Fulltext search: - TicketModel::hasFulltextIndex(): detects FULLTEXT index via information_schema - getAllTickets(): uses MATCH...AGAINST when fulltext index exists, LIKE fallback when not yet applied — zero-downtime rollout Single-query pagination: - getAllTickets() replaces separate COUNT + SELECT with COUNT(*) OVER() window function — one round trip to DB per page load instead of two Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,63 @@ class NotificationHelper {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all watchers of a ticket about an update event.
|
||||
*
|
||||
* Fetches watchers from the DB, resolves their Matrix IDs via Synapse,
|
||||
* and fires the appropriate event notification with them in notify_users.
|
||||
*
|
||||
* @param \mysqli $conn
|
||||
* @param string|int $ticketId
|
||||
* @param string $ticketTitle
|
||||
* @param string $event One of: status_changed, comment_added, assigned
|
||||
* @param array $extraData Merged into the payload (old_status/new_status, author, etc.)
|
||||
* @param int|null $excludeUserId Don't notify the actor themselves
|
||||
*/
|
||||
public static function notifyWatchers(\mysqli $conn, $ticketId, string $ticketTitle, string $event, array $extraData = [], ?int $excludeUserId = null): void {
|
||||
$webhookUrl = $GLOBALS['config']['MATRIX_WEBHOOK_URL'] ?? null;
|
||||
$domain = $GLOBALS['config']['MATRIX_DOMAIN'] ?? null;
|
||||
if (!$webhookUrl || !$domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch watcher usernames
|
||||
$sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("i", $ticketId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$stmt->close();
|
||||
|
||||
$usernames = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$usernames[] = $row['username'];
|
||||
}
|
||||
|
||||
if (empty($usernames)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve to Matrix IDs — skip users without Synapse accounts
|
||||
$matrixIds = SynapseHelper::resolveUsernames($usernames);
|
||||
if (empty($matrixIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the global notify list duplicates and build payload
|
||||
$allNotify = array_unique(array_merge($matrixIds, self::notifyUsers()));
|
||||
|
||||
$payload = array_merge($extraData, [
|
||||
'event' => $event,
|
||||
'ticket_id' => $ticketId,
|
||||
'title' => $ticketTitle,
|
||||
'url' => UrlHelper::ticketUrl($ticketId),
|
||||
'notify_users' => array_values($allNotify),
|
||||
]);
|
||||
|
||||
self::fire($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket assigned (or reassigned) to a user.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user