The number of unread alerts now appears in the page title.

Added the Back to Site link to Nox's Control Panel.
Added the panel_group_menu template and used it to de-dupe the group menu HTML.
Fixed a potential race condition with dismiss alert.
Fixed a strange bug where new alerts wouldn't appear.
Fixed a race condition where client templates sometimes wouldn't load.
Dramatically cut down on the number of DOM rebuilds for the alert list.
Added some missing error handling for ajax page block loads.
Fixed a bug where the dimiss alert endpoint wasn't sending a success payload.

Made the register_might_be_machine phrase more descriptive.
Added the panel_menu_aria phrase.
This commit is contained in:
Azareal 2019-03-16 21:31:10 +10:00
parent 414d9c4817
commit cb58c1c83f
14 changed files with 226 additions and 67 deletions

View File

@ -224,7 +224,7 @@ func (list SFileList) JSTmplInit() error {
for name, _ := range Themes {
if strings.HasSuffix(shortName, "_"+name) {
data = append(data, "\nlet Template_"+strings.TrimSuffix(shortName, "_"+name)+" = Template_"+shortName+";"...)
data = append(data, "\nvar Template_"+strings.TrimSuffix(shortName, "_"+name)+" = Template_"+shortName+";"...)
break
}
}

View File

@ -90,7 +90,7 @@
"id_must_be_integer": "The ID must be an integer.",
"url_id_must_be_integer": "The ID in the URL needs to be a valid integer.",
"register_might_be_machine":"You might be a machine.",
"register_might_be_machine":"Our algorithms have detected that you may be a machine. If not, please try to avoid acting so quickly.",
"register_need_username":"You didn't put in a username.",
"register_need_email":"You didn't put in an email.",
"register_first_word_numeric":"The first word of your name must not be purely numeric",
@ -709,6 +709,7 @@
"option_no":"No",
"panel_menu_head":"Control Panel",
"panel_menu_aria":"The control panel menu",
"panel_menu_users":"Users",
"panel_menu_groups":"Groups",
"panel_menu_forums":"Forums",

View File

@ -7,6 +7,7 @@ var moreTopicCount = 0;
var conn = false;
var selectedTopics = [];
var attachItemCallback = function(){}
var baseTitle = document.title;
// Topic move
var forumToMoveTo = 0;
@ -17,9 +18,7 @@ function ajaxError(xhr,status,errstr) {
console.log("xhr", xhr);
console.log("status", status);
console.log("errstr", errstr);
if(status=="parsererror") {
console.log("The server didn't respond with a valid JSON response");
}
if(status=="parsererror") console.log("The server didn't respond with a valid JSON response");
console.trace();
}
@ -30,19 +29,28 @@ function postLink(event) {
}
function bindToAlerts() {
console.log("bindToAlerts");
$(".alertItem.withAvatar a").unbind("click");
$(".alertItem.withAvatar a").click(function(event) {
event.stopPropagation();
$.ajax({ url: "/api/?action=set&module=dismiss-alert", type: "POST", dataType: "json", error: ajaxError, data: { asid: $(this).attr("data-asid") } });
event.preventDefault();
$.ajax({
url: "/api/?action=set&module=dismiss-alert",
type: "POST",
dataType: "json",
data: { asid: $(this).attr("data-asid") },
error: ajaxError,
success: () => {
window.location.href = this.getAttribute("href");
}
});
});
}
function addAlert(msg, notice = false) {
var mmsg = msg.msg;
if("sub" in msg) {
for(var i = 0; i < msg.sub.length; i++) {
mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
}
for(var i = 0; i < msg.sub.length; i++) mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
}
let aItem = Template_alert({
@ -51,7 +59,10 @@ function addAlert(msg, notice = false) {
Avatar: msg.avatar || "",
Message: mmsg
})
alertMapping[msg.asid] = aItem;
//alertMapping[msg.asid] = aItem;
let div = document.createElement('div');
div.innerHTML = aItem.trim();
alertMapping[msg.asid] = div.firstChild;
alertList.push(msg.asid);
if(notice) {
@ -74,21 +85,31 @@ function updateAlertList(menuAlerts) {
let alertCounterNode = menuAlerts.getElementsByClassName("alert_counter")[0];
alertCounterNode.textContent = "0";
let outList = "";
alertListNode.innerHTML = "";
let any = false;
/*let outList = "";
let j = 0;
for(var i = 0; i < alertList.length && j < 8; i++) {
outList += alertMapping[alertList[i]];
j++;
}*/
let j = 0;
for(var i = 0; i < alertList.length && j < 8; i++) {
any = true;
alertListNode.appendChild(alertMapping[alertList[i]]);
//outList += alertMapping[alertList[i]];
j++;
}
if(outList == "") outList = "<div class='alertItem'>"+phraseBox["alerts"]["alerts.no_alerts"]+"</div>";
alertListNode.innerHTML = outList;
if(!any) alertListNode.innerHTML = "<div class='alertItem'>"+phraseBox["alerts"]["alerts.no_alerts"]+"</div>";
if(alertCount != 0) {
alertCounterNode.textContent = alertCount;
menuAlerts.classList.add("has_alerts");
let nTitle = "("+alertCount+") "+baseTitle;
if(document.title!=nTitle) document.title = nTitle;
} else {
menuAlerts.classList.remove("has_alerts");
if(document.title!=baseTitle) document.title = baseTitle;
}
bindToAlerts();
@ -155,14 +176,20 @@ function SplitN(data,ch,n) {
}
function wsAlertEvent(data) {
console.log("wsAlertEvent:",data)
addAlert(data, true);
alertCount++;
var alist = "";
for (var i = 0; i < alertList.length; i++) alist += alertMapping[alertList[i]];
let aTmp = alertList;
alertList = [alertList[alertList.length-1]];
aTmp = aTmp.slice(0,-1);
for(let i = 0; i < aTmp.length; i++) alertList.push(aTmp[i]);
//var alist = "";
//for (var i = 0; i < alertList.length; i++) alist += alertMapping[alertList[i]];
// TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts");
// TODO: Make sure we update alertCount here
updateAlertList(generalAlerts, alist);
updateAlertList(generalAlerts/*, alist*/);
}
function runWebSockets() {
@ -179,9 +206,7 @@ function runWebSockets() {
console.log("The WebSockets connection was opened");
conn.send("page " + document.location.pathname + '\r');
// TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on
if(me.User.ID > 0) {
Notification.requestPermission();
}
if(me.User.ID > 0) Notification.requestPermission();
}
conn.onclose = () => {
@ -291,8 +316,13 @@ function runWebSockets() {
(() => {
addInitHook("pre_init", () => {
console.log("before notify on alert")
// We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :(
notifyOnScriptW("/static/template_alert", () => {}, () => {
notifyOnScriptW("template_alert", (e) => {
if(e!=undefined) console.log("failed alert? why?", e)
}, () => {
console.log("ha")
if(!Template_alert) throw("template function not found");
addInitHook("after_phrases", () => {
// TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred
$(document).ready(() => {
@ -438,10 +468,13 @@ function mainInit(){
// TODO: Try to de-duplicate some of these fetch calls
fetch(url+q+"&js=1", {credentials: "same-origin"})
.then((resp) => resp.json())
.then((data) => {
.then((resp) => {
if(!resp.ok) throw(url+q+"&js=1 failed to load");
return resp.json();
}).then((data) => {
if(!"Topics" in data) throw("no Topics in data");
let topics = data["Topics"];
console.log("ajax navigated to different page");
// TODO: Fix the data race where the function hasn't been loaded yet
let out = "";
@ -470,10 +503,14 @@ function mainInit(){
let url = "//"+window.location.host+"/topics/?fids="+fid;
fetch(url+"&js=1", {credentials: "same-origin"})
.then((resp) => resp.json())
.then((data) => {
.then((resp) => {
if(!resp.ok) throw(url+"&js=1 failed to load");
return resp.json();
}).then((data) => {
console.log("data:",data);
if(!"Topics" in data) throw("no Topics in data");
let topics = data["Topics"];
console.log("ajax navigated to "+that.innerText);
// TODO: Fix the data race where the function hasn't been loaded yet
let out = "";
@ -481,6 +518,9 @@ function mainInit(){
$(".topic_list").html(out);
//$(".topic_list").addClass("single_forum");
baseTitle = that.innerText;
if(alertCount > 0) document.title = "("+alertCount+") "+baseTitle;
else document.title = baseTitle;
let obj = {Title: document.title, Url: url};
history.pushState(obj, obj.Title, obj.Url);
rebuildPaginator(data.LastPage)
@ -515,18 +555,23 @@ function mainInit(){
// TODO: Try to de-duplicate some of these fetch calls
fetch(url+q+"&js=1", {credentials: "same-origin"})
.then((resp) => resp.json())
.then((data) => {
.then((resp) => {
if(!resp.ok) throw(url+q+"&js=1 failed to load");
return resp.json();
}).then((data) => {
if(!"Topics" in data) throw("no Topics in data");
let topics = data["Topics"];
console.log("ajax navigated to search page");
// TODO: Fix the data race where the function hasn't been loaded yet
let out = "";
for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]);
$(".topic_list").html(out);
document.title = phraseBox["topic_list"]["topic_list.search_head"];
baseTitle = phraseBox["topic_list"]["topic_list.search_head"];
$(".topic_list_title h1").text(phraseBox["topic_list"]["topic_list.search_head"]);
if(alertCount > 0) document.title = "("+alertCount+") "+baseTitle;
else document.title = baseTitle;
let obj = {Title: document.title, Url: url+q};
history.pushState(obj, obj.Title, obj.Url);
rebuildPaginator(data.LastPage);

View File

@ -81,23 +81,30 @@ function asyncGetScript(source) {
}
function notifyOnScript(source) {
source = "/static/"+source;
return new Promise((resolve, reject) => {
let ss = source.replace("/static/","");
try {
let ssp = ss.charAt(0).toUpperCase() + ss.slice(1)
console.log("ssp:",ssp)
if(window[ssp]) {
resolve();
return;
}
} catch(e) {}
console.log("source:",source)
let script = document.querySelectorAll('[src^="'+source+'"]')[0];
console.log("script:",script);
if(script===undefined) {
reject("no script found");
return;
}
if(!script.readyState) {
resolve();
return;
}
const onloadHandler = (e, isAbort) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
script.onload = null;
script.onreadystatechange = null;
isAbort ? reject(e) : resolve();
}
const onloadHandler = (e) => {
script.onload = null;
script.onreadystatechange = null;
resolve();
}
script.onerror = (e) => {
@ -105,7 +112,6 @@ function notifyOnScript(source) {
};
script.onload = onloadHandler;
script.onreadystatechange = onloadHandler;
script.src = source;
});
}
@ -119,7 +125,7 @@ function notifyOnScriptW(name, complete, success) {
console.log("Unable to get script name '"+name+"'");
console.log("e: ", e);
console.trace();
complete();
complete(e);
});
}
@ -168,6 +174,7 @@ function RelativeTime(date) {
function initPhrases() {
console.log("in initPhrases")
console.log("tmlInits:",tmplInits)
fetchPhrases("status,topic_list,alerts,paginator,analytics")
}
@ -206,11 +213,13 @@ function fetchPhrases(plist) {
runInitHook("pre_iife");
let toLoad = 2;
// TODO: Shunt this into loggedIn if there aren't any search and filter widgets?
notifyOnScriptW("/static/template_topics_topic", () => {
notifyOnScriptW("template_topics_topic", () => {
if(!Template_topics_topic) throw("template function not found");
toLoad--;
if(toLoad===0) initPhrases();
});
notifyOnScriptW("/static/template_paginator", () => {
notifyOnScriptW("template_paginator", () => {
if(!Template_paginator) throw("template function not found");
toLoad--;
if(toLoad===0) initPhrases();
});

View File

@ -63,6 +63,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
if common.EnableWebsockets && count > 0 {
_ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`)
}
w.Write(successJSONBytes)
// TODO: Split this into it's own function
case "alerts": // A feed of events tailored for a specific user
if !user.Loggedin {

View File

@ -778,8 +778,7 @@ func AccountPasswordReset(w http.ResponseWriter, r *http.Request, user common.Us
header.AddNotice("password_reset_email_sent")
}
header.Title = phrases.GetTitlePhrase("password_reset")
pi := common.Page{header, tList, nil}
return renderTemplate("password_reset", w, r, header, pi)
return renderTemplate("password_reset", w, r, header, common.Page{header, tList, nil})
}
// TODO: Ratelimit this

View File

@ -1,16 +1,6 @@
{{template "header.html" . }}
<div class="colstack panel_stack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>
{{template "panel_group_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div>

View File

@ -1,16 +1,6 @@
{{template "header.html" . }}
<div class="colstack panel_stack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>
{{template "panel_group_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div>

View File

@ -0,0 +1,11 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>

View File

@ -1 +1 @@
<nav class="colstack_left" aria-label="The control panel menu">{{template "panel_inner_menu.html" . }}</nav>
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">{{template "panel_inner_menu.html" . }}</nav>

View File

@ -0,0 +1,14 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<div class="rowitem back_to_site"><a href="/panel/">Back to site</a></div>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>

View File

@ -0,0 +1,91 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/">{{lang "panel_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive">
<a href="/panel/users/">{{lang "panel_menu_users"}}</a> <a class="menu_stats" href="#">({{.Stats.Users}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/groups/">{{lang "panel_menu_groups"}}</a> <a class="menu_stats" href="#">({{.Stats.Groups}})</a>
</div>
{{if .CurrentUser.Perms.ManageForums}}<div class="rowitem passive">
<a href="/panel/forums/">{{lang "panel_menu_forums"}}</a> <a class="menu_stats" href="#">({{.Stats.Forums}})</a>
</div>{{end}}
<div class="rowitem passive">
<a href="/panel/pages/">{{lang "panel_menu_pages"}}</a> <a class="menu_stats" href="#">({{.Stats.Pages}})</a>
</div>
{{if .CurrentUser.Perms.EditSettings}}<div class="rowitem passive">
<a href="/panel/settings/">{{lang "panel_menu_settings"}}</a> <a class="menu_stats" href="#">({{.Stats.Settings}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/settings/word-filters/">{{lang "panel_menu_word_filters"}}</a> <a class="menu_stats" href="#">({{.Stats.WordFilters}})</a>
</div>{{end}}
{{if .CurrentUser.Perms.ManageThemes}}
<div class="rowitem passive">
<a href="/panel/themes/">{{lang "panel_menu_themes"}}</a> <a class="menu_stats" href="#">({{.Stats.Themes}})</a>
</div>
{{if eq .Zone "themes"}}
<div class="rowitem passive submenu"><a href="/panel/themes/menus/">{{lang "panel_menu_menus"}}</a></div>
<div class="rowitem passive submenu"><a href="/panel/themes/widgets/">{{lang "panel_menu_widgets"}}</a></div>
{{end}}
{{end}}
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="#">{{lang "panel_menu_events"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive">
<a href="/panel/analytics/views/">{{lang "panel_menu_statistics"}}</a>
</div>
{{if eq .Zone "analytics"}}
<div class="rowitem passive submenu">
<a href="/panel/analytics/posts/">{{lang "panel_menu_statistics_posts"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/topics/">{{lang "panel_menu_statistics_topics"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/forums/">{{lang "panel_menu_statistics_forums"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/routes/">{{lang "panel_menu_statistics_routes"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/agents/">{{lang "panel_menu_statistics_agents"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/systems/">{{lang "panel_menu_statistics_systems"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/langs/">{{lang "panel_menu_statistics_languages"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/referrers/">{{lang "panel_menu_statistics_referrers"}}</a>
</div>
{{end}}
<div class="rowitem passive">
<a href="/forum/{{.ReportForumID}}">{{lang "panel_menu_reports"}}</a> <a class="menu_stats" href="#">({{.Stats.Reports}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/logs/mod/">{{lang "panel_menu_logs"}}</a>
</div>
{{if eq .Zone "logs"}}
<div class="rowitem passive submenu"><a href="/panel/logs/regs/">{{lang "panel_menu_logs_registrations"}}</a></div>
<div class="rowitem passive submenu"><a href="/panel/logs/mod/">{{lang "panel_menu_logs_moderators"}}</a></div>
{{if .CurrentUser.Perms.ViewAdminLogs}}<div class="rowitem passive submenu"><a>{{lang "panel_menu_logs_administrators"}}</a></div>{{end}}
{{end}}
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="#">{{lang "panel_menu_system"}}</a></div>
</div>
<div class="colstack_item rowmenu">
{{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive">
<a href="/panel/plugins/">{{lang "panel_menu_plugins"}}</a>
</div>{{end}}
{{if .CurrentUser.IsSuperAdmin}}<div class="rowitem passive">
<a href="/panel/backups/">{{lang "panel_menu_backups"}}</a>
</div>{{end}}
{{if .CurrentUser.IsAdmin}}<div class="rowitem passive">
<a href="/panel/debug/">{{lang "panel_menu_debug"}}</a>
</div>{{end}}
</div>

View File

@ -0,0 +1,5 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<div class="rowitem back_to_site"><a href="/panel/">Back to site</a></div>
</div>
{{template "panel_inner_menu.html" . }}</nav>

View File

@ -24,6 +24,9 @@
.menu_stats {
margin-left: 4px;
}
.back_to_site {
font-size: 18px;
}
.colstack_right {
background-color: #333333;