Deactivate Account; Friends Lists on Profiles
* Add a way for users to temporarily deactivate their accounts, in a recoverable way should they decide to return later. * A deactivated account may log in but have limited options: to reactivate their account, permanently delete it, or log out. * Fix several bugs around the display of comments, messages and forum threads for disabled, banned, or blocked users: * Messages (inbox and sentbox) will be hidden and the unread indicator will not count unread messages the user can't access. * Comments on photos and forum posts are hidden, and top-level threads on the "Newest" tab will show "[unavailable]" for their text and username. * Your historical notifications will hide users who are blocked, banned or disabled. * Add a "Friends" tab to user profile pages, to see other users' friends. * The page is Certification Required so non-cert users can't easily discover any members on the site.
This commit is contained in:
parent
0f35f135d2
commit
481bd0ae61
82
pkg/controller/account/deactivate.go
Normal file
82
pkg/controller/account/deactivate.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// Deactivate account page (self service).
|
||||
func Deactivate() http.HandlerFunc {
|
||||
tmpl := templates.Must("account/deactivate.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get your current user: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Confirm deletion.
|
||||
if r.Method == http.MethodPost {
|
||||
var password = strings.TrimSpace(r.PostFormValue("password"))
|
||||
if err := currentUser.CheckPassword(password); err != nil {
|
||||
session.FlashError(w, r, "You must enter your correct account password to delete your account.")
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Deactivate their account!
|
||||
currentUser.Status = models.UserStatusDisabled
|
||||
if err := currentUser.Save(); err != nil {
|
||||
session.FlashError(w, r, "Error while deactivating your account: %s", err)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign them out.
|
||||
session.LogoutUser(w, r)
|
||||
session.Flash(w, r, "Your account has been deactivated and you are now logged out. If you wish to re-activate your account, sign in again with your username and password.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Reactivate account page
|
||||
func Reactivate() http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get your current user: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
if currentUser.Status != models.UserStatusDisabled {
|
||||
session.FlashError(w, r, "Your account was not disabled in the first place!")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Reactivate them.
|
||||
currentUser.Status = models.UserStatusActive
|
||||
if err := currentUser.Save(); err != nil {
|
||||
session.FlashError(w, r, "Error while reactivating your account: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
session.Flash(w, r, "Welcome back! Your account has been reactivated.")
|
||||
templates.Redirect(w, "/")
|
||||
})
|
||||
}
|
88
pkg/controller/account/friends.go
Normal file
88
pkg/controller/account/friends.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
var UserFriendsRegexp = regexp.MustCompile(`^/friends/u/([^@]+?)$`)
|
||||
|
||||
// User friends page (/friends/u/username)
|
||||
func UserFriends() http.HandlerFunc {
|
||||
tmpl := templates.Must("account/friends.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the username out of the URL parameters.
|
||||
var username string
|
||||
m := UserFriendsRegexp.FindStringSubmatch(r.URL.Path)
|
||||
if m != nil {
|
||||
username = m[1]
|
||||
}
|
||||
|
||||
// Find this user.
|
||||
user, err := models.FindUser(username)
|
||||
if err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the viewer.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
var isSelf = currentUser.ID == user.ID
|
||||
|
||||
// Banned or disabled? Only admin can view then.
|
||||
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Is either one blocking?
|
||||
if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Get their friends.
|
||||
pager := &models.Pagination{
|
||||
PerPage: config.PageSizeFriends,
|
||||
Sort: "updated_at desc",
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
friends, err := models.PaginateOtherUserFriends(currentUser, user, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't paginate friends: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Inject relationship booleans.
|
||||
models.SetUserRelationships(currentUser, friends)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"User": user,
|
||||
"IsSelf": isSelf,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"FriendCount": models.CountFriends(user.ID),
|
||||
"Friends": friends,
|
||||
"Pager": pager,
|
||||
|
||||
// Map our friendships to these users.
|
||||
"FriendMap": models.MapFriends(currentUser, friends),
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -64,8 +64,8 @@ func Login() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Is their account banned or disabled?
|
||||
if user.Status != models.UserStatusActive {
|
||||
// Is their account banned?
|
||||
if user.Status == models.UserStatusBanned {
|
||||
session.FlashError(w, r, "Your account has been %s. If you believe this was done in error, please contact support.", user.Status)
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
|
|
|
@ -120,6 +120,7 @@ func Profile() http.HandlerFunc {
|
|||
"IsPrivate": isPrivate,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"FriendCount": models.CountFriends(user.ID),
|
||||
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
||||
|
||||
// Details on who likes the photo.
|
||||
|
|
|
@ -142,6 +142,7 @@ func UserNotes() http.HandlerFunc {
|
|||
"User": user,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"FriendCount": models.CountFriends(user.ID),
|
||||
"MyNote": myNote,
|
||||
|
||||
// Admin concerns.
|
||||
|
|
|
@ -191,7 +191,7 @@ func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics {
|
|||
|
||||
// Who are we blocking?
|
||||
var blockedUsernames = map[string]interface{}{}
|
||||
for _, username := range models.BlockedUsernames(currentUser.ID) {
|
||||
for _, username := range models.BlockedUsernames(currentUser) {
|
||||
blockedUsernames[username] = nil
|
||||
}
|
||||
|
||||
|
@ -211,7 +211,7 @@ func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics {
|
|||
// SendBlocklist syncs the user blocklist to the chat server prior to sending them over.
|
||||
func SendBlocklist(user *models.User) error {
|
||||
// Get the user's blocklist.
|
||||
blockedUsernames := models.BlockedUsernames(user.ID)
|
||||
blockedUsernames := models.BlockedUsernames(user)
|
||||
log.Info("SendBlocklist(%s) to BareRTC: %d blocked usernames", user.Username, len(blockedUsernames))
|
||||
|
||||
// API request struct for BareRTC /api/blocklist endpoint.
|
||||
|
|
|
@ -95,13 +95,13 @@ func Inbox() http.HandlerFunc {
|
|||
// On the main inbox view, ?page= params page thru the message list, not a thread.
|
||||
pager.ParsePage(r)
|
||||
}
|
||||
messages, err := models.GetMessages(currentUser.ID, showSent, pager)
|
||||
messages, err := models.GetMessages(currentUser, showSent, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get your messages from DB: %s", err)
|
||||
}
|
||||
|
||||
// How many unreads?
|
||||
unread, err := models.CountUnreadMessages(currentUser.ID)
|
||||
unread, err := models.CountUnreadMessages(currentUser)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get your unread message count from DB: %s", err)
|
||||
}
|
||||
|
|
|
@ -149,6 +149,7 @@ func UserPhotos() http.HandlerFunc {
|
|||
"Photos": photos,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"FriendCount": models.CountFriends(user.ID),
|
||||
"PublicPhotoCount": models.CountPublicPhotos(user.ID),
|
||||
"InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos,
|
||||
"Pager": pager,
|
||||
|
|
|
@ -27,19 +27,19 @@ func LoginRequired(handler http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
// Are they banned or disabled?
|
||||
if user.Status == models.UserStatusDisabled {
|
||||
session.LogoutUser(w, r)
|
||||
session.FlashError(w, r, "Your account has been disabled and you are now logged out.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else if user.Status == models.UserStatusBanned {
|
||||
// Are they banned?
|
||||
if user.Status == models.UserStatusBanned {
|
||||
session.LogoutUser(w, r)
|
||||
session.FlashError(w, r, "Your account has been banned and you are now logged out.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Is their account disabled?
|
||||
if DisabledAccount(user, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if MaintenanceMode(user, w, r) {
|
||||
return
|
||||
|
@ -115,19 +115,19 @@ func CertRequired(handler http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
// Are they banned or disabled?
|
||||
if currentUser.Status == models.UserStatusDisabled {
|
||||
session.LogoutUser(w, r)
|
||||
session.FlashError(w, r, "Your account has been disabled and you are now logged out.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else if currentUser.Status == models.UserStatusBanned {
|
||||
// Are they banned?
|
||||
if currentUser.Status == models.UserStatusBanned {
|
||||
session.LogoutUser(w, r)
|
||||
session.FlashError(w, r, "Your account has been banned and you are now logged out.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Is their account disabled?
|
||||
if DisabledAccount(currentUser, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if MaintenanceMode(currentUser, w, r) {
|
||||
return
|
||||
|
|
39
pkg/middleware/disabled_account.go
Normal file
39
pkg/middleware/disabled_account.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
var tmplDisabledAccount = templates.Must("errors/disabled_account.html")
|
||||
|
||||
// Whitelist of paths to allow disabled accounts to access.
|
||||
var disabledAccountPathWhitelist = []string{
|
||||
"/account/delete",
|
||||
"/account/reactivate",
|
||||
}
|
||||
|
||||
// DisabledAccount check that limits a logged-in user's options to either reactivate their account,
|
||||
// delete it, or log back out.
|
||||
func DisabledAccount(currentUser *models.User, w http.ResponseWriter, r *http.Request) bool {
|
||||
// Is their account disabled?
|
||||
if currentUser.Status == models.UserStatusDisabled {
|
||||
// Whitelisted paths?
|
||||
for _, path := range disabledAccountPathWhitelist {
|
||||
if strings.HasPrefix(r.URL.Path, path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Show the disabled account page to all other requests.
|
||||
if err := tmplDisabledAccount.Execute(w, r, nil); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -93,27 +93,36 @@ func PaginateBlockList(user *User, pager *Pagination) ([]*User, error) {
|
|||
}
|
||||
|
||||
// BlockedUserIDs returns all user IDs blocked by the user (bidirectional, source or target user).
|
||||
func BlockedUserIDs(userId uint64) []uint64 {
|
||||
func BlockedUserIDs(user *User) []uint64 {
|
||||
// Have we looked this up already on this request?
|
||||
if user.cacheBlockedUserIDs != nil {
|
||||
return user.cacheBlockedUserIDs
|
||||
}
|
||||
|
||||
var (
|
||||
bs = []*Block{}
|
||||
userIDs = []uint64{}
|
||||
)
|
||||
DB.Where("source_user_id = ? OR target_user_id = ?", userId, userId).Find(&bs)
|
||||
DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs)
|
||||
for _, row := range bs {
|
||||
for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} {
|
||||
if uid != userId {
|
||||
if uid != user.ID {
|
||||
userIDs = append(userIDs, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result in the User so we don't query it again.
|
||||
user.cacheBlockedUserIDs = userIDs
|
||||
|
||||
return userIDs
|
||||
}
|
||||
|
||||
// MapBlockedUserIDs returns BlockedUserIDs as a lookup hashmap (not for front-end templates currently).
|
||||
func MapBlockedUserIDs(userId uint64) map[uint64]interface{} {
|
||||
func MapBlockedUserIDs(user *User) map[uint64]interface{} {
|
||||
var (
|
||||
result = map[uint64]interface{}{}
|
||||
blockedIDs = BlockedUserIDs(userId)
|
||||
blockedIDs = BlockedUserIDs(user)
|
||||
)
|
||||
for _, uid := range blockedIDs {
|
||||
result[uid] = nil
|
||||
|
@ -125,7 +134,7 @@ func MapBlockedUserIDs(userId uint64) map[uint64]interface{} {
|
|||
func FilterBlockingUserIDs(currentUser *User, userIDs []uint64) []uint64 {
|
||||
var (
|
||||
// Get the IDs to exclude.
|
||||
blockedIDs = MapBlockedUserIDs(currentUser.ID)
|
||||
blockedIDs = MapBlockedUserIDs(currentUser)
|
||||
|
||||
// Filter the result.
|
||||
result = []uint64{}
|
||||
|
@ -157,9 +166,9 @@ func BlockedUserIDsByUser(userId uint64) []uint64 {
|
|||
}
|
||||
|
||||
// BlockedUsernames returns all usernames blocked by (or blocking) the user.
|
||||
func BlockedUsernames(userId uint64) []string {
|
||||
func BlockedUsernames(user *User) []string {
|
||||
var (
|
||||
userIDs = BlockedUserIDs(userId)
|
||||
userIDs = BlockedUserIDs(user)
|
||||
usernames = []string{}
|
||||
)
|
||||
|
||||
|
@ -174,7 +183,7 @@ func BlockedUsernames(userId uint64) []string {
|
|||
).Where(
|
||||
"id IN ?", userIDs,
|
||||
).Scan(&usernames); res.Error != nil {
|
||||
log.Error("BlockedUsernames(%d): %s", userId, res.Error)
|
||||
log.Error("BlockedUsernames(%d): %s", user.Username, res.Error)
|
||||
}
|
||||
|
||||
return usernames
|
||||
|
|
|
@ -71,7 +71,7 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
|
|||
var (
|
||||
cs = []*Comment{}
|
||||
query = (&Comment{}).Preload()
|
||||
blockedUserIDs = BlockedUserIDs(user.ID)
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
@ -84,6 +84,16 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
|
|||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show comments from banned or disabled accounts.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = comments.user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query = query.Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
|
@ -102,7 +112,7 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
|
|||
func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) {
|
||||
var (
|
||||
cs []*Comment
|
||||
blockedUserIDs = BlockedUserIDs(user.ID)
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
@ -115,6 +125,16 @@ func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, err
|
|||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show comments from banned or disabled accounts.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = comments.user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
result := (&Comment{}).Preload().Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
|
|
|
@ -24,6 +24,7 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
|
|||
var (
|
||||
result = []*RecentPost{}
|
||||
query = (&Comment{}).Preload()
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
wheres = []string{"table_name = 'threads'"}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
@ -43,6 +44,22 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
|
|||
wheres = append(wheres, "forums.inner_circle is not true")
|
||||
}
|
||||
|
||||
// Blocked users?
|
||||
if len(blockedUserIDs) > 0 {
|
||||
wheres = append(wheres, "comments.user_id NOT IN ?")
|
||||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show comments from banned or disabled accounts.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = comments.user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
// Get the page of recent forum comment IDs of all time.
|
||||
type scanner struct {
|
||||
CommentID uint64
|
||||
|
@ -130,7 +147,7 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
|
|||
comments, _ = GetComments(commentIDs)
|
||||
}
|
||||
if len(threadIDs) > 0 {
|
||||
threads, _ = GetThreads(threadIDs)
|
||||
threads, _ = GetThreadsAsUser(user, threadIDs)
|
||||
}
|
||||
if len(forumIDs) > 0 {
|
||||
forums, _ = GetForums(forumIDs)
|
||||
|
@ -154,6 +171,14 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
|
|||
thrs = append(thrs, thr)
|
||||
} else {
|
||||
log.Error("RecentPosts: didn't find thread ID %d in map!", rc.ThreadID)
|
||||
|
||||
// Create a dummy placeholder Thread (e.g.: the thread originator has been
|
||||
// banned or disabled, but the thread summary is shown on the new comment view)
|
||||
rc.Thread = &Thread{
|
||||
Comment: Comment{
|
||||
Message: "[unavailable]",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if f, ok := forums[rc.ForumID]; ok {
|
||||
|
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
|
@ -246,6 +247,20 @@ func CountFriendRequests(userID uint64) (int64, error) {
|
|||
return count, result.Error
|
||||
}
|
||||
|
||||
// CountFriends gets a count of friends for the user.
|
||||
func CountFriends(userID uint64) int64 {
|
||||
var count int64
|
||||
result := DB.Where(
|
||||
"target_user_id = ? AND approved = ? AND EXISTS (SELECT 1 FROM users WHERE users.id = friends.source_user_id AND users.status = 'active')",
|
||||
userID,
|
||||
true,
|
||||
).Model(&Friend{}).Count(&count)
|
||||
if result.Error != nil {
|
||||
log.Error("CountFriends(%d): %s", userID, result.Error)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/*
|
||||
PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered
|
||||
by friendship date.
|
||||
|
@ -302,6 +317,62 @@ func PaginateFriends(user *User, requests bool, sent bool, pager *Pagination) ([
|
|||
return GetUsers(user, userIDs)
|
||||
}
|
||||
|
||||
/*
|
||||
PaginateOtherUserFriends gets a page of friends from another user, for their profile page.
|
||||
*/
|
||||
func PaginateOtherUserFriends(currentUser *User, user *User, pager *Pagination) ([]*User, error) {
|
||||
// We paginate over the Friend table.
|
||||
var (
|
||||
fs = []*Friend{}
|
||||
blockedUserIDs = BlockedUserIDs(currentUser)
|
||||
userIDs = []uint64{}
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
query = DB.Model(&Friend{})
|
||||
)
|
||||
|
||||
// Get friends of the target user.
|
||||
wheres = append(wheres, "source_user_id = ? AND approved = ?")
|
||||
placeholders = append(placeholders, user.ID, true)
|
||||
|
||||
// Don't show our blocked users in the result.
|
||||
if len(blockedUserIDs) > 0 {
|
||||
wheres = append(wheres, "target_user_id NOT IN ?")
|
||||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show disabled or banned users.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = friends.target_user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query = query.Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
).Order(pager.Sort)
|
||||
|
||||
// Get the total count.
|
||||
query.Count(&pager.Total)
|
||||
|
||||
// Get the page.
|
||||
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
// Now of these friends get their User objects.
|
||||
for _, friend := range fs {
|
||||
userIDs = append(userIDs, friend.TargetUserID)
|
||||
}
|
||||
|
||||
return GetUsers(user, userIDs)
|
||||
}
|
||||
|
||||
// GetFriendRequests returns all pending friend requests for a user.
|
||||
func GetFriendRequests(userID uint64) ([]*Friend, error) {
|
||||
var fs = []*Friend{}
|
||||
|
|
|
@ -73,7 +73,7 @@ func WhoLikes(currentUser *User, tableName string, tableID uint64) ([]*User, int
|
|||
remainder = total
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
blockedUserIDs = BlockedUserIDs(currentUser.ID)
|
||||
blockedUserIDs = BlockedUserIDs(currentUser)
|
||||
)
|
||||
|
||||
wheres = append(wheres, "table_name = ? AND table_id = ?")
|
||||
|
@ -117,7 +117,7 @@ func PaginateLikes(currentUser *User, tableName string, tableID uint64, pager *P
|
|||
userIDs = []uint64{}
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
blockedUserIDs = BlockedUserIDs(currentUser.ID)
|
||||
blockedUserIDs = BlockedUserIDs(currentUser)
|
||||
)
|
||||
|
||||
wheres = append(wheres, "table_name = ? AND table_id = ?")
|
||||
|
|
|
@ -24,17 +24,17 @@ func GetMessage(id uint64) (*Message, error) {
|
|||
}
|
||||
|
||||
// GetMessages for a user.
|
||||
func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error) {
|
||||
func GetMessages(user *User, sent bool, pager *Pagination) ([]*Message, error) {
|
||||
var (
|
||||
m = []*Message{}
|
||||
blockedUserIDs = BlockedUserIDs(userID)
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
where = []string{}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
||||
if sent {
|
||||
where = append(where, "source_user_id = ?")
|
||||
placeholders = append(placeholders, userID)
|
||||
placeholders = append(placeholders, user.ID)
|
||||
|
||||
if len(blockedUserIDs) > 0 {
|
||||
where = append(where, "target_user_id NOT IN ?")
|
||||
|
@ -42,7 +42,7 @@ func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error
|
|||
}
|
||||
} else {
|
||||
where = append(where, "target_user_id = ?")
|
||||
placeholders = append(placeholders, userID)
|
||||
placeholders = append(placeholders, user.ID)
|
||||
|
||||
if len(blockedUserIDs) > 0 {
|
||||
where = append(where, "source_user_id NOT IN ?")
|
||||
|
@ -50,6 +50,16 @@ func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error
|
|||
}
|
||||
}
|
||||
|
||||
// Don't show messages from banned or disabled accounts.
|
||||
where = append(where, `
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id IN (messages.target_user_id, messages.source_user_id)
|
||||
AND users.status <> 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query := DB.Where(
|
||||
strings.Join(where, " AND "),
|
||||
placeholders...,
|
||||
|
@ -100,11 +110,36 @@ func DeleteMessageThread(message *Message) error {
|
|||
}
|
||||
|
||||
// CountUnreadMessages gets the count of unread messages for a user.
|
||||
func CountUnreadMessages(userID uint64) (int64, error) {
|
||||
query := DB.Where(
|
||||
func CountUnreadMessages(user *User) (int64, error) {
|
||||
var (
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
where = []string{
|
||||
"target_user_id = ? AND read = ?",
|
||||
userID,
|
||||
false,
|
||||
}
|
||||
placeholders = []interface{}{
|
||||
user.ID, false,
|
||||
}
|
||||
)
|
||||
|
||||
// Blocking user IDs?
|
||||
if len(blockedUserIDs) > 0 {
|
||||
where = append(where, "source_user_id NOT IN ?")
|
||||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show messages from banned or disabled accounts.
|
||||
where = append(where, `
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id IN (messages.target_user_id, messages.source_user_id)
|
||||
AND users.status <> 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query := DB.Where(
|
||||
strings.Join(where, " AND "),
|
||||
placeholders...,
|
||||
)
|
||||
|
||||
var count int64
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
|
@ -139,11 +140,36 @@ func ClearAllNotifications(user *User) error {
|
|||
}
|
||||
|
||||
// CountUnreadNotifications gets the count of unread Notifications for a user.
|
||||
func CountUnreadNotifications(userID uint64) (int64, error) {
|
||||
query := DB.Where(
|
||||
func CountUnreadNotifications(user *User) (int64, error) {
|
||||
var (
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
where = []string{
|
||||
"user_id = ? AND read = ?",
|
||||
userID,
|
||||
false,
|
||||
}
|
||||
placeholders = []interface{}{
|
||||
user.ID, false,
|
||||
}
|
||||
)
|
||||
|
||||
// Blocking user IDs?
|
||||
if len(blockedUserIDs) > 0 {
|
||||
where = append(where, "about_user_id NOT IN ?")
|
||||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show messages from banned or disabled accounts.
|
||||
where = append(where, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = notifications.about_user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query := DB.Where(
|
||||
strings.Join(where, " AND "),
|
||||
placeholders...,
|
||||
)
|
||||
|
||||
var count int64
|
||||
|
@ -153,11 +179,36 @@ func CountUnreadNotifications(userID uint64) (int64, error) {
|
|||
|
||||
// PaginateNotifications returns the user's notifications.
|
||||
func PaginateNotifications(user *User, pager *Pagination) ([]*Notification, error) {
|
||||
var ns = []*Notification{}
|
||||
var (
|
||||
ns = []*Notification{}
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
where = []string{
|
||||
"user_id = ?",
|
||||
}
|
||||
placeholders = []interface{}{
|
||||
user.ID,
|
||||
}
|
||||
)
|
||||
|
||||
// Suppress historic notifications about blocked users.
|
||||
if len(blockedUserIDs) > 0 {
|
||||
where = append(where, "about_user_id NOT IN ?")
|
||||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show notifications from banned or disabled accounts.
|
||||
where = append(where, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = notifications.about_user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query := (&Notification{}).Preload().Where(
|
||||
"user_id = ?",
|
||||
user.ID,
|
||||
strings.Join(where, " AND "),
|
||||
placeholders...,
|
||||
).Order(
|
||||
pager.Sort,
|
||||
)
|
||||
|
@ -348,8 +399,6 @@ func (nm NotificationMap) mapNotificationComments(IDs []uint64) {
|
|||
commentIDs = append(commentIDs, row.CommentID)
|
||||
}
|
||||
|
||||
log.Error("MAP COMMENT IDS GOT: %+v", commentIDs)
|
||||
|
||||
// Load the comments.
|
||||
if len(commentIDs) > 0 {
|
||||
if comments, err := GetComments(commentIDs); err != nil {
|
||||
|
|
|
@ -397,7 +397,7 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
|
|||
userID = user.ID
|
||||
explicitOK = user.Explicit // User opted-in for explicit content
|
||||
|
||||
blocklist = BlockedUserIDs(userID)
|
||||
blocklist = BlockedUserIDs(user)
|
||||
friendIDs = FriendIDs(userID)
|
||||
privateUserIDs = PrivateGrantedUserIDs(userID)
|
||||
wheres = []string{}
|
||||
|
|
|
@ -47,9 +47,65 @@ func GetThreads(IDs []uint64) (map[uint64]*Thread, error) {
|
|||
var (
|
||||
mt = map[uint64]*Thread{}
|
||||
ts = []*Thread{}
|
||||
wheres = []string{"threads.id IN ?"}
|
||||
placeholders = []interface{}{IDs}
|
||||
)
|
||||
|
||||
result := (&Thread{}).Preload().Where("id IN ?", IDs).Find(&ts)
|
||||
// Don't show threads from banned or disabled accounts.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = comments.user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
result := (&Thread{}).Preload().Joins(
|
||||
"LEFT OUTER JOIN comments ON (comments.id = threads.comment_id)",
|
||||
).Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
).Find(&ts)
|
||||
for _, row := range ts {
|
||||
mt[row.ID] = row
|
||||
}
|
||||
|
||||
return mt, result.Error
|
||||
}
|
||||
|
||||
// GetThreadsAsUser queries a set of thread IDs and returns them mapped, taking blocklists into consideration.
|
||||
func GetThreadsAsUser(currentUser *User, IDs []uint64) (map[uint64]*Thread, error) {
|
||||
var (
|
||||
mt = map[uint64]*Thread{}
|
||||
ts = []*Thread{}
|
||||
blockedUserIDs = BlockedUserIDs(currentUser)
|
||||
wheres = []string{"threads.id IN ?"}
|
||||
placeholders = []interface{}{IDs}
|
||||
)
|
||||
|
||||
// Blocked users?
|
||||
if len(blockedUserIDs) > 0 {
|
||||
wheres = append(wheres, "comments.user_id NOT IN ?")
|
||||
placeholders = append(placeholders, blockedUserIDs)
|
||||
}
|
||||
|
||||
// Don't show threads from banned or disabled accounts.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = comments.user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
result := (&Thread{}).Preload().Joins(
|
||||
"LEFT OUTER JOIN comments ON (comments.id = threads.comment_id)",
|
||||
).Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
).Find(&ts)
|
||||
for _, row := range ts {
|
||||
mt[row.ID] = row
|
||||
}
|
||||
|
@ -170,7 +226,19 @@ func PaginateThreads(user *User, forum *Forum, pager *Pagination) ([]*Thread, er
|
|||
wheres = append(wheres, "explicit IS NOT TRUE")
|
||||
}
|
||||
|
||||
query = query.Where(
|
||||
// Don't show threads from banned or disabled accounts.
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = comments.user_id
|
||||
AND users.status = 'active'
|
||||
)
|
||||
`)
|
||||
|
||||
query = query.Joins(
|
||||
"LEFT OUTER JOIN comments ON (comments.id = threads.comment_id)",
|
||||
).Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
).Order(pager.Sort)
|
||||
|
|
|
@ -44,6 +44,7 @@ type User struct {
|
|||
|
||||
// Caches
|
||||
cachePhotoTypes map[PhotoVisibility]struct{}
|
||||
cacheBlockedUserIDs []uint64
|
||||
}
|
||||
|
||||
type UserVisibility string
|
||||
|
@ -231,7 +232,7 @@ func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, er
|
|||
joins string // GPS location join.
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
blockedUserIDs = BlockedUserIDs(user.ID)
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
myLocation = GetUserLocation(user.ID)
|
||||
)
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ func New() http.Handler {
|
|||
mux.Handle("/settings/age-gate", middleware.LoginRequired(account.AgeGate()))
|
||||
mux.Handle("/account/two-factor/setup", middleware.LoginRequired(account.Setup2FA()))
|
||||
mux.Handle("/account/delete", middleware.LoginRequired(account.Delete()))
|
||||
mux.Handle("/account/deactivate", middleware.LoginRequired(account.Deactivate()))
|
||||
mux.Handle("/account/reactivate", middleware.LoginRequired(account.Reactivate()))
|
||||
mux.Handle("/u/", account.Profile()) // public access OK
|
||||
mux.Handle("/photo/upload", middleware.LoginRequired(photo.Upload()))
|
||||
mux.Handle("/photo/u/", middleware.LoginRequired(photo.UserPhotos()))
|
||||
|
@ -63,6 +65,7 @@ func New() http.Handler {
|
|||
mux.Handle("/messages/delete", middleware.LoginRequired(inbox.Delete()))
|
||||
mux.Handle("/friends", middleware.LoginRequired(friend.Friends()))
|
||||
mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend()))
|
||||
mux.Handle("/friends/u/", middleware.CertRequired(account.UserFriends()))
|
||||
mux.Handle("/users/block", middleware.LoginRequired(block.BlockUser()))
|
||||
mux.Handle("/users/blocked", middleware.LoginRequired(block.Blocked()))
|
||||
mux.Handle("/users/blocklist/add", middleware.LoginRequired(block.AddUser()))
|
||||
|
|
|
@ -77,7 +77,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
)
|
||||
|
||||
// Get unread message count.
|
||||
if count, err := models.CountUnreadMessages(user.ID); err == nil {
|
||||
if count, err := models.CountUnreadMessages(user); err == nil {
|
||||
m["NavUnreadMessages"] = count
|
||||
countMessages = count
|
||||
} else {
|
||||
|
@ -93,7 +93,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
}
|
||||
|
||||
// Count other notifications.
|
||||
if count, err := models.CountUnreadNotifications(user.ID); err == nil {
|
||||
if count, err := models.CountUnreadNotifications(user); err == nil {
|
||||
m["NavUnreadNotifications"] = count
|
||||
countNotifications = count
|
||||
} else {
|
||||
|
|
|
@ -236,7 +236,7 @@
|
|||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="/account/delete">
|
||||
<a href="/settings#deactivate">
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
Delete account
|
||||
</a>
|
||||
|
|
86
web/templates/account/deactivate.html
Normal file
86
web/templates/account/deactivate.html
Normal file
|
@ -0,0 +1,86 @@
|
|||
{{define "title"}}Deactivate Account{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero is-info is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Deactivate Account
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<div class="card" style="max-width: 512px">
|
||||
<header class="card-header has-background-danger">
|
||||
<p class="card-header-title has-text-light">
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
Deactivate My Account
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<form method="POST" action="/account/deactivate">
|
||||
{{InputCSRF}}
|
||||
<div class="block content">
|
||||
<p>
|
||||
On this page you may <strong>temporarily deactivate your account</strong>, which
|
||||
will hide your profile from everywhere on the website (as if your account were
|
||||
deleted), but in a recoverable way where you may log in and re-activate your
|
||||
account in the future, should you decide to come back.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When you deactivate your account:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Your profile will be hidden from everywhere on the website: for example
|
||||
you will not be searchable on the Member Directory.
|
||||
</li>
|
||||
<li>
|
||||
People you had exchanged Messages with will no longer see your conversations
|
||||
in their inbox or outbox page.
|
||||
</li>
|
||||
<li>
|
||||
All comments and forum posts you made will be hidden from everybody on
|
||||
the website.
|
||||
</li>
|
||||
<li>
|
||||
You will be signed out of your {{PrettyTitle}} account. If you wish to
|
||||
re-activate your account in the future, you may sign back in and the only
|
||||
options you will be given will be to re-activate your account, delete it
|
||||
permanently, or log out.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
To confirm deactivation of your account, please enter your current account
|
||||
password into the box below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="password">Your current password:</label>
|
||||
<input type="password" class="input"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Password">
|
||||
</div>
|
||||
|
||||
<div class="block has-text-center">
|
||||
<button type="submit" class="button is-danger">Deactivate My Account</button>
|
||||
<a href="/me" class="button is-success">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
159
web/templates/account/friends.html
Normal file
159
web/templates/account/friends.html
Normal file
|
@ -0,0 +1,159 @@
|
|||
{{define "title"}}Friends of {{.User.Username}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
{{$Root := .}}
|
||||
<section class="hero is-link is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
<i class="fa fa-user-group mr-2"></i>
|
||||
Friends of {{.User.Username}}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="tabs is-boxed mb-0">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-user"></i>
|
||||
</span>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/photo/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-image"></i>
|
||||
</span>
|
||||
<span>
|
||||
Photos
|
||||
{{if .PhotoCount}}<span class="tag is-link is-light ml-1">{{.PhotoCount}}</span>{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/notes/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-pen-to-square"></i>
|
||||
</span>
|
||||
<span>
|
||||
Notes
|
||||
{{if .NoteCount}}<span class="tag is-link is-light ml-1">{{.NoteCount}}</span>{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/friends/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-user-group"></i>
|
||||
</span>
|
||||
<span>
|
||||
Friends
|
||||
{{if .FriendCount}}<span class="tag is-link is-light ml-1">{{.FriendCount}}</span>{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
|
||||
<div class="block">
|
||||
Found {{.Pager.Total}} friend{{Pluralize64 .Pager.Total}}
|
||||
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
|
||||
{{range .Friends}}
|
||||
<div class="column is-half-tablet is-one-third-desktop">
|
||||
|
||||
<form action="/friends/add" method="POST">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="username" value="{{.Username}}">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="media block">
|
||||
<div class="media-left">
|
||||
{{template "avatar-64x64" .}}
|
||||
|
||||
<!-- Friendship badge -->
|
||||
{{if $Root.FriendMap.Get .ID}}
|
||||
<div class="has-text-centered">
|
||||
<span class="is-size-7 has-text-warning-dark">
|
||||
<i class="fa fa-user-group" title="Friends"></i>
|
||||
Friends
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">
|
||||
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||
{{if ne .Status "active"}}
|
||||
<del>{{.NameOrUsername}}</del>
|
||||
{{else}}
|
||||
{{.NameOrUsername}}
|
||||
{{end}}
|
||||
{{if and $Root.CurrentUser.IsInnerCircle .InnerCircle}}
|
||||
<img src="/static/img/circle-16.png">
|
||||
{{end}}
|
||||
</a>
|
||||
{{if eq .Visibility "private"}}
|
||||
<sup class="fa fa-mask is-size-7" title="Private Profile"></sup>
|
||||
{{end}}
|
||||
</p>
|
||||
<p class="subtitle is-6 mb-2">
|
||||
<span class="icon"><i class="fa fa-user"></i></span>
|
||||
<a href="/u/{{.Username}}">{{.Username}}</a>
|
||||
</p>
|
||||
{{if .GetProfileField "city"}}
|
||||
<p class="subtitle is-6 mb-2">
|
||||
{{.GetProfileField "city"}}
|
||||
</p>
|
||||
{{end}}
|
||||
<p class="subtitle is-7 mb-2">
|
||||
{{if or (ne .GetDisplayAge "n/a")}}
|
||||
<span class="mr-2">{{.GetDisplayAge}}</span>
|
||||
{{end}}
|
||||
|
||||
{{if .GetProfileField "gender"}}
|
||||
<span class="mr-2">{{.GetProfileField "gender"}}</span>
|
||||
{{end}}
|
||||
|
||||
{{if .GetProfileField "pronouns"}}
|
||||
<span class="mr-2">{{.GetProfileField "pronouns"}}</span>
|
||||
{{end}}
|
||||
|
||||
{{if .GetProfileField "orientation"}}
|
||||
<span class="mr-2">{{.GetProfileField "orientation"}}</span>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div><!-- media-block -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{{end}}<!-- range .Friends -->
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -295,6 +295,17 @@
|
|||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/friends/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-user-group"></i>
|
||||
</span>
|
||||
<span>
|
||||
Friends
|
||||
{{if .FriendCount}}<span class="tag is-link is-light ml-1">{{.FriendCount}}</span>{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -65,7 +65,16 @@
|
|||
<a href="/settings#account" class="nonshy-tab-button">
|
||||
<strong><i class="fa fa-user mr-1"></i> Account Settings</strong>
|
||||
<p class="help">
|
||||
Change password or e-mail; Two-factor auth (2FA); delete account.
|
||||
Change password or e-mail; set up Two-Factor Authentication (2FA).
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/settings#deactivate" class="nonshy-tab-button">
|
||||
<strong><i class="fa fa-exclamation-triangle mr-1"></i> Deactivate Account</strong>
|
||||
<p class="help">
|
||||
Temporarily deactivate or permanently delete my account.
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -744,13 +753,41 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deactivate or Delete Account -->
|
||||
<div id="deactivate">
|
||||
<div class="card mb-5">
|
||||
<header class="card-header has-background-warning">
|
||||
<p class="card-header-title has-text-dark-dark">
|
||||
<i class="fa fa-lock pr-2"></i>
|
||||
Deactivate My Account
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="card-content content">
|
||||
<p>
|
||||
If you'd like to take a break from {{PrettyTitle}} but think you may want to
|
||||
come back later, you may <strong>temporarily deactivate your account</strong>
|
||||
which will mark your profile as hidden from everywhere on the website (as if
|
||||
it were deleted), but in a way that you can recover your account and reactivate
|
||||
it again in the future.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="/account/deactivate" class="button is-primary">
|
||||
Temporarily Deactivate My Account
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Account -->
|
||||
<div class="card mb-5">
|
||||
<header class="card-header has-background-danger">
|
||||
<p class="card-header-title has-text-light">
|
||||
<i class="fa fa-exclamation-triangle pr-2"></i>
|
||||
Delete Account
|
||||
Permanently Delete My Account
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
@ -762,7 +799,7 @@
|
|||
|
||||
<p class="block">
|
||||
<a href="/account/delete" class="button is-danger">
|
||||
Delete My Account
|
||||
Permanently Delete My Account
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -782,7 +819,8 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
$prefs = document.querySelector("#prefs"),
|
||||
$location = document.querySelector("#location"),
|
||||
$privacy = document.querySelector("#privacy"),
|
||||
$account = document.querySelector("#account"),
|
||||
$account = document.querySelector("#account")
|
||||
$deactivate = document.querySelector("#deactivate"),
|
||||
buttons = Array.from(document.getElementsByClassName("nonshy-tab-button"));
|
||||
|
||||
// Hide all by default.
|
||||
|
@ -791,6 +829,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
$location.style.display = 'none';
|
||||
$privacy.style.display = 'none';
|
||||
$account.style.display = 'none';
|
||||
$deactivate.style.display = 'none';
|
||||
|
||||
// Current tab to select by default.
|
||||
let $activeTab = $profile;
|
||||
|
@ -813,6 +852,9 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
case "account":
|
||||
$activeTab = $account;
|
||||
break;
|
||||
case "deactivate":
|
||||
$activeTab = $deactivate;
|
||||
break;
|
||||
default:
|
||||
$activeTab = $profile;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,17 @@
|
|||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/friends/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-user-group"></i>
|
||||
</span>
|
||||
<span>
|
||||
Friends
|
||||
{{if .FriendCount}}<span class="tag is-link is-light ml-1">{{.FriendCount}}</span>{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
57
web/templates/errors/disabled_account.html
Normal file
57
web/templates/errors/disabled_account.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{{define "title"}}Reactivate Your Account{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero block is-link is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">Welcome back!</h1>
|
||||
<h2 class="subtitle">Reactivate your account?</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block content p-4 mb-0">
|
||||
<h1>Your account is currently deactivated</h1>
|
||||
|
||||
<p>
|
||||
Welcome back to {{PrettyTitle}}! You had requested to deactivate your account before, and you must make
|
||||
a decision as to how you want to proceed from here.
|
||||
</p>
|
||||
|
||||
<h2>Reactivate Your Account?</h2>
|
||||
|
||||
<p>
|
||||
If you wish to pick up where you left off, you may <strong>reactivate your account</strong> by clicking
|
||||
on the button below. It will be as if you never left: your profile, photo gallery, friends, messages and
|
||||
notifications will all be how you left them.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="/account/reactivate" class="button is-success">Reactivate My Account</a>
|
||||
</p>
|
||||
|
||||
<h2>Permanently Delete Your Account?</h2>
|
||||
|
||||
<p>
|
||||
If you've decided you really <em>don't</em> want to have a {{PrettyTitle}} account anymore, you may
|
||||
click the button below to <strong>permanently delete your account</strong> instead. We will delete your
|
||||
profile, photos, and all the associated data we have about your account. This will be an irreversible
|
||||
process!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="/account/delete" class="button is-danger">Permanently Delete My Account</a>
|
||||
</p>
|
||||
|
||||
<h2>Log Out</h2>
|
||||
|
||||
<p>
|
||||
If you're not sure what to do, you may log out of your account and come back later.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="/account/logout" class="button is-info">Log out of my account</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -51,7 +51,7 @@
|
|||
<a href="/u/{{$User.Username}}">
|
||||
{{template "avatar-96x96" $User}}
|
||||
<div>
|
||||
<a href="/u/{{$User.Username}}" class="is-size-7">{{$User.Username}}</a>
|
||||
<a href="/u/{{$User.Username}}" class="is-size-7">{{or $User.Username "[unavailable]"}}</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -145,6 +145,17 @@
|
|||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/friends/u/{{.User.Username}}">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-user-group"></i>
|
||||
</span>
|
||||
<span>
|
||||
Friends
|
||||
{{if .FriendCount}}<span class="tag is-link is-light ml-1">{{.FriendCount}}</span>{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user