From b5fa9c69f7dafff8481e54b36ea52e656d40e5c4 Mon Sep 17 00:00:00 2001 From: Azareal Date: Fri, 31 Jan 2020 20:48:55 +1000 Subject: [PATCH] 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. --- common/extend.go | 98 ++++++++++--------- common/group.go | 12 +-- common/menus.go | 8 +- common/reply.go | 21 +++- common/topic.go | 2 +- gen_router.go | 172 ++++++++++++++++++--------------- langs/english.json | 1 + public/global.js | 63 ++++++------ router_gen/routes.go | 1 + routes/reply.go | 62 +++++++++++- templates/topic.html | 11 ++- templates/topic_alt_posts.html | 5 +- templates/topic_posts.html | 2 +- themes/nox/public/main.css | 3 + 14 files changed, 286 insertions(+), 175 deletions(-) diff --git a/common/extend.go b/common/extend.go index 2855e97e..a3f3edb5 100644 --- a/common/extend.go +++ b/common/extend.go @@ -15,7 +15,7 @@ import ( "sync" "sync/atomic" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) var ErrPluginNotInstallable = errors.New("This plugin is not installable") @@ -72,7 +72,7 @@ var hookTable = &HookTable{ map[string]func(...interface{}) interface{}{ //"convo_post_update":nil, //"convo_post_create":nil, - + "forum_trow_assign": nil, "topics_topic_row_assign": nil, //"topics_user_row_assign": nil, @@ -89,15 +89,16 @@ var hookTable = &HookTable{ "route_topic_list_start": nil, "route_forum_list_start": nil, - "action_end_create_topic": nil, - "action_end_edit_topic":nil, - "action_end_delete_topic":nil, - "action_end_lock_topic":nil, - "action_end_unlock_topic": nil, - "action_end_stick_topic": nil, + "action_end_create_topic": nil, + "action_end_edit_topic": nil, + "action_end_delete_topic": nil, + "action_end_lock_topic": nil, + "action_end_unlock_topic": nil, + "action_end_stick_topic": nil, "action_end_unstick_topic": nil, - "action_end_move_topic": nil, - "action_end_like_topic":nil, + "action_end_move_topic": nil, + "action_end_like_topic": nil, + "action_end_unlike_topic": nil, "action_end_create_reply": nil, "action_end_edit_reply": nil, @@ -105,11 +106,12 @@ var hookTable = &HookTable{ "action_end_add_attach_to_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_unban_user":nil, - "action_end_activate_user":nil, + "action_end_ban_user": nil, + "action_end_unban_user": nil, + "action_end_activate_user": nil, "router_after_filters": 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? -func (table *HookTable) Hook(name string, data interface{}) interface{} { - hooks, ok := table.Hooks[name] +func (t *HookTable) Hook(name string, data interface{}) interface{} { + hooks, ok := t.Hooks[name] if ok { for _, hook := range hooks { 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 -func (table *HookTable) HookSkippable(name string, data interface{}) (skip bool) { - hooks, ok := table.Hooks[name] +func (t *HookTable) HookSkippable(name string, data interface{}) (skip bool) { + hooks, ok := t.Hooks[name] if ok { for _, hook := range hooks { 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 // TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn? -func (table *HookTable) Vhook(name string, data ...interface{}) interface{} { - hook := table.Vhooks[name] +func (t *HookTable) Vhook(name string, data ...interface{}) interface{} { + hook := t.Vhooks[name] if hook != nil { return hook(data...) } return nil } -func (table *HookTable) VhookNoRet(name string, data ...interface{}) { - hook := table.Vhooks[name] +func (t *HookTable) VhookNoRet(name string, data ...interface{}) { + hook := t.Vhooks[name] if hook != nil { _ = hook(data...) } } // TODO: Find a better way of doing this -func (table *HookTable) VhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) { - hook := table.Vhooks[name] +func (t *HookTable) VhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) { + hook := t.Vhooks[name] if hook != nil { 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 -func (table *HookTable) VhookSkippable(name string, data ...interface{}) (bool, RouteError) { - hook := table.VhookSkippable_[name] +func (t *HookTable) VhookSkippable(name string, data ...interface{}) (bool, RouteError) { + hook := t.VhookSkippable_[name] if hook != nil { 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 // 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 { - ssHooks, ok := table.Sshooks[name] +func (t *HookTable) Sshook(name, data string) string { + ssHooks, ok := t.Sshooks[name] if ok { for _, hook := range ssHooks { data = hook(data) @@ -331,17 +333,17 @@ type Plugin struct { Data interface{} // Usually used for hosting the VMs / reusable elements of non-native plugins } -func (plugin *Plugin) BypassActive() (active bool, err error) { - err = extendStmts.isActive.QueryRow(plugin.UName).Scan(&active) +func (pl *Plugin) BypassActive() (active bool, err error) { + err = extendStmts.isActive.QueryRow(pl.UName).Scan(&active) if err != nil && err != sql.ErrNoRows { return false, err } return active, nil } -func (plugin *Plugin) InDatabase() (exists bool, err error) { +func (pl *Plugin) InDatabase() (exists bool, err error) { 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 { 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? -func (plugin *Plugin) SetActive(active bool) (err error) { - _, err = extendStmts.setActive.Exec(active, plugin.UName) +func (pl *Plugin) SetActive(active bool) (err error) { + _, err = extendStmts.setActive.Exec(active, pl.UName) if err == nil { - plugin.Active = active + pl.Active = active } return err } // 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) { - if !plugin.Installable { +func (pl *Plugin) SetInstalled(installed bool) (err error) { + if !pl.Installable { return ErrPluginNotInstallable } - _, err = extendStmts.setInstalled.Exec(installed, plugin.UName) + _, err = extendStmts.setInstalled.Exec(installed, pl.UName) if err == nil { - plugin.Installed = installed + pl.Installed = installed } return err } -func (plugin *Plugin) AddToDatabase(active bool, installed bool) (err error) { - _, err = extendStmts.add.Exec(plugin.UName, active, installed) +func (pl *Plugin) AddToDatabase(active, installed bool) (err error) { + _, err = extendStmts.add.Exec(pl.UName, active, installed) if err == nil { - plugin.Active = active - plugin.Installed = installed + pl.Active = active + pl.Installed = installed } return err } @@ -393,12 +395,12 @@ func init() { DbInits.Add(func(acc *qgen.Accumulator) error { pl := "plugins" 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(), - setActive: acc.Update(pl).Set("active = ?").Where("uname = ?").Prepare(), - setInstalled: acc.Update(pl).Set("installed = ?").Where("uname = ?").Prepare(), - add: acc.Insert(pl).Columns("uname, active, installed").Fields("?,?,?").Prepare(), + isActive: acc.Select(pl).Columns("active").Where("uname=?").Prepare(), + setActive: acc.Update(pl).Set("active=?").Where("uname=?").Prepare(), + setInstalled: acc.Update(pl).Set("installed=?").Where("uname=?").Prepare(), + add: acc.Insert(pl).Columns("uname,active,installed").Fields("?,?,?").Prepare(), } return acc.FirstError() }) diff --git a/common/group.go b/common/group.go index 08a14b5e..f2a0e890 100644 --- a/common/group.go +++ b/common/group.go @@ -4,7 +4,7 @@ import ( "database/sql" "encoding/json" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) var blankGroup = Group{ID: 0, Name: ""} @@ -46,15 +46,15 @@ func init() { DbInits.Add(func(acc *qgen.Accumulator) error { ug := "users_groups" groupStmts = GroupStmts{ - updateGroup: acc.Update(ug).Set("name = ?, tag = ?").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(), + updateGroup: acc.Update(ug).Set("name=?,tag=?").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(), } 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) if err != nil { return err @@ -63,7 +63,7 @@ func (g *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err error) 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) if err != nil { return err diff --git a/common/menus.go b/common/menus.go index cd8366ac..7f9e98b6 100644 --- a/common/menus.go +++ b/common/menus.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/Azareal/Gosora/common/phrases" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) type MenuItemList []MenuItem @@ -66,10 +66,10 @@ func init() { DbInits.Add(func(acc *qgen.Accumulator) error { mi := "menu_items" 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(), - delete: acc.Delete(mi).Where("miid = ?").Prepare(), - updateOrder: acc.Update(mi).Set("order = ?").Where("miid = ?").Prepare(), + delete: acc.Delete(mi).Where("miid=?").Prepare(), + updateOrder: acc.Update(mi).Set("order=?").Where("miid=?").Prepare(), } return acc.FirstError() }) diff --git a/common/reply.go b/common/reply.go index d84535de..2968bf9a 100644 --- a/common/reply.go +++ b/common/reply.go @@ -10,8 +10,8 @@ import ( "database/sql" "errors" "html" - "time" "strconv" + "time" qgen "github.com/Azareal/Gosora/query_gen" ) @@ -93,7 +93,7 @@ func init() { updateTopicReplies: acc.RawPrepare("UPDATE topics t INNER JOIN replies r ON t.tid = r.tid SET t.lastReplyBy = r.createdBy, t.lastReplyAt = r.createdAt, t.lastReplyID = r.rid WHERE t.tid = ?"), updateTopicReplies2: acc.Update("topics").Set("lastReplyAt=createdAt,lastReplyBy=createdBy,lastReplyID=0").Where("postCount=1 AND tid=?").Prepare(), - getAidsOfReply: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='replies'").Prepare(), + getAidsOfReply: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='replies'").Prepare(), } return acc.FirstError() }) @@ -124,6 +124,21 @@ func (r *Reply) Like(uid int) (err error) { 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? func (r *Reply) Delete() error { creator, err := Users.Get(r.CreatedBy) @@ -166,7 +181,7 @@ func (r *Reply) Delete() error { if err != nil { 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 { return err } diff --git a/common/topic.go b/common/topic.go index 2384e6f8..41a2e7a4 100644 --- a/common/topic.go +++ b/common/topic.go @@ -336,7 +336,7 @@ func (t *Topic) Unlike(uid int) error { } _, err = userStmts.decLiked.Exec(1, uid) t.cacheRemove() - return nil + return err } func handleLikedTopicReplies(tid int) error { diff --git a/gen_router.go b/gen_router.go index 210dcf3b..2104ca9c 100644 --- a/gen_router.go +++ b/gen_router.go @@ -166,6 +166,7 @@ var RouteMap = map[string]interface{}{ "routes.ReplyEditSubmit": routes.ReplyEditSubmit, "routes.ReplyDeleteSubmit": routes.ReplyDeleteSubmit, "routes.ReplyLikeSubmit": routes.ReplyLikeSubmit, + "routes.ReplyUnlikeSubmit": routes.ReplyUnlikeSubmit, "routes.AddAttachToReplySubmit": routes.AddAttachToReplySubmit, "routes.RemoveAttachFromReplySubmit": routes.RemoveAttachFromReplySubmit, "routes.ProfileReplyCreateSubmit": routes.ProfileReplyCreateSubmit, @@ -339,32 +340,33 @@ var routeMapEnum = map[string]int{ "routes.ReplyEditSubmit": 140, "routes.ReplyDeleteSubmit": 141, "routes.ReplyLikeSubmit": 142, - "routes.AddAttachToReplySubmit": 143, - "routes.RemoveAttachFromReplySubmit": 144, - "routes.ProfileReplyCreateSubmit": 145, - "routes.ProfileReplyEditSubmit": 146, - "routes.ProfileReplyDeleteSubmit": 147, - "routes.PollVote": 148, - "routes.PollResults": 149, - "routes.AccountLogin": 150, - "routes.AccountRegister": 151, - "routes.AccountLogout": 152, - "routes.AccountLoginSubmit": 153, - "routes.AccountLoginMFAVerify": 154, - "routes.AccountLoginMFAVerifySubmit": 155, - "routes.AccountRegisterSubmit": 156, - "routes.AccountPasswordReset": 157, - "routes.AccountPasswordResetSubmit": 158, - "routes.AccountPasswordResetToken": 159, - "routes.AccountPasswordResetTokenSubmit": 160, - "routes.DynamicRoute": 161, - "routes.UploadedFile": 162, - "routes.StaticFile": 163, - "routes.RobotsTxt": 164, - "routes.SitemapXml": 165, - "routes.OpenSearchXml": 166, - "routes.BadRoute": 167, - "routes.HTTPSRedirect": 168, + "routes.ReplyUnlikeSubmit": 143, + "routes.AddAttachToReplySubmit": 144, + "routes.RemoveAttachFromReplySubmit": 145, + "routes.ProfileReplyCreateSubmit": 146, + "routes.ProfileReplyEditSubmit": 147, + "routes.ProfileReplyDeleteSubmit": 148, + "routes.PollVote": 149, + "routes.PollResults": 150, + "routes.AccountLogin": 151, + "routes.AccountRegister": 152, + "routes.AccountLogout": 153, + "routes.AccountLoginSubmit": 154, + "routes.AccountLoginMFAVerify": 155, + "routes.AccountLoginMFAVerifySubmit": 156, + "routes.AccountRegisterSubmit": 157, + "routes.AccountPasswordReset": 158, + "routes.AccountPasswordResetSubmit": 159, + "routes.AccountPasswordResetToken": 160, + "routes.AccountPasswordResetTokenSubmit": 161, + "routes.DynamicRoute": 162, + "routes.UploadedFile": 163, + "routes.StaticFile": 164, + "routes.RobotsTxt": 165, + "routes.SitemapXml": 166, + "routes.OpenSearchXml": 167, + "routes.BadRoute": 168, + "routes.HTTPSRedirect": 169, } var reverseRouteMapEnum = map[int]string{ 0: "routes.Overview", @@ -510,32 +512,33 @@ var reverseRouteMapEnum = map[int]string{ 140: "routes.ReplyEditSubmit", 141: "routes.ReplyDeleteSubmit", 142: "routes.ReplyLikeSubmit", - 143: "routes.AddAttachToReplySubmit", - 144: "routes.RemoveAttachFromReplySubmit", - 145: "routes.ProfileReplyCreateSubmit", - 146: "routes.ProfileReplyEditSubmit", - 147: "routes.ProfileReplyDeleteSubmit", - 148: "routes.PollVote", - 149: "routes.PollResults", - 150: "routes.AccountLogin", - 151: "routes.AccountRegister", - 152: "routes.AccountLogout", - 153: "routes.AccountLoginSubmit", - 154: "routes.AccountLoginMFAVerify", - 155: "routes.AccountLoginMFAVerifySubmit", - 156: "routes.AccountRegisterSubmit", - 157: "routes.AccountPasswordReset", - 158: "routes.AccountPasswordResetSubmit", - 159: "routes.AccountPasswordResetToken", - 160: "routes.AccountPasswordResetTokenSubmit", - 161: "routes.DynamicRoute", - 162: "routes.UploadedFile", - 163: "routes.StaticFile", - 164: "routes.RobotsTxt", - 165: "routes.SitemapXml", - 166: "routes.OpenSearchXml", - 167: "routes.BadRoute", - 168: "routes.HTTPSRedirect", + 143: "routes.ReplyUnlikeSubmit", + 144: "routes.AddAttachToReplySubmit", + 145: "routes.RemoveAttachFromReplySubmit", + 146: "routes.ProfileReplyCreateSubmit", + 147: "routes.ProfileReplyEditSubmit", + 148: "routes.ProfileReplyDeleteSubmit", + 149: "routes.PollVote", + 150: "routes.PollResults", + 151: "routes.AccountLogin", + 152: "routes.AccountRegister", + 153: "routes.AccountLogout", + 154: "routes.AccountLoginSubmit", + 155: "routes.AccountLoginMFAVerify", + 156: "routes.AccountLoginMFAVerifySubmit", + 157: "routes.AccountRegisterSubmit", + 158: "routes.AccountPasswordReset", + 159: "routes.AccountPasswordResetSubmit", + 160: "routes.AccountPasswordResetToken", + 161: "routes.AccountPasswordResetTokenSubmit", + 162: "routes.DynamicRoute", + 163: "routes.UploadedFile", + 164: "routes.StaticFile", + 165: "routes.RobotsTxt", + 166: "routes.SitemapXml", + 167: "routes.OpenSearchXml", + 168: "routes.BadRoute", + 169: "routes.HTTPSRedirect", } var osMapEnum = map[string]int{ "unknown": 0, @@ -693,7 +696,7 @@ type HTTPSRedirect struct {} func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Header().Set("Connection", "close") - co.RouteViewCounter.Bump(168) + co.RouteViewCounter.Bump(169) dest := "https://" + req.Host + req.URL.String() http.Redirect(w, req, dest, http.StatusTemporaryRedirect) } @@ -901,7 +904,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { co.GlobalViewCounter.Bump() if prefix == "/s" { //old prefix: /static - co.RouteViewCounter.Bump(163) + co.RouteViewCounter.Bump(164) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -2386,6 +2389,19 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c co.RouteViewCounter.Bump(142) 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/": err = c.MemberOnly(w,req,user) if err != nil { @@ -2401,7 +2417,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(143) + co.RouteViewCounter.Bump(144) err = routes.AddAttachToReplySubmit(w,req,user,extraData) case "/reply/attach/remove/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2414,7 +2430,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(144) + co.RouteViewCounter.Bump(145) err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData) } case "/profile": @@ -2430,7 +2446,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(145) + co.RouteViewCounter.Bump(146) err = routes.ProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2443,7 +2459,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(146) + co.RouteViewCounter.Bump(147) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2456,7 +2472,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(147) + co.RouteViewCounter.Bump(148) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } case "/poll": @@ -2472,23 +2488,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(148) + co.RouteViewCounter.Bump(149) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - co.RouteViewCounter.Bump(149) + co.RouteViewCounter.Bump(150) err = routes.PollResults(w,req,user,extraData) } case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - co.RouteViewCounter.Bump(150) + co.RouteViewCounter.Bump(151) head, err := c.UserCheck(w,req,&user) if err != nil { return err } err = routes.AccountLogin(w,req,user,head) case "/accounts/create/": - co.RouteViewCounter.Bump(151) + co.RouteViewCounter.Bump(152) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2505,7 +2521,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(152) + co.RouteViewCounter.Bump(153) err = routes.AccountLogout(w,req,user) case "/accounts/login/submit/": err = c.ParseForm(w,req,user) @@ -2513,10 +2529,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(153) + co.RouteViewCounter.Bump(154) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/mfa_verify/": - co.RouteViewCounter.Bump(154) + co.RouteViewCounter.Bump(155) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2528,7 +2544,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(155) + co.RouteViewCounter.Bump(156) err = routes.AccountLoginMFAVerifySubmit(w,req,user) case "/accounts/create/submit/": err = c.ParseForm(w,req,user) @@ -2536,10 +2552,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(156) + co.RouteViewCounter.Bump(157) err = routes.AccountRegisterSubmit(w,req,user) case "/accounts/password-reset/": - co.RouteViewCounter.Bump(157) + co.RouteViewCounter.Bump(158) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2551,10 +2567,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(158) + co.RouteViewCounter.Bump(159) err = routes.AccountPasswordResetSubmit(w,req,user) case "/accounts/password-reset/token/": - co.RouteViewCounter.Bump(159) + co.RouteViewCounter.Bump(160) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2566,7 +2582,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(160) + co.RouteViewCounter.Bump(161) err = routes.AccountPasswordResetTokenSubmit(w,req,user) } /*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-Encoding") } - co.RouteViewCounter.Bump(162) + co.RouteViewCounter.Bump(163) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? 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 switch(extraData) { case "robots.txt": - co.RouteViewCounter.Bump(164) + co.RouteViewCounter.Bump(165) return routes.RobotsTxt(w,req) case "favicon.ico": 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) return nil case "opensearch.xml": - co.RouteViewCounter.Bump(166) + co.RouteViewCounter.Bump(167) return routes.OpenSearchXml(w,req) /*case "sitemap.xml": - co.RouteViewCounter.Bump(165) + co.RouteViewCounter.Bump(166) return routes.SitemapXml(w,req)*/ } return c.NotFound(w,req,nil) @@ -2621,7 +2637,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c r.RUnlock() 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 return handle(w,req,user) } @@ -2632,7 +2648,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } else { r.DumpRequest(req,"Bad Route") } - co.RouteViewCounter.Bump(167) + co.RouteViewCounter.Bump(168) return c.NotFound(w,req,nil) } return err diff --git a/langs/english.json b/langs/english.json index df981a3b..62365b2d 100644 --- a/langs/english.json +++ b/langs/english.json @@ -394,6 +394,7 @@ "topic.view_count_suffix":" views", "topic.plus":"+", "topic.plus_one":"+1", + "topic.minus_one":"-1", "topic.gap_up":" up", "topic.quote_button_text":"Quote", "topic.edit_button_text":"Edit", diff --git a/public/global.js b/public/global.js index 8285d385..a953b18a 100644 --- a/public/global.js +++ b/public/global.js @@ -35,7 +35,7 @@ function ajaxError(xhr,status,errstr) { function postLink(event) { event.preventDefault(); 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() { @@ -98,12 +98,6 @@ function updateAlertList(menuAlerts) { alertListNode.innerHTML = ""; let any = false; - /*let outList = ""; - let j = 0; - for(var i = 0; i < alertList.length && j < 8; i++) { - outList += alertMapping[alertList[i]]; - j++; - }*/ let j = 0; for(var i = 0; i < alertList.length && j < 8; i++) { any = true; @@ -213,9 +207,9 @@ function wsAlertEvent(data) { } function runWebSockets(resume = false) { - if(window.location.protocol == "https:") { - conn = new WebSocket("wss://" + document.location.host + "/ws/"); - } else conn = new WebSocket("ws://" + document.location.host + "/ws/"); + let s = ""; + if(window.location.protocol == "https:") s = "s"; + conn = new WebSocket("ws"+s+"://" + document.location.host + "/ws/"); conn.onerror = (err) => { console.log(err); @@ -418,20 +412,32 @@ function mainInit(){ moreTopicCount = 0; }) - $(".add_like").click(function(event) { + $(".add_like,.remove_like").click(function(event) { event.preventDefault(); + //$(this).unbind("click"); let target = this.closest("a").getAttribute("href"); - console.log("target: ", target); - this.classList.remove("add_like"); - this.classList.add("remove_like"); + console.log("target:", target); + let controls = this.closest(".controls"); let hadLikes = controls.classList.contains("has_likes"); - if(!hadLikes) controls.classList.add("has_likes"); let likeCountNode = controls.getElementsByClassName("like_count")[0]; console.log("likeCountNode",likeCountNode); - likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) + 1; - let likeButton = this; - + 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; + } 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({ url: target, type: "POST", @@ -441,10 +447,7 @@ function mainInit(){ success: function (data, status, xhr) { if("success" in data && data["success"] == "1") return; // addNotice("Failed to add a like: {err}") - likeButton.classList.add("add_like"); - likeButton.classList.remove("remove_like"); - if(!hadLikes) controls.classList.remove("has_likes"); - likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1; + //likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1; console.log("data", data); console.log("status", status); console.log("xhr", xhr); @@ -691,9 +694,9 @@ function mainInit(){ $(blockParent).find('.hide_on_block_edit').removeClass("edit_opened"); $(blockParent).find('.show_on_block_edit').removeClass("edit_opened"); block.classList.remove("in_edit"); - let newContent = block.querySelector('textarea').value; - block.innerHTML = quickParse(newContent); - if(srcNode!=null) srcNode.innerText = newContent; + let content = block.querySelector('textarea').value; + block.innerHTML = quickParse(content); + if(srcNode!=null) srcNode.innerText = content; let formAction = this.closest('a').getAttribute("href"); // TODO: Bounce the parsed post back and set innerHTML to it? @@ -701,7 +704,7 @@ function mainInit(){ url: formAction, type: "POST", dataType: "json", - data: { js: "1", edit_item: newContent }, + data: { js: 1, edit_item: content }, error: ajaxError, success: (data,status,xhr) => { if("Content" in data) block.innerHTML = data["Content"]; @@ -720,8 +723,8 @@ function mainInit(){ event.preventDefault(); let blockParent = $(this).closest('.editable_parent'); let block = blockParent.find('.editable_block').eq(0); - let newContent = block.find('input').eq(0).val(); - block.html(newContent); + let content = block.find('input').eq(0).val(); + block.html(content); let formAction = $(this).closest('a').attr("href"); $.ajax({ @@ -729,7 +732,7 @@ function mainInit(){ type: "POST", dataType: "json", 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) { event.preventDefault(); - var outData = {js: "1"} + var outData = {js: 1} var blockParent = $(this).closest('.editable_parent'); blockParent.find('.editable_block').each(function() { var fieldName = this.getAttribute("data-field"); diff --git a/router_gen/routes.go b/router_gen/routes.go index edfbdd20..0e1198c9 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -140,6 +140,7 @@ func replyRoutes() *RouteGroup { Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"), Action("routes.ReplyDeleteSubmit", "/reply/delete/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.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)"), diff --git a/routes/reply.go b/routes/reply.go index 53c38046..39fa87ed 100644 --- a/routes/reply.go +++ b/routes/reply.go @@ -26,7 +26,7 @@ func init() { c.DbInits.Add(func(acc *qgen.Accumulator) error { replyStmts = ReplyStmts{ // 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(), } return acc.FirstError() @@ -524,3 +524,63 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid s } 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 +} diff --git a/templates/topic.html b/templates/topic.html index dcb9aca2..cc2a164a 100644 --- a/templates/topic.html +++ b/templates/topic.html @@ -38,8 +38,15 @@    {{if .CurrentUser.Loggedin}} - {{if .CurrentUser.Perms.LikeItem}} - {{end}} + {{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}} + + {{if .Topic.Liked}} + + {{else}} + + {{end}} + + {{end}}{{end}} diff --git a/templates/topic_alt_posts.html b/templates/topic_alt_posts.html index 3ce7b011..066a613b 100644 --- a/templates/topic_alt_posts.html +++ b/templates/topic_alt_posts.html @@ -31,7 +31,10 @@
{{if $.CurrentUser.Loggedin}} - {{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}}{{end}}{{end}} + {{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}} + {{if .Liked}}{{else}} + {{end}} + {{end}}{{end}} {{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}} {{if $.CurrentUser.Perms.EditReply}}{{end}} diff --git a/templates/topic_posts.html b/templates/topic_posts.html index 299dbd5f..2ee7d38e 100644 --- a/templates/topic_posts.html +++ b/templates/topic_posts.html @@ -13,7 +13,7 @@    - {{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}{{else}}{{end}}{{end}} + {{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}}{{if .Liked}}{{else}}{{end}}{{end}}{{end}} diff --git a/themes/nox/public/main.css b/themes/nox/public/main.css index 194b5f03..b0974652 100644 --- a/themes/nox/public/main.css +++ b/themes/nox/public/main.css @@ -1018,6 +1018,9 @@ input[type=checkbox]:not(:checked):hover + label .sel { .add_like:before, .remove_like:before { content: "{{lang "topic.plus_one" . }}"; } +.remove_like:before { + content: "{{lang "topic.minus_one" . }}"; +} .button_container .open_edit:after, .edit_item:after { content: "{{lang "topic.edit_button_text" . }}"; }