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,10 +87,12 @@ func Edit() http.HandlerFunc {
|
|||
}
|
||||
|
||||
// Set their profile pic to this one.
|
||||
currentUser.ProfilePhoto = *photo
|
||||
log.Error("Set user ProfilePhotoID=%d", photo.ID)
|
||||
if err := currentUser.Save(); err != nil {
|
||||
session.FlashError(w, r, "Couldn't save user: %s", err)
|
||||
if setProfilePic {
|
||||
currentUser.ProfilePhoto = *photo
|
||||
log.Error("Set user ProfilePhotoID=%d", photo.ID)
|
||||
if err := currentUser.Save(); err != nil {
|
||||
session.FlashError(w, r, "Couldn't save user: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Flash success.
|
||||
|
|
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(&Photo{})
|
||||
DB.AutoMigrate(&CertificationPhoto{})
|
||||
DB.AutoMigrate(&Message{})
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import (
|
|||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||
)
|
||||
|
||||
// Pagination result object.
|
||||
|
@ -26,15 +24,14 @@ type Page struct {
|
|||
func (p *Pagination) ParsePage(r *http.Request) {
|
||||
raw := r.FormValue("page")
|
||||
a, err := strconv.Atoi(raw)
|
||||
log.Debug("ParsePage: %s %d err=%s", raw, a, err)
|
||||
if err == nil {
|
||||
if a < 0 {
|
||||
if a <= 0 {
|
||||
a = 1
|
||||
}
|
||||
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.
|
||||
|
|
|
@ -101,11 +101,24 @@ type UserMap map[uint64]*User
|
|||
// 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.
|
||||
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 (
|
||||
users = []*User{}
|
||||
result = (&User{}).Preload().Where("id IN ?", userIDs).Find(&users)
|
||||
result = (&User{}).Preload().Where("id IN ?", distinct).Find(&users)
|
||||
)
|
||||
|
||||
if result.Error == nil {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"git.kirsle.net/apps/gosocial/pkg/controller/account"
|
||||
"git.kirsle.net/apps/gosocial/pkg/controller/admin"
|
||||
"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/photo"
|
||||
"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/delete", middleware.LoginRequired(photo.Delete()))
|
||||
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.
|
||||
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
||||
|
|
|
@ -41,6 +41,12 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
|||
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"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -26,6 +28,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
// Defaults
|
||||
m["LoggedIn"] = false
|
||||
m["CurrentUser"] = nil
|
||||
m["NavUnreadMessages"] = 0
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
|
@ -34,5 +37,12 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
if user, err := session.CurrentUser(r); err == nil {
|
||||
m["LoggedIn"] = true
|
||||
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
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/photo/certification">
|
||||
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||
Certification Photo
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/logout">
|
||||
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>
|
||||
|
|
|
@ -99,14 +99,14 @@
|
|||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button type="button" class="button">
|
||||
<a href="/messages/compose?to={{.User.Username}}" class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa fa-message"></i>
|
||||
</span>
|
||||
<span>Message</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
|
||||
<div class="block content p-4">
|
||||
|
||||
<pre>{{.}}</pre>
|
||||
|
||||
{{if or .SkipEmailVerification (not .SignupToken)}}
|
||||
<p>
|
||||
I'm glad you're thinking about joining us here!
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<ul class="menu-list">
|
||||
<li>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -56,13 +56,13 @@
|
|||
<a class="navbar-item" href="/friends">
|
||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||
<span>Friends</span>
|
||||
<span class="tag is-warning">42</span>
|
||||
<!-- <span class="tag is-warning">42</span> -->
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="/messages">
|
||||
<span class="icon"><i class="fa fa-envelope"></i></span>
|
||||
<span>Messages</span>
|
||||
<span class="tag is-warning">42</span>
|
||||
{{if .NavUnreadMessages}}<span class="tag is-warning">{{.NavUnreadMessages}}</span>{{end}}
|
||||
</a>
|
||||
{{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