iPad Friendly Nav Bar + Mobile NavBar Notifications

* iPad in landscape mode was "desktop" size so got the full nav bar but
  the "More" drop-down was unusable. Add work-arounds for large touch
  devices to make the nav bar functional.
* "Click" on the "More" button will pin it open so that the drop-down
  doesn't rely solely on mouseover events. Clicking off the open
  drop-down or clicking again on "More" toggles it hidden.
* The logged-in user menu now drops its menu on hover like "More" did.
* The logged-in user menu adds "TouchStart" events: touching the menu
  button toggles its drop-down to appear, canceling the link to "/me"
  that clicking the menu button does on desktops. Clicking off the open
  drop-down closes it.
* Add notification indicators for "mobile" devices which only showed the
  brand and hamburger menu by default. Next to the hamburger button will
  be badges for number of friend requests or messages, with icons. Click
  the badge to go to the relevant page, or it hints that there are
  notifications in the drop-down.
This commit is contained in:
Noah 2022-08-21 21:24:36 -07:00
parent 4e4d18470f
commit fb0e3651b0
5 changed files with 141 additions and 32 deletions

View File

@ -28,13 +28,18 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
// Defaults // Defaults
m["LoggedIn"] = false m["LoggedIn"] = false
m["CurrentUser"] = nil m["CurrentUser"] = nil
m["NavUnreadMessages"] = 0
m["NavFriendRequests"] = 0
m["NavAdminNotifications"] = 0 // total count of admin notifications for nav
m["NavCertificationPhotos"] = 0 // admin indicator for certification photos
m["NavAdminFeedback"] = 0 // admin indicator for unread feedback
m["SessionImpersonated"] = false m["SessionImpersonated"] = false
// User notification counts for nav bar.
m["NavUnreadMessages"] = 0 // New messages
m["NavFriendRequests"] = 0 // Friend requests
m["NavTotalNotifications"] = 0 // Total of above
// Admin notification counts for nav bar.
m["NavCertificationPhotos"] = 0 // Cert. photos needing approval
m["NavAdminFeedback"] = 0 // Unacknowledged feedback
m["NavAdminNotifications"] = 0 // Total of above
if r == nil { if r == nil {
return return
} }
@ -45,9 +50,21 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
m["LoggedIn"] = true m["LoggedIn"] = true
m["CurrentUser"] = user m["CurrentUser"] = user
// Collect notification counts.
var (
// For users
countMessages int64
countFriendReqs int64
// For admins
countCertPhotos int64
countFeedback int64
)
// Get unread message count. // Get unread message count.
if count, err := models.CountUnreadMessages(user.ID); err == nil { if count, err := models.CountUnreadMessages(user.ID); err == nil {
m["NavUnreadMessages"] = count m["NavUnreadMessages"] = count
countMessages = count
} else { } else {
log.Error("MergeUserVars: couldn't CountUnreadMessages for %d: %s", user.ID, err) log.Error("MergeUserVars: couldn't CountUnreadMessages for %d: %s", user.ID, err)
} }
@ -55,6 +72,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
// Get friend request count. // Get friend request count.
if count, err := models.CountFriendRequests(user.ID); err == nil { if count, err := models.CountFriendRequests(user.ID); err == nil {
m["NavFriendRequests"] = count m["NavFriendRequests"] = count
countFriendReqs = count
} else { } else {
log.Error("MergeUserVars: couldn't CountFriendRequests for %d: %s", user.ID, err) log.Error("MergeUserVars: couldn't CountFriendRequests for %d: %s", user.ID, err)
} }
@ -62,15 +80,16 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
// Are we admin? // Are we admin?
if user.IsAdmin { if user.IsAdmin {
// Any pending certification photos or feedback? // Any pending certification photos or feedback?
var ( countCertPhotos = models.CountCertificationPhotosNeedingApproval()
certPhotos = models.CountCertificationPhotosNeedingApproval() countFeedback = models.CountUnreadFeedback()
feedback = models.CountUnreadFeedback() m["NavCertificationPhotos"] = countCertPhotos
) m["NavAdminFeedback"] = countFeedback
m["NavCertificationPhotos"] = certPhotos
m["NavAdminFeedback"] = feedback
// Total notification count for admin actions. // Total notification count for admin actions.
m["NavAdminNotifications"] = certPhotos + feedback m["NavAdminNotifications"] = countCertPhotos + countFeedback
} }
// Total count for user notifications.
m["NavTotalNotifications"] = countMessages + countFriendReqs + countCertPhotos + countFeedback
} }
} }

View File

@ -31,4 +31,17 @@
.tag:not(body).is-private.is-light { .tag:not(body).is-private.is-light {
color: #CC00CC; color: #CC00CC;
background-color: #FFEEFF; background-color: #FFEEFF;
}
/* Mobile: notification badge near the hamburger menu */
.nonshy-mobile-notification {
position: absolute;
top: 12px;
right: 50px;
z-index: 1000;
}
@media screen and (min-width: 1024px) {
.nonshy-mobile-notification {
display: none;
}
} }

View File

@ -1,29 +1,75 @@
// Hamburger menu script for mobile. // Hamburger menu script for mobile.
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements // Hamburger menu script.
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); (function() {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them // Add a click event on each of them
$navbarBurgers.forEach( el => { $navbarBurgers.forEach( el => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
// Get the target from the "data-target" attribute // Get the target from the "data-target" attribute
const target = el.dataset.target; const target = el.dataset.target;
const $target = document.getElementById(target); const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active'); el.classList.toggle('is-active');
$target.classList.toggle('is-active'); $target.classList.toggle('is-active');
});
}); });
}); })();
// Allow the "More" drop-down to work on mobile (toggle is-active on click instead of requiring mouseover)
(function() {
const menu = document.querySelector("#navbar-more"),
userMenu = document.querySelector("#navbar-user"),
activeClass = "is-active";
if (!menu) return;
// Click the "More" menu to permanently toggle the menu.
menu.addEventListener("click", (e) => {
if (menu.classList.contains(activeClass)) {
menu.classList.remove(activeClass);
} else {
menu.classList.add(activeClass);
}
e.stopPropagation();
});
// Touching the user drop-down button toggles it.
userMenu.addEventListener("touchstart", (e) => {
e.preventDefault();
if (userMenu.classList.contains(activeClass)) {
userMenu.classList.remove(activeClass);
} else {
userMenu.classList.add(activeClass);
}
});
// Touching a link from the user menu lets it click thru.
(document.querySelectorAll(".navbar-dropdown") || []).forEach(node => {
node.addEventListener("touchstart", (e) => {
e.stopPropagation();
});
});
// Clicking the rest of the body will close an active navbar-dropdown.
(document.addEventListener("click", (e) => {
(document.querySelectorAll(".navbar-dropdown.is-active, .navbar-item.is-active") || []).forEach(node => {
node.classList.remove(activeClass);
});
}));
})();
// Common event handlers for bulma modals. // Common event handlers for bulma modals.
(document.querySelectorAll(".modal-background, .modal-close, .photo-modal") || []).forEach(node => { (document.querySelectorAll(".modal-background, .modal-close, .photo-modal") || []).forEach(node => {
const target = node.closest(".modal"); const target = node.closest(".modal");
node.addEventListener("click", () => { node.addEventListener("click", () => {
target.classList.remove("is-active"); target.classList.remove("is-active");
}) });
}) });
}); });

View File

@ -57,7 +57,7 @@
<span>Forums</span> <span>Forums</span>
</a> --> </a> -->
<a class="navbar-item" href="/friends"> <a class="navbar-item" href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
<span class="icon"><i class="fa fa-user-group"></i></span> <span class="icon"><i class="fa fa-user-group"></i></span>
<span>Friends</span> <span>Friends</span>
{{if .NavFriendRequests}}<span class="tag is-warning ml-1">{{.NavFriendRequests}}</span>{{end}} {{if .NavFriendRequests}}<span class="tag is-warning ml-1">{{.NavFriendRequests}}</span>{{end}}
@ -70,16 +70,18 @@
</a> </a>
{{end}} {{end}}
<div class="navbar-item has-dropdown is-hoverable"> <div id="navbar-more" class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
More More
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown is-active">
{{if .LoggedIn}}
<a class="navbar-item" href="/members"> <a class="navbar-item" href="/members">
<span class="icon"><i class="fa fa-people-group"></i></span> <span class="icon"><i class="fa fa-people-group"></i></span>
<span>People</span> <span>People</span>
</a> </a>
{{end}}
<a class="navbar-item" href="/about"> <a class="navbar-item" href="/about">
About About
</a> </a>
@ -105,7 +107,7 @@
<div class="navbar-end"> <div class="navbar-end">
{{if .LoggedIn }} {{if .LoggedIn }}
<div class="navbar-item has-dropdown is-hoverable"> <div id="navbar-user" class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="/me"> <a class="navbar-link" href="/me">
<div class="columns is-mobile is-gapless"> <div class="columns is-mobile is-gapless">
<div class="column is-narrow"> <div class="column is-narrow">
@ -124,7 +126,7 @@
</div> </div>
</a> </a>
<div class="navbar-dropdown is-right"> <div class="navbar-dropdown is-right is-hoverable">
<a class="navbar-item" href="/me">Dashboard</a> <a class="navbar-item" href="/me">Dashboard</a>
<a class="navbar-item" href="/u/{{.CurrentUser.Username}}">My Profile</a> <a class="navbar-item" href="/u/{{.CurrentUser.Username}}">My Profile</a>
<a class="navbar-item" href="/photo/u/{{.CurrentUser.Username}}">My Photos</a> <a class="navbar-item" href="/photo/u/{{.CurrentUser.Username}}">My Photos</a>
@ -161,6 +163,32 @@
</div> </div>
</nav> </nav>
<!-- Mobile: notifications badge next to hamburger menu -->
{{if gt .NavTotalNotifications 0}}
<div class="mobile nonshy-mobile-notification">
{{if gt .NavFriendRequests 0}}
<a class="tag is-warning" href="/friends?view=requests">
<span class="icon"><i class="fa fa-user-group"></i></span>
<span>{{.NavFriendRequests}}</span>
</a>
{{end}}
{{if gt .NavUnreadMessages 0}}
<a class="tag is-warning" href="/messages">
<span class="icon"><i class="fa fa-envelope"></i></span>
<span>{{.NavUnreadMessages}}</span>
</a>
{{end}}
{{if gt .NavAdminNotifications 0}}
<a class="tag is-danger" href="/admin">
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>{{.NavAdminNotifications}}</span>
</a>
{{end}}
</div>
{{end}}
<div class="container is-fullhd"> <div class="container is-fullhd">
{{if .Flashes}} {{if .Flashes}}
<div class="notification block is-success"> <div class="notification block is-success">

View File

@ -90,6 +90,7 @@
</div> </div>
<div class="block"> <div class="block">
<em>Sent on {{.CreatedAt.Format "2006-01-02 15:04:05"}}</em> <em>Sent on {{.CreatedAt.Format "2006-01-02 15:04:05"}}</em>
{{if not .Read}}<span class="tag is-success ml-2">UNREAD</span>{{end}}
</div> </div>
<hr class="block"> <hr class="block">
@ -157,7 +158,9 @@
{{$User := $UserMap.Get .SourceUserID}} {{$User := $UserMap.Get .SourceUserID}}
<strong>From {{$User.Username}}</strong> <strong>From {{$User.Username}}</strong>
{{end}} {{end}}
{{if not .Read}}<span class="tag is-success">NEW</span>{{end}} {{if not .Read}}
<span class="tag is-success">{{if $IsSentBox}}UNREAD{{else}}NEW{{end}}</span>
{{end}}
</div> </div>
<div class="my-1"> <div class="my-1">
<em> <em>