Compare commits
2 Commits
61ad033520
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cc759ec694 | |||
| f0fc70eac6 |
@@ -19,8 +19,7 @@ type MessageService interface {
|
|||||||
GetByID(userID, id int64) (*Message, error)
|
GetByID(userID, id int64) (*Message, error)
|
||||||
Create(userID int64, in CreateMessageInput) (int64, error)
|
Create(userID int64, in CreateMessageInput) (int64, error)
|
||||||
Archive(userID, id int64) error
|
Archive(userID, id int64) error
|
||||||
//Restore()
|
|
||||||
//ToDo: implement
|
|
||||||
Unarchive(userID, id int64) error
|
Unarchive(userID, id int64) error
|
||||||
//MarkRead(userID, id int64) error
|
MarkRead(userID, id int64) error
|
||||||
|
//MarkUnread(userID, id int64) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package accountMessageHandler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -137,3 +138,36 @@ func (h *AccountMessageHandlers) ReadGet(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
|
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AccountMessageHandlers) MarkReadPost(c *gin.Context) {
|
||||||
|
app := c.MustGet("app").(*bootstrap.App)
|
||||||
|
sm := app.SessionManager
|
||||||
|
userID := mustUserID(c)
|
||||||
|
|
||||||
|
idStr := c.PostForm("id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
sm.Put(c.Request.Context(), "flash", "Invalid message id.")
|
||||||
|
c.Redirect(http.StatusSeeOther, c.Request.Referer()) // back to where they came from
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Svc.MarkRead(userID, id); err != nil {
|
||||||
|
logging.Info("❌ MarkRead error: %v", err)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
sm.Put(c.Request.Context(), "flash", "Message not found or not permitted.")
|
||||||
|
} else {
|
||||||
|
sm.Put(c.Request.Context(), "flash", "Could not mark message as read.")
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/messages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.Put(c.Request.Context(), "flash", "Message marked as read.")
|
||||||
|
// Redirect back to referer when possible so UX is smooth.
|
||||||
|
if ref := c.Request.Referer(); ref != "" {
|
||||||
|
c.Redirect(http.StatusSeeOther, ref)
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ func RegisterAccountRoutes(app *bootstrap.App) {
|
|||||||
messages.GET("/archive", msgH.ArchivedList) // view archived messages
|
messages.GET("/archive", msgH.ArchivedList) // view archived messages
|
||||||
messages.POST("/archive", msgH.ArchivePost) // archive a message
|
messages.POST("/archive", msgH.ArchivePost) // archive a message
|
||||||
messages.POST("/restore", msgH.RestoreArchived)
|
messages.POST("/restore", msgH.RestoreArchived)
|
||||||
|
messages.POST("/mark-read", msgH.MarkReadPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications (auth-required)
|
// Notifications (auth-required)
|
||||||
|
|||||||
@@ -277,3 +277,25 @@ func (s *Service) Unarchive(userID, id int64) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) MarkRead(userID, id int64) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := `
|
||||||
|
UPDATE user_messages
|
||||||
|
SET is_read = 1
|
||||||
|
WHERE id = ? AND recipientId = ?
|
||||||
|
`
|
||||||
|
q = s.bind(q)
|
||||||
|
|
||||||
|
res, err := s.DB.ExecContext(ctx, q, id, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, _ := res.RowsAffected()
|
||||||
|
if n == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,16 +5,33 @@
|
|||||||
{{ if .Messages }}
|
{{ if .Messages }}
|
||||||
<ul class="list-group mb-4">
|
<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 {{ if .IsRead }}read{{ end }}" data-msg-id="{{ .ID }}">
|
||||||
<div>
|
<div>
|
||||||
<a href="/account/messages/read?id={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a><br>
|
<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>
|
<small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/account/messages/archive" class="m-0">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
<input type="hidden" name="id" value="{{ .ID }}">
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
|
{{/* Archive form (existing) */}}
|
||||||
</form>
|
<form method="POST" action="/account/messages/archive" class="m-0">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
||||||
|
<input type="hidden" name="id" value="{{ .ID }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{/* Mark-read: only show when unread */}}
|
||||||
|
{{ if not .IsRead }}
|
||||||
|
<!-- Non-AJAX fallback form (submit will refresh) -->
|
||||||
|
<form method="POST" action="/account/messages/mark-read" class="m-0 d-inline-block mark-read-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
||||||
|
<input type="hidden" name="id" value="{{ .ID }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary mark-read-btn"
|
||||||
|
data-msg-id="{{ .ID }}"
|
||||||
|
data-csrf="{{ $.CSRFToken }}">Mark read</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -49,4 +66,74 @@
|
|||||||
<a href="/account/messages/archive" class="btn btn-outline-secondary ms-2">View Archived</a>
|
<a href="/account/messages/archive" class="btn btn-outline-secondary ms-2">View Archived</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{/* AJAX enhancement: unobtrusive — safe fallback to regular form when JS disabled */}}
|
||||||
|
<script>
|
||||||
|
;(function(){
|
||||||
|
// Ensure browser supports fetch + FormData; otherwise we fallback to regular form submit.
|
||||||
|
if (!window.fetch || !window.FormData) return;
|
||||||
|
|
||||||
|
// Helper to decrement topbar message count badge (assumes badge element id="message-count")
|
||||||
|
function decrementMessageCount() {
|
||||||
|
var el = document.getElementById('message-count');
|
||||||
|
if (!el) return;
|
||||||
|
var current = parseInt(el.textContent || el.innerText || '0', 10) || 0;
|
||||||
|
var next = Math.max(0, current - 1);
|
||||||
|
if (next <= 0) {
|
||||||
|
// remove badge or hide it
|
||||||
|
el.remove();
|
||||||
|
} else {
|
||||||
|
el.textContent = String(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle clicks on mark-read buttons, submit via fetch, update DOM
|
||||||
|
document.addEventListener('click', function(e){
|
||||||
|
var btn = e.target.closest('.mark-read-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
// Prevent the default form POST (non-AJAX fallback)
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var msgID = btn.dataset.msgId;
|
||||||
|
var csrf = btn.dataset.csrf;
|
||||||
|
|
||||||
|
if (!msgID) {
|
||||||
|
// fallback to normal submit if something's wrong
|
||||||
|
var frm = btn.closest('form');
|
||||||
|
if (frm) frm.submit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build urlencoded body like a regular form
|
||||||
|
var body = new URLSearchParams();
|
||||||
|
body.append('id', msgID);
|
||||||
|
if (csrf) body.append('csrf_token', csrf);
|
||||||
|
|
||||||
|
fetch('/account/messages/mark-read', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: body.toString(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(function(resp){
|
||||||
|
if (resp.ok) {
|
||||||
|
// UI update: remove the mark-read button, give item a .read class, update topbar count
|
||||||
|
var li = document.querySelector('li[data-msg-id="' + msgID + '"]');
|
||||||
|
if (li) {
|
||||||
|
li.classList.add('read');
|
||||||
|
// remove any mark-read form/button inside
|
||||||
|
var form = li.querySelector('.mark-read-form');
|
||||||
|
if (form) form.remove();
|
||||||
|
}
|
||||||
|
decrementMessageCount();
|
||||||
|
} else {
|
||||||
|
// If server returned non-2xx, fall back to full reload to show flash
|
||||||
|
resp.text().then(function(){ window.location.reload(); }).catch(function(){ window.location.reload(); });
|
||||||
|
}
|
||||||
|
}).catch(function(){ window.location.reload(); });
|
||||||
|
}, false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -6,17 +6,63 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p>{{ .Message.Body }}</p>
|
<p>{{ .Message.Body }}</p>
|
||||||
|
|
||||||
<form method="POST" action="/account/messages/archive" class="d-inline">
|
<div class="mt-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
<button id="mark-read-btn" data-id="{{ .Message.ID }}" class="btn btn-outline-success">Mark As Read</button>
|
||||||
<input type="hidden" name="id" value="{{ .Message.ID }}">
|
|
||||||
<button type="submit" class="btn btn-outline-danger mt-3">Archive</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
|
<form method="POST" action="/account/messages/archive" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
||||||
|
<input type="hidden" name="id" value="{{ .Message.ID }}">
|
||||||
|
<button type="submit" class="btn btn-outline-danger">Archive</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="/account/messages" class="btn btn-secondary">Back to Inbox</a>
|
||||||
|
</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="alert alert-danger text-center">
|
<div class="alert alert-danger text-center">
|
||||||
Message not found or access denied.
|
Message not found or access denied.
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const btn = document.getElementById("mark-read-btn");
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.addEventListener("click", async function () {
|
||||||
|
const id = this.dataset.id;
|
||||||
|
const res = await fetch("/account/messages/mark-read", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
id: id,
|
||||||
|
csrf_token: "{{ $.CSRFToken }}"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.classList.remove("btn-outline-success");
|
||||||
|
this.classList.add("btn-success");
|
||||||
|
this.textContent = "Marked As Read ✔";
|
||||||
|
|
||||||
|
const badge = document.getElementById("message-count");
|
||||||
|
if (badge) {
|
||||||
|
let count = parseInt(badge.textContent);
|
||||||
|
if (!isNaN(count)) {
|
||||||
|
count = Math.max(count - 1, 0);
|
||||||
|
if (count === 0) {
|
||||||
|
badge.remove();
|
||||||
|
} else {
|
||||||
|
badge.textContent = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Failed to mark as read.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<h2>Log My Ticket</h2>
|
<h2>Log My Ticket</h2>
|
||||||
|
|
||||||
<form method="POST" action="/account/tickets/add_ticket" enctype="multipart/form-data" id="ticketForm">
|
<form method="POST" action="/account/tickets/add_ticket" enctype="multipart/form-data" id="ticketForm">
|
||||||
{{ .csrfField }}
|
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<label>Game:
|
<label>Game:
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ticketLinesContainer">
|
<div id="ticketLinesContainer">
|
||||||
<!-- JS will insert ticket lines here -->
|
<!-- todo, maybe ajax so it doesnt refresh?-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="bi bi-bell fs-5 position-relative">
|
<i class="bi bi-bell fs-5 position-relative">
|
||||||
{{ if gt .NotificationCount 0 }}
|
{{ if gt .NotificationCount 0 }}
|
||||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
<span id="notification-count"
|
||||||
|
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 }}
|
{{ if gt .NotificationCount 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -41,7 +42,6 @@
|
|||||||
aria-labelledby="notificationDropdown">
|
aria-labelledby="notificationDropdown">
|
||||||
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
|
||||||
{{ $total := len .Notifications }}
|
{{ $total := len .Notifications }}
|
||||||
{{ range $i, $n := .Notifications }}
|
{{ range $i, $n := .Notifications }}
|
||||||
<li class="px-3 py-2">
|
<li class="px-3 py-2">
|
||||||
@@ -55,15 +55,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{ if lt (add $i 1) $total }}
|
{{ if lt (add $i 1) $total }}<li><hr class="dropdown-divider"></li>{{ end }}
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ if not .Notifications }}
|
{{ if not .Notifications }}
|
||||||
<li class="text-center text-muted py-2">No notifications</li>
|
<li class="text-center text-muted py-2">No notifications</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<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>
|
<li class="text-center"><a href="/account/notifications" class="dropdown-item">View all notifications</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -75,7 +71,8 @@
|
|||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="bi bi-envelope fs-5 position-relative">
|
<i class="bi bi-envelope fs-5 position-relative">
|
||||||
{{ if gt .MessageCount 0 }}
|
{{ if gt .MessageCount 0 }}
|
||||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">
|
<span id="message-count"
|
||||||
|
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 }}
|
{{ if gt .MessageCount 15 }}15+{{ else }}{{ .MessageCount }}{{ end }}
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -85,7 +82,6 @@
|
|||||||
aria-labelledby="messageDropdown">
|
aria-labelledby="messageDropdown">
|
||||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
|
||||||
{{ if .Messages }}
|
{{ if .Messages }}
|
||||||
{{ range $i, $m := .Messages }}
|
{{ range $i, $m := .Messages }}
|
||||||
<li class="px-3 py-2">
|
<li class="px-3 py-2">
|
||||||
@@ -103,15 +99,15 @@
|
|||||||
{{ 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><hr class="dropdown-divider"></li>
|
||||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Greeting/Dropdown -->
|
<!-- User Dropdown -->
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="nav-link dropdown-toggle text-dark" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle text-dark" href="#" id="userDropdown" role="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
Hello, {{ .User.Username }}
|
Hello, {{ .User.Username }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userDropdown">
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userDropdown">
|
||||||
|
|||||||
Reference in New Issue
Block a user