User Notes + Bring Back Online Chatters List
New feature: User Notes * Add a "Notes" tab to user profile pages and galleries. * Users can create one private note about another user. * Admins can see all notes left about a user. * Admins also see Feedback & Reports regarding the user on that page. Bring back the online chatters list * The Usernames are filtered down based on blocklist status.
This commit is contained in:
parent
a70e1c2b73
commit
49b5387750
|
@ -15,6 +15,8 @@ var (
|
||||||
PageSizePrivatePhotoGrantees = 12
|
PageSizePrivatePhotoGrantees = 12
|
||||||
PageSizeAdminCertification = 20
|
PageSizeAdminCertification = 20
|
||||||
PageSizeAdminFeedback = 20
|
PageSizeAdminFeedback = 20
|
||||||
|
PageSizeAdminFeedbackNotesPage = 5 // feedback on User Notes page
|
||||||
|
PageSizeAdminUserNotes = 10 // other users' notes
|
||||||
PageSizeSiteGallery = 16
|
PageSizeSiteGallery = 16
|
||||||
PageSizeUserGallery = 16
|
PageSizeUserGallery = 16
|
||||||
PageSizeInboxList = 20 // sidebar list
|
PageSizeInboxList = 20 // sidebar list
|
||||||
|
|
|
@ -119,6 +119,7 @@ func Profile() http.HandlerFunc {
|
||||||
"IsFriend": isFriend,
|
"IsFriend": isFriend,
|
||||||
"IsPrivate": isPrivate,
|
"IsPrivate": isPrivate,
|
||||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||||
|
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||||
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
||||||
|
|
||||||
// Details on who likes the photo.
|
// Details on who likes the photo.
|
||||||
|
|
160
pkg/controller/account/user_note.go
Normal file
160
pkg/controller/account/user_note.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NotesURLRegexp = regexp.MustCompile(`^/notes/u/([^@]+?)$`)
|
||||||
|
|
||||||
|
// User notes page (/notes/u/username)
|
||||||
|
func UserNotes() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("account/user_notes.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the username out of the URL parameters.
|
||||||
|
var username string
|
||||||
|
m := NotesURLRegexp.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 current user.
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "You must be signed in to view this page.")
|
||||||
|
templates.Redirect(w, "/login?next="+url.QueryEscape(r.URL.String()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the site under a Maintenance Mode restriction?
|
||||||
|
if middleware.MaintenanceMode(currentUser, w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up our current note about this person.
|
||||||
|
var myNote = models.GetNoteBetweenUsers(currentUser, user)
|
||||||
|
|
||||||
|
// Are we submitting a note?
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var message = r.FormValue("message")
|
||||||
|
|
||||||
|
// Update & save our note.
|
||||||
|
if message == "" {
|
||||||
|
// Delete it.
|
||||||
|
if err := myNote.Delete(); err != nil && err != models.ErrNoUserNoteToDelete {
|
||||||
|
session.FlashError(w, r, "Error deleting your note: %s", err)
|
||||||
|
} else if err == nil {
|
||||||
|
session.Flash(w, r, "Your note was deleted!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update it.
|
||||||
|
myNote.Message = message
|
||||||
|
if err := myNote.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Error saving your note: %s", err)
|
||||||
|
} else {
|
||||||
|
session.Flash(w, r, "Your notes have been saved!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin view: paginate their feedback & reports.
|
||||||
|
var (
|
||||||
|
feedback = []*models.Feedback{}
|
||||||
|
otherNotes = []*models.UserNote{}
|
||||||
|
userMap = models.UserMap{}
|
||||||
|
notePager = &models.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
PerPage: config.PageSizeAdminUserNotes,
|
||||||
|
Sort: "updated_at desc",
|
||||||
|
}
|
||||||
|
fbPager = &models.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
PerPage: config.PageSizeAdminFeedbackNotesPage,
|
||||||
|
Sort: "created_at desc",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
notePager.ParsePage(r)
|
||||||
|
fbPager.ParsePage(r)
|
||||||
|
if currentUser.IsAdmin {
|
||||||
|
// Paginate all notes written about this user.
|
||||||
|
if on, err := models.PaginateUserNotes(user, notePager); err != nil {
|
||||||
|
session.FlashError(w, r, "Paginating user notes: %s", err)
|
||||||
|
} else {
|
||||||
|
otherNotes = on
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginate feedback & reports.
|
||||||
|
if fb, err := models.PaginateFeedbackAboutUser(user, fbPager); err != nil {
|
||||||
|
session.FlashError(w, r, "Paginating feedback on this user: %s", err)
|
||||||
|
} else {
|
||||||
|
feedback = fb
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map user IDs for the Feedback Reply-To line and the Note Givers for the paginated notes.
|
||||||
|
var userIDs = []uint64{}
|
||||||
|
for _, p := range feedback {
|
||||||
|
if p.UserID > 0 {
|
||||||
|
userIDs = append(userIDs, p.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range otherNotes {
|
||||||
|
if p.UserID > 0 {
|
||||||
|
userIDs = append(userIDs, p.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if um, err := models.MapUsers(currentUser, userIDs); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
|
||||||
|
} else {
|
||||||
|
userMap = um
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
"User": user,
|
||||||
|
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||||
|
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||||
|
"MyNote": myNote,
|
||||||
|
|
||||||
|
// Admin concerns.
|
||||||
|
"Feedback": feedback,
|
||||||
|
"FeedbackPager": fbPager,
|
||||||
|
"OtherNotes": otherNotes,
|
||||||
|
"NotePager": notePager,
|
||||||
|
"UserMap": userMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ func Landing() http.HandlerFunc {
|
||||||
"IsShyUser": isShy,
|
"IsShyUser": isShy,
|
||||||
|
|
||||||
// Pre-populate the "who's online" widget from backend cache data
|
// Pre-populate the "who's online" widget from backend cache data
|
||||||
"ChatStatistics": worker.GetChatStatistics(),
|
"ChatStatistics": FilteredChatStatistics(currentUser),
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -163,6 +164,35 @@ func Landing() http.HandlerFunc {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilteredChatStatistics will return a copy of the cached ChatStatistics but where the Usernames list is
|
||||||
|
// filtered down (and the online user counts, accordingly) by blocklists.
|
||||||
|
func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics {
|
||||||
|
var stats = worker.GetChatStatistics()
|
||||||
|
var result = worker.ChatStatistics{
|
||||||
|
UserCount: stats.UserCount,
|
||||||
|
Usernames: []string{},
|
||||||
|
Cameras: stats.Cameras,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Who are we blocking?
|
||||||
|
var blockedUsernames = map[string]interface{}{}
|
||||||
|
for _, username := range models.BlockedUsernames(currentUser.ID) {
|
||||||
|
blockedUsernames[username] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the online users listing.
|
||||||
|
for _, username := range stats.Usernames {
|
||||||
|
if _, ok := blockedUsernames[username]; !ok {
|
||||||
|
result.Usernames = append(result.Usernames, username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the names.
|
||||||
|
sort.Strings(result.Usernames)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// SendBlocklist syncs the user blocklist to the chat server prior to sending them over.
|
// SendBlocklist syncs the user blocklist to the chat server prior to sending them over.
|
||||||
func SendBlocklist(user *models.User) error {
|
func SendBlocklist(user *models.User) error {
|
||||||
// Get the user's blocklist.
|
// Get the user's blocklist.
|
||||||
|
|
|
@ -148,6 +148,7 @@ func UserPhotos() http.HandlerFunc {
|
||||||
"User": user,
|
"User": user,
|
||||||
"Photos": photos,
|
"Photos": photos,
|
||||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||||
|
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||||
"PublicPhotoCount": models.CountPublicPhotos(user.ID),
|
"PublicPhotoCount": models.CountPublicPhotos(user.ID),
|
||||||
"InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos,
|
"InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos,
|
||||||
"Pager": pager,
|
"Pager": pager,
|
||||||
|
|
|
@ -35,6 +35,7 @@ func DeleteUser(user *models.User) error {
|
||||||
{"Messages", DeleteUserMessages},
|
{"Messages", DeleteUserMessages},
|
||||||
{"Friends", DeleteFriends},
|
{"Friends", DeleteFriends},
|
||||||
{"Profile Fields", DeleteProfile},
|
{"Profile Fields", DeleteProfile},
|
||||||
|
{"User Notes", DeleteUserNotes},
|
||||||
}
|
}
|
||||||
for _, item := range todo {
|
for _, item := range todo {
|
||||||
if err := item.Fn(user.ID); err != nil {
|
if err := item.Fn(user.ID); err != nil {
|
||||||
|
@ -280,3 +281,13 @@ func DeleteComments(userID uint64) error {
|
||||||
).Delete(&models.Comment{})
|
).Delete(&models.Comment{})
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUserNotes scrubs data for deleting a user.
|
||||||
|
func DeleteUserNotes(userID uint64) error {
|
||||||
|
log.Error("DeleteUser: DeleteUserNotes(%d)", userID)
|
||||||
|
result := models.DB.Where(
|
||||||
|
"user_id = ? OR about_user_id = ?",
|
||||||
|
userID, userID,
|
||||||
|
).Delete(&models.UserNote{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
|
@ -76,6 +76,41 @@ func PaginateFeedback(acknowledged bool, intent string, pager *Pagination) ([]*F
|
||||||
return fb, result.Error
|
return fb, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaginateFeedbackAboutUser digs through feedback about a specific user ID or one of their Photos.
|
||||||
|
//
|
||||||
|
// It returns reports where table_name=users and their user ID, or where table_name=photos and about any
|
||||||
|
// of their current photo IDs. Additionally, it will look for chat room reports which were about their
|
||||||
|
// username.
|
||||||
|
func PaginateFeedbackAboutUser(user *User, pager *Pagination) ([]*Feedback, error) {
|
||||||
|
var (
|
||||||
|
fb = []*Feedback{}
|
||||||
|
photoIDs, _ = user.AllPhotoIDs()
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
wheres = append(wheres, `
|
||||||
|
(table_name = 'users' AND table_id = ?) OR
|
||||||
|
(table_name = 'photos' AND table_id IN ?)
|
||||||
|
`)
|
||||||
|
placeholders = append(placeholders, user.ID, photoIDs)
|
||||||
|
|
||||||
|
query := DB.Where(
|
||||||
|
strings.Join(wheres, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
).Order(
|
||||||
|
pager.Sort,
|
||||||
|
)
|
||||||
|
|
||||||
|
query.Model(&Feedback{}).Count(&pager.Total)
|
||||||
|
|
||||||
|
result := query.Offset(
|
||||||
|
pager.GetOffset(),
|
||||||
|
).Limit(pager.PerPage).Find(&fb)
|
||||||
|
|
||||||
|
return fb, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
// CreateFeedback saves a new Feedback row to the DB.
|
// CreateFeedback saves a new Feedback row to the DB.
|
||||||
func CreateFeedback(fb *Feedback) error {
|
func CreateFeedback(fb *Feedback) error {
|
||||||
result := DB.Create(fb)
|
result := DB.Create(fb)
|
||||||
|
|
|
@ -29,4 +29,5 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&AdminGroup{})
|
DB.AutoMigrate(&AdminGroup{})
|
||||||
DB.AutoMigrate(&AdminScope{})
|
DB.AutoMigrate(&AdminScope{})
|
||||||
DB.AutoMigrate(&UserLocation{})
|
DB.AutoMigrate(&UserLocation{})
|
||||||
|
DB.AutoMigrate(&UserNote{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,25 @@ func (u *User) AllPrivatePhotoIDs() ([]uint64, error) {
|
||||||
return photoIDs, nil
|
return photoIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllPhotoIDs returns the listing of all IDs of the user's photos.
|
||||||
|
func (u *User) AllPhotoIDs() ([]uint64, error) {
|
||||||
|
var photoIDs = []uint64{}
|
||||||
|
err := DB.Table(
|
||||||
|
"photos",
|
||||||
|
).Select(
|
||||||
|
"photos.id AS id",
|
||||||
|
).Where(
|
||||||
|
"user_id = ?",
|
||||||
|
u.ID,
|
||||||
|
).Scan(&photoIDs)
|
||||||
|
|
||||||
|
if err.Error != nil {
|
||||||
|
return photoIDs, fmt.Errorf("AllPhotoIDs(%s): %s", u.Username, err.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return photoIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsPrivateUnlocked quickly sees if sourceUserID has unlocked private photos for targetUserID to see.
|
// IsPrivateUnlocked quickly sees if sourceUserID has unlocked private photos for targetUserID to see.
|
||||||
func IsPrivateUnlocked(sourceUserID, targetUserID uint64) bool {
|
func IsPrivateUnlocked(sourceUserID, targetUserID uint64) bool {
|
||||||
pb := &PrivatePhoto{}
|
pb := &PrivatePhoto{}
|
||||||
|
|
112
pkg/models/user_note.go
Normal file
112
pkg/models/user_note.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserNote table where users can write private notes about each other.
|
||||||
|
type UserNote struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
UserID uint64 `gorm:"index"` // author ID
|
||||||
|
AboutUserID uint64 `gorm:"index"` // target user ID
|
||||||
|
Message string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNoteBetweenUsers finds the existing note or creates an empty model if not found.
|
||||||
|
func GetNoteBetweenUsers(currentUser *User, user *User) *UserNote {
|
||||||
|
var (
|
||||||
|
note = &UserNote{}
|
||||||
|
result = DB.Model(note).Where(
|
||||||
|
"user_id = ? AND about_user_id = ?",
|
||||||
|
currentUser.ID, user.ID,
|
||||||
|
).First(¬e)
|
||||||
|
)
|
||||||
|
if result.Error != nil {
|
||||||
|
note = &UserNote{
|
||||||
|
UserID: currentUser.ID,
|
||||||
|
AboutUserID: user.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountNotesAboutUser returns the number of notes (the current user) has about the other user.
|
||||||
|
//
|
||||||
|
// For regular user, will return zero or one; for admins, will return the total count of notes
|
||||||
|
// left by any other users about this user.
|
||||||
|
func CountNotesAboutUser(currentUser *User, user *User) int64 {
|
||||||
|
var (
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
count int64
|
||||||
|
)
|
||||||
|
|
||||||
|
if currentUser.IsAdmin {
|
||||||
|
wheres = append(wheres, "about_user_id = ?")
|
||||||
|
placeholders = append(placeholders, user.ID)
|
||||||
|
} else {
|
||||||
|
wheres = append(wheres, "user_id = ? AND about_user_id = ?")
|
||||||
|
placeholders = append(placeholders, currentUser.ID, user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := DB.Model(&UserNote{}).Where(
|
||||||
|
strings.Join(wheres, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
).Count(&count)
|
||||||
|
if query.Error != nil {
|
||||||
|
log.Error("CountNotesAboutUser(%s, %s): %s", currentUser.Username, user.Username, query.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateUserNotes shows all notes written by users about the target user.
|
||||||
|
func PaginateUserNotes(user *User, pager *Pagination) ([]*UserNote, error) {
|
||||||
|
var (
|
||||||
|
notes = []*UserNote{}
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
wheres = append(wheres, "about_user_id = ?")
|
||||||
|
placeholders = append(placeholders, user.ID)
|
||||||
|
|
||||||
|
query := DB.Joins("JOIN users ON users.id = user_notes.user_id").Where(
|
||||||
|
strings.Join(wheres, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
).Order(
|
||||||
|
"users.is_admin desc, " + pager.Sort,
|
||||||
|
)
|
||||||
|
|
||||||
|
query.Model(&UserNote{}).Count(&pager.Total)
|
||||||
|
|
||||||
|
result := query.Offset(
|
||||||
|
pager.GetOffset(),
|
||||||
|
).Limit(pager.PerPage).Find(¬es)
|
||||||
|
|
||||||
|
return notes, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the note.
|
||||||
|
func (p *UserNote) Save() error {
|
||||||
|
if p.ID == 0 {
|
||||||
|
return DB.Create(p).Error
|
||||||
|
}
|
||||||
|
return DB.Save(p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoUserNoteToDelete = errors.New("you had no user note to delete")
|
||||||
|
|
||||||
|
// Delete the DB entry.
|
||||||
|
func (p *UserNote) Delete() error {
|
||||||
|
if p.ID == 0 {
|
||||||
|
return ErrNoUserNoteToDelete
|
||||||
|
}
|
||||||
|
return DB.Delete(p).Error
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ func New() http.Handler {
|
||||||
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
||||||
mux.Handle("/photo/private", middleware.LoginRequired(photo.Private()))
|
mux.Handle("/photo/private", middleware.LoginRequired(photo.Private()))
|
||||||
mux.Handle("/photo/private/share", middleware.LoginRequired(photo.Share()))
|
mux.Handle("/photo/private/share", middleware.LoginRequired(photo.Share()))
|
||||||
|
mux.Handle("/notes/u/", middleware.LoginRequired(account.UserNotes()))
|
||||||
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
// ChatStatistics is the json result of the BareRTC /api/statistics endpoint.
|
// ChatStatistics is the json result of the BareRTC /api/statistics endpoint.
|
||||||
type ChatStatistics struct {
|
type ChatStatistics struct {
|
||||||
UserCount int
|
UserCount int
|
||||||
Usernames []string `json:",omitempty"`
|
Usernames []string
|
||||||
Cameras struct {
|
Cameras struct {
|
||||||
Blue int
|
Blue int
|
||||||
Red int
|
Red int
|
||||||
|
@ -41,14 +41,6 @@ func SetChatStatistics(stats *ChatStatistics) {
|
||||||
cachedChatStatistics = stats
|
cachedChatStatistics = stats
|
||||||
}
|
}
|
||||||
|
|
||||||
// Privatized returns a copy of ChatStatistics with the usernames list scrubbed.
|
|
||||||
func (cs ChatStatistics) Privatized() ChatStatistics {
|
|
||||||
return ChatStatistics{
|
|
||||||
UserCount: cs.UserCount,
|
|
||||||
Cameras: cs.Cameras,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsOnline returns whether the username is currently logged-in to chat.
|
// IsOnline returns whether the username is currently logged-in to chat.
|
||||||
func (cs ChatStatistics) IsOnline(username string) bool {
|
func (cs ChatStatistics) IsOnline(username string) bool {
|
||||||
for _, user := range cs.Usernames {
|
for _, user := range cs.Usernames {
|
||||||
|
|
|
@ -284,6 +284,17 @@
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
270
web/templates/account/user_notes.html
Normal file
270
web/templates/account/user_notes.html
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
{{define "title"}}
|
||||||
|
Notes about {{.User.Username}}
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-info is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<h1 class="title">
|
||||||
|
<span class="icon mr-4"><i class="fa fa-pen-square"></i></span>
|
||||||
|
<span>{{template "title" .}}</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ugly hack.. needed by the card-footers later below. -->
|
||||||
|
{{$Root := .}}
|
||||||
|
|
||||||
|
<div class="block p-4">
|
||||||
|
<!-- Tab bar -->
|
||||||
|
<div class="tabs is-boxed">
|
||||||
|
<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 class="is-active">
|
||||||
|
<a>
|
||||||
|
<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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notification is-info is-light content">
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-info-circle mr-1"></i> On this page you may jot down some private notes for yourself
|
||||||
|
about <strong>{{.User.Username}}</strong>, for example to remember a topic you discussed on chat or
|
||||||
|
to remember what they said their favorite color was -- it's up to you!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your notes will not be visible to <strong>{{.User.Username}}</strong> but <em>will</em> be visible
|
||||||
|
to website administrators.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<!-- User column -->
|
||||||
|
<div class="column">
|
||||||
|
<form action="{{.Request.URL.Path}}" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<div class="field mb-0">
|
||||||
|
<label class="label" for="message">My private notes about {{.User.Username}}</label>
|
||||||
|
<textarea class="textarea"
|
||||||
|
style="overflow: auto"
|
||||||
|
cols="80" rows="6"
|
||||||
|
name="message"
|
||||||
|
id="message"
|
||||||
|
placeholder="On one sunny day...">{{.MyNote.Message}}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show last updated on our note -->
|
||||||
|
{{if not .MyNote.UpdatedAt.IsZero}}
|
||||||
|
<div class="mt-1">
|
||||||
|
<em>You last updated your notes <span title="{{.MyNote.UpdatedAt.Format "Jan _2 2006"}}">{{SincePrettyCoarse .MyNote.UpdatedAt}} ago.</span></em>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="field mt-4">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
<i class="fa fa-save mr-1"></i> Save my notes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Admin view: everyone else's notes -->
|
||||||
|
{{if .CurrentUser.IsAdmin}}
|
||||||
|
<div class="card mt-6">
|
||||||
|
<div class="card-header has-background-info">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<i class="fa fa-peace mr-1"></i>
|
||||||
|
Everyone Else's Notes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
{{if .NotePager.Total}}
|
||||||
|
<p class="block">
|
||||||
|
Found <strong>{{.NotePager.Total}}</strong> note{{Pluralize64 .NotePager.Total}} about this user (page {{.NotePager.Page}} of {{.NotePager.Pages}}).
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
Note: admin notes are shown first, and the rest are ordered by recently updated.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
{{SimplePager .NotePager}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{{range .OtherNotes}}
|
||||||
|
<div class="card has-background-link-light mb-4">
|
||||||
|
{{$User := $Root.UserMap.Get .UserID}}
|
||||||
|
<div class="card-content" style="position: relative">
|
||||||
|
<strong>From user:</strong>
|
||||||
|
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
|
||||||
|
{{if $User.IsAdmin}}
|
||||||
|
<span class="tag ml-2 is-danger is-light">
|
||||||
|
<i class="fa fa-peace mr-1"></i> Admin
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="my-2" style="white-space: pre-wrap; line-break: anywhere; overflow: auto">{{.Message}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
{{SimplePager .NotePager}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Feedback & Notes column -->
|
||||||
|
{{if .CurrentUser.IsAdmin}}
|
||||||
|
<div class="column">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header has-background-danger">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<i class="fa fa-peace mr-1"></i> Admin Feedback & Reports
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
{{if .FeedbackPager.Total}}
|
||||||
|
<span>
|
||||||
|
Found <strong>{{.FeedbackPager.Total}}</strong> report{{Pluralize64 .FeedbackPager.Total}} about this user (page {{.FeedbackPager.Page}} of {{.FeedbackPager.Pages}}).
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
{{SimplePager .FeedbackPager}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{range .Feedback}}
|
||||||
|
{{$User := $Root.UserMap.Get .UserID}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right is-narrow">
|
||||||
|
<strong>Intent:</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{.Intent}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<strong>Subject:</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{.Subject}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<strong>Table:</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if eq .TableName ""}}
|
||||||
|
n/a
|
||||||
|
{{if ne .TableID 0}} - {{.TableID}}{{end}}
|
||||||
|
{{else if eq .TableName "users"}}
|
||||||
|
Users: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
||||||
|
class="fa fa-external-link ml-2"
|
||||||
|
target="_blank"
|
||||||
|
title="Visit the reported user's profile"></a>
|
||||||
|
{{else if eq .TableName "photos"}}
|
||||||
|
Photos: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
||||||
|
class="fa fa-external-link mx-2"
|
||||||
|
target="_blank"
|
||||||
|
title="Visit the reported photo"></a>
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true&profile=true"
|
||||||
|
class="fa fa-user"
|
||||||
|
target="_blank"
|
||||||
|
title="Visit the user profile who owns the reported photo"></a>
|
||||||
|
{{else if eq .TableName "messages"}}
|
||||||
|
Messages: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
||||||
|
class="fa fa-ghost ml-2"
|
||||||
|
target="_blank"
|
||||||
|
title="Impersonate the reporter and view this message thread"></a>
|
||||||
|
{{else}}
|
||||||
|
{{.TableName}}: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true" class="fa fa-external-link ml-2" target="_blank"></a>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<strong>Reply To:</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if $User}}
|
||||||
|
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
|
||||||
|
{{else if ne .ReplyTo ""}}
|
||||||
|
<a href="mailto:{{.ReplyTo}}">{{.ReplyTo}}</a>
|
||||||
|
{{else}}
|
||||||
|
n/a
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{{if eq .Message ""}}
|
||||||
|
<p><em>No message attached.</em></p>
|
||||||
|
{{else}}
|
||||||
|
{{ToMarkdown .Message}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
{{SimplePager .FeedbackPager}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}
|
|
@ -44,6 +44,8 @@
|
||||||
|
|
||||||
<div class="notification is-success is-light" id="chatStatsBanner" style="display: none">
|
<div class="notification is-success is-light" id="chatStatsBanner" style="display: none">
|
||||||
<span id="usersOnline"></span>
|
<span id="usersOnline"></span>
|
||||||
|
<a id="whoLink" href="#" style="visibility: hidden">Who?</a>
|
||||||
|
<span id="whoList" style="display: none"></span>
|
||||||
|
|
||||||
<!-- Camera stats if active -->
|
<!-- Camera stats if active -->
|
||||||
<div id="cameraStats" style="display: none" class="mt-2">
|
<div id="cameraStats" style="display: none" class="mt-2">
|
||||||
|
@ -157,6 +159,8 @@
|
||||||
<script>
|
<script>
|
||||||
function showWhoBanner(chatStatistics) {
|
function showWhoBanner(chatStatistics) {
|
||||||
const $banner = document.querySelector("#chatStatsBanner"),
|
const $banner = document.querySelector("#chatStatsBanner"),
|
||||||
|
$whoLink = document.querySelector("#whoLink"),
|
||||||
|
$whoList = document.querySelector("#whoList"),
|
||||||
$usersOnline = document.querySelector("#usersOnline")
|
$usersOnline = document.querySelector("#usersOnline")
|
||||||
$cameraStats = document.querySelector("#cameraStats")
|
$cameraStats = document.querySelector("#cameraStats")
|
||||||
$cameraCount = document.querySelector("#cameraCount")
|
$cameraCount = document.querySelector("#cameraCount")
|
||||||
|
@ -167,7 +171,16 @@ function showWhoBanner(chatStatistics) {
|
||||||
|
|
||||||
let people = chatStatistics.UserCount === 1 ? 'person' : 'people';
|
let people = chatStatistics.UserCount === 1 ? 'person' : 'people';
|
||||||
let isAre = chatStatistics.UserCount === 1 ? 'is' : 'are';
|
let isAre = chatStatistics.UserCount === 1 ? 'is' : 'are';
|
||||||
$usersOnline.innerHTML = `There ${isAre} currently <strong>${chatStatistics.UserCount}</strong> ${people}</span> in the chat room.`;
|
$usersOnline.innerHTML = `There ${isAre} currently <strong>${chatStatistics.UserCount}</strong> ${people}</span> in the chat room`;
|
||||||
|
$whoList.innerHTML = chatStatistics.Usernames !== null ? chatStatistics.Usernames.join(", ") : "";
|
||||||
|
|
||||||
|
// Show the "Who?" link if there's anybody.
|
||||||
|
if (chatStatistics.UserCount > 0) {
|
||||||
|
$usersOnline.innerHTML += ":";
|
||||||
|
$whoLink.style.visibility = "visible";
|
||||||
|
} else {
|
||||||
|
$usersOnline.innerHTML += ".";
|
||||||
|
}
|
||||||
|
|
||||||
// Camera stats too?
|
// Camera stats too?
|
||||||
if (chatStatistics.Cameras.Blue + chatStatistics.Cameras.Red > 0) {
|
if (chatStatistics.Cameras.Blue + chatStatistics.Cameras.Red > 0) {
|
||||||
|
@ -177,9 +190,15 @@ function showWhoBanner(chatStatistics) {
|
||||||
$cameraRed.innerHTML = chatStatistics.Cameras.Red;
|
$cameraRed.innerHTML = chatStatistics.Cameras.Red;
|
||||||
$cameraBlue.innerHTML = chatStatistics.Cameras.Blue;
|
$cameraBlue.innerHTML = chatStatistics.Cameras.Blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$whoLink.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
$whoLink.style.display = "none";
|
||||||
|
$whoList.style.display = "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
let ChatStatistics = {{.ChatStatistics.Privatized}},
|
let ChatStatistics = {{.ChatStatistics}},
|
||||||
$banner = document.querySelector("#chatStatsBanner"),
|
$banner = document.querySelector("#chatStatsBanner"),
|
||||||
$usersOnline = document.querySelector("#usersOnline");
|
$usersOnline = document.querySelector("#usersOnline");
|
||||||
|
|
||||||
|
|
|
@ -134,6 +134,17 @@
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user