refactor(messageslist): draw embeds and the date separator as boxes
This commit is contained in:
parent
68ee346501
commit
b46624796d
5 changed files with 105 additions and 97 deletions
|
|
@ -8,22 +8,28 @@ import (
|
||||||
"github.com/ayn2op/discordo/internal/config"
|
"github.com/ayn2op/discordo/internal/config"
|
||||||
"github.com/ayn2op/discordo/internal/markdown"
|
"github.com/ayn2op/discordo/internal/markdown"
|
||||||
"github.com/ayn2op/tview"
|
"github.com/ayn2op/tview"
|
||||||
|
"github.com/ayn2op/tview/box"
|
||||||
|
"github.com/ayn2op/tview/layout"
|
||||||
"github.com/ayn2op/tview/text"
|
"github.com/ayn2op/tview/text"
|
||||||
"github.com/gdamore/tcell/v3"
|
"github.com/gdamore/tcell/v3"
|
||||||
"github.com/rockorager/go-uucode"
|
|
||||||
"github.com/yuin/goldmark/v2/ast"
|
"github.com/yuin/goldmark/v2/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// embedPrefix is drawn before each line of an embed, in the embed's color.
|
const (
|
||||||
const embedPrefix = " ▎ "
|
// embedBar is drawn down the left of an embed, in the embed's color.
|
||||||
|
embedBar = "▎"
|
||||||
// embedPrefixWidth is how many cells embedPrefix takes.
|
// embedIndent is the cells before the bar, and embedBarGap those between the bar and the text.
|
||||||
var embedPrefixWidth = uucode.StringWidth(embedPrefix)
|
embedIndent, embedBarGap = 2, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// embedViews returns each embed of message as its text with a bar down its left, or nil if the message does not show its embeds: one without any, by a blocked user, or that is not drawn with its own content, as a forward.
|
||||||
|
// The views capture the model and the message, so they are made once with the text of the message rather than in buildItem.
|
||||||
// root is the parsed content of message, or nil to parse it here.
|
// root is the parsed content of message, or nil to parse it here.
|
||||||
func (m Model) drawEmbeds(builder *markdown.Builder, message discord.Message, baseStyle tview.Style, width int, root ast.Node) {
|
func (m Model) embedViews(message discord.Message, root ast.Node) []tview.Widget {
|
||||||
if len(message.Embeds) == 0 {
|
forward := message.Reference != nil && message.Reference.Type == discord.MessageReferenceTypeForward
|
||||||
return
|
ownContent := message.Type == discord.DefaultMessage && !forward || message.Type == discord.InlinedReplyMessage
|
||||||
|
if len(message.Embeds) == 0 || m.blocked(message.Author) || !ownContent {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embed URLs are deduplicated against links already shown in the message body.
|
// Embed URLs are deduplicated against links already shown in the message body.
|
||||||
|
|
@ -33,47 +39,52 @@ func (m Model) drawEmbeds(builder *markdown.Builder, message discord.Message, ba
|
||||||
seenURLs[u] = struct{}{}
|
seenURLs[u] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseStyle := m.cfg.Theme.MessagesList.MessageStyle.Style
|
||||||
lineStyles := embedLineStyles(baseStyle, m.cfg.Theme.MessagesList.Embeds)
|
lineStyles := embedLineStyles(baseStyle, m.cfg.Theme.MessagesList.Embeds)
|
||||||
defaultPrefixStyle := baseStyle.Dim(true)
|
var views []tview.Widget
|
||||||
// Wrap against the list's width, so the message is rendered again when it changes.
|
|
||||||
wrapWidth := max(width-embedPrefixWidth, 1)
|
|
||||||
|
|
||||||
for _, embed := range message.Embeds {
|
for _, embed := range message.Embeds {
|
||||||
lines := embedLines(embed, seenURLs)
|
lines := embedLines(embed, seenURLs)
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixStyle := defaultPrefixStyle
|
barStyle := baseStyle.Dim(true)
|
||||||
if embed.Color != discord.NullColor && embed.Color != 0 {
|
if embed.Color != discord.NullColor && embed.Color != 0 {
|
||||||
prefixStyle = prefixStyle.Foreground(tcell.NewHexColor(int32(embed.Color)))
|
barStyle = barStyle.Foreground(tcell.NewHexColor(int32(embed.Color)))
|
||||||
}
|
}
|
||||||
prefix := text.Span{Text: embedPrefix, Style: prefixStyle}
|
|
||||||
builder.NewLine()
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.TrimSpace(line.text) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msg := message
|
|
||||||
msg.Content = line.text
|
|
||||||
lineStyle := lineStyles[line.kind]
|
|
||||||
// Embed descriptions are always markdown-rendered to match Discord's rich embed semantics, even when message markdown is globally disabled.
|
|
||||||
rendered := m.renderContent(msg, nil, lineStyle, line.kind == embedLineDescription)
|
|
||||||
if line.url != "" {
|
|
||||||
rendered = spansWithURL(rendered, line.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The prefix is added after wrapping, so every visual line has it, not only the first.
|
content := &messageView{render: func(int) []text.Span { return m.renderEmbed(message, lines, lineStyles) }}
|
||||||
for _, wrapped := range text.Lines(rendered, wrapWidth, text.WrappingGlyph) {
|
bar := box.New(content).
|
||||||
if !builder.LineEmpty() {
|
Borders(tview.BordersLeft).
|
||||||
builder.NewLine()
|
BorderSet(tview.BorderSet{Left: embedBar}).
|
||||||
}
|
BorderStyle(barStyle).
|
||||||
|
Padding(0, 0, embedBarGap, 0).
|
||||||
builder.WriteSpans(prefix)
|
Height(layout.Shrink)
|
||||||
builder.WriteSpans(wrapped...)
|
views = append(views, box.New(bar).Padding(0, 0, embedIndent, 0).Height(layout.Shrink))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return views
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderEmbed returns lines of an embed of message, each on its own line in the style of its kind.
|
||||||
|
func (m Model) renderEmbed(message discord.Message, lines []embedLine, lineStyles [embedLineKinds]tview.Style) []text.Span {
|
||||||
|
builder := new(markdown.Builder)
|
||||||
|
for _, line := range lines {
|
||||||
|
if !builder.LineEmpty() {
|
||||||
|
builder.NewLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := message
|
||||||
|
msg.Content = line.text
|
||||||
|
// Embed descriptions are always markdown-rendered to match Discord's rich embed semantics, even when message markdown is globally disabled.
|
||||||
|
rendered := m.renderContent(msg, nil, lineStyles[line.kind], line.kind == embedLineDescription)
|
||||||
|
if line.url != "" {
|
||||||
|
rendered = spansWithURL(rendered, line.url)
|
||||||
|
}
|
||||||
|
builder.WriteSpans(rendered...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
func spansWithURL(spans []text.Span, rawURL string) []text.Span {
|
func spansWithURL(spans []text.Span, rawURL string) []text.Span {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/ayn2op/arikawa/v3/discord"
|
"github.com/ayn2op/arikawa/v3/discord"
|
||||||
md "github.com/ayn2op/arikawa/v3/markdown"
|
md "github.com/ayn2op/arikawa/v3/markdown"
|
||||||
|
|
@ -25,7 +24,7 @@ import (
|
||||||
// root is the parsed content of message, or nil to parse it when it is rendered.
|
// root is the parsed content of message, or nil to parse it when it is rendered.
|
||||||
func (m Model) messageViews(message discord.Message, root ast.Node, grouped bool) (content, reply *messageView) {
|
func (m Model) messageViews(message discord.Message, root ast.Node, grouped bool) (content, reply *messageView) {
|
||||||
style := m.cfg.Theme.MessagesList.MessageStyle.Style
|
style := m.cfg.Theme.MessagesList.MessageStyle.Style
|
||||||
content = &messageView{render: func(width int) []text.Span { return m.renderMessage(message, root, style, width, grouped) }}
|
content = &messageView{render: func(int) []text.Span { return m.renderMessage(message, root, style, grouped) }}
|
||||||
reply = &messageView{render: func(int) []text.Span {
|
reply = &messageView{render: func(int) []text.Span {
|
||||||
builder := new(markdown.Builder)
|
builder := new(markdown.Builder)
|
||||||
m.drawReply(builder, message, style)
|
m.drawReply(builder, message, style)
|
||||||
|
|
@ -70,35 +69,9 @@ func (v *messageView) Draw(screen tview.Screen, area tview.Rectangle) {
|
||||||
|
|
||||||
func (v *messageView) Handle(msg tview.Msg, _ tview.Rectangle) tview.Msg { return msg }
|
func (v *messageView) Handle(msg tview.Msg, _ tview.Rectangle) tview.Msg { return msg }
|
||||||
|
|
||||||
// dateSeparator is a line with the date in the middle, filled to the width it is drawn at.
|
func (m Model) renderMessage(message discord.Message, root ast.Node, baseStyle tview.Style, grouped bool) []text.Span {
|
||||||
type dateSeparator struct {
|
|
||||||
date, fill string
|
|
||||||
style tview.Style
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns Fill and a height of one row.
|
|
||||||
func (dateSeparator) Size() (width, height layout.Length) { return layout.Fill, layout.Fixed(1) }
|
|
||||||
|
|
||||||
// Layout returns a height of one row.
|
|
||||||
func (dateSeparator) Layout(limits layout.Limits) layout.Size {
|
|
||||||
return layout.Atomic(limits, layout.Fill, layout.Fixed(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d dateSeparator) Draw(screen tview.Screen, area tview.Rectangle) {
|
|
||||||
line := d.date
|
|
||||||
label := " " + d.date + " "
|
|
||||||
if labelWidth := utf8.RuneCountInString(label); area.Width > labelWidth {
|
|
||||||
fill := area.Width - labelWidth
|
|
||||||
line = strings.Repeat(d.fill, fill/2) + label + strings.Repeat(d.fill, fill-fill/2)
|
|
||||||
}
|
|
||||||
tview.Print(screen, line, area.X, area.Y, area.Width, tview.HorizontalAlignmentLeft, d.style)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dateSeparator) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg }
|
|
||||||
|
|
||||||
func (m Model) renderMessage(message discord.Message, root ast.Node, baseStyle tview.Style, width int, grouped bool) []text.Span {
|
|
||||||
builder := new(markdown.Builder)
|
builder := new(markdown.Builder)
|
||||||
m.writeMessage(builder, message, root, baseStyle, width, grouped)
|
m.writeMessage(builder, message, root, baseStyle, grouped)
|
||||||
// Without a header or content, the message starts with the line break that attachments and embeds start below.
|
// Without a header or content, the message starts with the line break that attachments and embeds start below.
|
||||||
return trimLeadingLineBreaks(builder.Finish())
|
return trimLeadingLineBreaks(builder.Finish())
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +87,7 @@ func trimLeadingLineBreaks(spans []text.Span) []text.Span {
|
||||||
return spans
|
return spans
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) writeMessage(builder *markdown.Builder, message discord.Message, root ast.Node, baseStyle tview.Style, width int, grouped bool) {
|
func (m Model) writeMessage(builder *markdown.Builder, message discord.Message, root ast.Node, baseStyle tview.Style, grouped bool) {
|
||||||
if m.blocked(message.Author) {
|
if m.blocked(message.Author) {
|
||||||
builder.Write("Blocked message", baseStyle.Foreground(color.Red).Bold(true))
|
builder.Write("Blocked message", baseStyle.Foreground(color.Red).Bold(true))
|
||||||
return
|
return
|
||||||
|
|
@ -125,12 +98,12 @@ func (m Model) writeMessage(builder *markdown.Builder, message discord.Message,
|
||||||
if message.Reference != nil && message.Reference.Type == discord.MessageReferenceTypeForward {
|
if message.Reference != nil && message.Reference.Type == discord.MessageReferenceTypeForward {
|
||||||
m.drawForwardedMessage(builder, message, baseStyle, grouped)
|
m.drawForwardedMessage(builder, message, baseStyle, grouped)
|
||||||
} else {
|
} else {
|
||||||
m.drawDefaultMessage(builder, message, root, baseStyle, width, grouped)
|
m.drawDefaultMessage(builder, message, root, baseStyle, grouped)
|
||||||
}
|
}
|
||||||
case discord.GuildMemberJoinMessage:
|
case discord.GuildMemberJoinMessage:
|
||||||
m.drawLayout(builder, message, baseStyle, false, func() { builder.Write("joined the server.", baseStyle) })
|
m.drawLayout(builder, message, baseStyle, false, func() { builder.Write("joined the server.", baseStyle) })
|
||||||
case discord.InlinedReplyMessage:
|
case discord.InlinedReplyMessage:
|
||||||
m.drawDefaultMessage(builder, message, root, baseStyle, width, false)
|
m.drawDefaultMessage(builder, message, root, baseStyle, false)
|
||||||
case discord.ChannelPinnedMessage:
|
case discord.ChannelPinnedMessage:
|
||||||
drawPinnedMessage(builder, message, baseStyle)
|
drawPinnedMessage(builder, message, baseStyle)
|
||||||
default:
|
default:
|
||||||
|
|
@ -338,14 +311,13 @@ func (m Model) drawSnapshotContent(builder *markdown.Builder, parent discord.Mes
|
||||||
m.drawContent(builder, message, nil, baseStyle)
|
m.drawContent(builder, message, nil, baseStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) drawDefaultMessage(builder *markdown.Builder, message discord.Message, root ast.Node, baseStyle tview.Style, width int, grouped bool) {
|
func (m Model) drawDefaultMessage(builder *markdown.Builder, message discord.Message, root ast.Node, baseStyle tview.Style, grouped bool) {
|
||||||
content := func() { m.drawContent(builder, message, root, baseStyle) }
|
content := func() { m.drawContent(builder, message, root, baseStyle) }
|
||||||
if message.Content == "" {
|
if message.Content == "" {
|
||||||
content = nil
|
content = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.drawLayout(builder, message, baseStyle, grouped, content)
|
m.drawLayout(builder, message, baseStyle, grouped, content)
|
||||||
m.drawEmbeds(builder, message, baseStyle, width, root)
|
|
||||||
|
|
||||||
attachmentStyle := tview.MergeStyle(baseStyle, m.cfg.Theme.MessagesList.AttachmentStyle.Style)
|
attachmentStyle := tview.MergeStyle(baseStyle, m.cfg.Theme.MessagesList.AttachmentStyle.Style)
|
||||||
// A voice message is shown as its player.
|
// A voice message is shown as its player.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/ayn2op/discordo/internal/voice"
|
"github.com/ayn2op/discordo/internal/voice"
|
||||||
"github.com/ayn2op/ningen/v3"
|
"github.com/ayn2op/ningen/v3"
|
||||||
"github.com/ayn2op/tview"
|
"github.com/ayn2op/tview"
|
||||||
|
"github.com/ayn2op/tview/box"
|
||||||
"github.com/ayn2op/tview/column"
|
"github.com/ayn2op/tview/column"
|
||||||
tviewimage "github.com/ayn2op/tview/image"
|
tviewimage "github.com/ayn2op/tview/image"
|
||||||
"github.com/ayn2op/tview/keybind"
|
"github.com/ayn2op/tview/keybind"
|
||||||
|
|
@ -19,6 +20,7 @@ import (
|
||||||
"github.com/ayn2op/tview/row"
|
"github.com/ayn2op/tview/row"
|
||||||
"github.com/ayn2op/tview/scrollbar"
|
"github.com/ayn2op/tview/scrollbar"
|
||||||
"github.com/ayn2op/tview/spacer"
|
"github.com/ayn2op/tview/spacer"
|
||||||
|
"github.com/ayn2op/tview/text"
|
||||||
"github.com/ayn2op/tview/viewport"
|
"github.com/ayn2op/tview/viewport"
|
||||||
"github.com/yuin/goldmark/v2/ast"
|
"github.com/yuin/goldmark/v2/ast"
|
||||||
)
|
)
|
||||||
|
|
@ -27,8 +29,10 @@ type messageItem struct {
|
||||||
message discord.Message
|
message discord.Message
|
||||||
// text and reply are the message and what it replies to as wrapped, or nil until they are shown.
|
// text and reply are the message and what it replies to as wrapped, or nil until they are shown.
|
||||||
text, reply *messageView
|
text, reply *messageView
|
||||||
// A separator shows the date of the message after it.
|
// embeds are the embeds of the message, made with text.
|
||||||
separator bool
|
embeds []tview.Widget
|
||||||
|
// separator is the line with the date of the message after it, or nil for a message.
|
||||||
|
separator tview.Widget
|
||||||
// grouped reports whether the message is drawn without a header, under that of the message before it.
|
// grouped reports whether the message is drawn without a header, under that of the message before it.
|
||||||
grouped bool
|
grouped bool
|
||||||
|
|
||||||
|
|
@ -181,7 +185,7 @@ func (m Model) indexOf(channelID discord.ChannelID, id discord.MessageID) int {
|
||||||
if m.channel == nil || m.channel.ID != channelID {
|
if m.channel == nil || m.channel.ID != channelID {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return slices.IndexFunc(m.items, func(item messageItem) bool { return !item.separator && item.message.ID == id })
|
return slices.IndexFunc(m.items, func(item messageItem) bool { return item.separator == nil && item.message.ID == id })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) reset() {
|
func (m *Model) reset() {
|
||||||
|
|
@ -209,7 +213,7 @@ func (m *Model) AddMessage(message discord.Message) tview.Cmd {
|
||||||
|
|
||||||
separator := m.shouldSeparate(previous, message)
|
separator := m.shouldSeparate(previous, message)
|
||||||
if separator {
|
if separator {
|
||||||
m.items = append(m.items, messageItem{separator: true})
|
m.items = append(m.items, messageItem{separator: m.separatorView(message)})
|
||||||
}
|
}
|
||||||
|
|
||||||
grouped := !separator && m.shouldGroup(previous, message)
|
grouped := !separator && m.shouldGroup(previous, message)
|
||||||
|
|
@ -218,7 +222,7 @@ func (m *Model) AddMessage(message discord.Message) tview.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) setMessage(index int, message discord.Message) {
|
func (m *Model) setMessage(index int, message discord.Message) {
|
||||||
if index < 0 || index >= len(m.items) || m.items[index].separator {
|
if index < 0 || index >= len(m.items) || m.items[index].separator != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +231,7 @@ func (m *Model) setMessage(index int, message discord.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) deleteMessage(index int) {
|
func (m *Model) deleteMessage(index int) {
|
||||||
if index < 0 || index >= len(m.items) || m.items[index].separator {
|
if index < 0 || index >= len(m.items) || m.items[index].separator != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,17 +259,22 @@ func (m Model) buildItem(index int) tview.Widget {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
item := &m.items[index]
|
item := &m.items[index]
|
||||||
if item.separator {
|
if item.separator != nil {
|
||||||
date := m.items[index+1].message.Timestamp.Time().In(time.Local).Format(m.cfg.DateSeparator.Format)
|
return item.separator
|
||||||
return dateSeparator{date: date, fill: m.cfg.DateSeparator.Character, style: m.cfg.Theme.MessagesList.MessageStyle.Dim(true)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message := &item.message
|
message := &item.message
|
||||||
if item.text == nil {
|
if item.text == nil {
|
||||||
item.text, item.reply = m.messageViews(*message, m.contentRoot(item), item.grouped)
|
root := m.contentRoot(item)
|
||||||
|
item.text, item.reply = m.messageViews(*message, root, item.grouped)
|
||||||
|
item.embeds = m.embedViews(*message, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachments are named in the text, before the embeds.
|
||||||
body := column.New(item.text).Height(layout.Shrink)
|
body := column.New(item.text).Height(layout.Shrink)
|
||||||
|
for _, embed := range item.embeds {
|
||||||
|
body = body.Push(embed)
|
||||||
|
}
|
||||||
if isVoiceMessage(*message) {
|
if isVoiceMessage(*message) {
|
||||||
body = body.Push(m.voiceMessageView(message.Attachments[0]))
|
body = body.Push(m.voiceMessageView(message.Attachments[0]))
|
||||||
}
|
}
|
||||||
|
|
@ -310,19 +319,35 @@ func (m Model) shouldGroup(previous *discord.Message, message discord.Message) b
|
||||||
message.Timestamp.Time().Sub(previous.Timestamp.Time()) <= cfg.Interval
|
message.Timestamp.Time().Sub(previous.Timestamp.Time()) <= cfg.Interval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// separatorDatePadding is on either side of the date on its separator line.
|
||||||
|
const separatorDatePadding = " "
|
||||||
|
|
||||||
|
// separatorView returns the line with the date of message in the middle of it. It is made once, when the separator is added, as the list builds every item each time it is laid out.
|
||||||
|
func (m Model) separatorView(message discord.Message) tview.Widget {
|
||||||
|
date := message.Timestamp.Time().In(time.Local).Format(m.cfg.DateSeparator.Format)
|
||||||
|
style := m.cfg.Theme.MessagesList.MessageStyle.Dim(true)
|
||||||
|
return box.New(spacer.New()).
|
||||||
|
Borders(tview.BordersTop).
|
||||||
|
BorderSet(tview.BorderSet{Top: m.cfg.DateSeparator.Character}).
|
||||||
|
BorderStyle(style).
|
||||||
|
// The separator is kept, so its date is laid out once.
|
||||||
|
Title(text.New(separatorDatePadding + date + separatorDatePadding).Style(style).Wrapping(text.WrappingNone).Paragraph(new(text.Paragraph))).
|
||||||
|
Height(layout.Fixed(1))
|
||||||
|
}
|
||||||
|
|
||||||
// rebuildItems replaces date separators and groups while retaining message views and selection.
|
// rebuildItems replaces date separators and groups while retaining message views and selection.
|
||||||
func (m *Model) rebuildItems() {
|
func (m *Model) rebuildItems() {
|
||||||
items := make([]messageItem, 0, len(m.items))
|
items := make([]messageItem, 0, len(m.items))
|
||||||
cursor, target := -1, -1
|
cursor, target := -1, -1
|
||||||
var previous *discord.Message
|
var previous *discord.Message
|
||||||
for i, item := range m.items {
|
for i, item := range m.items {
|
||||||
if item.separator {
|
if item.separator != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
separator := m.shouldSeparate(previous, item.message)
|
separator := m.shouldSeparate(previous, item.message)
|
||||||
if separator {
|
if separator {
|
||||||
items = append(items, messageItem{separator: true})
|
items = append(items, messageItem{separator: m.separatorView(item.message)})
|
||||||
}
|
}
|
||||||
// A message that joins or leaves a group is drawn again, with or without its header.
|
// A message that joins or leaves a group is drawn again, with or without its header.
|
||||||
if grouped := !separator && m.shouldGroup(previous, item.message); grouped != item.grouped {
|
if grouped := !separator && m.shouldGroup(previous, item.message); grouped != item.grouped {
|
||||||
|
|
@ -356,7 +381,7 @@ func sameLocalDate(a discord.Timestamp, b discord.Timestamp) bool {
|
||||||
// messageIndex finds the next message in direction (-1 or 1), excluding start.
|
// messageIndex finds the next message in direction (-1 or 1), excluding start.
|
||||||
func (m Model) messageIndex(start, direction int) int {
|
func (m Model) messageIndex(start, direction int) int {
|
||||||
for i := start + direction; i >= 0 && i < len(m.items); i += direction {
|
for i := start + direction; i >= 0 && i < len(m.items); i += direction {
|
||||||
if !m.items[i].separator {
|
if m.items[i].separator == nil {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -364,7 +389,7 @@ func (m Model) messageIndex(start, direction int) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) onRowCursorChanged(index int) {
|
func (m *Model) onRowCursorChanged(index int) {
|
||||||
if index < 0 || index >= len(m.items) || !m.items[index].separator {
|
if index < 0 || index >= len(m.items) || m.items[index].separator == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
target := m.messageIndex(index, -1)
|
target := m.messageIndex(index, -1)
|
||||||
|
|
@ -376,7 +401,7 @@ func (m *Model) onRowCursorChanged(index int) {
|
||||||
|
|
||||||
func (m Model) selectedMessage() (*discord.Message, bool) {
|
func (m Model) selectedMessage() (*discord.Message, bool) {
|
||||||
cursor := m.cursor
|
cursor := m.cursor
|
||||||
if cursor < 0 || cursor >= len(m.items) || m.items[cursor].separator {
|
if cursor < 0 || cursor >= len(m.items) || m.items[cursor].separator != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,7 +504,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
index := slices.IndexFunc(m.items, func(item messageItem) bool { return !item.separator && item.message.ID == msg.messageID })
|
index := slices.IndexFunc(m.items, func(item messageItem) bool { return item.separator == nil && item.message.ID == msg.messageID })
|
||||||
if index < 0 || m.items[index].previews == nil {
|
if index < 0 || m.items[index].previews == nil {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
@ -584,7 +609,7 @@ func (m *Model) selectReply() {
|
||||||
|
|
||||||
if ref := messages[cursor].message.ReferencedMessage; ref != nil {
|
if ref := messages[cursor].message.ReferencedMessage; ref != nil {
|
||||||
refIdx := slices.IndexFunc(messages, func(m messageItem) bool {
|
refIdx := slices.IndexFunc(messages, func(m messageItem) bool {
|
||||||
return !m.separator && m.message.ID == ref.ID
|
return m.separator == nil && m.message.ID == ref.ID
|
||||||
})
|
})
|
||||||
if refIdx != -1 {
|
if refIdx != -1 {
|
||||||
m.setCursor(refIdx)
|
m.setCursor(refIdx)
|
||||||
|
|
@ -600,7 +625,7 @@ func (m *Model) SelectLastOwn() (discord.Message, bool) {
|
||||||
}
|
}
|
||||||
for i, item := range slices.Backward(m.items) {
|
for i, item := range slices.Backward(m.items) {
|
||||||
message := item.message
|
message := item.message
|
||||||
if item.separator || message.ChannelID != channel.ID || !ui.IsMe(m.state, message.Author.ID) ||
|
if item.separator != nil || message.ChannelID != channel.ID || !ui.IsMe(m.state, message.Author.ID) ||
|
||||||
!message.ID.IsValid() || message.Content == "" || len(message.MessageSnapshots) > 0 ||
|
!message.ID.IsValid() || message.Content == "" || len(message.MessageSnapshots) > 0 ||
|
||||||
(message.Type != discord.DefaultMessage && message.Type != discord.InlinedReplyMessage) {
|
(message.Type != discord.DefaultMessage && message.Type != discord.InlinedReplyMessage) {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ func TestModelRebuildItems(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var got []discord.MessageID
|
var got []discord.MessageID
|
||||||
for i, item := range ml.items {
|
for i, item := range ml.items {
|
||||||
if item.separator {
|
if item.separator != nil {
|
||||||
if !tt.separators || i+1 == len(ml.items) || ml.items[i+1].separator {
|
if !tt.separators || i+1 == len(ml.items) || ml.items[i+1].separator != nil {
|
||||||
t.Fatalf("item %d: orphaned separator", i)
|
t.Fatalf("item %d: orphaned separator", i)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -223,7 +223,7 @@ func TestModelDrawLayout(t *testing.T) {
|
||||||
cfg.UI.MessagesList.Compact = compact
|
cfg.UI.MessagesList.Compact = compact
|
||||||
|
|
||||||
var got strings.Builder
|
var got strings.Builder
|
||||||
for _, span := range ml.renderMessage(message, nil, tview.Style{}, 80, false) {
|
for _, span := range ml.renderMessage(message, nil, tview.Style{}, false) {
|
||||||
got.WriteString(span.Text)
|
got.WriteString(span.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +272,7 @@ func TestModelShouldGroup(t *testing.T) {
|
||||||
if !ml.items[newest].grouped || ml.items[ml.messageIndex(-1, 1)].grouped {
|
if !ml.items[newest].grouped || ml.items[ml.messageIndex(-1, 1)].grouped {
|
||||||
t.Fatal("only the first message has a header")
|
t.Fatal("only the first message has a header")
|
||||||
}
|
}
|
||||||
if got := ml.renderMessage(ml.items[newest].message, nil, tview.Style{}, 80, true); len(got) != 1 || got[0].Text != "hi" {
|
if got := ml.renderMessage(ml.items[newest].message, nil, tview.Style{}, true); len(got) != 1 || got[0].Text != "hi" {
|
||||||
t.Fatalf("grouped message = %v, want only its content", got)
|
t.Fatalf("grouped message = %v, want only its content", got)
|
||||||
}
|
}
|
||||||
second := ml.messageIndex(newest, -1)
|
second := ml.messageIndex(newest, -1)
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ func (m Model) loadPreviews() tview.Cmd {
|
||||||
var cmds []tview.Cmd
|
var cmds []tview.Cmd
|
||||||
for i := range m.items {
|
for i := range m.items {
|
||||||
item := &m.items[i]
|
item := &m.items[i]
|
||||||
if item.separator {
|
if item.separator != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue