Messages: Threaded view for better user experience

This commit is contained in:
Noah Petherbridge 2023-12-25 21:36:41 -08:00
parent f986c6f0a5
commit cadb759ff4
3 changed files with 157 additions and 29 deletions

View File

@ -24,14 +24,17 @@ func Inbox() http.HandlerFunc {
return
}
// Default is inbox, what about sentbox?
var showSent = r.FormValue("box") == "sent"
// What view are we looking at? Threads (default), Inbox, or Outbox?
var box = r.FormValue("box")
if box != "inbox" && box != "sent" {
box = "threads"
}
// Are we reading a specific message?
var (
viewThread []*models.Message
threadPager *models.Pagination
composeToUsername string
composeToUser *models.User
msgId int
)
if uri := ReadURLRegexp.FindStringSubmatch(r.URL.Path); uri != nil {
@ -58,7 +61,7 @@ func Inbox() http.HandlerFunc {
if err != nil {
session.FlashError(w, r, "Couldn't get sender of that message: %s", err)
}
composeToUsername = sender.Username
composeToUser = sender
// Get the full chat thread (paginated).
threadPager = &models.Pagination{
@ -86,6 +89,7 @@ func Inbox() http.HandlerFunc {
}
// Get the inbox list of messages.
var messages []*models.Message
pager := &models.Pagination{
Page: 1,
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.
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)
} 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?
@ -111,11 +126,9 @@ func Inbox() http.HandlerFunc {
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(currentUser, userIDs)
if err != nil {
session.FlashError(w, r, "Couldn't map users: %s", err)
@ -126,10 +139,10 @@ func Inbox() http.HandlerFunc {
"UserMap": userMap,
"Unread": unread,
"Pager": pager,
"IsSentBox": showSent,
"Box": box,
"ViewThread": viewThread, // nil on inbox page
"ThreadPager": threadPager,
"ReplyTo": composeToUsername,
"ReplyTo": composeToUser,
"MessageID": msgId,
}
if err := tmpl.Execute(w, r, vars); err != nil {

View File

@ -23,7 +23,7 @@ func GetMessage(id uint64) (*Message, 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) {
var (
m = []*Message{}
@ -70,6 +70,73 @@ func GetMessages(user *User, sent bool, pager *Pagination) ([]*Message, 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.
func GetMessageThread(sourceUserID, targetUserID uint64, pager *Pagination) ([]*Message, error) {
var m = []*Message{}

View File

@ -8,7 +8,14 @@
<i class="fa fa-envelope mr-2"></i>
Messages
</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>
</section>
@ -23,16 +30,29 @@
<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}}
{{if .ViewThread}}Conversation with {{.ReplyTo.Username}}{{else}}Inbox{{end}}
</p>
</header>
{{if .ViewThread}}
<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">
<form action="/messages/compose" method="POST">
{{InputCSRF}}
<input type="hidden" name="to" value="{{.ReplyTo}}">
<input type="hidden" name="to" value="{{.ReplyTo.Username}}">
<input type="hidden" name="from" value="inbox">
<textarea class="textarea" cols="80" rows="4"
name="message"
@ -170,28 +190,56 @@
<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>
<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>
<div class="card-content">
<div class="tabs is-toggle is-fullwidth">
<ul>
<li{{if not .IsSentBox}} class="is-active"{{end}}>
<a href="/messages">Inbox</a>
<li{{if eq .Box "threads"}} class="is-active"{{end}}>
<a href="/messages">Threads</a>
</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>
</li>
</ul>
</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">
{{$IsSentBox := .IsSentBox}}
{{range .Messages}}
<li>
<a href="/messages/read/{{.ID}}">
<div>
{{if $IsSentBox}}
{{if eq $Root.Box "sent"}}
{{$User := $UserMap.Get .TargetUserID}}
<strong>Sent to {{$User.Username}}</strong>
{{else}}
@ -199,7 +247,7 @@
<strong>From {{$User.Username}}</strong>
{{end}}
{{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}}
</div>
<div class="my-1">
@ -217,8 +265,8 @@
<!-- Pager footer -->
<div class="block">
<div>
Found <strong>{{.Pager.Total}}</strong> message{{Pluralize64 .Pager.Total}}
<div class="mb-4">
Found <strong>{{.Pager.Total}}</strong> {{if eq .Box "threads"}}conversation{{else}}message{{end}}{{Pluralize64 .Pager.Total}}
(page {{.Pager.Page}} of {{.Pager.Pages}}).
</div>