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 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 {

View File

@ -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{}

View File

@ -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>