From 8419958b252d54e107474d27c74e2ae0c258a0b8 Mon Sep 17 00:00:00 2001 From: Noah Date: Mon, 29 Aug 2022 20:00:15 -0700 Subject: [PATCH] Likes on Comments, and other minor improvements * Add "Like" buttons to comments and forum posts. * Make "private" profiles more private (logged-in users see only their profile pic, display name, and can friend request or message, if they are not approved friends of the private user) * Add "logged-out view" visibility setting to profiles: to share a link to your page on other sites. Opt-in setting - default is login required to view your public profile page. * CSRF cookie fix. * Updated FAQ & Privacy pages. --- pkg/config/config.go | 2 +- pkg/controller/account/profile.go | 20 +++++++++++-- pkg/controller/account/settings.go | 14 ++++++---- pkg/controller/api/json_layer.go | 4 --- pkg/controller/api/likes.go | 24 +++++++++++++++- pkg/controller/forum/thread.go | 10 ++++++- pkg/controller/photo/view.go | 22 ++++++++++----- pkg/middleware/csrf.go | 1 + pkg/models/like.go | 5 ++-- pkg/models/user.go | 12 ++++++-- web/static/js/bulma.js | 26 +++++++++-------- web/static/js/likes.js | 2 ++ web/templates/account/dashboard.html | 6 ++++ web/templates/account/profile.html | 42 +++++++++++++++------------- web/templates/account/settings.html | 38 +++++++++++++++++++++---- web/templates/faq.html | 31 ++++++++++++++++++++ web/templates/forum/thread.html | 15 ++++++++++ web/templates/index.html | 17 ++++++----- web/templates/photo/permalink.html | 15 ++++++++++ web/templates/privacy.html | 18 +++++++++++- 20 files changed, 250 insertions(+), 74 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index e79b586..ac34500 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,7 +29,7 @@ const ( const ( BcryptCost = 14 SessionCookieName = "session_id" - CSRFCookieName = "csrf_token" + CSRFCookieName = "xsrf_token" CSRFInputName = "_csrf" // html input name SessionCookieMaxAge = 60 * 60 * 24 * 30 SessionRedisKeyFormat = "session/%s" diff --git a/pkg/controller/account/profile.go b/pkg/controller/account/profile.go index 198ecd3..ff7a12c 100644 --- a/pkg/controller/account/profile.go +++ b/pkg/controller/account/profile.go @@ -30,12 +30,26 @@ func Profile() http.HandlerFunc { return } - // Get the current user (if logged in). + // Forcing an external view? (preview of logged-out profile view for visibility=external accounts) + if r.FormValue("view") == "external" { + vars := map[string]interface{}{ + "User": user, + "IsPrivate": true, + "IsExternalView": true, + } + if err := tmpl.Execute(w, r, vars); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + + // Get the current user (if logged in). If not, check for external view. currentUser, err := session.CurrentUser(r) if err != nil { // The viewer is not logged in, bail now with the basic profile page. If this - // user is private, redirect to login. - if user.Visibility == models.UserVisibilityPrivate { + // user doesn't allow external viewers, redirect to login page. + if user.Visibility != models.UserVisibilityExternal { session.FlashError(w, r, "You must be signed in to view this page.") templates.Redirect(w, "/login?next="+url.QueryEscape(r.URL.String())) return diff --git a/pkg/controller/account/settings.go b/pkg/controller/account/settings.go index 6b0385f..c9e808f 100644 --- a/pkg/controller/account/settings.go +++ b/pkg/controller/account/settings.go @@ -92,15 +92,17 @@ func Settings() http.HandlerFunc { session.Flash(w, r, "Profile settings updated!") case "preferences": var ( - explicit = r.PostFormValue("explicit") == "true" - private = r.PostFormValue("private") == "true" + explicit = r.PostFormValue("explicit") == "true" + visibility = models.UserVisibility(r.PostFormValue("visibility")) ) user.Explicit = explicit - if private { - user.Visibility = models.UserVisibilityPrivate - } else { - user.Visibility = models.UserVisibilityPublic + user.Visibility = models.UserVisibilityPublic + + for _, cmp := range models.UserVisibilityOptions { + if visibility == cmp { + user.Visibility = visibility + } } if err := user.Save(); err != nil { diff --git a/pkg/controller/api/json_layer.go b/pkg/controller/api/json_layer.go index b165c48..518953f 100644 --- a/pkg/controller/api/json_layer.go +++ b/pkg/controller/api/json_layer.go @@ -5,8 +5,6 @@ import ( "errors" "io/ioutil" "net/http" - - "code.nonshy.com/nonshy/website/pkg/log" ) // Envelope is the standard JSON response envelope. @@ -27,8 +25,6 @@ func ParseJSON(r *http.Request, v interface{}) error { return err } - log.Error("body: %+v", body) - // Parse params from JSON. if err := json.Unmarshal(body, v); err != nil { return err diff --git a/pkg/controller/api/likes.go b/pkg/controller/api/likes.go index 4dad7f5..a19a77a 100644 --- a/pkg/controller/api/likes.go +++ b/pkg/controller/api/likes.go @@ -16,6 +16,7 @@ func Likes() http.HandlerFunc { TableName string `json:"name"` TableID uint64 `json:"id"` Unlike bool `json:"unlike,omitempty"` + Referrer string `json:"page"` } // Response JSON schema. @@ -51,8 +52,18 @@ func Likes() http.HandlerFunc { return } + // Sanity check things. The page= param (Referrer) must be a relative URL, the path + // is useful for "liked your comment" notifications to supply the Link URL for the + // notification. + if len(req.Referrer) > 0 && req.Referrer[0] != '/' { + req.Referrer = "" + } + // Who do we notify about this like? - var targetUser *models.User + var ( + targetUser *models.User + notificationMessage string + ) switch req.TableName { case "photos": if photo, err := models.GetPhoto(req.TableID); err == nil { @@ -70,6 +81,15 @@ func Likes() http.HandlerFunc { } else { log.Error("For like on users table: didn't find user %d: %s", req.TableID, err) } + case "comments": + log.Error("subject is users, find %d", req.TableID) + if comment, err := models.GetComment(req.TableID); err == nil { + targetUser = &comment.User + notificationMessage = comment.Message + log.Warn("found user %s", targetUser.Username) + } else { + log.Error("For like on users table: didn't find user %d: %s", req.TableID, err) + } } // Is the table likeable? @@ -108,6 +128,8 @@ func Likes() http.HandlerFunc { Type: models.NotificationLike, TableName: req.TableName, TableID: req.TableID, + Message: notificationMessage, + Link: req.Referrer, } if err := models.CreateNotification(notif); err != nil { log.Error("Couldn't create Likes notification: %s", err) diff --git a/pkg/controller/forum/thread.go b/pkg/controller/forum/thread.go index e8379a9..05bb7cf 100644 --- a/pkg/controller/forum/thread.go +++ b/pkg/controller/forum/thread.go @@ -14,7 +14,7 @@ import ( var ThreadPathRegexp = regexp.MustCompile(`^/forum/thread/(\d+)$`) -// Thread view for a specific board index. +// Thread view for the comment thread body of a forum post. func Thread() http.HandlerFunc { tmpl := templates.Must("forum/thread.html") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -74,6 +74,13 @@ func Thread() http.HandlerFunc { return } + // Get the like map for these comments. + commentIDs := []uint64{} + for _, com := range comments { + commentIDs = append(commentIDs, com.ID) + } + commentLikeMap := models.MapLikes(currentUser, "comments", commentIDs) + // Is the current user subscribed to notifications on this thread? _, isSubscribed := models.IsSubscribed(currentUser, "threads", thread.ID) @@ -81,6 +88,7 @@ func Thread() http.HandlerFunc { "Forum": forum, "Thread": thread, "Comments": comments, + "LikeMap": commentLikeMap, "Pager": pager, "IsSubscribed": isSubscribed, } diff --git a/pkg/controller/photo/view.go b/pkg/controller/photo/view.go index fce7aa5..7e6db8a 100644 --- a/pkg/controller/photo/view.go +++ b/pkg/controller/photo/view.go @@ -76,17 +76,25 @@ func View() http.HandlerFunc { log.Error("Couldn't list comments for photo %d: %s", photo.ID, err) } + // Get the like map for these comments. + commentIDs := []uint64{} + for _, com := range comments { + commentIDs = append(commentIDs, com.ID) + } + commentLikeMap := models.MapLikes(currentUser, "comments", commentIDs) + // Is the current user subscribed to notifications on this thread? _, isSubscribed := models.IsSubscribed(currentUser, "photos", photo.ID) var vars = map[string]interface{}{ - "IsOwnPhoto": currentUser.ID == user.ID, - "User": user, - "Photo": photo, - "LikeMap": likeMap, - "CommentMap": commentMap, - "Comments": comments, - "IsSubscribed": isSubscribed, + "IsOwnPhoto": currentUser.ID == user.ID, + "User": user, + "Photo": photo, + "LikeMap": likeMap, + "CommentMap": commentMap, + "Comments": comments, + "CommentLikeMap": commentLikeMap, + "IsSubscribed": isSubscribed, } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/middleware/csrf.go b/pkg/middleware/csrf.go index d22eeda..398e91d 100644 --- a/pkg/middleware/csrf.go +++ b/pkg/middleware/csrf.go @@ -58,6 +58,7 @@ func MakeCSRFCookie(r *http.Request, w http.ResponseWriter) string { Name: config.CSRFCookieName, Value: token, HttpOnly: true, + Path: "/", } // log.Debug("MakeCSRFCookie: giving cookie value %s to user", token) http.SetCookie(w, cookie) diff --git a/pkg/models/like.go b/pkg/models/like.go index 6d54cb0..851be16 100644 --- a/pkg/models/like.go +++ b/pkg/models/like.go @@ -18,8 +18,9 @@ type Like struct { // LikeableTables are the set of table names that allow likes (used by the JSON API). var LikeableTables = map[string]interface{}{ - "photos": nil, - "users": nil, + "photos": nil, + "users": nil, + "comments": nil, } // AddLike to something. diff --git a/pkg/models/user.go b/pkg/models/user.go index 88dd13c..de32bb6 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -39,10 +39,18 @@ type User struct { type UserVisibility string const ( - UserVisibilityPublic UserVisibility = "public" - UserVisibilityPrivate = "private" + UserVisibilityPublic UserVisibility = "public" + UserVisibilityExternal = "external" + UserVisibilityPrivate = "private" ) +// All visibility options. +var UserVisibilityOptions = []UserVisibility{ + UserVisibilityPublic, + UserVisibilityExternal, + UserVisibilityPrivate, +} + // Preload related tables for the user (classmethod). func (u *User) Preload() *gorm.DB { return DB.Preload("ProfileField").Preload("ProfilePhoto") diff --git a/web/static/js/bulma.js b/web/static/js/bulma.js index 138efda..f31e3e3 100644 --- a/web/static/js/bulma.js +++ b/web/static/js/bulma.js @@ -51,19 +51,21 @@ document.addEventListener('DOMContentLoaded', () => { }); // Touching the user drop-down button toggles it. - userMenu.addEventListener("touchstart", (e) => { - // On mobile/tablet screens they had to hamburger menu their way here anyway, let it thru. - if (screen.width < 1024) { - return; - } + if (userMenu !== null) { + userMenu.addEventListener("touchstart", (e) => { + // On mobile/tablet screens they had to hamburger menu their way here anyway, let it thru. + if (screen.width < 1024) { + return; + } - e.preventDefault(); - if (userMenu.classList.contains(activeClass)) { - userMenu.classList.remove(activeClass); - } else { - userMenu.classList.add(activeClass); - } - }); + e.preventDefault(); + if (userMenu.classList.contains(activeClass)) { + userMenu.classList.remove(activeClass); + } else { + userMenu.classList.add(activeClass); + } + }); + } // Touching a link from the user menu lets it click thru. (document.querySelectorAll(".navbar-dropdown") || []).forEach(node => { diff --git a/web/static/js/likes.js b/web/static/js/likes.js index 4f5ae26..7a122ef 100644 --- a/web/static/js/likes.js +++ b/web/static/js/likes.js @@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => { // Bind to the like buttons. (document.querySelectorAll(".nonshy-like-button") || []).forEach(node => { node.addEventListener("click", (e) => { + e.preventDefault(); if (busy) return; let $icon = node.querySelector(".icon"), @@ -36,6 +37,7 @@ document.addEventListener('DOMContentLoaded', () => { "name": tableName, // TODO "id": parseInt(tableID), "unlike": !liking, + "page": window.location.pathname + window.location.search + window.location.hash, }), }) .then((response) => response.json()) diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index ce9ca94..5edea79 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -201,6 +201,12 @@ {{end}} {{else if eq .TableName "users"}} profile page. + {{else if eq .TableName "comments"}} + {{if .Link}} + comment: + {{else}} + comment. + {{end}} {{else}} {{.TableName}}. {{end}} diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index 96f970a..19a5dbc 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -1,7 +1,7 @@ {{define "title"}}{{.User.Username}}{{end}} {{define "content"}}
-
+
- {{if not .LoggedIn}} + {{if or (not .LoggedIn) .IsPrivate}}
{{else if .IsPrivate}}
diff --git a/web/templates/account/settings.html b/web/templates/account/settings.html index 8dc9784..9ee8dff 100644 --- a/web/templates/account/settings.html +++ b/web/templates/account/settings.html @@ -220,18 +220,46 @@
diff --git a/web/templates/faq.html b/web/templates/faq.html index 4b0c374..2d88c7d 100644 --- a/web/templates/faq.html +++ b/web/templates/faq.html @@ -42,6 +42,37 @@ until your profile has been certified.

+

What are the visibility options for my profile page?

+ +

+ There are currently three different choices for your profile visibility on your + Settings page: +

+ +
    +
  • + The default visibility is "Public + Login Required." Users must be + logged-in to an account in order to see anything about your profile page - if an + external (logged out) browser visits your profile URL, they will be redirected to + log in to an account first. +
  • +
  • + You may optionally go more public with a "Limited Logged-out View." + This enables your profile URL (e.g., + {{if .LoggedIn}}/u/{{.CurrentUser.Username}}{{else}}/u/username{{end}}) + to show a basic page (with your square profile picture and display name) to + logged-out browsers. This may be useful if you wish to link to your page from an external + site (e.g. your Twitter page) and present new users with a better experience than just + a redirect to login page. +
  • +
  • + You may "Mark my profile as 'private'" to + be private even from other logged-in members who are not on your Friends + list. Logged-in users will see only your square profile picture and display + name, and be able only to send you a friend request or a message. +
  • +
+

Photo FAQs

Do I have to post my nudes here?

diff --git a/web/templates/forum/thread.html b/web/templates/forum/thread.html index f75372a..075f597 100644 --- a/web/templates/forum/thread.html +++ b/web/templates/forum/thread.html @@ -157,6 +157,21 @@
+ + +
+ {{$Like := $Root.CommentLikeMap.Get .ID}} + + + + Like + {{if gt $Like.Count 0}} + ({{$Like.Count}}) + {{end}} + + +
+