From cb58c1c83f5b3322b618770a158f28e54aa88a12 Mon Sep 17 00:00:00 2001 From: Azareal Date: Sat, 16 Mar 2019 21:31:10 +1000 Subject: [PATCH] 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. --- common/files.go | 2 +- langs/english.json | 3 +- public/global.js | 97 ++++++++++++++++------ public/init.js | 37 +++++---- routes.go | 1 + routes/account.go | 3 +- templates/panel_group_edit.html | 12 +-- templates/panel_group_edit_perms.html | 12 +-- templates/panel_group_menu.html | 11 +++ templates/panel_menu.html | 2 +- themes/nox/overrides/panel_group_menu.html | 14 ++++ themes/nox/overrides/panel_inner_menu.html | 91 ++++++++++++++++++++ themes/nox/overrides/panel_menu.html | 5 ++ themes/nox/public/panel.css | 3 + 14 files changed, 226 insertions(+), 67 deletions(-) create mode 100644 templates/panel_group_menu.html create mode 100644 themes/nox/overrides/panel_group_menu.html create mode 100644 themes/nox/overrides/panel_inner_menu.html create mode 100644 themes/nox/overrides/panel_menu.html diff --git a/common/files.go b/common/files.go index f192e2d9..468945ed 100644 --- a/common/files.go +++ b/common/files.go @@ -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 } } diff --git a/langs/english.json b/langs/english.json index b9593f14..09f11142 100644 --- a/langs/english.json +++ b/langs/english.json @@ -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", diff --git a/public/global.js b/public/global.js index e8474753..f1f5949f 100644 --- a/public/global.js +++ b/public/global.js @@ -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 = "
"+phraseBox["alerts"]["alerts.no_alerts"]+"
"; - alertListNode.innerHTML = outList; + if(!any) alertListNode.innerHTML = "
"+phraseBox["alerts"]["alerts.no_alerts"]+"
"; 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); diff --git a/public/init.js b/public/init.js index 1c1e0d1e..5c50a79d 100644 --- a/public/init.js +++ b/public/init.js @@ -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(); }); diff --git a/routes.go b/routes.go index 629fd906..0f74669f 100644 --- a/routes.go +++ b/routes.go @@ -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 { diff --git a/routes/account.go b/routes/account.go index a36a3732..78d0c392 100644 --- a/routes/account.go +++ b/routes/account.go @@ -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 diff --git a/templates/panel_group_edit.html b/templates/panel_group_edit.html index 2a25fc7d..205f50b9 100644 --- a/templates/panel_group_edit.html +++ b/templates/panel_group_edit.html @@ -1,16 +1,6 @@ {{template "header.html" . }}
- +{{template "panel_group_menu.html" . }}

{{.Name}}{{lang "panel_group_head_suffix"}}

diff --git a/templates/panel_group_edit_perms.html b/templates/panel_group_edit_perms.html index 93390594..c479d0e3 100644 --- a/templates/panel_group_edit_perms.html +++ b/templates/panel_group_edit_perms.html @@ -1,16 +1,6 @@ {{template "header.html" . }}
- +{{template "panel_group_menu.html" . }}

{{.Name}}{{lang "panel_group_head_suffix"}}

diff --git a/templates/panel_group_menu.html b/templates/panel_group_menu.html new file mode 100644 index 00000000..6794ebd4 --- /dev/null +++ b/templates/panel_group_menu.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/templates/panel_menu.html b/templates/panel_menu.html index d0bc677d..dd39062a 100644 --- a/templates/panel_menu.html +++ b/templates/panel_menu.html @@ -1 +1 @@ - + diff --git a/themes/nox/overrides/panel_group_menu.html b/themes/nox/overrides/panel_group_menu.html new file mode 100644 index 00000000..98be77e0 --- /dev/null +++ b/themes/nox/overrides/panel_group_menu.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/themes/nox/overrides/panel_inner_menu.html b/themes/nox/overrides/panel_inner_menu.html new file mode 100644 index 00000000..bdc5d943 --- /dev/null +++ b/themes/nox/overrides/panel_inner_menu.html @@ -0,0 +1,91 @@ + + + + + +
+ {{if .CurrentUser.Perms.ManagePlugins}}{{end}} + {{if .CurrentUser.IsSuperAdmin}}{{end}} + {{if .CurrentUser.IsAdmin}}{{end}} +
diff --git a/themes/nox/overrides/panel_menu.html b/themes/nox/overrides/panel_menu.html new file mode 100644 index 00000000..09fb2b3d --- /dev/null +++ b/themes/nox/overrides/panel_menu.html @@ -0,0 +1,5 @@ + diff --git a/themes/nox/public/panel.css b/themes/nox/public/panel.css index 176d1235..3d143b4c 100644 --- a/themes/nox/public/panel.css +++ b/themes/nox/public/panel.css @@ -24,6 +24,9 @@ .menu_stats { margin-left: 4px; } +.back_to_site { + font-size: 18px; +} .colstack_right { background-color: #333333;