Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Individual message rendering overhaul #451

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
682adb0
Prototype using textviews in a box
Retropaint Sep 28, 2024
cc508f7
stopgap for integrating message rendering with API
Retropaint Sep 28, 2024
b01b6d2
Rendering body only
Retropaint Sep 28, 2024
e7d2526
Revamped drawing logic + highlighting
Retropaint Sep 29, 2024
152dfa2
Proper header & body rendering
Retropaint Sep 29, 2024
59518d1
added scrolling (WIP)
Retropaint Sep 30, 2024
e298c6c
Showing replies + fixed line count calculation
Retropaint Sep 30, 2024
bcdf544
this todo is already done :)
Retropaint Oct 1, 2024
4e267ea
bottom-first scrolling
Retropaint Oct 1, 2024
709bd5a
Old messagesText struct deleted for good
Retropaint Oct 1, 2024
551af61
Overhauled scrolling system (verbose, but stable)
Retropaint Oct 6, 2024
9812472
uncommented
Retropaint Oct 6, 2024
3bbe7d1
messages don't draw thru right border
Retropaint Oct 6, 2024
a5715e7
Uncommented guilds tree
Retropaint Oct 6, 2024
8e1446e
Removed explicit .Box from main_flex
Retropaint Oct 6, 2024
c29669e
Line count calculations for single long words
Retropaint Oct 7, 2024
dc5cb24
Rendering pin messages
Retropaint Oct 7, 2024
e05fe7e
Oops
Retropaint Oct 7, 2024
5985eab
More compact and optimized scrolling
Retropaint Oct 8, 2024
df5830b
Reverted scrollOffset to 20
Retropaint Oct 8, 2024
98e8eeb
Caching line counts
Retropaint Oct 8, 2024
86c8376
Clarification comment for renderTopBorder
Retropaint Oct 8, 2024
0cbcaba
Merge branch 'main' into individual-messages
Retropaint Oct 15, 2024
a177731
Fixed merge conflicts
Retropaint Oct 15, 2024
19d0bc6
Truncating reply content to single line to not need complex line calc…
Retropaint Oct 22, 2024
70f29e0
Removed unused var name
Retropaint Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/guilds_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (gt *GuildsTree) onSelected(n *tview.TreeNode) {
gt.createChannelNodes(n, cs)
case discord.ChannelID:
layout.messagesText.drawMsgs(ref)
layout.messagesText.ScrollToEnd()
layout.messagesText.drawMsgs(ref)

c, err := discordState.Cabinet.Channel(ref)
if err != nil {
Expand Down
62 changes: 62 additions & 0 deletions cmd/message_box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cmd

import (
"strings"
"github.com/rivo/tview"
"github.com/gdamore/tcell/v2"
"github.com/diamondburned/arikawa/v3/discord"
)

type MessageBox struct {
*tview.TextView
*discord.Message
lineCount int
}

func newMessageBox(bgColor string) *MessageBox {
mb := &MessageBox{
TextView: tview.NewTextView(),
}

mb.SetDynamicColors(true)
mb.SetWordWrap(false)
mb.SetRegions(true)
mb.SetBackgroundColor(tcell.GetColor(bgColor))

return mb
}

func (m *MessageBox) getLineCount(width int) int {
lineCount := 0
charCount := 0

for _, w := range strings.Split(m.Content, " ") {
// Don't count \n, since GetOriginalLineCount() already does that
if strings.Index(w, "\n") != -1 {
charCount = 0
continue
}

charCount += len(w) + 1
for charCount > width {
lineCount++
charCount -= width
}
}

return lineCount + m.GetOriginalLineCount()
}

func (m *MessageBox) Draw(screen tcell.Screen) {
m.DrawForSubclass(screen, m)
}

// To render the message, Draw() needs to be called once after any TextView func that returns itself
// There has to be a better way of handling that
func (m *MessageBox) Render(highlight bool, screen tcell.Screen) {
if highlight {
m.Highlight("msg").Draw(screen)
} else {
m.Highlight().Draw(screen)
}
}
3 changes: 0 additions & 3 deletions cmd/message_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,6 @@ func (mi *MessageInput) send() {

mi.replyMessageID = 0
mi.reset()

layout.messagesText.Highlight()
layout.messagesText.ScrollToEnd()
}

func (mi *MessageInput) editor() {
Expand Down
210 changes: 131 additions & 79 deletions cmd/messages_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,51 +21,128 @@ import (
)

type MessagesText struct {
*tview.TextView
*tview.Box
cfg *config.Config
app *tview.Application
selectedMessageID discord.MessageID
messageBoxes []*MessageBox
}

func newMessagesText(app *tview.Application, cfg *config.Config) *MessagesText {
mt := &MessagesText{
TextView: tview.NewTextView(),
cfg: cfg,
app: app,
}

mt.SetDynamicColors(true)
mt.SetRegions(true)
mt.SetWordWrap(true)
mt.SetInputCapture(mt.onInputCapture)
mt.ScrollToEnd()
mt.SetChangedFunc(func() {
app.Draw()
})

mt.SetTextColor(tcell.GetColor(mt.cfg.Theme.MessagesText.ContentColor))
mt.SetBackgroundColor(tcell.GetColor(mt.cfg.Theme.BackgroundColor))
Box: tview.NewBox(),
cfg: cfg,
app: app,
}

mt.SetBorder(true)
mt.SetTitle("Messages")
mt.SetTitleColor(tcell.GetColor(mt.cfg.Theme.TitleColor))
mt.SetTitleAlign(tview.AlignLeft)
mt.SetBackgroundColor(tcell.GetColor(mt.cfg.Theme.BackgroundColor))
mt.Box.SetInputCapture(mt.onInputCapture)

mt.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) {
messageIdx, err := mt.getSelectedMessageIndex()
if err != nil {
slog.Error("failed to get selected message", "err", err)
}
messageIdx = min(messageIdx, len(mt.messageBoxes)-1)

// force bottom-first if no message is selected
if messageIdx == -1 {
mt.renderMessagesBottomFirst(x, y, width, height, screen)
mt.renderTopBorder(x, y, width, height, screen)
return x, y, width, height
}

// Position messages without any scrolling offset
prevLineCount := 0
for _, m := range mt.messageBoxes {
mY := y + 1 + prevLineCount
mH := height - 2 - prevLineCount
m.SetRect(x+1, mY, width-2, mH)
prevLineCount += m.lineCount
}

p := mt.cfg.Theme.BorderPadding
mt.SetBorder(mt.cfg.Theme.Border)
mt.SetBorderColor(tcell.GetColor(mt.cfg.Theme.BorderColor))
mt.SetBorderPadding(p[0], p[1], p[2], p[3])
// Get scrolling offset based on selected message
_, selectedY, _, _ := mt.messageBoxes[len(mt.messageBoxes)-1-messageIdx].GetRect()
scrollOffset := 20
scrollY := -(selectedY - (y + 1) - scrollOffset)

// Get first and last message coordinates to determine the render options
_, firstY, _, _ := mt.messageBoxes[0].GetRect()
_, lastY, _, _ := mt.messageBoxes[len(mt.messageBoxes)-1].GetRect()
firstY += scrollY
lastY += scrollY
lastLineCount := mt.messageBoxes[len(mt.messageBoxes)-1].lineCount

if firstY > y+1 {
// top-first rendering
prevLineCount = 0
for _, m := range mt.messageBoxes {
m.SetRect(x+1, y+1+prevLineCount, width-2, height-2-prevLineCount)
prevLineCount += m.lineCount
m.Render(mt.selectedMessageID == m.ID, screen)
if y+1+prevLineCount > height-2 {
break
}
}
} else if lastY+lastLineCount < height-1 {
mt.renderMessagesBottomFirst(x, y, width, height, screen)
mt.renderTopBorder(x, y, width, height, screen)
} else {
// in-between rendering
for _, m := range mt.messageBoxes {
mX, mY, mW, mH := m.GetRect()
mY += scrollY
mH -= scrollY
if mY+m.lineCount < y+1 || mY > height-1 {
continue
}

m.SetRect(mX, mY, mW, mH)
m.Render(mt.selectedMessageID == m.ID, screen)
}
mt.renderTopBorder(x, y, width, height, screen)
}

return x, y, width, height
})

markdown.DefaultRenderer.AddOptions(
renderer.WithOption("emojiColor", mt.cfg.Theme.MessagesText.EmojiColor),
renderer.WithOption("linkColor", mt.cfg.Theme.MessagesText.LinkColor),
)
return mt
}

mt.SetHighlightedFunc(mt.onHighlighted)
func (mt *MessagesText) renderMessagesBottomFirst(x int, y int, width int, height int, screen tcell.Screen) {
prevLineCount := 0
for _, m := range slices.Backward(mt.messageBoxes) {
prevLineCount += m.lineCount
m.SetRect(x+1, height-1-prevLineCount, width-2, m.lineCount)
m.Render(mt.selectedMessageID == m.ID, screen)
if height-1-prevLineCount+m.lineCount < y+1 {
break
}
}
}

return mt
// Rendering the top border manually is the easiest way to cut text from the top
func (mt *MessagesText) renderTopBorder(x int, y int, width int, height int, screen tcell.Screen) {
topLine := mt.GetTitle()
for i := 0; i < width-2-len(mt.GetTitle()); i++ {
if mt.HasFocus() {
topLine += "═"
} else {
topLine += "─"
}
}
tview.PrintSimple(screen, topLine, x+1, y)
}

func (mt *MessagesText) drawMsgs(cID discord.ChannelID) {
mt.messageBoxes = nil
ms, err := discordState.Messages(cID, uint(mt.cfg.MessagesLimit))
if err != nil {
slog.Error("failed to get messages", "err", err, "channel_id", cID)
Expand All @@ -79,54 +156,33 @@ func (mt *MessagesText) drawMsgs(cID discord.ChannelID) {

func (mt *MessagesText) reset() {
layout.messagesText.selectedMessageID = 0

mt.SetTitle("")
mt.Clear()
mt.Highlight()
}

// Region tags are square brackets that contain a region ID in double quotes
// https://pkg.go.dev/github.com/rivo/tview#hdr-Regions_and_Highlights
func (mt *MessagesText) startRegion(msgID discord.MessageID) {
fmt.Fprintf(mt, `["%s"]`, msgID)
}

// Tags with no region ID ([""]) don't start new regions. They can therefore be used to mark the end of a region.
func (mt *MessagesText) endRegion() {
fmt.Fprint(mt, `[""]`)
}

func (mt *MessagesText) createMessage(m discord.Message) {
mt.startRegion(m.ID)
defer mt.endRegion()

if mt.cfg.HideBlockedUsers {
isBlocked := discordState.UserIsBlocked(m.Author.ID)
if isBlocked {
fmt.Fprintln(mt, "[:red:b]Blocked message[:-:-]")
return
}
}
mb := newMessageBox(mt.cfg.Theme.BackgroundColor)
mb.Message = &m
fmt.Fprintf(mb, `["msg"]`)

switch m.Type {
case discord.ChannelPinnedMessage:
fmt.Fprint(mt, "["+mt.cfg.Theme.MessagesText.ContentColor+"]"+m.Author.Username+" pinned a message"+"[-:-:-]")
fmt.Fprint(mb, "["+mt.cfg.Theme.MessagesText.ContentColor+"]"+m.Author.Username+" pinned a message"+"[-:-:-]")
case discord.DefaultMessage, discord.InlinedReplyMessage:
if m.ReferencedMessage != nil {
mt.createHeader(mt, *m.ReferencedMessage, true)
mt.createBody(mt, *m.ReferencedMessage, true)
mt.createHeader(mb, *m.ReferencedMessage, true)
mt.createBody(mb, *m.ReferencedMessage, true)

fmt.Fprint(mt, "[::-]\n")
fmt.Fprint(mb, "[::-]\n")
}

mt.createHeader(mt, m, false)
mt.createBody(mt, m, false)
mt.createFooter(mt, m)
default:
mt.createHeader(mt, m, false)
mt.createHeader(mb, m, false)
mt.createBody(mb, m, false)
mt.createFooter(mb, m)
}

fmt.Fprintln(mt)
_, _, width, _ := mt.GetRect()
mb.lineCount = mb.getLineCount(width - 1)
mt.messageBoxes = append(mt.messageBoxes, mb)
}

func (mt *MessagesText) createHeader(w io.Writer, m discord.Message, isReply bool) {
Expand All @@ -136,18 +192,27 @@ func (mt *MessagesText) createHeader(w io.Writer, m discord.Message, isReply boo
}

if isReply {
fmt.Fprintf(mt, "[::d]%s", mt.cfg.Theme.MessagesText.ReplyIndicator)
fmt.Fprintf(w, "[::d]%s", mt.cfg.Theme.MessagesText.ReplyIndicator)
}

fmt.Fprintf(w, "[%s]%s[-:-:-] ", mt.cfg.Theme.MessagesText.AuthorColor, m.Author.Username)
}

func (mt *MessagesText) createBody(w io.Writer, m discord.Message, isReply bool) {
content := m.Content

if isReply {
fmt.Fprint(w, "[::d]")

// truncate reply content to a single line
headerLen := len(m.Author.Username) + 8
_, _, w, _ := mt.GetRect()
if headerLen+len(content) > w {
content = m.Content[0:max(0, min(w-headerLen, len(m.Content)))] + "..."
}
}

src := []byte(m.Content)
src := []byte(content)
ast := discordmd.ParseWithMessage(src, *discordState.Cabinet, &m, false)
markdown.DefaultRenderer.Render(w, src, ast)

Expand Down Expand Up @@ -236,25 +301,17 @@ func (mt *MessagesText) _select(name string) {
switch name {
case mt.cfg.Keys.SelectPrevious:
// If no message is currently selected, select the latest message.
if len(mt.GetHighlights()) == 0 {
if messageIdx == -1 {
mt.selectedMessageID = ms[0].ID
} else {
if messageIdx < len(ms)-1 {
mt.selectedMessageID = ms[messageIdx+1].ID
} else {
return
}
} else if messageIdx < len(ms)-1 {
mt.selectedMessageID = ms[messageIdx+1].ID
}
case mt.cfg.Keys.SelectNext:
// If no message is currently selected, select the latest message.
if len(mt.GetHighlights()) == 0 {
if messageIdx == -1 {
mt.selectedMessageID = ms[0].ID
} else {
if messageIdx > 0 {
mt.selectedMessageID = ms[messageIdx-1].ID
} else {
return
}
} else if messageIdx > 0 {
mt.selectedMessageID = ms[messageIdx-1].ID
}
case mt.cfg.Keys.SelectFirst:
mt.selectedMessageID = ms[len(ms)-1].ID
Expand All @@ -281,9 +338,6 @@ func (mt *MessagesText) _select(name string) {
}
}
}

mt.Highlight(mt.selectedMessageID.String())
mt.ScrollToHighlight()
}

func (mt *MessagesText) onHighlighted(added, removed, remaining []string) {
Expand Down Expand Up @@ -391,8 +445,6 @@ func (mt *MessagesText) delete() {
return
}

mt.Clear()

for _, m := range slices.Backward(ms) {
layout.messagesText.createMessage(m)
}
Expand Down
2 changes: 0 additions & 2 deletions cmd/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ func (s *State) onMessageCreate(m *gateway.MessageCreateEvent) {
func (s *State) onMessageDelete(m *gateway.MessageDeleteEvent) {
if layout.guildsTree.selectedChannelID == m.ChannelID {
layout.messagesText.selectedMessageID = 0
layout.messagesText.Highlight()
layout.messagesText.Clear()

layout.messagesText.drawMsgs(m.ChannelID)
}
Expand Down