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"
"strconv"
"strings"
"time"
"github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen"
@ -23,6 +24,7 @@ type Alert struct {
Event string
ElementType string
ElementID int
CreatedAt time.Time
Actor *User
}
@ -42,14 +44,14 @@ var alertStmts AlertStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
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(
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 = ?", "", ""},
),
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 = ?", "", ""),
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()
})
@ -237,7 +239,7 @@ func notifyWatchers(asid int64) {
}
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 {
LogError(err)
return

2
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/go-sql-driver/mysql v1.4.0
github.com/gorilla/websocket v1.4.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/oschwald/geoip2-golang v1.2.1
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/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-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/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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) {
m, err := c.Meta.Get("magic")
expect(t, m == "", "meta var should be empty")
recordMustNotExist(t, err, "meta var shouldn't exist")
expect(t, m == "", "meta var magic should be empty")
recordMustNotExist(t, err, "meta var magic should not exist")
err = c.Meta.Set("magic","lol")
expectNilErr(t,err)
m, err = c.Meta.Get("magic")
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) {

View File

@ -54,7 +54,7 @@ func initMySQL() (err error) {
// TODO: Is there a less noisy way of doing this for tests?
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 {
return errors.WithStack(err)
}

View File

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

View File

@ -10,12 +10,13 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"errors"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"unicode"
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
_, ok := w.(c.GzipResponseWriter)
if ok {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"-ng\""
etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "-ng\""
} else {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"-n\""
etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "-n\""
}
w.Header().Set("ETag", etag)
if match := r.Header.Get("If-None-Match"); match != "" {
@ -97,27 +98,44 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
return c.InternalErrorJS(err, w, r)
}
rCreatedAt, _ := strconv.ParseInt(r.FormValue("t"), 10, 64)
rCount, _ := strconv.Atoi(r.FormValue("c"))
//log.Print("rCreatedAt:", rCreatedAt)
//log.Print("rCount:", rCount)
var actors []int
var alerts []c.Alert
var createdAt time.Time
var topCreatedAt int64
if count != 0 {
rows, err := stmts.getActivityFeedByWatcher.Query(user.ID)
if err != nil {
return c.InternalErrorJS(err, w, r)
}
defer rows.Close()
var actors []int
var alerts []c.Alert
for rows.Next() {
var alert c.Alert
err = rows.Scan(&alert.ASID, &alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID)
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 {
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
list, err := c.Users.BulkGetMap(actors)
@ -134,23 +152,21 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
if !ok {
return c.InternalErrorJS(errors.New("No such actor"), w, r)
}
res, err := c.BuildAlert(alert, user)
if err != nil {
return c.LocalErrorJS(err.Error(), w, r)
}
//sb.Write(res)
msglist += res + ","
}
if len(msglist) != 0 {
msglist = msglist[0 : len(msglist)-1]
}
if count == 0 {
if count == 0 || msglist == "" || (rCreatedAt != 0 && rCreatedAt >= topCreatedAt && count == rCount) {
_, _ = io.WriteString(w, `{}`)
} 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:
return c.PreErrorJS("Invalid Module", w, r)
@ -218,9 +234,9 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
var etag string
_, ok := w.(c.GzipResponseWriter)
if ok {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"-g\""
etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "-g\""
} else {
etag = "\""+strconv.FormatInt(c.StartTime.Unix(), 10)+"\""
etag = "\"" + strconv.FormatInt(c.StartTime.Unix(), 10) + "\""
}
var plist map[string]string
@ -229,7 +245,7 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
for _, item := range phraseWhitelist {
if strings.HasPrefix(positive, item) {
// 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")
ok = user.IsSuperMod
} else {
@ -306,7 +322,7 @@ func routeJSAntispam(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
jsToken := hex.EncodeToString(h.Sum(nil))
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');
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
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)
if err != nil {
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
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)
if err != nil {
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
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)
if err != nil {
return c.InternalErrorJSQ(err, w, r, isJs)

View File

@ -11,7 +11,7 @@
</div>
{{if .CurrentUser.IsAdmin}}<div class="elapsed">{{.Header.Elapsed1}} | {{elapsed .Header.StartedAt}}</div>{{end}}
<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"}}">
{{range .Header.Themes}}
{{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}}">
<div class="rowitem topic_left passive datarow">
<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">
<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>
@ -72,7 +72,7 @@
</div>
<div class="rowitem topic_right passive datarow">
<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>
<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>

View File

@ -16,7 +16,7 @@
{{end}}
</span>
<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>
<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}}

View File

@ -15,7 +15,7 @@
{{if .IP}}
<div class="rowblock rowlist bgavatars micro_grid">
{{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>
</div>
{{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">
{{range .ItemList}}
<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>
<span class="panel_floater">
<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 class="topBlock">
<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 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}}

View File

@ -2,7 +2,7 @@
<div class="rowitem passive deletable_block editable_parent comment {{.ClassName}}">
<div class="topRow">
<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">
<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}}

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="rowitem topic_left passive datarow">
<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">
<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>
@ -24,11 +24,11 @@
</div>
<div class="rowitem topic_right passive datarow">
<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>
<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>
</span>
</div>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
<div class="rowblock rowlist bgavatars not_grid widget_online">
{{if lt .UserCount 30}}
{{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>
</div>
{{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="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="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>
@ -15,7 +15,7 @@
</div>
<div class="rowitem topic_right passive datarow">
<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>
<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>

View File

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

View File

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