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)
|
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", 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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 = `
|
||||||
|
|||||||
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" }}
|
{{ 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>
|
||||||
{{ if not .IsRead }}
|
<form method="POST" action="/account/messages/archive" class="m-0">
|
||||||
<span class="badge bg-primary">Unread</span>
|
{{ $.CSRFField }}
|
||||||
{{ end }}
|
<input type="hidden" name="id" value="{{ .ID }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
|
||||||
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{{ else }}
|
|
||||||
<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 }}
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user