Let users unlike posts.

Hide like buttons on own posts for Tempra Simple and Shadow themes too.
fix like visual ui.
fix topic.Unlike err return.

Add topic.minus_one phrase.
This commit is contained in:
Azareal 2020-01-31 20:48:55 +10:00
parent 6935637867
commit b5fa9c69f7
14 changed files with 286 additions and 175 deletions

View File

@ -15,7 +15,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
var ErrPluginNotInstallable = errors.New("This plugin is not installable") var ErrPluginNotInstallable = errors.New("This plugin is not installable")
@ -90,14 +90,15 @@ var hookTable = &HookTable{
"route_forum_list_start": nil, "route_forum_list_start": nil,
"action_end_create_topic": nil, "action_end_create_topic": nil,
"action_end_edit_topic":nil, "action_end_edit_topic": nil,
"action_end_delete_topic":nil, "action_end_delete_topic": nil,
"action_end_lock_topic":nil, "action_end_lock_topic": nil,
"action_end_unlock_topic": nil, "action_end_unlock_topic": nil,
"action_end_stick_topic": nil, "action_end_stick_topic": nil,
"action_end_unstick_topic": nil, "action_end_unstick_topic": nil,
"action_end_move_topic": nil, "action_end_move_topic": nil,
"action_end_like_topic":nil, "action_end_like_topic": nil,
"action_end_unlike_topic": nil,
"action_end_create_reply": nil, "action_end_create_reply": nil,
"action_end_edit_reply": nil, "action_end_edit_reply": nil,
@ -105,11 +106,12 @@ var hookTable = &HookTable{
"action_end_add_attach_to_reply": nil, "action_end_add_attach_to_reply": nil,
"action_end_remove_attach_from_reply": nil, "action_end_remove_attach_from_reply": nil,
"action_end_like_reply":nil, "action_end_like_reply": nil,
"action_end_unlike_reply": nil,
"action_end_ban_user":nil, "action_end_ban_user": nil,
"action_end_unban_user":nil, "action_end_unban_user": nil,
"action_end_activate_user":nil, "action_end_activate_user": nil,
"router_after_filters": nil, "router_after_filters": nil,
"router_pre_route": nil, "router_pre_route": nil,
@ -143,8 +145,8 @@ func GetHookTable() *HookTable {
} }
// Hooks with a single argument. Is this redundant? Might be useful for inlining, as variadics aren't inlined? Are closures even inlined to begin with? // Hooks with a single argument. Is this redundant? Might be useful for inlining, as variadics aren't inlined? Are closures even inlined to begin with?
func (table *HookTable) Hook(name string, data interface{}) interface{} { func (t *HookTable) Hook(name string, data interface{}) interface{} {
hooks, ok := table.Hooks[name] hooks, ok := t.Hooks[name]
if ok { if ok {
for _, hook := range hooks { for _, hook := range hooks {
data = hook(data) data = hook(data)
@ -154,8 +156,8 @@ func (table *HookTable) Hook(name string, data interface{}) interface{} {
} }
// To cover the case in routes/topic.go's CreateTopic route, we could probably obsolete this use and replace it // To cover the case in routes/topic.go's CreateTopic route, we could probably obsolete this use and replace it
func (table *HookTable) HookSkippable(name string, data interface{}) (skip bool) { func (t *HookTable) HookSkippable(name string, data interface{}) (skip bool) {
hooks, ok := table.Hooks[name] hooks, ok := t.Hooks[name]
if ok { if ok {
for _, hook := range hooks { for _, hook := range hooks {
skip = hook(data).(bool) skip = hook(data).(bool)
@ -169,24 +171,24 @@ func (table *HookTable) HookSkippable(name string, data interface{}) (skip bool)
// Hooks with a variable number of arguments // Hooks with a variable number of arguments
// TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn? // TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn?
func (table *HookTable) Vhook(name string, data ...interface{}) interface{} { func (t *HookTable) Vhook(name string, data ...interface{}) interface{} {
hook := table.Vhooks[name] hook := t.Vhooks[name]
if hook != nil { if hook != nil {
return hook(data...) return hook(data...)
} }
return nil return nil
} }
func (table *HookTable) VhookNoRet(name string, data ...interface{}) { func (t *HookTable) VhookNoRet(name string, data ...interface{}) {
hook := table.Vhooks[name] hook := t.Vhooks[name]
if hook != nil { if hook != nil {
_ = hook(data...) _ = hook(data...)
} }
} }
// TODO: Find a better way of doing this // TODO: Find a better way of doing this
func (table *HookTable) VhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) { func (t *HookTable) VhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) {
hook := table.Vhooks[name] hook := t.Vhooks[name]
if hook != nil { if hook != nil {
return hook(data...), true return hook(data...), true
} }
@ -194,8 +196,8 @@ func (table *HookTable) VhookNeedHook(name string, data ...interface{}) (ret int
} }
// Hooks with a variable number of arguments and return values for skipping the parent function and propagating an error upwards // Hooks with a variable number of arguments and return values for skipping the parent function and propagating an error upwards
func (table *HookTable) VhookSkippable(name string, data ...interface{}) (bool, RouteError) { func (t *HookTable) VhookSkippable(name string, data ...interface{}) (bool, RouteError) {
hook := table.VhookSkippable_[name] hook := t.VhookSkippable_[name]
if hook != nil { if hook != nil {
return hook(data...) return hook(data...)
} }
@ -204,8 +206,8 @@ func (table *HookTable) VhookSkippable(name string, data ...interface{}) (bool,
// Hooks which take in and spit out a string. This is usually used for parser components // Hooks which take in and spit out a string. This is usually used for parser components
// Trying to get a teeny bit of type-safety where-ever possible, especially for such a critical set of hooks // Trying to get a teeny bit of type-safety where-ever possible, especially for such a critical set of hooks
func (table *HookTable) Sshook(name string, data string) string { func (t *HookTable) Sshook(name, data string) string {
ssHooks, ok := table.Sshooks[name] ssHooks, ok := t.Sshooks[name]
if ok { if ok {
for _, hook := range ssHooks { for _, hook := range ssHooks {
data = hook(data) data = hook(data)
@ -331,17 +333,17 @@ type Plugin struct {
Data interface{} // Usually used for hosting the VMs / reusable elements of non-native plugins Data interface{} // Usually used for hosting the VMs / reusable elements of non-native plugins
} }
func (plugin *Plugin) BypassActive() (active bool, err error) { func (pl *Plugin) BypassActive() (active bool, err error) {
err = extendStmts.isActive.QueryRow(plugin.UName).Scan(&active) err = extendStmts.isActive.QueryRow(pl.UName).Scan(&active)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return false, err return false, err
} }
return active, nil return active, nil
} }
func (plugin *Plugin) InDatabase() (exists bool, err error) { func (pl *Plugin) InDatabase() (exists bool, err error) {
var sink bool var sink bool
err = extendStmts.isActive.QueryRow(plugin.UName).Scan(&sink) err = extendStmts.isActive.QueryRow(pl.UName).Scan(&sink)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return false, err return false, err
} }
@ -349,31 +351,31 @@ func (plugin *Plugin) InDatabase() (exists bool, err error) {
} }
// TODO: Silently add to the database, if it doesn't exist there rather than forcing users to call AddToDatabase instead? // TODO: Silently add to the database, if it doesn't exist there rather than forcing users to call AddToDatabase instead?
func (plugin *Plugin) SetActive(active bool) (err error) { func (pl *Plugin) SetActive(active bool) (err error) {
_, err = extendStmts.setActive.Exec(active, plugin.UName) _, err = extendStmts.setActive.Exec(active, pl.UName)
if err == nil { if err == nil {
plugin.Active = active pl.Active = active
} }
return err return err
} }
// TODO: Silently add to the database, if it doesn't exist there rather than forcing users to call AddToDatabase instead? // TODO: Silently add to the database, if it doesn't exist there rather than forcing users to call AddToDatabase instead?
func (plugin *Plugin) SetInstalled(installed bool) (err error) { func (pl *Plugin) SetInstalled(installed bool) (err error) {
if !plugin.Installable { if !pl.Installable {
return ErrPluginNotInstallable return ErrPluginNotInstallable
} }
_, err = extendStmts.setInstalled.Exec(installed, plugin.UName) _, err = extendStmts.setInstalled.Exec(installed, pl.UName)
if err == nil { if err == nil {
plugin.Installed = installed pl.Installed = installed
} }
return err return err
} }
func (plugin *Plugin) AddToDatabase(active bool, installed bool) (err error) { func (pl *Plugin) AddToDatabase(active, installed bool) (err error) {
_, err = extendStmts.add.Exec(plugin.UName, active, installed) _, err = extendStmts.add.Exec(pl.UName, active, installed)
if err == nil { if err == nil {
plugin.Active = active pl.Active = active
plugin.Installed = installed pl.Installed = installed
} }
return err return err
} }
@ -393,12 +395,12 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
pl := "plugins" pl := "plugins"
extendStmts = ExtendStmts{ extendStmts = ExtendStmts{
getPlugins: acc.Select(pl).Columns("uname, active, installed").Prepare(), getPlugins: acc.Select(pl).Columns("uname,active,installed").Prepare(),
isActive: acc.Select(pl).Columns("active").Where("uname = ?").Prepare(), isActive: acc.Select(pl).Columns("active").Where("uname=?").Prepare(),
setActive: acc.Update(pl).Set("active = ?").Where("uname = ?").Prepare(), setActive: acc.Update(pl).Set("active=?").Where("uname=?").Prepare(),
setInstalled: acc.Update(pl).Set("installed = ?").Where("uname = ?").Prepare(), setInstalled: acc.Update(pl).Set("installed=?").Where("uname=?").Prepare(),
add: acc.Insert(pl).Columns("uname, active, installed").Fields("?,?,?").Prepare(), add: acc.Insert(pl).Columns("uname,active,installed").Fields("?,?,?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })

View File

@ -4,7 +4,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
var blankGroup = Group{ID: 0, Name: ""} var blankGroup = Group{ID: 0, Name: ""}
@ -46,15 +46,15 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
ug := "users_groups" ug := "users_groups"
groupStmts = GroupStmts{ groupStmts = GroupStmts{
updateGroup: acc.Update(ug).Set("name = ?, tag = ?").Where("gid = ?").Prepare(), updateGroup: acc.Update(ug).Set("name=?,tag=?").Where("gid=?").Prepare(),
updateGroupRank: acc.Update(ug).Set("is_admin = ?, is_mod = ?, is_banned = ?").Where("gid = ?").Prepare(), updateGroupRank: acc.Update(ug).Set("is_admin=?,is_mod=?,is_banned=?").Where("gid=?").Prepare(),
updateGroupPerms: acc.Update(ug).Set("permissions = ?").Where("gid = ?").Prepare(), updateGroupPerms: acc.Update(ug).Set("permissions=?").Where("gid=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
} }
func (g *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err error) { func (g *Group) ChangeRank(isAdmin, isMod, isBanned bool) (err error) {
_, err = groupStmts.updateGroupRank.Exec(isAdmin, isMod, isBanned, g.ID) _, err = groupStmts.updateGroupRank.Exec(isAdmin, isMod, isBanned, g.ID)
if err != nil { if err != nil {
return err return err
@ -63,7 +63,7 @@ func (g *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err error)
return nil return nil
} }
func (g *Group) Update(name string, tag string) (err error) { func (g *Group) Update(name, tag string) (err error) {
_, err = groupStmts.updateGroup.Exec(name, tag, g.ID) _, err = groupStmts.updateGroup.Exec(name, tag, g.ID)
if err != nil { if err != nil {
return err return err

View File

@ -10,7 +10,7 @@ import (
"strings" "strings"
"github.com/Azareal/Gosora/common/phrases" "github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
type MenuItemList []MenuItem type MenuItemList []MenuItem
@ -66,10 +66,10 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
mi := "menu_items" mi := "menu_items"
menuItemStmts = MenuItemStmts{ menuItemStmts = MenuItemStmts{
update: acc.Update(mi).Set("name = ?, htmlID = ?, cssClass = ?, position = ?, path = ?, aria = ?, tooltip = ?, tmplName = ?, guestOnly = ?, memberOnly = ?, staffOnly = ?, adminOnly = ?").Where("miid = ?").Prepare(), update: acc.Update(mi).Set("name=?,htmlID=?,cssClass=?,position=?,path=?,aria=?,tooltip=?,tmplName=?,guestOnly=?,memberOnly=?,staffOnly=?,adminOnly=?").Where("miid=?").Prepare(),
insert: acc.Insert(mi).Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(), insert: acc.Insert(mi).Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(),
delete: acc.Delete(mi).Where("miid = ?").Prepare(), delete: acc.Delete(mi).Where("miid=?").Prepare(),
updateOrder: acc.Update(mi).Set("order = ?").Where("miid = ?").Prepare(), updateOrder: acc.Update(mi).Set("order=?").Where("miid=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })

View File

@ -10,8 +10,8 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"html" "html"
"time"
"strconv" "strconv"
"time"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -124,6 +124,21 @@ func (r *Reply) Like(uid int) (err error) {
return err return err
} }
// TODO: Use a transaction
func (r *Reply) Unlike(uid int) error {
err := Likes.Delete(r.ID, "replies")
if err != nil {
return err
}
_, err = replyStmts.addLikesToReply.Exec(-1, r.ID)
if err != nil {
return err
}
_, err = userStmts.decLiked.Exec(1, uid)
_ = Rstore.GetCache().Remove(r.ID)
return err
}
// TODO: Refresh topic list? // TODO: Refresh topic list?
func (r *Reply) Delete() error { func (r *Reply) Delete() error {
creator, err := Users.Get(r.CreatedBy) creator, err := Users.Get(r.CreatedBy)
@ -166,7 +181,7 @@ func (r *Reply) Delete() error {
if err != nil { if err != nil {
return err return err
} }
err = Activity.DeleteByParamsExtra("reply",r.ParentID,"topic",strconv.Itoa(r.ID)) err = Activity.DeleteByParamsExtra("reply", r.ParentID, "topic", strconv.Itoa(r.ID))
if err != nil { if err != nil {
return err return err
} }

View File

@ -336,7 +336,7 @@ func (t *Topic) Unlike(uid int) error {
} }
_, err = userStmts.decLiked.Exec(1, uid) _, err = userStmts.decLiked.Exec(1, uid)
t.cacheRemove() t.cacheRemove()
return nil return err
} }
func handleLikedTopicReplies(tid int) error { func handleLikedTopicReplies(tid int) error {

View File

@ -166,6 +166,7 @@ var RouteMap = map[string]interface{}{
"routes.ReplyEditSubmit": routes.ReplyEditSubmit, "routes.ReplyEditSubmit": routes.ReplyEditSubmit,
"routes.ReplyDeleteSubmit": routes.ReplyDeleteSubmit, "routes.ReplyDeleteSubmit": routes.ReplyDeleteSubmit,
"routes.ReplyLikeSubmit": routes.ReplyLikeSubmit, "routes.ReplyLikeSubmit": routes.ReplyLikeSubmit,
"routes.ReplyUnlikeSubmit": routes.ReplyUnlikeSubmit,
"routes.AddAttachToReplySubmit": routes.AddAttachToReplySubmit, "routes.AddAttachToReplySubmit": routes.AddAttachToReplySubmit,
"routes.RemoveAttachFromReplySubmit": routes.RemoveAttachFromReplySubmit, "routes.RemoveAttachFromReplySubmit": routes.RemoveAttachFromReplySubmit,
"routes.ProfileReplyCreateSubmit": routes.ProfileReplyCreateSubmit, "routes.ProfileReplyCreateSubmit": routes.ProfileReplyCreateSubmit,
@ -339,32 +340,33 @@ var routeMapEnum = map[string]int{
"routes.ReplyEditSubmit": 140, "routes.ReplyEditSubmit": 140,
"routes.ReplyDeleteSubmit": 141, "routes.ReplyDeleteSubmit": 141,
"routes.ReplyLikeSubmit": 142, "routes.ReplyLikeSubmit": 142,
"routes.AddAttachToReplySubmit": 143, "routes.ReplyUnlikeSubmit": 143,
"routes.RemoveAttachFromReplySubmit": 144, "routes.AddAttachToReplySubmit": 144,
"routes.ProfileReplyCreateSubmit": 145, "routes.RemoveAttachFromReplySubmit": 145,
"routes.ProfileReplyEditSubmit": 146, "routes.ProfileReplyCreateSubmit": 146,
"routes.ProfileReplyDeleteSubmit": 147, "routes.ProfileReplyEditSubmit": 147,
"routes.PollVote": 148, "routes.ProfileReplyDeleteSubmit": 148,
"routes.PollResults": 149, "routes.PollVote": 149,
"routes.AccountLogin": 150, "routes.PollResults": 150,
"routes.AccountRegister": 151, "routes.AccountLogin": 151,
"routes.AccountLogout": 152, "routes.AccountRegister": 152,
"routes.AccountLoginSubmit": 153, "routes.AccountLogout": 153,
"routes.AccountLoginMFAVerify": 154, "routes.AccountLoginSubmit": 154,
"routes.AccountLoginMFAVerifySubmit": 155, "routes.AccountLoginMFAVerify": 155,
"routes.AccountRegisterSubmit": 156, "routes.AccountLoginMFAVerifySubmit": 156,
"routes.AccountPasswordReset": 157, "routes.AccountRegisterSubmit": 157,
"routes.AccountPasswordResetSubmit": 158, "routes.AccountPasswordReset": 158,
"routes.AccountPasswordResetToken": 159, "routes.AccountPasswordResetSubmit": 159,
"routes.AccountPasswordResetTokenSubmit": 160, "routes.AccountPasswordResetToken": 160,
"routes.DynamicRoute": 161, "routes.AccountPasswordResetTokenSubmit": 161,
"routes.UploadedFile": 162, "routes.DynamicRoute": 162,
"routes.StaticFile": 163, "routes.UploadedFile": 163,
"routes.RobotsTxt": 164, "routes.StaticFile": 164,
"routes.SitemapXml": 165, "routes.RobotsTxt": 165,
"routes.OpenSearchXml": 166, "routes.SitemapXml": 166,
"routes.BadRoute": 167, "routes.OpenSearchXml": 167,
"routes.HTTPSRedirect": 168, "routes.BadRoute": 168,
"routes.HTTPSRedirect": 169,
} }
var reverseRouteMapEnum = map[int]string{ var reverseRouteMapEnum = map[int]string{
0: "routes.Overview", 0: "routes.Overview",
@ -510,32 +512,33 @@ var reverseRouteMapEnum = map[int]string{
140: "routes.ReplyEditSubmit", 140: "routes.ReplyEditSubmit",
141: "routes.ReplyDeleteSubmit", 141: "routes.ReplyDeleteSubmit",
142: "routes.ReplyLikeSubmit", 142: "routes.ReplyLikeSubmit",
143: "routes.AddAttachToReplySubmit", 143: "routes.ReplyUnlikeSubmit",
144: "routes.RemoveAttachFromReplySubmit", 144: "routes.AddAttachToReplySubmit",
145: "routes.ProfileReplyCreateSubmit", 145: "routes.RemoveAttachFromReplySubmit",
146: "routes.ProfileReplyEditSubmit", 146: "routes.ProfileReplyCreateSubmit",
147: "routes.ProfileReplyDeleteSubmit", 147: "routes.ProfileReplyEditSubmit",
148: "routes.PollVote", 148: "routes.ProfileReplyDeleteSubmit",
149: "routes.PollResults", 149: "routes.PollVote",
150: "routes.AccountLogin", 150: "routes.PollResults",
151: "routes.AccountRegister", 151: "routes.AccountLogin",
152: "routes.AccountLogout", 152: "routes.AccountRegister",
153: "routes.AccountLoginSubmit", 153: "routes.AccountLogout",
154: "routes.AccountLoginMFAVerify", 154: "routes.AccountLoginSubmit",
155: "routes.AccountLoginMFAVerifySubmit", 155: "routes.AccountLoginMFAVerify",
156: "routes.AccountRegisterSubmit", 156: "routes.AccountLoginMFAVerifySubmit",
157: "routes.AccountPasswordReset", 157: "routes.AccountRegisterSubmit",
158: "routes.AccountPasswordResetSubmit", 158: "routes.AccountPasswordReset",
159: "routes.AccountPasswordResetToken", 159: "routes.AccountPasswordResetSubmit",
160: "routes.AccountPasswordResetTokenSubmit", 160: "routes.AccountPasswordResetToken",
161: "routes.DynamicRoute", 161: "routes.AccountPasswordResetTokenSubmit",
162: "routes.UploadedFile", 162: "routes.DynamicRoute",
163: "routes.StaticFile", 163: "routes.UploadedFile",
164: "routes.RobotsTxt", 164: "routes.StaticFile",
165: "routes.SitemapXml", 165: "routes.RobotsTxt",
166: "routes.OpenSearchXml", 166: "routes.SitemapXml",
167: "routes.BadRoute", 167: "routes.OpenSearchXml",
168: "routes.HTTPSRedirect", 168: "routes.BadRoute",
169: "routes.HTTPSRedirect",
} }
var osMapEnum = map[string]int{ var osMapEnum = map[string]int{
"unknown": 0, "unknown": 0,
@ -693,7 +696,7 @@ type HTTPSRedirect struct {}
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
co.RouteViewCounter.Bump(168) co.RouteViewCounter.Bump(169)
dest := "https://" + req.Host + req.URL.String() dest := "https://" + req.Host + req.URL.String()
http.Redirect(w, req, dest, http.StatusTemporaryRedirect) http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
} }
@ -901,7 +904,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
co.GlobalViewCounter.Bump() co.GlobalViewCounter.Bump()
if prefix == "/s" { //old prefix: /static if prefix == "/s" { //old prefix: /static
co.RouteViewCounter.Bump(163) co.RouteViewCounter.Bump(164)
req.URL.Path += extraData req.URL.Path += extraData
routes.StaticFile(w, req) routes.StaticFile(w, req)
return return
@ -2386,6 +2389,19 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
co.RouteViewCounter.Bump(142) co.RouteViewCounter.Bump(142)
err = routes.ReplyLikeSubmit(w,req,user,extraData) err = routes.ReplyLikeSubmit(w,req,user,extraData)
case "/reply/unlike/submit/":
err = c.NoSessionMismatch(w,req,user)
if err != nil {
return err
}
err = c.MemberOnly(w,req,user)
if err != nil {
return err
}
co.RouteViewCounter.Bump(143)
err = routes.ReplyUnlikeSubmit(w,req,user,extraData)
case "/reply/attach/add/submit/": case "/reply/attach/add/submit/":
err = c.MemberOnly(w,req,user) err = c.MemberOnly(w,req,user)
if err != nil { if err != nil {
@ -2401,7 +2417,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(143) co.RouteViewCounter.Bump(144)
err = routes.AddAttachToReplySubmit(w,req,user,extraData) err = routes.AddAttachToReplySubmit(w,req,user,extraData)
case "/reply/attach/remove/submit/": case "/reply/attach/remove/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2414,7 +2430,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(144) co.RouteViewCounter.Bump(145)
err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData) err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData)
} }
case "/profile": case "/profile":
@ -2430,7 +2446,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(145) co.RouteViewCounter.Bump(146)
err = routes.ProfileReplyCreateSubmit(w,req,user) err = routes.ProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/": case "/profile/reply/edit/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2443,7 +2459,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(146) co.RouteViewCounter.Bump(147)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData) err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/": case "/profile/reply/delete/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2456,7 +2472,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(147) co.RouteViewCounter.Bump(148)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
} }
case "/poll": case "/poll":
@ -2472,23 +2488,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(148) co.RouteViewCounter.Bump(149)
err = routes.PollVote(w,req,user,extraData) err = routes.PollVote(w,req,user,extraData)
case "/poll/results/": case "/poll/results/":
co.RouteViewCounter.Bump(149) co.RouteViewCounter.Bump(150)
err = routes.PollResults(w,req,user,extraData) err = routes.PollResults(w,req,user,extraData)
} }
case "/accounts": case "/accounts":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/accounts/login/": case "/accounts/login/":
co.RouteViewCounter.Bump(150) co.RouteViewCounter.Bump(151)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
} }
err = routes.AccountLogin(w,req,user,head) err = routes.AccountLogin(w,req,user,head)
case "/accounts/create/": case "/accounts/create/":
co.RouteViewCounter.Bump(151) co.RouteViewCounter.Bump(152)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2505,7 +2521,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(152) co.RouteViewCounter.Bump(153)
err = routes.AccountLogout(w,req,user) err = routes.AccountLogout(w,req,user)
case "/accounts/login/submit/": case "/accounts/login/submit/":
err = c.ParseForm(w,req,user) err = c.ParseForm(w,req,user)
@ -2513,10 +2529,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(153) co.RouteViewCounter.Bump(154)
err = routes.AccountLoginSubmit(w,req,user) err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/mfa_verify/": case "/accounts/mfa_verify/":
co.RouteViewCounter.Bump(154) co.RouteViewCounter.Bump(155)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2528,7 +2544,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(155) co.RouteViewCounter.Bump(156)
err = routes.AccountLoginMFAVerifySubmit(w,req,user) err = routes.AccountLoginMFAVerifySubmit(w,req,user)
case "/accounts/create/submit/": case "/accounts/create/submit/":
err = c.ParseForm(w,req,user) err = c.ParseForm(w,req,user)
@ -2536,10 +2552,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(156) co.RouteViewCounter.Bump(157)
err = routes.AccountRegisterSubmit(w,req,user) err = routes.AccountRegisterSubmit(w,req,user)
case "/accounts/password-reset/": case "/accounts/password-reset/":
co.RouteViewCounter.Bump(157) co.RouteViewCounter.Bump(158)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2551,10 +2567,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(158) co.RouteViewCounter.Bump(159)
err = routes.AccountPasswordResetSubmit(w,req,user) err = routes.AccountPasswordResetSubmit(w,req,user)
case "/accounts/password-reset/token/": case "/accounts/password-reset/token/":
co.RouteViewCounter.Bump(159) co.RouteViewCounter.Bump(160)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2566,7 +2582,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(160) co.RouteViewCounter.Bump(161)
err = routes.AccountPasswordResetTokenSubmit(w,req,user) err = routes.AccountPasswordResetTokenSubmit(w,req,user)
} }
/*case "/sitemaps": // TODO: Count these views /*case "/sitemaps": // TODO: Count these views
@ -2583,7 +2599,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
h.Del("Content-Type") h.Del("Content-Type")
h.Del("Content-Encoding") h.Del("Content-Encoding")
} }
co.RouteViewCounter.Bump(162) co.RouteViewCounter.Bump(163)
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
r.UploadHandler(w,req) // TODO: Count these views r.UploadHandler(w,req) // TODO: Count these views
@ -2593,7 +2609,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
// TODO: Add support for favicons and robots.txt files // TODO: Add support for favicons and robots.txt files
switch(extraData) { switch(extraData) {
case "robots.txt": case "robots.txt":
co.RouteViewCounter.Bump(164) co.RouteViewCounter.Bump(165)
return routes.RobotsTxt(w,req) return routes.RobotsTxt(w,req)
case "favicon.ico": case "favicon.ico":
gzw, ok := w.(c.GzipResponseWriter) gzw, ok := w.(c.GzipResponseWriter)
@ -2607,10 +2623,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
routes.StaticFile(w,req) routes.StaticFile(w,req)
return nil return nil
case "opensearch.xml": case "opensearch.xml":
co.RouteViewCounter.Bump(166) co.RouteViewCounter.Bump(167)
return routes.OpenSearchXml(w,req) return routes.OpenSearchXml(w,req)
/*case "sitemap.xml": /*case "sitemap.xml":
co.RouteViewCounter.Bump(165) co.RouteViewCounter.Bump(166)
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/
} }
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
@ -2621,7 +2637,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
r.RUnlock() r.RUnlock()
if ok { if ok {
co.RouteViewCounter.Bump(161) // TODO: Be more specific about *which* dynamic route it is co.RouteViewCounter.Bump(162) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
return handle(w,req,user) return handle(w,req,user)
} }
@ -2632,7 +2648,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} else { } else {
r.DumpRequest(req,"Bad Route") r.DumpRequest(req,"Bad Route")
} }
co.RouteViewCounter.Bump(167) co.RouteViewCounter.Bump(168)
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
} }
return err return err

View File

@ -394,6 +394,7 @@
"topic.view_count_suffix":" views", "topic.view_count_suffix":" views",
"topic.plus":"+", "topic.plus":"+",
"topic.plus_one":"+1", "topic.plus_one":"+1",
"topic.minus_one":"-1",
"topic.gap_up":" up", "topic.gap_up":" up",
"topic.quote_button_text":"Quote", "topic.quote_button_text":"Quote",
"topic.edit_button_text":"Edit", "topic.edit_button_text":"Edit",

View File

@ -35,7 +35,7 @@ function ajaxError(xhr,status,errstr) {
function postLink(event) { function postLink(event) {
event.preventDefault(); event.preventDefault();
let formAction = $(event.target).closest('a').attr("href"); let formAction = $(event.target).closest('a').attr("href");
$.ajax({ url: formAction, type: "POST", dataType: "json", error: ajaxError, data: {js: "1"} }); $.ajax({ url: formAction, type: "POST", dataType: "json", error: ajaxError, data: {js: 1} });
} }
function bindToAlerts() { function bindToAlerts() {
@ -98,12 +98,6 @@ function updateAlertList(menuAlerts) {
alertListNode.innerHTML = ""; alertListNode.innerHTML = "";
let any = false; 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; let j = 0;
for(var i = 0; i < alertList.length && j < 8; i++) { for(var i = 0; i < alertList.length && j < 8; i++) {
any = true; any = true;
@ -213,9 +207,9 @@ function wsAlertEvent(data) {
} }
function runWebSockets(resume = false) { function runWebSockets(resume = false) {
if(window.location.protocol == "https:") { let s = "";
conn = new WebSocket("wss://" + document.location.host + "/ws/"); if(window.location.protocol == "https:") s = "s";
} else conn = new WebSocket("ws://" + document.location.host + "/ws/"); conn = new WebSocket("ws"+s+"://" + document.location.host + "/ws/");
conn.onerror = (err) => { conn.onerror = (err) => {
console.log(err); console.log(err);
@ -418,20 +412,32 @@ function mainInit(){
moreTopicCount = 0; moreTopicCount = 0;
}) })
$(".add_like").click(function(event) { $(".add_like,.remove_like").click(function(event) {
event.preventDefault(); event.preventDefault();
//$(this).unbind("click");
let target = this.closest("a").getAttribute("href"); let target = this.closest("a").getAttribute("href");
console.log("target: ", target); console.log("target:", target);
this.classList.remove("add_like");
this.classList.add("remove_like");
let controls = this.closest(".controls"); let controls = this.closest(".controls");
let hadLikes = controls.classList.contains("has_likes"); let hadLikes = controls.classList.contains("has_likes");
if(!hadLikes) controls.classList.add("has_likes");
let likeCountNode = controls.getElementsByClassName("like_count")[0]; let likeCountNode = controls.getElementsByClassName("like_count")[0];
console.log("likeCountNode",likeCountNode); console.log("likeCountNode",likeCountNode);
if(this.classList.contains("add_like")) {
this.classList.remove("add_like");
this.classList.add("remove_like");
if(!hadLikes) controls.classList.add("has_likes");
this.closest("a").setAttribute("href", target.replace("like","unlike"));
likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) + 1; likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) + 1;
let likeButton = this; } else {
this.classList.remove("remove_like");
this.classList.add("add_like");
let likeCount = parseInt(likeCountNode.innerHTML);
if(likeCount==1) controls.classList.remove("has_likes");
this.closest("a").setAttribute("href", target.replace("unlike","like"));
likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1;
}
//let likeButton = this;
$.ajax({ $.ajax({
url: target, url: target,
type: "POST", type: "POST",
@ -441,10 +447,7 @@ function mainInit(){
success: function (data, status, xhr) { success: function (data, status, xhr) {
if("success" in data && data["success"] == "1") return; if("success" in data && data["success"] == "1") return;
// addNotice("Failed to add a like: {err}") // addNotice("Failed to add a like: {err}")
likeButton.classList.add("add_like"); //likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1;
likeButton.classList.remove("remove_like");
if(!hadLikes) controls.classList.remove("has_likes");
likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1;
console.log("data", data); console.log("data", data);
console.log("status", status); console.log("status", status);
console.log("xhr", xhr); console.log("xhr", xhr);
@ -691,9 +694,9 @@ function mainInit(){
$(blockParent).find('.hide_on_block_edit').removeClass("edit_opened"); $(blockParent).find('.hide_on_block_edit').removeClass("edit_opened");
$(blockParent).find('.show_on_block_edit').removeClass("edit_opened"); $(blockParent).find('.show_on_block_edit').removeClass("edit_opened");
block.classList.remove("in_edit"); block.classList.remove("in_edit");
let newContent = block.querySelector('textarea').value; let content = block.querySelector('textarea').value;
block.innerHTML = quickParse(newContent); block.innerHTML = quickParse(content);
if(srcNode!=null) srcNode.innerText = newContent; if(srcNode!=null) srcNode.innerText = content;
let formAction = this.closest('a').getAttribute("href"); let formAction = this.closest('a').getAttribute("href");
// TODO: Bounce the parsed post back and set innerHTML to it? // TODO: Bounce the parsed post back and set innerHTML to it?
@ -701,7 +704,7 @@ function mainInit(){
url: formAction, url: formAction,
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: { js: "1", edit_item: newContent }, data: { js: 1, edit_item: content },
error: ajaxError, error: ajaxError,
success: (data,status,xhr) => { success: (data,status,xhr) => {
if("Content" in data) block.innerHTML = data["Content"]; if("Content" in data) block.innerHTML = data["Content"];
@ -720,8 +723,8 @@ function mainInit(){
event.preventDefault(); event.preventDefault();
let blockParent = $(this).closest('.editable_parent'); let blockParent = $(this).closest('.editable_parent');
let block = blockParent.find('.editable_block').eq(0); let block = blockParent.find('.editable_block').eq(0);
let newContent = block.find('input').eq(0).val(); let content = block.find('input').eq(0).val();
block.html(newContent); block.html(content);
let formAction = $(this).closest('a').attr("href"); let formAction = $(this).closest('a').attr("href");
$.ajax({ $.ajax({
@ -729,7 +732,7 @@ function mainInit(){
type: "POST", type: "POST",
dataType: "json", dataType: "json",
error: ajaxError, error: ajaxError,
data: { js: "1", edit_item: newContent } data: { js: 1, edit_item: content }
}); });
}); });
}); });
@ -771,7 +774,7 @@ function mainInit(){
$(".submit_edit").click(function(event) { $(".submit_edit").click(function(event) {
event.preventDefault(); event.preventDefault();
var outData = {js: "1"} var outData = {js: 1}
var blockParent = $(this).closest('.editable_parent'); var blockParent = $(this).closest('.editable_parent');
blockParent.find('.editable_block').each(function() { blockParent.find('.editable_block').each(function() {
var fieldName = this.getAttribute("data-field"); var fieldName = this.getAttribute("data-field");

View File

@ -140,6 +140,7 @@ func replyRoutes() *RouteGroup {
Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"), Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"),
Action("routes.ReplyDeleteSubmit", "/reply/delete/submit/", "extraData"), Action("routes.ReplyDeleteSubmit", "/reply/delete/submit/", "extraData"),
Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData"), Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData"),
Action("routes.ReplyUnlikeSubmit", "/reply/unlike/submit/", "extraData"),
//MemberView("routes.ReplyEdit","/reply/edit/","extraData"), // No js fallback //MemberView("routes.ReplyEdit","/reply/edit/","extraData"), // No js fallback
//MemberView("routes.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case //MemberView("routes.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case
UploadAction("routes.AddAttachToReplySubmit", "/reply/attach/add/submit/", "extraData").MaxSizeVar("int(c.Config.MaxRequestSize)"), UploadAction("routes.AddAttachToReplySubmit", "/reply/attach/add/submit/", "extraData").MaxSizeVar("int(c.Config.MaxRequestSize)"),

View File

@ -26,7 +26,7 @@ func init() {
c.DbInits.Add(func(acc *qgen.Accumulator) error { c.DbInits.Add(func(acc *qgen.Accumulator) error {
replyStmts = ReplyStmts{ replyStmts = ReplyStmts{
// TODO: Less race-y attachment count updates // TODO: Less race-y attachment count updates
updateAttachs: acc.Update("replies").Set("attachCount = ?").Where("rid = ?").Prepare(), updateAttachs: acc.Update("replies").Set("attachCount=?").Where("rid=?").Prepare(),
createReplyPaging: acc.Select("replies").Cols("rid").Where("rid >= ? - 1 AND tid = ?").Orderby("rid ASC").Prepare(), createReplyPaging: acc.Select("replies").Cols("rid").Where("rid >= ? - 1 AND tid = ?").Orderby("rid ASC").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
@ -524,3 +524,63 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid s
} }
return nil return nil
} }
func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid)
if err != nil {
return c.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, js)
}
reply, err := c.Rstore.Get(rid)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("You can't unlike something which doesn't exist!", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
topic, err := c.Topics.Get(reply.ParentID)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("The parent topic doesn't exist.", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
// TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js)
}
_, err = c.Users.Get(reply.CreatedBy)
if err != nil && err != sql.ErrNoRows {
return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
err = reply.Unlike(user.ID)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
// TODO: Push dismiss-event alerts to the users.
err = c.Activity.DeleteByParams("like", topic.ID, "reply")
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_reply", reply.ID, &user)
if skip || rerr != nil {
return rerr
}
if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(reply.ParentID), http.StatusSeeOther)
} else {
_, _ = w.Write(successJSONBytes)
}
return nil
}

View File

@ -38,8 +38,15 @@
<a href="{{.Topic.UserLink}}" class="username real_username" rel="author">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp; <a href="{{.Topic.UserLink}}" class="username real_username" rel="author">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp;
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="mod_button" {{if .Topic.Liked}}title="{{lang "topic.unlike_tooltip"}}" aria-label="{{lang "topic.unlike_aria"}}"{{else}}title="{{lang "topic.like_tooltip"}}" aria-label="{{lang "topic.like_aria"}}"{{end}}> {{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}}
<button class="username like_label {{if .Topic.Liked}}remove{{else}}add{{end}}_like"></button></a>{{end}}
{{if .Topic.Liked}}
<a href="/topic/unlike/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.unlike_tooltip"}}" aria-label="{{lang "topic.unlike_aria"}}">
<button class="username like_label remove_like"></button></a>{{else}}
<a href="/topic/like/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.like_tooltip"}}" aria-label="{{lang "topic.like_aria"}}">
<button class="username like_label add_like"></button></a>{{end}}
{{end}}{{end}}
<a href="" class="mod_button quote_item" title="{{lang "topic.quote_tooltip"}}" aria-label="{{lang "topic.quote_aria"}}"><button class="username quote_label"></button></a> <a href="" class="mod_button quote_item" title="{{lang "topic.quote_tooltip"}}" aria-label="{{lang "topic.quote_aria"}}"><button class="username quote_label"></button></a>

View File

@ -31,7 +31,10 @@
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}"> <div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
<div class="action_button_left"> <div class="action_button_left">
{{if $.CurrentUser.Loggedin}} {{if $.CurrentUser.Loggedin}}
{{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}}<a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button like_item {{if .Liked}}remove{{else}}add{{end}}_like" aria-label="{{lang "topic.post_like_aria"}}" data-action="like"></a>{{end}}{{end}} {{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}}
{{if .Liked}}<a href="/reply/unlike/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button like_item remove_like" aria-label="{{lang "topic.post_unlike_aria"}}" data-action="unlike"></a>{{else}}
<a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button like_item add_like" aria-label="{{lang "topic.post_like_aria"}}" data-action="like"></a>{{end}}
{{end}}{{end}}
<a href="" class="action_button quote_item" aria-label="{{lang "topic.quote_aria"}}" data-action="quote"></a> <a href="" class="action_button quote_item" aria-label="{{lang "topic.quote_aria"}}" data-action="quote"></a>
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}} {{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button edit_item" aria-label="{{lang "topic.post_edit_aria"}}" data-action="edit"></a>{{end}} {{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button edit_item" aria-label="{{lang "topic.post_edit_aria"}}" data-action="edit"></a>{{end}}

View File

@ -13,7 +13,7 @@
<span class="controls{{if .LikeCount}} has_likes{{end}}"> <span class="controls{{if .LikeCount}} has_likes{{end}}">
<a href="{{.UserLink}}" class="username real_username" rel="author">{{.CreatedByName}}</a>&nbsp;&nbsp; <a href="{{.UserLink}}" class="username real_username" rel="author">{{.CreatedByName}}</a>&nbsp;&nbsp;
{{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}<a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_like_tooltip"}}" aria-label="{{lang "topic.post_like_aria"}}"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_unlike_tooltip"}}" aria-label="{{lang "topic.post_unlike_aria"}}"><button class="username like_label add_like"></button></a>{{end}}{{end}} {{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}}{{if .Liked}}<a href="/reply/unlike/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_unlike_tooltip"}}" aria-label="{{lang "topic.post_unlike_aria"}}"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_like_tooltip"}}" aria-label="{{lang "topic.post_like_aria"}}"><button class="username like_label add_like"></button></a>{{end}}{{end}}{{end}}
<a href="" class="mod_button quote_item" title="{{lang "topic.quote_tooltip"}}" aria-label="{{lang "topic.quote_aria"}}"><button class="username quote_label"></button></a> <a href="" class="mod_button quote_item" title="{{lang "topic.quote_tooltip"}}" aria-label="{{lang "topic.quote_aria"}}"><button class="username quote_label"></button></a>

View File

@ -1018,6 +1018,9 @@ input[type=checkbox]:not(:checked):hover + label .sel {
.add_like:before, .remove_like:before { .add_like:before, .remove_like:before {
content: "{{lang "topic.plus_one" . }}"; content: "{{lang "topic.plus_one" . }}";
} }
.remove_like:before {
content: "{{lang "topic.minus_one" . }}";
}
.button_container .open_edit:after, .edit_item:after { .button_container .open_edit:after, .edit_item:after {
content: "{{lang "topic.edit_button_text" . }}"; content: "{{lang "topic.edit_button_text" . }}";
} }