Reduce the amount of data sent for alert resumes in preparation for moving resumes to the WebSockets endpoint.

Remove a bit of superfluous information from the avatar alt attributes.
Improved upon the meta store tests.
Move float: right; out of the footer and into the Tempra Simple and Shadow stylesheets.
Added a missing level label to Shadow.
This commit is contained in:
Azareal 2019-05-12 09:07:24 +10:00
parent afb94eb1d1
commit c2f2dd7f10
21 changed files with 138 additions and 92 deletions

View File

@ -11,6 +11,7 @@ import (
"errors" "errors"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/Azareal/Gosora/common/phrases" "github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/query_gen"
@ -23,6 +24,7 @@ type Alert struct {
Event string Event string
ElementType string ElementType string
ElementID int ElementID int
CreatedAt time.Time
Actor *User Actor *User
} }
@ -42,14 +44,14 @@ var alertStmts AlertStmts
func init() { func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
alertStmts = AlertStmts{ alertStmts = AlertStmts{
addActivity: acc.Insert("activity_stream").Columns("actor, targetUser, event, elementType, elementID").Fields("?,?,?,?,?").Prepare(), addActivity: acc.Insert("activity_stream").Columns("actor, targetUser, event, elementType, elementID, createdAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
notifyWatchers: acc.SimpleInsertInnerJoin( notifyWatchers: acc.SimpleInsertInnerJoin(
qgen.DBInsert{"activity_stream_matches", "watcher, asid", ""}, qgen.DBInsert{"activity_stream_matches", "watcher, asid", ""},
qgen.DBJoin{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""}, qgen.DBJoin{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""},
), ),
notifyOne: acc.Insert("activity_stream_matches").Columns("watcher, asid").Fields("?,?").Prepare(), notifyOne: acc.Insert("activity_stream_matches").Columns("watcher, asid").Fields("?,?").Prepare(),
getWatchers: acc.SimpleInnerJoin("activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""), getWatchers: acc.SimpleInnerJoin("activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""),
getActivityEntry: acc.Select("activity_stream").Columns("actor, targetUser, event, elementType, elementID").Where("asid = ?").Prepare(), getActivityEntry: acc.Select("activity_stream").Columns("actor, targetUser, event, elementType, elementID, createdAt").Where("asid = ?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -237,7 +239,7 @@ func notifyWatchers(asid int64) {
} }
var alert = Alert{ASID: int(asid)} var alert = Alert{ASID: int(asid)}
err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID) err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID, &alert.CreatedAt)
if err != nil && err != ErrNoRows { if err != nil && err != ErrNoRows {
LogError(err) LogError(err)
return return

2
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/go-sql-driver/mysql v1.4.0 github.com/go-sql-driver/mysql v1.4.0
github.com/gorilla/websocket v1.4.0 github.com/gorilla/websocket v1.4.0
github.com/lib/pq v1.0.0 github.com/lib/pq v1.0.0
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983
github.com/olivere/elastic v6.2.16+incompatible // indirect github.com/olivere/elastic v6.2.16+incompatible // indirect
github.com/oschwald/geoip2-golang v1.2.1 github.com/oschwald/geoip2-golang v1.2.1
github.com/oschwald/maxminddb-golang v1.3.0 // indirect github.com/oschwald/maxminddb-golang v1.3.0 // indirect

2
go.sum
View File

@ -45,6 +45,8 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 h1:wL11wNW7dhKIcRCHSm4sHKPWz0tt4mwBsVodG7+Xyqg=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/olivere/elastic v6.2.16+incompatible h1:+mQIHbkADkOgq9tFqnbyg7uNFVV6swGU07EoK1u0nEQ= github.com/olivere/elastic v6.2.16+incompatible h1:+mQIHbkADkOgq9tFqnbyg7uNFVV6swGU07EoK1u0nEQ=

View File

@ -990,13 +990,26 @@ func TestPhrases(t *testing.T) {
func TestMetaStore(t *testing.T) { func TestMetaStore(t *testing.T) {
m, err := c.Meta.Get("magic") m, err := c.Meta.Get("magic")
expect(t, m == "", "meta var should be empty") expect(t, m == "", "meta var magic should be empty")
recordMustNotExist(t, err, "meta var shouldn't exist") recordMustNotExist(t, err, "meta var magic should not exist")
err = c.Meta.Set("magic","lol") err = c.Meta.Set("magic","lol")
expectNilErr(t,err) expectNilErr(t,err)
m, err = c.Meta.Get("magic") m, err = c.Meta.Get("magic")
expectNilErr(t,err) expectNilErr(t,err)
expect(t,m=="lol","meta var should be lol") expect(t,m=="lol","meta var magic should be lol")
err = c.Meta.Set("magic","wha")
expectNilErr(t,err)
m, err = c.Meta.Get("magic")
expectNilErr(t,err)
expect(t,m=="wha","meta var magic should be wha")
m, err = c.Meta.Get("giggle")
expect(t, m == "", "meta var giggle should be empty")
recordMustNotExist(t, err, "meta var giggle should not exist")
} }
func TestWordFilters(t *testing.T) { func TestWordFilters(t *testing.T) {

View File

@ -54,7 +54,7 @@ func initMySQL() (err error) {
// TODO: Is there a less noisy way of doing this for tests? // TODO: Is there a less noisy way of doing this for tests?
log.Print("Preparing getActivityFeedByWatcher statement.") log.Print("Preparing getActivityFeedByWatcher statement.")
stmts.getActivityFeedByWatcher, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM `activity_stream_matches` INNER JOIN `activity_stream` ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE `watcher` = ? ORDER BY activity_stream.asid DESC LIMIT 16") stmts.getActivityFeedByWatcher, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID, activity_stream.createdAt FROM `activity_stream_matches` INNER JOIN `activity_stream` ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE `watcher` = ? ORDER BY activity_stream.asid DESC LIMIT 16")
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }

View File

@ -49,7 +49,7 @@ function bindToAlerts() {
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: { id: $(this).attr("data-asid") }, data: { id: $(this).attr("data-asid") },
error: ajaxError, //error: ajaxError,
success: () => { success: () => {
window.location.href = this.getAttribute("href"); window.location.href = this.getAttribute("href");
} }
@ -134,12 +134,15 @@ function setAlertError(menuAlerts,msg) {
} }
var alertsInitted = false; var alertsInitted = false;
function loadAlerts(menuAlerts) { var lastTc = 0;
function loadAlerts(menuAlerts, eTc = false) {
if(!alertsInitted) return; if(!alertsInitted) return;
let tc = "";
if(eTc && lastTc != 0) tc = "&t=" + lastTc + "&c=" + alertCount;
$.ajax({ $.ajax({
type: 'get', type: 'get',
dataType: 'json', dataType: 'json',
url:'/api/?module=alerts', url:'/api/?module=alerts' + tc,
success: (data) => { success: (data) => {
if("errmsg" in data) { if("errmsg" in data) {
setAlertError(menuAlerts,data.errmsg) setAlertError(menuAlerts,data.errmsg)
@ -147,11 +150,17 @@ function loadAlerts(menuAlerts) {
} }
alertList = []; alertList = [];
alertMapping = {}; alertMapping = {};
if(!data.hasOwnProperty("msgs")) data = {"msgs":[],"count":0}; if(!data.hasOwnProperty("msgs")) data = {"msgs":[],"count":alertCount,"tc":lastTc};
for(var i in data.msgs) addAlert(data.msgs[i]); /*else if(data.count != (alertCount + data.msgs.length)) tc = false;
console.log("data.count:",data.count) if(eTc && lastTc != 0) {
alertCount = data.count; for(var i in data.msgs) wsAlertEvent(data.msgs[i]);
updateAlertList(menuAlerts) } else {*/
for(var i in data.msgs) addAlert(data.msgs[i]);
console.log("data.count:",data.count);
alertCount = data.count;
updateAlertList(menuAlerts);
//}
lastTc = data.tc;
}, },
error: (magic,theStatus,error) => { error: (magic,theStatus,error) => {
let errtxt = "Unable to get the alerts"; let errtxt = "Unable to get the alerts";
@ -196,11 +205,10 @@ function wsAlertEvent(data) {
alertList = [alertList[alertList.length-1]]; alertList = [alertList[alertList.length-1]];
aTmp = aTmp.slice(0,-1); aTmp = aTmp.slice(0,-1);
for(let i = 0; i < aTmp.length; i++) alertList.push(aTmp[i]); 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 // TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts"); var generalAlerts = document.getElementById("general_alerts");
// TODO: Make sure we update alertCount here // TODO: Make sure we update alertCount here
lastTc = 0;
updateAlertList(generalAlerts/*, alist*/); updateAlertList(generalAlerts/*, alist*/);
} }
@ -259,28 +267,27 @@ function runWebSockets(resume = false) {
else if("event" in data) { else if("event" in data) {
if(data.event == "dismiss-alert"){ if(data.event == "dismiss-alert"){
Object.keys(alertMapping).forEach((key) => { Object.keys(alertMapping).forEach((key) => {
if(key==data.id) { if(key!=data.id) return;
alertCount--; alertCount--;
let index = -1; let index = -1;
for(var i = 0; i < alertList.length; i++) { for(var i = 0; i < alertList.length; i++) {
if(alertList[i]==key) { if(alertList[i]==key) {
alertList[i] = 0; alertList[i] = 0;
index = i; index = i;
}
} }
if(index==-1) return;
for(var i = index; (i+1) < alertList.length; i++) {
alertList[i] = alertList[i+1];
}
alertList.splice(alertList.length-1,1);
delete alertMapping[key];
// TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts");
if(alertList.length < 8) loadAlerts(generalAlerts);
else updateAlertList(generalAlerts);
} }
if(index==-1) return;
for(var i = index; (i+1) < alertList.length; i++) {
alertList[i] = alertList[i+1];
}
alertList.splice(alertList.length-1,1);
delete alertMapping[key];
// TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts");
if(alertList.length < 8) loadAlerts(generalAlerts);
else updateAlertList(generalAlerts);
}); });
} }
} else if("Topics" in data) { } else if("Topics" in data) {
@ -366,18 +373,15 @@ function runWebSockets(resume = false) {
function PageOffset(count, page, perPage) { function PageOffset(count, page, perPage) {
let offset = 0; let offset = 0;
let lastPage = LastPage(count, perPage) let lastPage = LastPage(count, perPage)
if(page > 1) { if(page > 1) offset = (perPage * page) - perPage;
offset = (perPage * page) - perPage else if (page == -1) {
} else if (page == -1) { page = lastPage;
page = lastPage offset = (perPage * page) - perPage;
offset = (perPage * page) - perPage } else page = 1;
} else {
page = 1
}
// We don't want the offset to overflow the slices, if everything's in memory // We don't want the offset to overflow the slices, if everything's in memory
//if(offset >= (count - 1)) offset = 0; //if(offset >= (count - 1)) offset = 0;
return {Offset:offset, Page:page, LastPage:lastPage} return {Offset:offset, Page:page, LastPage:lastPage};
} }
function LastPage(count, perPage) { function LastPage(count, perPage) {
return (count / perPage) + 1 return (count / perPage) + 1

View File

@ -10,12 +10,13 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"io"
"errors" "errors"
"io"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"unicode" "unicode"
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
@ -74,9 +75,9 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
var etag string var etag string
_, ok := w.(c.GzipResponseWriter) _, ok := w.(c.GzipResponseWriter)
if ok { if ok {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"-ng\"" etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "-ng\""
} else { } else {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"-n\"" etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "-n\""
} }
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
if match := r.Header.Get("If-None-Match"); match != "" { if match := r.Header.Get("If-None-Match"); match != "" {
@ -97,26 +98,43 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
return c.InternalErrorJS(err, w, r) return c.InternalErrorJS(err, w, r)
} }
rows, err := stmts.getActivityFeedByWatcher.Query(user.ID) rCreatedAt, _ := strconv.ParseInt(r.FormValue("t"), 10, 64)
if err != nil { rCount, _ := strconv.Atoi(r.FormValue("c"))
return c.InternalErrorJS(err, w, r) //log.Print("rCreatedAt:", rCreatedAt)
} //log.Print("rCount:", rCount)
defer rows.Close()
var actors []int var actors []int
var alerts []c.Alert var alerts []c.Alert
for rows.Next() { var createdAt time.Time
var alert c.Alert var topCreatedAt int64
err = rows.Scan(&alert.ASID, &alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID)
if count != 0 {
rows, err := stmts.getActivityFeedByWatcher.Query(user.ID)
if err != nil {
return c.InternalErrorJS(err, w, r)
}
defer rows.Close()
for rows.Next() {
var alert c.Alert
err = rows.Scan(&alert.ASID, &alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID, &createdAt)
if err != nil {
return c.InternalErrorJS(err, w, r)
}
uCreatedAt := createdAt.Unix()
//log.Print("uCreatedAt", uCreatedAt)
//if rCreatedAt == 0 || rCreatedAt < uCreatedAt {
alerts = append(alerts, alert)
actors = append(actors, alert.ActorID)
//}
if uCreatedAt > topCreatedAt {
topCreatedAt = uCreatedAt
}
}
err = rows.Err()
if err != nil { if err != nil {
return c.InternalErrorJS(err, w, r) return c.InternalErrorJS(err, w, r)
} }
alerts = append(alerts, alert)
actors = append(actors, alert.ActorID)
}
err = rows.Err()
if err != nil {
return c.InternalErrorJS(err, w, r)
} }
// Might not want to error here, if the account was deleted properly, we might want to figure out how we should handle deletions in general // Might not want to error here, if the account was deleted properly, we might want to figure out how we should handle deletions in general
@ -134,23 +152,21 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
if !ok { if !ok {
return c.InternalErrorJS(errors.New("No such actor"), w, r) return c.InternalErrorJS(errors.New("No such actor"), w, r)
} }
res, err := c.BuildAlert(alert, user) res, err := c.BuildAlert(alert, user)
if err != nil { if err != nil {
return c.LocalErrorJS(err.Error(), w, r) return c.LocalErrorJS(err.Error(), w, r)
} }
//sb.Write(res) //sb.Write(res)
msglist += res + "," msglist += res + ","
} }
if len(msglist) != 0 { if len(msglist) != 0 {
msglist = msglist[0 : len(msglist)-1] msglist = msglist[0 : len(msglist)-1]
} }
if count == 0 {
if count == 0 || msglist == "" || (rCreatedAt != 0 && rCreatedAt >= topCreatedAt && count == rCount) {
_, _ = io.WriteString(w, `{}`) _, _ = io.WriteString(w, `{}`)
} else { } else {
_, _ = io.WriteString(w, `{"msgs":[` + msglist + `],"count":` + strconv.Itoa(count) + `}`) _, _ = io.WriteString(w, `{"msgs":[`+msglist+`],"count":`+strconv.Itoa(count)+`,"tc":`+strconv.Itoa(int(topCreatedAt))+`}`)
} }
default: default:
return c.PreErrorJS("Invalid Module", w, r) return c.PreErrorJS("Invalid Module", w, r)
@ -218,18 +234,18 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
var etag string var etag string
_, ok := w.(c.GzipResponseWriter) _, ok := w.(c.GzipResponseWriter)
if ok { if ok {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"-g\"" etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "-g\""
} else { } else {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"\"" etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "\""
} }
var plist map[string]string var plist map[string]string
var posLoop = func(positive string) c.RouteError { var posLoop = func(positive string) c.RouteError {
// ! Constrain it to a subset of phrases for now // ! Constrain it to a subset of phrases for now
for _, item := range phraseWhitelist { for _, item := range phraseWhitelist {
if strings.HasPrefix(positive, item) { if strings.HasPrefix(positive, item) {
// TODO: Break this down into smaller security boundaries based on control panel sections? // TODO: Break this down into smaller security boundaries based on control panel sections?
if strings.HasPrefix(positive,"panel") { if strings.HasPrefix(positive, "panel") {
w.Header().Set("Cache-Control", "private") w.Header().Set("Cache-Control", "private")
ok = user.IsSuperMod ok = user.IsSuperMod
} else { } else {
@ -306,7 +322,7 @@ func routeJSAntispam(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
jsToken := hex.EncodeToString(h.Sum(nil)) jsToken := hex.EncodeToString(h.Sum(nil))
var innerCode = "`document.getElementByld('golden-watch').value = '" + jsToken + "';`" var innerCode = "`document.getElementByld('golden-watch').value = '" + jsToken + "';`"
io.WriteString(w, `let hihi = ` + innerCode + `; io.WriteString(w, `let hihi = `+innerCode+`;
hihi = hihi.replace('ld','Id'); hihi = hihi.replace('ld','Id');
eval(hihi);`) eval(hihi);`)

View File

@ -487,7 +487,7 @@ func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.Use
} }
// ! Be careful about leaking per-route permission state with &user // ! Be careful about leaking per-route permission state with &user
alert := c.Alert{0, user.ID, profileOwner.ID, "reply", "user", profileOwner.ID, &user} alert := c.Alert{ActorID: user.ID, TargetUserID: profileOwner.ID, Event: "reply", ElementType: "user", ElementID: profileOwner.ID, Actor: &user}
err = c.AddActivityAndNotifyTarget(alert) err = c.AddActivityAndNotifyTarget(alert)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
@ -623,7 +623,7 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid s
} }
// ! Be careful about leaking per-route permission state with &user // ! Be careful about leaking per-route permission state with &user
alert := c.Alert{0, user.ID, reply.CreatedBy, "like", "post", rid, &user} alert := c.Alert{ActorID: user.ID, TargetUserID: reply.CreatedBy, Event: "like", ElementType: "post", ElementID: rid, Actor: &user}
err = c.AddActivityAndNotifyTarget(alert) err = c.AddActivityAndNotifyTarget(alert)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, isJs) return c.InternalErrorJSQ(err, w, r, isJs)

View File

@ -1031,7 +1031,7 @@ func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User, stid s
} }
// ! Be careful about leaking per-route permission state with &user // ! Be careful about leaking per-route permission state with &user
alert := c.Alert{0, user.ID, topic.CreatedBy, "like", "topic", tid, &user} alert := c.Alert{ActorID: user.ID, TargetUserID: topic.CreatedBy, Event: "like", ElementType: "topic", ElementID: tid, Actor: &user}
err = c.AddActivityAndNotifyTarget(alert) err = c.AddActivityAndNotifyTarget(alert)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, isJs) return c.InternalErrorJSQ(err, w, r, isJs)

View File

@ -11,7 +11,7 @@
</div> </div>
{{if .CurrentUser.IsAdmin}}<div class="elapsed">{{.Header.Elapsed1}} | {{elapsed .Header.StartedAt}}</div>{{end}} {{if .CurrentUser.IsAdmin}}<div class="elapsed">{{.Header.Elapsed1}} | {{elapsed .Header.StartedAt}}</div>{{end}}
<form action="/theme/" method="post"> <form action="/theme/" method="post">
<div id="themeSelector" style="float: right;"> <div id="themeSelector">
<select id="themeSelectorSelect" name="themeSelector" aria-label="{{lang "footer_theme_selector_aria"}}"> <select id="themeSelectorSelect" name="themeSelector" aria-label="{{lang "footer_theme_selector_aria"}}">
{{range .Header.Themes}} {{range .Header.Themes}}
{{if not .HideFromThemes}}<option val="{{.Name}}"{{if eq $.Header.Theme.Name .Name}} selected{{end}}>{{.FriendlyName}}</option>{{end}} {{if not .HideFromThemes}}<option val="{{.Name}}"{{if eq $.Header.Theme.Name .Name}} selected{{end}}>{{.FriendlyName}}</option>{{end}}

View File

@ -49,7 +49,7 @@
{{range .ItemList}}<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}"> {{range .ItemList}}<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow"> <div class="rowitem topic_left passive datarow">
<span class="selector"></span> <span class="selector"></span>
<a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height="64" alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a> <a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height="64" alt="Avatar" title="{{.Creator.Name}}'s Avatar" aria-hidden="true" /></a>
<span class="topic_inner_left"> <span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> <a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a>
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a> <br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
@ -72,7 +72,7 @@
</div> </div>
<div class="rowitem topic_right passive datarow"> <div class="rowitem topic_right passive datarow">
<div class="topic_right_inside"> <div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height="64" alt="Avatar" title="{{.LastUser.Name}}'s Avatar" aria-hidden="true" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a> <a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>

View File

@ -16,7 +16,7 @@
{{end}} {{end}}
</span> </span>
<span class="forum_right shift_right"> <span class="forum_right shift_right">
{{if .LastReplyer.MicroAvatar}}<img class="extra_little_row_avatar" src="{{.LastReplyer.MicroAvatar}}" height=64 width=64 alt="{{.LastReplyer.Name}}'s Avatar" title="{{.LastReplyer.Name}}'s Avatar" />{{end}} {{if .LastReplyer.MicroAvatar}}<img class="extra_little_row_avatar" src="{{.LastReplyer.MicroAvatar}}" height=64 width=64 alt="Avatar" title="{{.LastReplyer.Name}}'s Avatar" aria-hidden="true" />{{end}}
<span> <span>
<a {{if .LastTopic.Link}}href="{{.LastTopic.Link}}"{{else}}class="forum_no_poster"{{end}}>{{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}{{lang "forums_none"}}{{end}}</a> <a {{if .LastTopic.Link}}href="{{.LastTopic.Link}}"{{else}}class="forum_no_poster"{{end}}>{{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}{{lang "forums_none"}}{{end}}</a>
{{if .LastTopicTime}}<br /><span class="rowsmall" title="{{abstime .LastTopic.LastReplyAt}}">{{.LastTopicTime}}</span>{{end}} {{if .LastTopicTime}}<br /><span class="rowsmall" title="{{abstime .LastTopic.LastReplyAt}}">{{.LastTopicTime}}</span>{{end}}

View File

@ -15,7 +15,7 @@
{{if .IP}} {{if .IP}}
<div class="rowblock rowlist bgavatars micro_grid"> <div class="rowblock rowlist bgavatars micro_grid">
{{range .ItemList}}<div class="rowitem" style="background-image: url('{{.Avatar}}');"> {{range .ItemList}}<div class="rowitem" style="background-image: url('{{.Avatar}}');">
<img src="{{.Avatar}}" class="bgsub" alt="{{.Name}}'s Avatar" /> <img src="{{.Avatar}}" class="bgsub" alt="Avatar" aria-hidden="true" />
<a class="rowTitle" href="{{.Link}}">{{.Name}}</a> <a class="rowTitle" href="{{.Link}}">{{.Name}}</a>
</div> </div>
{{else}}<div class="rowitem rowmsg">{{lang "ip_search_no_users"}}</div>{{end}} {{else}}<div class="rowitem rowmsg">{{lang "ip_search_no_users"}}</div>{{end}}

View File

@ -4,7 +4,7 @@
<div id="panel_users" class="colstack_item rowlist bgavatars"> <div id="panel_users" class="colstack_item rowlist bgavatars">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem editable_parent" style="background-image: url('{{.Avatar}}');"> <div class="rowitem editable_parent" style="background-image: url('{{.Avatar}}');">
<img class="bgsub" src="{{.Avatar}}" alt="{{.Name}}'s Avatar" /> <img class="bgsub" src="{{.Avatar}}" alt="Avatar" aria-hidden="true" />
<a class="rowTitle editable_block"{{if $.CurrentUser.Perms.EditUser}} href="/panel/users/edit/{{.ID}}?session={{$.CurrentUser.Session}}"{{end}}>{{.Name}}</a> <a class="rowTitle editable_block"{{if $.CurrentUser.Perms.EditUser}} href="/panel/users/edit/{{.ID}}?session={{$.CurrentUser.Session}}"{{end}}>{{.Name}}</a>
<span class="panel_floater"> <span class="panel_floater">
<a href="{{.Link}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a> <a href="{{.Link}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a>

View File

@ -6,7 +6,7 @@
<div id="profile_left_pane" class="rowmenu"> <div id="profile_left_pane" class="rowmenu">
<div class="topBlock"> <div class="topBlock">
<div class="rowitem avatarRow"> <div class="rowitem avatarRow">
<img src="{{.ProfileOwner.Avatar}}" class="avatar" alt="{{.ProfileOwner.Name}}'s Avatar" title="{{.ProfileOwner.Name}}'s Avatar" /> <img src="{{.ProfileOwner.Avatar}}" class="avatar" alt="Avatar" title="{{.ProfileOwner.Name}}'s Avatar" aria-hidden="true" />
</div> </div>
<div class="rowitem nameRow"> <div class="rowitem nameRow">
<span class="profileName" title="{{.ProfileOwner.Name}}">{{.ProfileOwner.Name}}</span>{{if .ProfileOwner.Tag}}<span class="username" title="{{.ProfileOwner.Tag}}">{{.ProfileOwner.Tag}}</span>{{end}} <span class="profileName" title="{{.ProfileOwner.Name}}">{{.ProfileOwner.Name}}</span>{{if .ProfileOwner.Tag}}<span class="username" title="{{.ProfileOwner.Tag}}">{{.ProfileOwner.Tag}}</span>{{end}}

View File

@ -2,7 +2,7 @@
<div class="rowitem passive deletable_block editable_parent comment {{.ClassName}}"> <div class="rowitem passive deletable_block editable_parent comment {{.ClassName}}">
<div class="topRow"> <div class="topRow">
<div class="userbit"> <div class="userbit">
<img src="{{.MicroAvatar}}" alt="{{.CreatedByName}}'s Avatar" title="{{.CreatedByName}}'s Avatar" /> <img src="{{.MicroAvatar}}" alt="Avatar" title="{{.CreatedByName}}'s Avatar" aria-hidden="true" />
<span class="nameAndTitle"> <span class="nameAndTitle">
<a href="{{.UserLink}}" class="real_username username">{{.CreatedByName}}</a> <a href="{{.UserLink}}" class="real_username username">{{.CreatedByName}}</a>
{{if .Tag}}<a class="username hide_on_mobile user_tag" style="float: right;">{{.Tag}}</a>{{end}} {{if .Tag}}<a class="username hide_on_mobile user_tag" style="float: right;">{{.Tag}}</a>{{end}}

View File

@ -1,7 +1,7 @@
<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}"> <div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow"> <div class="rowitem topic_left passive datarow">
<span class="selector"></span> <span class="selector"></span>
<a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height=64 alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a> <a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height=64 alt="Avatar" title="{{.Creator.Name}}'s Avatar" aria-hidden="true" /></a>
<span class="topic_inner_left"> <span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}" title="{{.ForumName}}">{{.ForumName}}</a>{{end}} <a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}" title="{{.ForumName}}">{{.ForumName}}</a>{{end}}
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a> <br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
@ -24,11 +24,11 @@
</div> </div>
<div class="rowitem topic_right passive datarow"> <div class="rowitem topic_right passive datarow">
<div class="topic_right_inside"> <div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height=64 alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height=64 alt="Avatar" title="{{.LastUser.Name}}'s Avatar" aria-hidden="true" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a> <a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>
</span> </span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<div class="rowblock rowlist bgavatars not_grid widget_online"> <div class="rowblock rowlist bgavatars not_grid widget_online">
{{if lt .UserCount 30}} {{if lt .UserCount 30}}
{{range .Users}}<div class="rowitem" style="background-image: url('{{.Avatar}}');"> {{range .Users}}<div class="rowitem" style="background-image: url('{{.Avatar}}');">
<img src="{{.Avatar}}" class="bgsub" alt="{{.Name}}'s Avatar" /> <img src="{{.Avatar}}" class="bgsub" alt="Avatar" aria-hidden="true" />
<a class="rowTitle" href="{{.Link}}">{{.Name}}</a> <a class="rowTitle" href="{{.Link}}">{{.Name}}</a>
</div> </div>
{{else}}<div class="rowitem rowmsg">{{lang "widget.online_none_online"}}</div>{{end}} {{else}}<div class="rowitem rowmsg">{{lang "widget.online_none_online"}}</div>{{end}}

View File

@ -1,6 +1,6 @@
<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}"> <div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow"> <div class="rowitem topic_left passive datarow">
<a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height=64 alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a> <a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height=64 alt="Avatar" title="{{.Creator.Name}}'s Avatar" aria-hidden="true" /></a>
<span class="topic_inner_left"> <span class="topic_inner_left">
<span class="rowtopic" itemprop="itemListElement" title="{{.Title}}"><a href="{{.Link}}">{{.Title}}</a>{{if .ForumName}}<a class="parent_forum_sep">-</a><a href="{{.ForumLink}}" title="{{.ForumName}}" class="rowsmall parent_forum">{{.ForumName}}</a>{{end}}</span> <span class="rowtopic" itemprop="itemListElement" title="{{.Title}}"><a href="{{.Link}}">{{.Title}}</a>{{if .ForumName}}<a class="parent_forum_sep">-</a><a href="{{.ForumLink}}" title="{{.ForumName}}" class="rowsmall parent_forum">{{.ForumName}}</a>{{end}}</span>
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a> <br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
@ -15,7 +15,7 @@
</div> </div>
<div class="rowitem topic_right passive datarow"> <div class="rowitem topic_right passive datarow">
<div class="topic_right_inside"> <div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height=64 alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a> <a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height=64 alt="Avatar" title="{{.LastUser.Name}}'s Avatar" aria-hidden="true" /></a>
<span> <span>
<a href="{{.LastUser.Link}}" class="lastName" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br> <a href="{{.LastUser.Link}}" class="lastName" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a> <a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>

View File

@ -357,6 +357,9 @@ red {
color: var(--dim-text-color); color: var(--dim-text-color);
float: right; float: right;
} }
.level_label:before {
content: "{{lang "topic.level_tooltip" . }}";
}
.level { .level {
margin-left: 3px; margin-left: 3px;
} }
@ -632,6 +635,9 @@ input, select, textarea {
#poweredBy span { #poweredBy span {
font-size: 12px; font-size: 12px;
} }
#themeSelector {
float: right;
}
.poll_item { .poll_item {
display: flex; display: flex;

View File

@ -959,6 +959,9 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
color: black; color: black;
text-decoration: none; text-decoration: none;
} }
#themeSelector {
float: right;
}
.sidebar .rowhead:not(:first-child) { .sidebar .rowhead:not(:first-child) {
margin-top: 12px; margin-top: 12px;