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:
@@ -72,3 +72,67 @@ func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
main.go
2
main.go
@@ -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/read", middleware.Auth(true)(handlers.ReadMessageHandler(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/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db)))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type Message struct {
|
||||
Message string
|
||||
IsRead bool
|
||||
CreatedAt time.Time
|
||||
ArchivedAt *time.Time
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
@@ -85,8 +85,45 @@ func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
|
||||
func ArchiveMessage(db *sql.DB, userID, messageID int) error {
|
||||
_, err := db.Exec(`
|
||||
UPDATE users_messages
|
||||
SET is_archived = TRUE
|
||||
SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND recipientId = ?
|
||||
`, messageID, userID)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -115,12 +115,13 @@ const SchemaUsersMessages = `
|
||||
CREATE TABLE IF NOT EXISTS users_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
senderId INTEGER NOT NULL REFERENCES users(id),
|
||||
recipientId int,
|
||||
recipientId INTEGER NOT NULL REFERENCES users(id),
|
||||
subject TEXT NOT NULL,
|
||||
message TEXT,
|
||||
is_read 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 = `
|
||||
|
||||
39
templates/account/messages/achived.html
Normal file
39
templates/account/messages/achived.html
Normal 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 }}
|
||||
@@ -1,22 +1,51 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Your Messages</h2>
|
||||
<ul class="list-group mt-3">
|
||||
<div class="container py-5">
|
||||
<h2>Your Inbox</h2>
|
||||
{{ if .Messages }}
|
||||
<ul class="list-group mb-4">
|
||||
{{ range .Messages }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="/account/messages/read?id={{ .ID }}" class="{{ if not .IsRead }}fw-bold{{ end }}">
|
||||
{{ .Subject }}
|
||||
</a>
|
||||
<div class="small text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</div>
|
||||
<a href="/account/messages/read?id={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a>
|
||||
<br>
|
||||
<small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small>
|
||||
</div>
|
||||
{{ if not .IsRead }}
|
||||
<span class="badge bg-primary">Unread</span>
|
||||
{{ end }}
|
||||
<form method="POST" action="/account/messages/archive" class="m-0">
|
||||
{{ $.CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .ID }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
|
||||
</form>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li class="list-group-item text-muted text-center">No messages</li>
|
||||
{{ end }}
|
||||
</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>
|
||||
{{ end }}
|
||||
|
||||
27
templates/account/messages/send.html
Normal file
27
templates/account/messages/send.html
Normal 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 }}
|
||||
@@ -100,11 +100,11 @@
|
||||
</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 }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user