Feature: complete message inbox, view, and topbar integration
Added users_messages schema with correct field naming (senderId, recipientId) Implemented message count and recent message fetch via storage.GetMessageCount and GetRecentMessages Fixed field mismatches in SQL queries (recipientId vs recipient_id) Displayed unread message badge in topbar with truncation for body preview Linked messages in dropdown to full view (/account/messages/read?id=...) Added fallback handling for unauthorized/invalid message access Cleaned up BuildTemplateData to support full message context Ensured CSRF/session/user context remains intact throughout
This commit is contained in:
@@ -9,17 +9,17 @@ import (
|
||||
func GetMessageCount(db *sql.DB, userID int) (int, error) {
|
||||
var count int
|
||||
err := db.QueryRow(`
|
||||
SELECT COUNT(*) FROM user_messages
|
||||
WHERE recipient_id = ? AND is_read = FALSE
|
||||
SELECT COUNT(*) FROM users_messages
|
||||
WHERE recipientId = ? AND is_read = FALSE
|
||||
`, userID).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, sender_id, recipient_id, subject, body, is_read, created_at
|
||||
FROM user_messages
|
||||
WHERE recipient_id = ?
|
||||
SELECT id, senderId, recipientId, subject, message, is_read, created_at
|
||||
FROM users_messages
|
||||
WHERE recipientId = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`, userID, limit)
|
||||
@@ -49,9 +49,9 @@ func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
|
||||
|
||||
func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id, sender_id, recipient_id, subject, body, is_read, created_at
|
||||
FROM user_messages
|
||||
WHERE id = ? AND recipient_id = ?
|
||||
SELECT id, senderId, recipientId, subject, message, is_read, created_at
|
||||
FROM users_messages
|
||||
WHERE id = ? AND recipientId = ?
|
||||
`, messageID, userID)
|
||||
|
||||
var m models.Message
|
||||
@@ -64,9 +64,9 @@ func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error)
|
||||
|
||||
func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
|
||||
result, err := db.Exec(`
|
||||
UPDATE user_messages
|
||||
UPDATE users_messages
|
||||
SET is_read = TRUE
|
||||
WHERE id = ? AND recipient_id = ?
|
||||
WHERE id = ? AND recipientId = ?
|
||||
`, messageID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -119,7 +119,6 @@ CREATE TABLE IF NOT EXISTS users_messages (
|
||||
subject TEXT NOT NULL,
|
||||
message TEXT,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
type VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h2>{{ .Message.Subject }}</h2>
|
||||
<p class="text-muted">Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}</p>
|
||||
<hr>
|
||||
<p>{{ .Message.Body }}</p>
|
||||
<p>{{ .Message.Message }}</p>
|
||||
<a href="/account/messages" class="btn btn-secondary mt-4">Back to Inbox</a>
|
||||
{{ else }}
|
||||
<div class="alert alert-danger text-center">
|
||||
|
||||
@@ -6,121 +6,114 @@
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-3">
|
||||
{{ if .User }}
|
||||
{{ if .IsAdmin }}
|
||||
<!-- Admin Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link text-dark" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-shield-lock fs-5 position-relative"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="adminDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Admin Menu</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Tools</a></li>
|
||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
<!-- Notification Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link text-dark" href="#" id="notificationDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-bell fs-5 position-relative">
|
||||
{{ if gt ( .NotificationCount) 0 }}
|
||||
<span
|
||||
class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
||||
{{ if gt (.NotificationCount) 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="notificationDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
{{ if .IsAdmin }}
|
||||
<!-- Admin Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link text-dark" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-shield-lock fs-5 position-relative"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="adminDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Admin Menu</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Tools</a></li>
|
||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ $total := len .Notifications }}
|
||||
{{ range $i, $n := .Notifications }}
|
||||
<li class="px-3 py-2">
|
||||
<a href="/account/notifications/read?id={{ $n.ID }}" class="text-decoration-none text-dark d-block">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-info-circle text-primary me-2 fs-4"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $n.Subject }}</div>
|
||||
<small class="text-muted">{{ $n.Body }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{{ if lt (add $i 1) $total }}
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No notifications</li>
|
||||
{{ end }}
|
||||
<!-- Notification Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link text-dark" href="#" id="notificationDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-bell fs-5 position-relative">
|
||||
{{ if gt .NotificationCount 0 }}
|
||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
||||
{{ if gt .NotificationCount 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="notificationDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li class="text-center"><a href="/account/notifications" class="dropdown-item">View all notifications</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Message Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-envelope fs-5 position-relative">
|
||||
{{ if gt .MessageCount 0 }}
|
||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">
|
||||
{{ if gt .MessageCount 15 }}15+{{ else }}{{ .MessageCount }}{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="messageDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
{{ if .Messages }}
|
||||
{{ range $i, $m := .Messages }}
|
||||
{{ $total := len .Notifications }}
|
||||
{{ range $i, $n := .Notifications }}
|
||||
<li class="px-3 py-2">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $m.Subject }}</div>
|
||||
<small class="text-muted">{{ truncate $m.Body 40 }}</small>
|
||||
<a href="/account/notifications/read?id={{ $n.ID }}" class="text-decoration-none text-dark d-block">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-info-circle text-primary me-2 fs-4"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $n.Subject }}</div>
|
||||
<small class="text-muted">{{ $n.Body }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{{ if lt (add $i 1) $total }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{{ end }}
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No messages</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if not .Notifications }}
|
||||
<li class="text-center text-muted py-2">No notifications</li>
|
||||
{{ end }}
|
||||
|
||||
<!-- User Greeting -->
|
||||
<span class="navbar-text">Hello, {{ .User.Username }}</span>
|
||||
<a class="btn btn-outline-danger btn-xs" href="/logout">Logout</a>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/notifications" class="dropdown-item">View all notifications</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Message Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-envelope fs-5 position-relative">
|
||||
{{ if gt .MessageCount 0 }}
|
||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">
|
||||
{{ if gt .MessageCount 15 }}15+{{ else }}{{ .MessageCount }}{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="messageDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
{{ if .Messages }}
|
||||
{{ range $i, $m := .Messages }}
|
||||
<li class="px-3 py-2">
|
||||
<a href="/account/messages/read?id={{ $m.ID }}" class="text-decoration-none text-dark d-block">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $m.Subject }}</div>
|
||||
<small class="text-muted">{{ truncate $m.Message 40 }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No messages</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- User Greeting -->
|
||||
<span class="navbar-text">Hello, {{ .User.Username }}</span>
|
||||
<a class="btn btn-outline-danger btn-xs" href="/logout">Logout</a>
|
||||
{{ else }}
|
||||
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
|
||||
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user