Messages: Threaded view for better user experience
This commit is contained in:
parent
f986c6f0a5
commit
cadb759ff4
|
@ -24,14 +24,17 @@ func Inbox() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default is inbox, what about sentbox?
|
// What view are we looking at? Threads (default), Inbox, or Outbox?
|
||||||
var showSent = r.FormValue("box") == "sent"
|
var box = r.FormValue("box")
|
||||||
|
if box != "inbox" && box != "sent" {
|
||||||
|
box = "threads"
|
||||||
|
}
|
||||||
|
|
||||||
// Are we reading a specific message?
|
// Are we reading a specific message?
|
||||||
var (
|
var (
|
||||||
viewThread []*models.Message
|
viewThread []*models.Message
|
||||||
threadPager *models.Pagination
|
threadPager *models.Pagination
|
||||||
composeToUsername string
|
composeToUser *models.User
|
||||||
msgId int
|
msgId int
|
||||||
)
|
)
|
||||||
if uri := ReadURLRegexp.FindStringSubmatch(r.URL.Path); uri != nil {
|
if uri := ReadURLRegexp.FindStringSubmatch(r.URL.Path); uri != nil {
|
||||||
|
@ -58,7 +61,7 @@ func Inbox() http.HandlerFunc {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't get sender of that message: %s", err)
|
session.FlashError(w, r, "Couldn't get sender of that message: %s", err)
|
||||||
}
|
}
|
||||||
composeToUsername = sender.Username
|
composeToUser = sender
|
||||||
|
|
||||||
// Get the full chat thread (paginated).
|
// Get the full chat thread (paginated).
|
||||||
threadPager = &models.Pagination{
|
threadPager = &models.Pagination{
|
||||||
|
@ -86,6 +89,7 @@ func Inbox() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the inbox list of messages.
|
// Get the inbox list of messages.
|
||||||
|
var messages []*models.Message
|
||||||
pager := &models.Pagination{
|
pager := &models.Pagination{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: config.PageSizeInboxList,
|
PerPage: config.PageSizeInboxList,
|
||||||
|
@ -95,9 +99,20 @@ func Inbox() http.HandlerFunc {
|
||||||
// On the main inbox view, ?page= params page thru the message list, not a thread.
|
// On the main inbox view, ?page= params page thru the message list, not a thread.
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
}
|
}
|
||||||
messages, err := models.GetMessages(currentUser, showSent, pager)
|
|
||||||
if err != nil {
|
// Viewing the threads, or a specific inbox/sent box?
|
||||||
|
if box == "threads" {
|
||||||
|
if result, err := models.GetMessageThreads(currentUser, pager); err != nil {
|
||||||
session.FlashError(w, r, "Couldn't get your messages from DB: %s", err)
|
session.FlashError(w, r, "Couldn't get your messages from DB: %s", err)
|
||||||
|
} else {
|
||||||
|
messages = result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if result, err := models.GetMessages(currentUser, box == "sent", pager); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get your messages from DB: %s", err)
|
||||||
|
} else {
|
||||||
|
messages = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many unreads?
|
// How many unreads?
|
||||||
|
@ -111,11 +126,9 @@ func Inbox() http.HandlerFunc {
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
||||||
}
|
}
|
||||||
if viewThread != nil {
|
|
||||||
for _, m := range viewThread {
|
for _, m := range viewThread {
|
||||||
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
userMap, err := models.MapUsers(currentUser, userIDs)
|
userMap, err := models.MapUsers(currentUser, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't map users: %s", err)
|
session.FlashError(w, r, "Couldn't map users: %s", err)
|
||||||
|
@ -126,10 +139,10 @@ func Inbox() http.HandlerFunc {
|
||||||
"UserMap": userMap,
|
"UserMap": userMap,
|
||||||
"Unread": unread,
|
"Unread": unread,
|
||||||
"Pager": pager,
|
"Pager": pager,
|
||||||
"IsSentBox": showSent,
|
"Box": box,
|
||||||
"ViewThread": viewThread, // nil on inbox page
|
"ViewThread": viewThread, // nil on inbox page
|
||||||
"ThreadPager": threadPager,
|
"ThreadPager": threadPager,
|
||||||
"ReplyTo": composeToUsername,
|
"ReplyTo": composeToUser,
|
||||||
"MessageID": msgId,
|
"MessageID": msgId,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
|
|
@ -23,7 +23,7 @@ func GetMessage(id uint64) (*Message, error) {
|
||||||
return m, result.Error
|
return m, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMessages for a user.
|
// GetMessages for a user, e-mail style for the inbox or sent box view.
|
||||||
func GetMessages(user *User, sent bool, pager *Pagination) ([]*Message, error) {
|
func GetMessages(user *User, sent bool, pager *Pagination) ([]*Message, error) {
|
||||||
var (
|
var (
|
||||||
m = []*Message{}
|
m = []*Message{}
|
||||||
|
@ -70,6 +70,73 @@ func GetMessages(user *User, sent bool, pager *Pagination) ([]*Message, error) {
|
||||||
return m, result.Error
|
return m, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMessageThreads for a user: combined inbox/sent view grouped by username.
|
||||||
|
func GetMessageThreads(user *User, pager *Pagination) ([]*Message, error) {
|
||||||
|
var (
|
||||||
|
m = []*Message{}
|
||||||
|
blockedUserIDs = BlockedUserIDs(user)
|
||||||
|
where = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
where = append(where, "target_user_id = ?")
|
||||||
|
placeholders = append(placeholders, user.ID)
|
||||||
|
|
||||||
|
if len(blockedUserIDs) > 0 {
|
||||||
|
where = append(where, "source_user_id NOT IN ?")
|
||||||
|
placeholders = append(placeholders, blockedUserIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show messages from banned or disabled accounts.
|
||||||
|
where = append(where, `
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM users
|
||||||
|
WHERE users.id IN (messages.target_user_id, messages.source_user_id)
|
||||||
|
AND users.status <> 'active'
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
type newest struct {
|
||||||
|
ID uint64
|
||||||
|
SourceUserID uint64
|
||||||
|
TargetUserID uint64
|
||||||
|
}
|
||||||
|
var scan []newest
|
||||||
|
|
||||||
|
// Get the newest message IDs grouped by username for everyone we are chatting with.
|
||||||
|
query := DB.Model(&Message{}).Select(
|
||||||
|
"max(id) AS id",
|
||||||
|
"source_user_id",
|
||||||
|
"target_user_id",
|
||||||
|
).Where(
|
||||||
|
strings.Join(where, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
).Group(
|
||||||
|
"source_user_id, target_user_id",
|
||||||
|
).Order("id desc").Scan(&scan)
|
||||||
|
if query.Error != nil {
|
||||||
|
return nil, query.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
pager.Total = int64(len(scan))
|
||||||
|
|
||||||
|
// Get the details from these message IDs.
|
||||||
|
var messageIDs = []uint64{}
|
||||||
|
for _, row := range scan {
|
||||||
|
messageIDs = append(messageIDs, row.ID)
|
||||||
|
}
|
||||||
|
query = DB.Where(
|
||||||
|
"id IN ?",
|
||||||
|
messageIDs,
|
||||||
|
).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.
|
// GetMessageThread returns paginated message history between two people.
|
||||||
func GetMessageThread(sourceUserID, targetUserID uint64, pager *Pagination) ([]*Message, error) {
|
func GetMessageThread(sourceUserID, targetUserID uint64, pager *Pagination) ([]*Message, error) {
|
||||||
var m = []*Message{}
|
var m = []*Message{}
|
||||||
|
|
|
@ -8,7 +8,14 @@
|
||||||
<i class="fa fa-envelope mr-2"></i>
|
<i class="fa fa-envelope mr-2"></i>
|
||||||
Messages
|
Messages
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="subtitle">{{if .IsSentBox}}Sent{{else}}Inbox{{end}}</h2>
|
<h2 class="subtitle">
|
||||||
|
{{if eq .Box "threads"}}
|
||||||
|
Threads
|
||||||
|
{{else if eq .Box "sent"}}
|
||||||
|
Sent
|
||||||
|
{{else}}
|
||||||
|
Inbox
|
||||||
|
{{end}}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -23,16 +30,29 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">
|
<p class="card-header-title has-text-light">
|
||||||
{{if .ViewThread}}Conversation with {{.ReplyTo}}{{else}}Inbox{{end}}
|
{{if .ViewThread}}Conversation with {{.ReplyTo.Username}}{{else}}Inbox{{end}}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{if .ViewThread}}
|
{{if .ViewThread}}
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
<div class="block">
|
||||||
|
<div class="columns is-mobile is-gapless">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<strong>To:</strong>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow mx-2">
|
||||||
|
{{template "avatar-24x24" .ReplyTo}}
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<a href="/u/{{.ReplyTo.Username}}">{{.ReplyTo.Username}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<form action="/messages/compose" method="POST">
|
<form action="/messages/compose" method="POST">
|
||||||
{{InputCSRF}}
|
{{InputCSRF}}
|
||||||
<input type="hidden" name="to" value="{{.ReplyTo}}">
|
<input type="hidden" name="to" value="{{.ReplyTo.Username}}">
|
||||||
<input type="hidden" name="from" value="inbox">
|
<input type="hidden" name="from" value="inbox">
|
||||||
<textarea class="textarea" cols="80" rows="4"
|
<textarea class="textarea" cols="80" rows="4"
|
||||||
name="message"
|
name="message"
|
||||||
|
@ -170,28 +190,56 @@
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<div class="card block">
|
<div class="card block">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">Messages</p>
|
<p class="card-header-title has-text-light">
|
||||||
|
{{if eq .Box "threads"}}
|
||||||
|
Conversations
|
||||||
|
{{else if eq .Box "inbox"}}
|
||||||
|
All Inbox Messages
|
||||||
|
{{else}}
|
||||||
|
Sent Messages
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="tabs is-toggle is-fullwidth">
|
<div class="tabs is-toggle is-fullwidth">
|
||||||
<ul>
|
<ul>
|
||||||
<li{{if not .IsSentBox}} class="is-active"{{end}}>
|
<li{{if eq .Box "threads"}} class="is-active"{{end}}>
|
||||||
<a href="/messages">Inbox</a>
|
<a href="/messages">Threads</a>
|
||||||
</li>
|
</li>
|
||||||
<li{{if .IsSentBox}} class="is-active"{{end}}>
|
<li{{if eq .Box "inbox"}} class="is-active"{{end}}>
|
||||||
|
<a href="/messages?box=inbox">Inbox</a>
|
||||||
|
</li>
|
||||||
|
<li{{if eq .Box "sent"}} class="is-active"{{end}}>
|
||||||
<a href="/messages?box=sent">Sent</a>
|
<a href="/messages?box=sent">Sent</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if eq .Box "threads"}}
|
||||||
|
<div class="block is-size-7">
|
||||||
|
Showing {{.Pager.Total}} conversation threads with others (ordered by most
|
||||||
|
recent, grouped by sender name). <strong>Note:</strong> messages you
|
||||||
|
have Sent but which have not been replied to will only appear on the
|
||||||
|
<a href="/messages?box=sent">"Sent"</a> tab.
|
||||||
|
</div>
|
||||||
|
{{else if eq .Box "inbox"}}
|
||||||
|
<div class="block is-size-7">
|
||||||
|
Showing <strong>all</strong> {{.Pager.Total}} inbound messages to you, ordered
|
||||||
|
by most recent.
|
||||||
|
</div>
|
||||||
|
{{else if eq .Box "sent"}}
|
||||||
|
<div class="block is-size-7">
|
||||||
|
Showing {{.Pager.Total}} messages sent by you to other people.
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<ul class="menu-list block">
|
<ul class="menu-list block">
|
||||||
{{$IsSentBox := .IsSentBox}}
|
|
||||||
{{range .Messages}}
|
{{range .Messages}}
|
||||||
<li>
|
<li>
|
||||||
<a href="/messages/read/{{.ID}}">
|
<a href="/messages/read/{{.ID}}">
|
||||||
<div>
|
<div>
|
||||||
{{if $IsSentBox}}
|
{{if eq $Root.Box "sent"}}
|
||||||
{{$User := $UserMap.Get .TargetUserID}}
|
{{$User := $UserMap.Get .TargetUserID}}
|
||||||
<strong>Sent to {{$User.Username}}</strong>
|
<strong>Sent to {{$User.Username}}</strong>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -199,7 +247,7 @@
|
||||||
<strong>From {{$User.Username}}</strong>
|
<strong>From {{$User.Username}}</strong>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Read}}
|
{{if not .Read}}
|
||||||
<span class="tag is-success">{{if $IsSentBox}}UNREAD{{else}}NEW{{end}}</span>
|
<span class="tag is-success">{{if eq $Root.Box "sent"}}UNREAD{{else}}NEW{{end}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="my-1">
|
<div class="my-1">
|
||||||
|
@ -217,8 +265,8 @@
|
||||||
|
|
||||||
<!-- Pager footer -->
|
<!-- Pager footer -->
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div>
|
<div class="mb-4">
|
||||||
Found <strong>{{.Pager.Total}}</strong> message{{Pluralize64 .Pager.Total}}
|
Found <strong>{{.Pager.Total}}</strong> {{if eq .Box "threads"}}conversation{{else}}message{{end}}{{Pluralize64 .Pager.Total}}
|
||||||
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user