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
|
||||
PageSizeAdminCertification = 20
|
||||
PageSizeAdminFeedback = 20
|
||||
PageSizeAdminFeedbackNotesPage = 5 // feedback on User Notes page
|
||||
PageSizeAdminUserNotes = 10 // other users' notes
|
||||
PageSizeSiteGallery = 16
|
||||
PageSizeUserGallery = 16
|
||||
PageSizeInboxList = 20 // sidebar list
|
||||
|
|
|
@ -119,6 +119,7 @@ func Profile() http.HandlerFunc {
|
|||
"IsFriend": isFriend,
|
||||
"IsPrivate": isPrivate,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
||||
|
||||
// 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"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -154,7 +155,7 @@ func Landing() http.HandlerFunc {
|
|||
"IsShyUser": isShy,
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
func SendBlocklist(user *models.User) error {
|
||||
// Get the user's blocklist.
|
||||
|
|
|
@ -148,6 +148,7 @@ func UserPhotos() http.HandlerFunc {
|
|||
"User": user,
|
||||
"Photos": photos,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"PublicPhotoCount": models.CountPublicPhotos(user.ID),
|
||||
"InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos,
|
||||
"Pager": pager,
|
||||
|
|
|
@ -35,6 +35,7 @@ func DeleteUser(user *models.User) error {
|
|||
{"Messages", DeleteUserMessages},
|
||||
{"Friends", DeleteFriends},
|
||||
{"Profile Fields", DeleteProfile},
|
||||
{"User Notes", DeleteUserNotes},
|
||||
}
|
||||
for _, item := range todo {
|
||||
if err := item.Fn(user.ID); err != nil {
|
||||
|
@ -280,3 +281,13 @@ func DeleteComments(userID uint64) error {
|
|||
).Delete(&models.Comment{})
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func CreateFeedback(fb *Feedback) error {
|
||||
result := DB.Create(fb)
|
||||
|
|
|
@ -29,4 +29,5 @@ func AutoMigrate() {
|
|||
DB.AutoMigrate(&AdminGroup{})
|
||||
DB.AutoMigrate(&AdminScope{})
|
||||
DB.AutoMigrate(&UserLocation{})
|
||||
DB.AutoMigrate(&UserNote{})
|
||||
}
|
||||
|
|
|
@ -100,6 +100,25 @@ func (u *User) AllPrivatePhotoIDs() ([]uint64, error) {
|
|||
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.
|
||||
func IsPrivateUnlocked(sourceUserID, targetUserID uint64) bool {
|
||||
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/private", middleware.LoginRequired(photo.Private()))
|
||||
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/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
// ChatStatistics is the json result of the BareRTC /api/statistics endpoint.
|
||||
type ChatStatistics struct {
|
||||
UserCount int
|
||||
Usernames []string `json:",omitempty"`
|
||||
Usernames []string
|
||||
Cameras struct {
|
||||
Blue int
|
||||
Red int
|
||||
|
@ -41,14 +41,6 @@ func SetChatStatistics(stats *ChatStatistics) {
|
|||
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.
|
||||
func (cs ChatStatistics) IsOnline(username string) bool {
|
||||
for _, user := range cs.Usernames {
|
||||
|
|
|
@ -284,6 +284,17 @@
|
|||
</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>
|
||||
</ul>
|
||||
</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">
|
||||
<span id="usersOnline"></span>
|
||||
<a id="whoLink" href="#" style="visibility: hidden">Who?</a>
|
||||
<span id="whoList" style="display: none"></span>
|
||||
|
||||
<!-- Camera stats if active -->
|
||||
<div id="cameraStats" style="display: none" class="mt-2">
|
||||
|
@ -157,6 +159,8 @@
|
|||
<script>
|
||||
function showWhoBanner(chatStatistics) {
|
||||
const $banner = document.querySelector("#chatStatsBanner"),
|
||||
$whoLink = document.querySelector("#whoLink"),
|
||||
$whoList = document.querySelector("#whoList"),
|
||||
$usersOnline = document.querySelector("#usersOnline")
|
||||
$cameraStats = document.querySelector("#cameraStats")
|
||||
$cameraCount = document.querySelector("#cameraCount")
|
||||
|
@ -167,7 +171,16 @@ function showWhoBanner(chatStatistics) {
|
|||
|
||||
let people = chatStatistics.UserCount === 1 ? 'person' : 'people';
|
||||
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?
|
||||
if (chatStatistics.Cameras.Blue + chatStatistics.Cameras.Red > 0) {
|
||||
|
@ -177,9 +190,15 @@ function showWhoBanner(chatStatistics) {
|
|||
$cameraRed.innerHTML = chatStatistics.Cameras.Red;
|
||||
$cameraBlue.innerHTML = chatStatistics.Cameras.Blue;
|
||||
}
|
||||
|
||||
$whoLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
$whoLink.style.display = "none";
|
||||
$whoList.style.display = "";
|
||||
});
|
||||
}
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
let ChatStatistics = {{.ChatStatistics.Privatized}},
|
||||
let ChatStatistics = {{.ChatStatistics}},
|
||||
$banner = document.querySelector("#chatStatsBanner"),
|
||||
$usersOnline = document.querySelector("#usersOnline");
|
||||
|
||||
|
|
|
@ -134,6 +134,17 @@
|
|||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user