Implement Direct Messaging
This commit is contained in:
parent
f6d076f7c2
commit
c8f6cf2e4d
73
pkg/controller/inbox/compose.go
Normal file
73
pkg/controller/inbox/compose.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package inbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compose a new chat coming from a user's profile page.
|
||||||
|
func Compose() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("inbox/compose.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// To whom?
|
||||||
|
username := r.FormValue("to")
|
||||||
|
user, err := models.FindUser(username)
|
||||||
|
if err != nil {
|
||||||
|
templates.NotFoundPage(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentUser.ID == user.ID {
|
||||||
|
session.FlashError(w, r, "You cannot send a message to yourself.")
|
||||||
|
templates.Redirect(w, "/messages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTing?
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var (
|
||||||
|
message = r.FormValue("message")
|
||||||
|
from = r.FormValue("from") // e.g. "inbox", default "profile", where to redirect to
|
||||||
|
)
|
||||||
|
if len(message) == 0 {
|
||||||
|
session.FlashError(w, r, "A message is required.")
|
||||||
|
templates.Redirect(w, r.URL.Path+"?to="+username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post it!
|
||||||
|
m, err := models.SendMessage(currentUser.ID, user.ID, message)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Failed to create the message in the database: %s", err)
|
||||||
|
templates.Redirect(w, r.URL.Path+"?to="+username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Flash(w, r, "Your message has been delivered!")
|
||||||
|
if from == "inbox" {
|
||||||
|
templates.Redirect(w, fmt.Sprintf("/messages/read/%d", m.ID))
|
||||||
|
}
|
||||||
|
templates.Redirect(w, "/messages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"User": user,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
135
pkg/controller/inbox/inbox.go
Normal file
135
pkg/controller/inbox/inbox.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package inbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ReadURLRegexp = regexp.MustCompile(`^/messages/read/(\d+)$`)
|
||||||
|
|
||||||
|
// Inbox is where users receive direct messages.
|
||||||
|
func Inbox() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("inbox/inbox.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default is inbox, what about sentbox?
|
||||||
|
var showSent = r.FormValue("box") == "sent"
|
||||||
|
|
||||||
|
// Are we reading a specific message?
|
||||||
|
var viewThread []*models.Message
|
||||||
|
var threadPager *models.Pagination
|
||||||
|
var composeToUsername string
|
||||||
|
if uri := ReadURLRegexp.FindStringSubmatch(r.URL.Path); uri != nil {
|
||||||
|
msgId, _ := strconv.Atoi(uri[1])
|
||||||
|
if msg, err := models.GetMessage(uint64(msgId)); err != nil {
|
||||||
|
session.FlashError(w, r, "Message not found.")
|
||||||
|
templates.Redirect(w, "/messages")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// We must be a party to this thread.
|
||||||
|
if msg.SourceUserID != currentUser.ID && msg.TargetUserID != currentUser.ID {
|
||||||
|
templates.ForbiddenPage(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the other party in this thread.
|
||||||
|
var senderUserID = msg.SourceUserID
|
||||||
|
if senderUserID == currentUser.ID {
|
||||||
|
senderUserID = msg.TargetUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the sender's username to compose a response to them.
|
||||||
|
sender, err := models.GetUser(senderUserID)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get sender of that message: %s", err)
|
||||||
|
}
|
||||||
|
composeToUsername = sender.Username
|
||||||
|
|
||||||
|
// Get the full chat thread (paginated).
|
||||||
|
threadPager = &models.Pagination{
|
||||||
|
PerPage: 5,
|
||||||
|
Sort: "created_at desc",
|
||||||
|
}
|
||||||
|
threadPager.ParsePage(r)
|
||||||
|
thread, err := models.GetMessageThread(msg.SourceUserID, msg.TargetUserID, threadPager)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get chat history: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewThread = thread
|
||||||
|
|
||||||
|
// Mark all these messages as read if the recipient sees them.
|
||||||
|
for _, m := range viewThread {
|
||||||
|
if m.TargetUserID == currentUser.ID && !m.Read {
|
||||||
|
m.Read = true
|
||||||
|
if err := m.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't mark message as read: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the inbox list of messages.
|
||||||
|
pager := &models.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 5,
|
||||||
|
Sort: "created_at desc",
|
||||||
|
}
|
||||||
|
if viewThread == nil {
|
||||||
|
// 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)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get your unread message count from DB: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map sender data on these messages.
|
||||||
|
var userIDs = []uint64{}
|
||||||
|
for _, m := range messages {
|
||||||
|
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
||||||
|
}
|
||||||
|
if viewThread != nil {
|
||||||
|
for _, m := range viewThread {
|
||||||
|
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userMap, err := models.MapUsers(userIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't map users: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"Messages": messages,
|
||||||
|
"UserMap": userMap,
|
||||||
|
"Unread": unread,
|
||||||
|
"Pager": pager,
|
||||||
|
"IsSentBox": showSent,
|
||||||
|
"ViewThread": viewThread, // nil on inbox page
|
||||||
|
"ThreadPager": threadPager,
|
||||||
|
"ReplyTo": composeToUsername,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -87,11 +87,13 @@ func Edit() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set their profile pic to this one.
|
// Set their profile pic to this one.
|
||||||
|
if setProfilePic {
|
||||||
currentUser.ProfilePhoto = *photo
|
currentUser.ProfilePhoto = *photo
|
||||||
log.Error("Set user ProfilePhotoID=%d", photo.ID)
|
log.Error("Set user ProfilePhotoID=%d", photo.ID)
|
||||||
if err := currentUser.Save(); err != nil {
|
if err := currentUser.Save(); err != nil {
|
||||||
session.FlashError(w, r, "Couldn't save user: %s", err)
|
session.FlashError(w, r, "Couldn't save user: %s", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Flash success.
|
// Flash success.
|
||||||
session.Flash(w, r, "Photo settings updated!")
|
session.Flash(w, r, "Photo settings updated!")
|
||||||
|
|
89
pkg/models/message.go
Normal file
89
pkg/models/message.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message table.
|
||||||
|
type Message struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
SourceUserID uint64 `gorm:"index"`
|
||||||
|
TargetUserID uint64 `gorm:"index"`
|
||||||
|
Read bool `gorm:"index"`
|
||||||
|
Message string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage by ID.
|
||||||
|
func GetMessage(id uint64) (*Message, error) {
|
||||||
|
m := &Message{}
|
||||||
|
result := DB.First(&m, id)
|
||||||
|
return m, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessages for a user.
|
||||||
|
func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error) {
|
||||||
|
var (
|
||||||
|
m = []*Message{}
|
||||||
|
where = "target_user_id = ?"
|
||||||
|
)
|
||||||
|
if sent {
|
||||||
|
where = "source_user_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := DB.Where(
|
||||||
|
where, userID,
|
||||||
|
).Order(pager.Sort)
|
||||||
|
|
||||||
|
query.Model(&Message{}).Count(&pager.Total)
|
||||||
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&m)
|
||||||
|
return m, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessageThread returns paginated message history between two people.
|
||||||
|
func GetMessageThread(sourceUserID, targetUserID uint64, pager *Pagination) ([]*Message, error) {
|
||||||
|
var m = []*Message{}
|
||||||
|
|
||||||
|
query := DB.Where(
|
||||||
|
"(source_user_id = ? AND target_user_id = ?) OR (source_user_id = ? AND target_user_id = ?)",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
targetUserID, sourceUserID,
|
||||||
|
).Order(pager.Sort)
|
||||||
|
|
||||||
|
query.Model(&Message{}).Count(&pager.Total)
|
||||||
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&m)
|
||||||
|
return m, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountUnreadMessages gets the count of unread messages for a user.
|
||||||
|
func CountUnreadMessages(userID uint64) (int64, error) {
|
||||||
|
query := DB.Where(
|
||||||
|
"target_user_id = ? AND read = ?",
|
||||||
|
userID,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
result := query.Model(&Message{}).Count(&count)
|
||||||
|
return count, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessage from a source to a target user.
|
||||||
|
func SendMessage(sourceUserID, targetUserID uint64, message string) (*Message, error) {
|
||||||
|
m := &Message{
|
||||||
|
SourceUserID: sourceUserID,
|
||||||
|
TargetUserID: targetUserID,
|
||||||
|
Message: message,
|
||||||
|
Read: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := DB.Create(m)
|
||||||
|
return m, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save message.
|
||||||
|
func (m *Message) Save() error {
|
||||||
|
result := DB.Save(m)
|
||||||
|
return result.Error
|
||||||
|
}
|
|
@ -12,4 +12,5 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&ProfileField{})
|
DB.AutoMigrate(&ProfileField{})
|
||||||
DB.AutoMigrate(&Photo{})
|
DB.AutoMigrate(&Photo{})
|
||||||
DB.AutoMigrate(&CertificationPhoto{})
|
DB.AutoMigrate(&CertificationPhoto{})
|
||||||
|
DB.AutoMigrate(&Message{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,6 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pagination result object.
|
// Pagination result object.
|
||||||
|
@ -26,15 +24,14 @@ type Page struct {
|
||||||
func (p *Pagination) ParsePage(r *http.Request) {
|
func (p *Pagination) ParsePage(r *http.Request) {
|
||||||
raw := r.FormValue("page")
|
raw := r.FormValue("page")
|
||||||
a, err := strconv.Atoi(raw)
|
a, err := strconv.Atoi(raw)
|
||||||
log.Debug("ParsePage: %s %d err=%s", raw, a, err)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if a < 0 {
|
if a <= 0 {
|
||||||
a = 1
|
a = 1
|
||||||
}
|
}
|
||||||
p.Page = a
|
p.Page = a
|
||||||
log.Warn("set page1=%+v =XXXXX%d", p, a)
|
} else {
|
||||||
|
p.Page = 1
|
||||||
}
|
}
|
||||||
log.Warn("set page=%+v", p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iter the pages, for templates.
|
// Iter the pages, for templates.
|
||||||
|
|
|
@ -101,11 +101,24 @@ type UserMap map[uint64]*User
|
||||||
// Useful to avoid circular reference issues with Photos especially; the Site Gallery queries
|
// Useful to avoid circular reference issues with Photos especially; the Site Gallery queries
|
||||||
// photos of ALL users and MapUsers helps stitch them together for the frontend.
|
// photos of ALL users and MapUsers helps stitch them together for the frontend.
|
||||||
func MapUsers(userIDs []uint64) (UserMap, error) {
|
func MapUsers(userIDs []uint64) (UserMap, error) {
|
||||||
var usermap = UserMap{}
|
var (
|
||||||
|
usermap = UserMap{}
|
||||||
|
set = map[uint64]interface{}{}
|
||||||
|
distinct = []uint64{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uniqueify users.
|
||||||
|
for _, uid := range userIDs {
|
||||||
|
if _, ok := set[uid]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set[uid] = nil
|
||||||
|
distinct = append(distinct, uid)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
users = []*User{}
|
users = []*User{}
|
||||||
result = (&User{}).Preload().Where("id IN ?", userIDs).Find(&users)
|
result = (&User{}).Preload().Where("id IN ?", distinct).Find(&users)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.Error == nil {
|
if result.Error == nil {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/account"
|
"git.kirsle.net/apps/gosocial/pkg/controller/account"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/admin"
|
"git.kirsle.net/apps/gosocial/pkg/controller/admin"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/api"
|
"git.kirsle.net/apps/gosocial/pkg/controller/api"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/controller/inbox"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/index"
|
"git.kirsle.net/apps/gosocial/pkg/controller/index"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/middleware"
|
"git.kirsle.net/apps/gosocial/pkg/middleware"
|
||||||
|
@ -31,6 +32,9 @@ func New() http.Handler {
|
||||||
mux.Handle("/photo/edit", middleware.LoginRequired(photo.Edit()))
|
mux.Handle("/photo/edit", middleware.LoginRequired(photo.Edit()))
|
||||||
mux.Handle("/photo/delete", middleware.LoginRequired(photo.Delete()))
|
mux.Handle("/photo/delete", middleware.LoginRequired(photo.Delete()))
|
||||||
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
||||||
|
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
||||||
|
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||||
|
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||||
|
|
||||||
// Certification Required. Pages that only full (verified) members can access.
|
// Certification Required. Pages that only full (verified) members can access.
|
||||||
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
||||||
|
|
|
@ -41,6 +41,12 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
||||||
return labels[1]
|
return labels[1]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Substring": func(value string, n int) string {
|
||||||
|
if n > len(value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:n]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gosocial/pkg/config"
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,6 +28,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
// Defaults
|
// Defaults
|
||||||
m["LoggedIn"] = false
|
m["LoggedIn"] = false
|
||||||
m["CurrentUser"] = nil
|
m["CurrentUser"] = nil
|
||||||
|
m["NavUnreadMessages"] = 0
|
||||||
|
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
|
@ -34,5 +37,12 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
if user, err := session.CurrentUser(r); err == nil {
|
if user, err := session.CurrentUser(r); err == nil {
|
||||||
m["LoggedIn"] = true
|
m["LoggedIn"] = true
|
||||||
m["CurrentUser"] = user
|
m["CurrentUser"] = user
|
||||||
|
|
||||||
|
// Get unread message count.
|
||||||
|
if count, err := models.CountUnreadMessages(user.ID); err == nil {
|
||||||
|
m["NavUnreadMessages"] = count
|
||||||
|
} else {
|
||||||
|
log.Error("MergeUserVars: couldn't CountUnreadMessages for %d: %s", user.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,12 @@
|
||||||
Edit Profile & Settings
|
Edit Profile & Settings
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/photo/certification">
|
||||||
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
|
Certification Photo
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/logout">
|
<a href="/logout">
|
||||||
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>
|
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>
|
||||||
|
|
|
@ -99,14 +99,14 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button type="button" class="button">
|
<a href="/messages/compose?to={{.User.Username}}" class="button">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-message"></i>
|
<i class="fa fa-message"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>Message</span>
|
<span>Message</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="control">
|
<p class="control">
|
||||||
|
|
|
@ -11,8 +11,6 @@
|
||||||
|
|
||||||
<div class="block content p-4">
|
<div class="block content p-4">
|
||||||
|
|
||||||
<pre>{{.}}</pre>
|
|
||||||
|
|
||||||
{{if or .SkipEmailVerification (not .SignupToken)}}
|
{{if or .SkipEmailVerification (not .SignupToken)}}
|
||||||
<p>
|
<p>
|
||||||
I'm glad you're thinking about joining us here!
|
I'm glad you're thinking about joining us here!
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="/admin/photo/certification">
|
<a href="/admin/photo/certification">
|
||||||
<span class="icon"><i class="fa fa-badge"></i></span>
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
Certification Photos
|
Certification Photos
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -56,13 +56,13 @@
|
||||||
<a class="navbar-item" href="/friends">
|
<a class="navbar-item" href="/friends">
|
||||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||||
<span>Friends</span>
|
<span>Friends</span>
|
||||||
<span class="tag is-warning">42</span>
|
<!-- <span class="tag is-warning">42</span> -->
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="navbar-item" href="/messages">
|
<a class="navbar-item" href="/messages">
|
||||||
<span class="icon"><i class="fa fa-envelope"></i></span>
|
<span class="icon"><i class="fa fa-envelope"></i></span>
|
||||||
<span>Messages</span>
|
<span>Messages</span>
|
||||||
<span class="tag is-warning">42</span>
|
{{if .NavUnreadMessages}}<span class="tag is-warning">{{.NavUnreadMessages}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
89
web/templates/inbox/compose.html
Normal file
89
web/templates/inbox/compose.html
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{{define "title"}}Compose a Message{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-info is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Write to: {{.User.Username}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="block p-4">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-half">
|
||||||
|
|
||||||
|
<div class="card" style="width: 100%; max-width: 640px">
|
||||||
|
<header class="card-header has-background-link">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<span class="icon"><i class="fa fa-message"></i></span>
|
||||||
|
Compose a Message
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<div class="media block">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-64x64">
|
||||||
|
{{if .User.ProfilePhoto.ID}}
|
||||||
|
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{or .User.Name "(no name)"}}</p>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="/messages/compose" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="to" value="{{.User.Username}}">
|
||||||
|
|
||||||
|
<div class="field block">
|
||||||
|
<label for="message" class="label">Message</label>
|
||||||
|
<textarea class="textarea" cols="80" rows="8"
|
||||||
|
name="message"
|
||||||
|
id="message"
|
||||||
|
required
|
||||||
|
placeholder="Message"></textarea>
|
||||||
|
<p class="help">
|
||||||
|
Markdown formatting supported.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field has-text-centered">
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
let $file = document.querySelector("#file"),
|
||||||
|
$fileName = document.querySelector("#fileName");
|
||||||
|
|
||||||
|
$file.addEventListener("change", function() {
|
||||||
|
let file = this.files[0];
|
||||||
|
$fileName.innerHTML = file.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
182
web/templates/inbox/inbox.html
Normal file
182
web/templates/inbox/inbox.html
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
{{define "title"}}Message Inbox{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-info is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">Messages</h1>
|
||||||
|
<h2 class="subtitle">Inbox</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{$UserMap := .UserMap}}
|
||||||
|
{{$Request := .Request}}
|
||||||
|
|
||||||
|
<div class="block p-4">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header has-background-link">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
{{if .ViewThread}}Conversation with {{.ReplyTo}}{{else}}Inbox{{end}}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{if .ViewThread}}
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="block">
|
||||||
|
<form action="/messages/compose" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="to" value="{{.ReplyTo}}">
|
||||||
|
<input type="hidden" name="from" value="inbox">
|
||||||
|
<textarea class="textarea block" cols="80" rows="4"
|
||||||
|
name="message"
|
||||||
|
placeholder="Write a response"></textarea>
|
||||||
|
<button type="submit" class="button is-success">Send Reply</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
{{range .ViewThread}}
|
||||||
|
<div class="media block">
|
||||||
|
{{$SourceUser := $UserMap.Get .SourceUserID}}
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-64x64">
|
||||||
|
{{if $SourceUser.ProfilePhoto.ID}}
|
||||||
|
<img src="{{PhotoURL $SourceUser.ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{or $SourceUser.Name "(no name)"}}</p>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
<a href="/u/{{$SourceUser.Username}}">{{$SourceUser.Username}}</a>
|
||||||
|
{{if not $SourceUser.Certified}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
|
<span>Not Certified!</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if $SourceUser.IsAdmin}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||||
|
<span>Admin</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block content">
|
||||||
|
{{ToMarkdown .Message}}
|
||||||
|
</div>
|
||||||
|
<div class="block">
|
||||||
|
<em>Sent on {{.CreatedAt.Format "2006-01-02 15:04:05"}}</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="block">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Pager footer -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<div>
|
||||||
|
Found <strong>{{.ThreadPager.Total}}</strong> message{{Pluralize64 .ThreadPager.Total}} in this thread
|
||||||
|
(page {{.ThreadPager.Page}} of {{.ThreadPager.Pages}}).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
{{if .ThreadPager.HasPrevious}}
|
||||||
|
<a href="{{$Request.URL.Path}}?page={{.ThreadPager.Previous}}" class="button">Previous</a>
|
||||||
|
{{end}}
|
||||||
|
{{if .ThreadPager.HasNext}}
|
||||||
|
<a href="{{$Request.URL.Path}}?page={{.ThreadPager.Next}}" class="button">Next Page</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="card-content content">
|
||||||
|
<p>
|
||||||
|
You have <strong>{{.Unread}}</strong> unread message{{Pluralize64 .Unread}}. Select a message on the
|
||||||
|
other column to read the conversation here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-one-third">
|
||||||
|
<div class="card block">
|
||||||
|
<header class="card-header has-background-link">
|
||||||
|
<p class="card-header-title has-text-light">Messages</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="tabs is-toggle">
|
||||||
|
<ul>
|
||||||
|
<li{{if not .IsSentBox}} class="is-active"{{end}}>
|
||||||
|
<a href="/messages">Inbox</a>
|
||||||
|
</li>
|
||||||
|
<li{{if .IsSentBox}} class="is-active"{{end}}>
|
||||||
|
<a href="/messages?box=sent">Sent</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="menu-list block">
|
||||||
|
{{$IsSentBox := .IsSentBox}}
|
||||||
|
{{range .Messages}}
|
||||||
|
<li>
|
||||||
|
<a href="/messages/read/{{.ID}}">
|
||||||
|
<div>
|
||||||
|
{{if $IsSentBox}}
|
||||||
|
{{$User := $UserMap.Get .TargetUserID}}
|
||||||
|
<strong>Sent to {{$User.Username}}</strong>
|
||||||
|
{{else}}
|
||||||
|
{{$User := $UserMap.Get .SourceUserID}}
|
||||||
|
<strong>From {{$User.Username}}</strong>
|
||||||
|
{{end}}
|
||||||
|
{{if not .Read}}<span class="tag is-success">NEW</span>{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="my-1">
|
||||||
|
<em>
|
||||||
|
{{Substring .Message 48}}…
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Sent {{.CreatedAt.Format "2006-01-02 15:04:05"}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Pager footer -->
|
||||||
|
<div class="block">
|
||||||
|
<div>
|
||||||
|
Found <strong>{{.Pager.Total}}</strong> message{{Pluralize64 .Pager.Total}}
|
||||||
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Pager.HasPrevious}}
|
||||||
|
<a href="/messages?{{if .IsSentBox}}box=sent&{{end}}page={{.Pager.Previous}}" class="button">Previous</a>
|
||||||
|
{{end}}
|
||||||
|
{{if .Pager.HasNext}}
|
||||||
|
<a href="/messages?{{if .IsSentBox}}box=sent&{{end}}page={{.Pager.Next}}" class="button">Next Page</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
Reference in New Issue
Block a user