Added full message handling system with archive view, pagination, and send support

- Implemented message inbox and archived messages view
- Added pagination logic to both inbox and archive handlers
- Integrated message sending functionality with CSRF protection
- Updated schema to include `archived_at` timestamp
- Included archive button and logic with feedback flash messaging
- Fixed message dropdown routing and rendering in topbar
- Cleaned up template load paths and error handling
This commit is contained in:
2025-04-02 21:29:54 +01:00
parent e3428911b9
commit dd83081271
9 changed files with 221 additions and 21 deletions

View File

@@ -72,3 +72,67 @@ func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
http.Redirect(w, r, "/account/messages", http.StatusSeeOther) http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
} }
} }
func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, 403)
return
}
page := helpers.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
perPage := 10
messages := storage.GetArchivedMessages(db, userID, page, perPage)
hasMore := len(messages) == perPage
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
context["Messages"] = messages
context["Page"] = page
context["HasMore"] = hasMore
tmpl := helpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html")
tmpl.ExecuteTemplate(w, "layout", context)
}
}
func SendMessageHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// Display the form
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
tmpl := helpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html")
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
helpers.RenderError(w, r, 500)
}
case http.MethodPost:
// Handle form submission
senderID, ok := helpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, 403)
return
}
recipientID := helpers.Atoi(r.FormValue("recipient_id"))
subject := r.FormValue("subject")
body := r.FormValue("message")
if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil {
helpers.SetFlash(w, r, "Failed to send message.")
} else {
helpers.SetFlash(w, r, "Message sent.")
}
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
default:
helpers.RenderError(w, r, 405)
}
}
}

View File

@@ -71,6 +71,8 @@ func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/account/messages", middleware.Auth(true)(handlers.MessagesInboxHandler(db))) mux.HandleFunc("/account/messages", middleware.Auth(true)(handlers.MessagesInboxHandler(db)))
mux.HandleFunc("/account/messages/read", middleware.Auth(true)(handlers.ReadMessageHandler(db))) mux.HandleFunc("/account/messages/read", middleware.Auth(true)(handlers.ReadMessageHandler(db)))
mux.HandleFunc("/account/messages/archive", middleware.Auth(true)(handlers.ArchiveMessageHandler(db))) mux.HandleFunc("/account/messages/archive", middleware.Auth(true)(handlers.ArchiveMessageHandler(db)))
mux.HandleFunc("/account/messages/archived", middleware.Auth(true)(handlers.ArchivedMessagesHandler(db)))
mux.HandleFunc("/account/messages/send", middleware.Auth(true)(handlers.SendMessageHandler(db)))
mux.HandleFunc("/account/notifications", middleware.Auth(true)(handlers.NotificationsHandler(db))) mux.HandleFunc("/account/notifications", middleware.Auth(true)(handlers.NotificationsHandler(db)))
mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db))) mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db)))
} }

View File

@@ -30,6 +30,7 @@ type Message struct {
Message string Message string
IsRead bool IsRead bool
CreatedAt time.Time CreatedAt time.Time
ArchivedAt *time.Time
} }
var db *sql.DB var db *sql.DB

View File

@@ -85,8 +85,45 @@ func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
func ArchiveMessage(db *sql.DB, userID, messageID int) error { func ArchiveMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(` _, err := db.Exec(`
UPDATE users_messages UPDATE users_messages
SET is_archived = TRUE SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
WHERE id = ? AND recipientId = ? WHERE id = ? AND recipientId = ?
`, messageID, userID) `, messageID, userID)
return err return err
} }
func SendMessage(db *sql.DB, senderID, recipientID int, subject, message string) error {
_, err := db.Exec(`
INSERT INTO users_messages (senderId, recipientId, subject, message)
VALUES (?, ?, ?, ?)
`, senderID, recipientID, subject, message)
return err
}
func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
offset := (page - 1) * perPage
rows, err := db.Query(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at, archived_at
FROM users_messages
WHERE recipientId = ? AND is_archived = TRUE
ORDER BY archived_at DESC
LIMIT ? OFFSET ?
`, userID, perPage, offset)
if err != nil {
return nil
}
defer rows.Close()
var messages []models.Message
for rows.Next() {
var m models.Message
err := rows.Scan(
&m.ID, &m.SenderId, &m.RecipientId,
&m.Subject, &m.Message, &m.IsRead,
&m.CreatedAt, &m.ArchivedAt,
)
if err == nil {
messages = append(messages, m)
}
}
return messages
}

View File

@@ -115,12 +115,13 @@ const SchemaUsersMessages = `
CREATE TABLE IF NOT EXISTS users_messages ( CREATE TABLE IF NOT EXISTS users_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
senderId INTEGER NOT NULL REFERENCES users(id), senderId INTEGER NOT NULL REFERENCES users(id),
recipientId int, recipientId INTEGER NOT NULL REFERENCES users(id),
subject TEXT NOT NULL, subject TEXT NOT NULL,
message TEXT, message TEXT,
is_read BOOLEAN DEFAULT FALSE, is_read BOOLEAN DEFAULT FALSE,
is_archived BOOLEAN DEFAULT FALSE, is_archived BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
archived_at TIMESTAMP
);` );`
const SchemaUsersNotifications = ` const SchemaUsersNotifications = `

View File

@@ -0,0 +1,39 @@
{{ define "content" }}
<div class="container py-4">
<h2>Archived Messages</h2>
{{ if .Messages }}
{{ range .Messages }}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ .Subject }}</h5>
<p class="card-text">{{ .Message }}</p>
<p class="card-text">
<small class="text-muted">Archived: {{ .ArchivedAt.Format "02 Jan 2006 15:04" }}</small>
</p>
</div>
</div>
{{ end }}
<!-- Pagination Controls -->
<nav>
<ul class="pagination">
{{ if gt .Page 1 }}
<li class="page-item">
<a class="page-link" href="?page={{ minus1 .Page }}">Previous</a>
</li>
{{ end }}
{{ if .HasMore }}
<li class="page-item">
<a class="page-link" href="?page={{ plus1 .Page }}">Next</a>
</li>
{{ end }}
</ul>
</nav>
{{ else }}
<div class="alert alert-info text-center">No archived messages.</div>
{{ end }}
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
</div>
{{ end }}

View File

@@ -1,22 +1,51 @@
{{ define "content" }} {{ define "content" }}
<div class="container py-4"> <div class="container py-5">
<h2>Your Messages</h2> <h2>Your Inbox</h2>
<ul class="list-group mt-3"> {{ if .Messages }}
<ul class="list-group mb-4">
{{ range .Messages }} {{ range .Messages }}
<li class="list-group-item d-flex justify-content-between align-items-center"> <li class="list-group-item d-flex justify-content-between align-items-center">
<div> <div>
<a href="/account/messages/read?id={{ .ID }}" class="{{ if not .IsRead }}fw-bold{{ end }}"> <a href="/account/messages/read?id={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a>
{{ .Subject }} <br>
</a> <small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small>
<div class="small text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</div> </div>
</div> <form method="POST" action="/account/messages/archive" class="m-0">
{{ if not .IsRead }} {{ $.CSRFField }}
<span class="badge bg-primary">Unread</span> <input type="hidden" name="id" value="{{ .ID }}">
{{ end }} <button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
</li> </form>
{{ else }} </li>
<li class="list-group-item text-muted text-center">No messages</li>
{{ end }} {{ end }}
</ul> </ul>
<!-- Pagination -->
<nav>
<ul class="pagination">
{{ if gt .CurrentPage 1 }}
<li class="page-item">
<a class="page-link" href="?page={{ sub .CurrentPage 1 }}">Previous</a>
</li>
{{ end }}
{{ range $i := .PageRange }}
<li class="page-item {{ if eq $i $.CurrentPage }}active{{ end }}">
<a class="page-link" href="?page={{ $i }}">{{ $i }}</a>
</li>
{{ end }}
{{ if lt .CurrentPage .TotalPages }}
<li class="page-item">
<a class="page-link" href="?page={{ add .CurrentPage 1 }}">Next</a>
</li>
{{ end }}
</ul>
</nav>
{{ else }}
<div class="alert alert-info">No messages found.</div>
{{ end }}
<a href="/account/messages/send" class="btn btn-primary mt-3">Compose Message</a>
</div> </div>
{{ end }} {{ end }}

View File

@@ -0,0 +1,27 @@
{{ define "content" }}
<div class="container py-5">
<h2>Send a Message</h2>
{{ if .Flash }}
<div class="alert alert-info">{{ .Flash }}</div>
{{ end }}
<form method="POST" action="/account/messages/send">
{{ .CSRFField }}
<div class="mb-3">
<label for="recipient_id" class="form-label">Recipient User ID</label>
<input type="number" class="form-control" name="recipient_id" required>
</div>
<div class="mb-3">
<label for="subject" class="form-label">Subject</label>
<input type="text" class="form-control" name="subject" required>
</div>
<div class="mb-3">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
</div>
{{ end }}

View File

@@ -100,11 +100,11 @@
</a> </a>
</li> </li>
{{ end }} {{ end }}
<li><hr class="dropdown-divider"></li>
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
{{ else }} {{ else }}
<li class="text-center text-muted py-2">No messages</li> <li class="text-center text-muted py-2">No messages</li>
{{ end }} {{ end }}
<li><hr class="dropdown-divider"></li>
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
</ul> </ul>
</div> </div>