feat(ui/chat): render embeds (#766)
This commit is contained in:
parent
37e99bf50d
commit
9dcd43475b
4 changed files with 424 additions and 34 deletions
|
|
@ -224,6 +224,16 @@ attachment_style = { foreground = "yellow" }
|
|||
message_style = {}
|
||||
selected_message_style = { attributes = "reverse" }
|
||||
|
||||
[theme.messages_list.embeds]
|
||||
provider_style = { attributes = ["dim", "italic"] }
|
||||
author_style = { attributes = "italic" }
|
||||
title_style = { foreground = "blue", attributes = "bold" }
|
||||
description_style = { attributes = "dim" }
|
||||
field_name_style = { attributes = ["bold", "underline"] }
|
||||
field_value_style = {}
|
||||
footer_style = { attributes = ["dim", "italic"] }
|
||||
url_style = { foreground = "blue", underline = "solid" }
|
||||
|
||||
[theme.mentions_list]
|
||||
# Note: width and height are capped to the avaliable space
|
||||
# Minimum width
|
||||
|
|
|
|||
|
|
@ -69,12 +69,18 @@ func (sw *StyleWrapper) UnmarshalTOML(v any) error {
|
|||
case "underline":
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "": sw.Style = sw.Underline(tcell.UnderlineStyleNone)
|
||||
case "solid": sw.Style = sw.Underline(tcell.UnderlineStyleSolid)
|
||||
case "double": sw.Style = sw.Underline(tcell.UnderlineStyleDouble)
|
||||
case "curly": sw.Style = sw.Underline(tcell.UnderlineStyleCurly)
|
||||
case "dotted": sw.Style = sw.Underline(tcell.UnderlineStyleDotted)
|
||||
case "dashed": sw.Style = sw.Underline(tcell.UnderlineStyleDashed)
|
||||
case "":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleNone)
|
||||
case "solid":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleSolid)
|
||||
case "double":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleDouble)
|
||||
case "curly":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleCurly)
|
||||
case "dotted":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleDotted)
|
||||
case "dashed":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleDashed)
|
||||
}
|
||||
}
|
||||
case "underline_color":
|
||||
|
|
@ -229,6 +235,19 @@ type (
|
|||
|
||||
MessageStyle StyleWrapper `toml:"message_style"`
|
||||
SelectedMessageStyle StyleWrapper `toml:"selected_message_style"`
|
||||
|
||||
Embeds MessagesListEmbedsTheme `toml:"embeds"`
|
||||
}
|
||||
|
||||
MessagesListEmbedsTheme struct {
|
||||
ProviderStyle StyleWrapper `toml:"provider_style"`
|
||||
AuthorStyle StyleWrapper `toml:"author_style"`
|
||||
TitleStyle StyleWrapper `toml:"title_style"`
|
||||
DescriptionStyle StyleWrapper `toml:"description_style"`
|
||||
FieldNameStyle StyleWrapper `toml:"field_name_style"`
|
||||
FieldValueStyle StyleWrapper `toml:"field_value_style"`
|
||||
FooterStyle StyleWrapper `toml:"footer_style"`
|
||||
URLStyle StyleWrapper `toml:"url_style"`
|
||||
}
|
||||
|
||||
MentionsListTheme struct {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ func (r *Renderer) RenderLines(source []byte, node ast.Node, base tcell.Style) [
|
|||
|
||||
builder := tview.NewLineBuilder()
|
||||
styleStack := []tcell.Style{base}
|
||||
linkDepth := 0
|
||||
|
||||
currentStyle := func() tcell.Style {
|
||||
return styleStack[len(styleStack)-1]
|
||||
|
|
@ -76,12 +77,19 @@ func (r *Renderer) RenderLines(source []byte, node ast.Node, base tcell.Style) [
|
|||
}
|
||||
case *ast.AutoLink:
|
||||
if entering {
|
||||
builder.Write(string(node.URL(source)), ui.MergeStyle(currentStyle(), theme.URLStyle.Style))
|
||||
url := string(node.URL(source))
|
||||
style := ui.MergeStyle(currentStyle(), theme.URLStyle.Style).Url(url)
|
||||
builder.Write(url, style)
|
||||
}
|
||||
case *ast.Link:
|
||||
if entering {
|
||||
pushStyle(ui.MergeStyle(currentStyle(), theme.URLStyle.Style))
|
||||
url := string(node.Destination)
|
||||
linkDepth++
|
||||
pushStyle(ui.MergeStyle(currentStyle(), theme.URLStyle.Style).Url(url))
|
||||
} else {
|
||||
if linkDepth > 0 {
|
||||
linkDepth--
|
||||
}
|
||||
popStyle()
|
||||
}
|
||||
case *ast.List:
|
||||
|
|
@ -112,7 +120,7 @@ func (r *Renderer) RenderLines(source []byte, node ast.Node, base tcell.Style) [
|
|||
}
|
||||
case *discordmd.Inline:
|
||||
if entering {
|
||||
pushStyle(applyInlineAttr(currentStyle(), node.Attr))
|
||||
pushStyle(applyInlineAttr(currentStyle(), node.Attr, linkDepth > 0))
|
||||
} else {
|
||||
popStyle()
|
||||
}
|
||||
|
|
@ -256,7 +264,7 @@ func mentionText(node *discordmd.Mention) string {
|
|||
}
|
||||
}
|
||||
|
||||
func applyInlineAttr(style tcell.Style, attr discordmd.Attribute) tcell.Style {
|
||||
func applyInlineAttr(style tcell.Style, attr discordmd.Attribute, inLink bool) tcell.Style {
|
||||
switch attr {
|
||||
case discordmd.AttrBold:
|
||||
return style.Bold(true)
|
||||
|
|
@ -267,6 +275,11 @@ func applyInlineAttr(style tcell.Style, attr discordmd.Attribute) tcell.Style {
|
|||
case discordmd.AttrStrikethrough:
|
||||
return style.StrikeThrough(true)
|
||||
case discordmd.AttrMonospace:
|
||||
// Avoid reverse-video inside links. Link labels like `hash` should still
|
||||
// look like links, not highlighted blocks.
|
||||
if inLink {
|
||||
return style
|
||||
}
|
||||
return style.Reverse(true)
|
||||
}
|
||||
return style
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
|
@ -32,6 +33,7 @@ import (
|
|||
"github.com/diamondburned/ningen/v3/discordmd"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/gdamore/tcell/v3/color"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
|
|
@ -408,32 +410,45 @@ func (ml *messagesList) memberForMessage(message discord.Message) *discord.Membe
|
|||
}
|
||||
|
||||
func (ml *messagesList) drawContent(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
||||
c := []byte(message.Content)
|
||||
if ml.chatView.cfg.Markdown.Enabled {
|
||||
root := discordmd.ParseWithMessage(c, *ml.chatView.state.Cabinet, &message, false)
|
||||
lines := ml.renderer.RenderLines(c, root, baseStyle)
|
||||
if builder.HasCurrentLine() {
|
||||
startsWithCodeBlock := false
|
||||
lines, root := ml.renderContentLines(message, baseStyle)
|
||||
if ml.chatView.cfg.Markdown.Enabled && builder.HasCurrentLine() {
|
||||
startsWithCodeBlock := false
|
||||
if root != nil {
|
||||
if first := root.FirstChild(); first != nil {
|
||||
_, startsWithCodeBlock = first.(*ast.FencedCodeBlock)
|
||||
}
|
||||
}
|
||||
|
||||
if startsWithCodeBlock {
|
||||
// Keep code blocks visually separate from "timestamp + author".
|
||||
builder.NewLine()
|
||||
for len(lines) > 0 && len(lines[0]) == 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
} else {
|
||||
for len(lines) > 1 && len(lines[0]) == 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
if startsWithCodeBlock {
|
||||
// Keep code blocks visually separate from "timestamp + author".
|
||||
builder.NewLine()
|
||||
for len(lines) > 0 && len(lines[0]) == 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
} else {
|
||||
for len(lines) > 1 && len(lines[0]) == 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
}
|
||||
builder.AppendLines(lines)
|
||||
} else {
|
||||
builder.Write(message.Content, baseStyle)
|
||||
}
|
||||
builder.AppendLines(lines)
|
||||
}
|
||||
|
||||
func (ml *messagesList) renderContentLines(message discord.Message, baseStyle tcell.Style) ([]tview.Line, ast.Node) {
|
||||
return ml.renderContentLinesWithMarkdown(message, baseStyle, false)
|
||||
}
|
||||
|
||||
func (ml *messagesList) renderContentLinesWithMarkdown(message discord.Message, baseStyle tcell.Style, forceMarkdown bool) ([]tview.Line, ast.Node) {
|
||||
// Keep one rendering path for both normal messages and embed fragments so we preserve mention/link parsing behavior consistently across both.
|
||||
if forceMarkdown || ml.chatView.cfg.Markdown.Enabled {
|
||||
c := []byte(message.Content)
|
||||
root := discordmd.ParseWithMessage(c, *ml.chatView.state.Cabinet, &message, false)
|
||||
return ml.renderer.RenderLines(c, root, baseStyle), root
|
||||
}
|
||||
|
||||
b := tview.NewLineBuilder()
|
||||
b.Write(message.Content, baseStyle)
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (ml *messagesList) drawSnapshotContent(builder *tview.LineBuilder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tcell.Style) {
|
||||
|
|
@ -469,6 +484,8 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d
|
|||
builder.Write(" (edited)", dimStyle)
|
||||
}
|
||||
|
||||
ml.drawEmbeds(builder, message, baseStyle)
|
||||
|
||||
attachmentStyle := ui.MergeStyle(baseStyle, ml.cfg.Theme.MessagesList.AttachmentStyle.Style)
|
||||
for _, a := range message.Attachments {
|
||||
builder.NewLine()
|
||||
|
|
@ -480,6 +497,304 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d
|
|||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
||||
if len(message.Embeds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
contentListURLs := extractURLs(message.Content)
|
||||
contentURLs := make(map[string]struct{}, len(contentListURLs))
|
||||
for _, u := range contentListURLs {
|
||||
contentURLs[u] = struct{}{}
|
||||
}
|
||||
|
||||
lineStyles := embedLineStyles(baseStyle, ml.cfg.Theme.MessagesList.Embeds)
|
||||
defaultBarStyle := baseStyle.Dim(true)
|
||||
prefixText := " ▎ "
|
||||
prefixWidth := tview.TaggedStringWidth(prefixText)
|
||||
_, _, innerWidth, _ := ml.GetInnerRect()
|
||||
// Wrap against the current list viewport. This keeps embed wrapping stable even when sidebars/panes are resized.
|
||||
wrapWidth := innerWidth - prefixWidth
|
||||
if wrapWidth < 1 {
|
||||
wrapWidth = 1
|
||||
}
|
||||
|
||||
for _, embed := range message.Embeds {
|
||||
lines := embedLines(embed, contentURLs)
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
embedContentLines := make([]tview.Line, 0, len(lines)*2)
|
||||
barStyle := defaultBarStyle
|
||||
if embed.Color != discord.NullColor && embed.Color != 0 {
|
||||
barStyle = barStyle.Foreground(tcell.NewHexColor(int32(embed.Color.Uint32())))
|
||||
}
|
||||
prefix := tview.NewSegment(prefixText, barStyle)
|
||||
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, _ := ml.renderContentLinesWithMarkdown(msg, lineStyle, line.Kind == embedLineDescription)
|
||||
for _, renderedLine := range rendered {
|
||||
if line.URL != "" {
|
||||
renderedLine = lineWithURL(renderedLine, line.URL)
|
||||
}
|
||||
// Prefix must be applied after wrapping so every visual line keeps the embed bar marker ("▎"), not only the first logical line.
|
||||
for _, wrapped := range wrapStyledLine(renderedLine, wrapWidth) {
|
||||
prefixed := make(tview.Line, 0, len(wrapped)+1)
|
||||
prefixed = append(prefixed, prefix)
|
||||
prefixed = append(prefixed, wrapped...)
|
||||
embedContentLines = append(embedContentLines, prefixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(embedContentLines) > 0 {
|
||||
builder.AppendLines(embedContentLines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wrapStyledLine(line tview.Line, width int) []tview.Line {
|
||||
if width <= 0 {
|
||||
return []tview.Line{line}
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return []tview.Line{line}
|
||||
}
|
||||
|
||||
lines := make([]tview.Line, 0, 2)
|
||||
current := make(tview.Line, 0, len(line))
|
||||
currentWidth := 0
|
||||
|
||||
pushSegment := func(text string, style tcell.Style) {
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
if n := len(current); n > 0 && current[n-1].Style == style {
|
||||
current[n-1].Text += text
|
||||
return
|
||||
}
|
||||
current = append(current, tview.Segment{Text: text, Style: style})
|
||||
}
|
||||
|
||||
flush := func() {
|
||||
lineCopy := make(tview.Line, len(current))
|
||||
copy(lineCopy, current)
|
||||
lines = append(lines, lineCopy)
|
||||
current = current[:0]
|
||||
currentWidth = 0
|
||||
}
|
||||
|
||||
for _, segment := range line {
|
||||
state := -1
|
||||
rest := segment.Text
|
||||
for len(rest) > 0 {
|
||||
cluster, nextRest, boundaries, nextState := uniseg.StepString(rest, state)
|
||||
state = nextState
|
||||
rest = nextRest
|
||||
if cluster == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use grapheme width (not rune count) so wrapping stays correct with wide glyphs, emoji, and combining characters.
|
||||
clusterWidth := graphemeClusterWidth(boundaries)
|
||||
if currentWidth > 0 && currentWidth+clusterWidth > width {
|
||||
flush()
|
||||
}
|
||||
pushSegment(cluster, segment.Style)
|
||||
currentWidth += clusterWidth
|
||||
|
||||
if currentWidth >= width {
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(current) > 0 {
|
||||
flush()
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return []tview.Line{{}}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func graphemeClusterWidth(boundaries int) int {
|
||||
return boundaries >> uniseg.ShiftWidth
|
||||
}
|
||||
|
||||
func lineWithURL(line tview.Line, rawURL string) tview.Line {
|
||||
out := make(tview.Line, len(line))
|
||||
for i, segment := range line {
|
||||
out[i] = segment
|
||||
out[i].Style = out[i].Style.Url(rawURL)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type embedLine struct {
|
||||
Text string
|
||||
Kind embedLineKind
|
||||
URL string
|
||||
}
|
||||
|
||||
type embedLineKind uint8
|
||||
|
||||
const (
|
||||
// Keep this ordering stable: drawEmbeds indexes precomputed style slots by this enum.
|
||||
embedLineProvider embedLineKind = iota
|
||||
embedLineAuthor
|
||||
embedLineTitle
|
||||
embedLineDescription
|
||||
embedLineFieldName
|
||||
embedLineFieldValue
|
||||
embedLineFooter
|
||||
embedLineURL
|
||||
)
|
||||
|
||||
func embedLineStyles(baseStyle tcell.Style, theme config.MessagesListEmbedsTheme) [8]tcell.Style {
|
||||
styles := [8]tcell.Style{}
|
||||
styles[embedLineProvider] = ui.MergeStyle(baseStyle, theme.ProviderStyle.Style)
|
||||
styles[embedLineAuthor] = ui.MergeStyle(baseStyle, theme.AuthorStyle.Style)
|
||||
styles[embedLineTitle] = ui.MergeStyle(baseStyle, theme.TitleStyle.Style)
|
||||
styles[embedLineDescription] = ui.MergeStyle(baseStyle, theme.DescriptionStyle.Style)
|
||||
styles[embedLineFieldName] = ui.MergeStyle(baseStyle, theme.FieldNameStyle.Style)
|
||||
styles[embedLineFieldValue] = ui.MergeStyle(baseStyle, theme.FieldValueStyle.Style)
|
||||
styles[embedLineFooter] = ui.MergeStyle(baseStyle, theme.FooterStyle.Style)
|
||||
styles[embedLineURL] = ui.MergeStyle(baseStyle, theme.URLStyle.Style)
|
||||
return styles
|
||||
}
|
||||
|
||||
type embedLineDedupKey struct {
|
||||
kind embedLineKind
|
||||
text string
|
||||
}
|
||||
|
||||
func embedLines(embed discord.Embed, contentURLs map[string]struct{}) []embedLine {
|
||||
lines := make([]embedLine, 0, 8)
|
||||
seen := make(map[embedLineDedupKey]struct{}, 8)
|
||||
|
||||
appendUnique := func(s string, kind embedLineKind, rawURL string) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
// Deduplicate by kind+text so the same value can intentionally appear in multiple semantic slots with different styles (e.g. title vs. field).
|
||||
key := embedLineDedupKey{kind: kind, text: s}
|
||||
if _, ok := seen[key]; ok {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
lines = append(lines, embedLine{
|
||||
Text: s,
|
||||
Kind: kind,
|
||||
URL: rawURL,
|
||||
})
|
||||
}
|
||||
|
||||
appendURL := func(url discord.URL) {
|
||||
u := strings.TrimSpace(string(url))
|
||||
if u == "" {
|
||||
return
|
||||
}
|
||||
// Avoid duplicating links that already appear in message body content.
|
||||
if _, ok := contentURLs[u]; ok {
|
||||
return
|
||||
}
|
||||
appendUnique(linkDisplayText(u), embedLineURL, u)
|
||||
}
|
||||
|
||||
if embed.Provider != nil {
|
||||
appendUnique(embed.Provider.Name, embedLineProvider, "")
|
||||
}
|
||||
if embed.Author != nil {
|
||||
appendUnique(embed.Author.Name, embedLineAuthor, "")
|
||||
}
|
||||
appendUnique(embed.Title, embedLineTitle, string(embed.URL))
|
||||
// Some Discord embeds include markdown-escaped punctuation in raw payload text (e.g. "\."), so normalize for display.
|
||||
appendUnique(unescapeMarkdownEscapes(embed.Description), embedLineDescription, "")
|
||||
|
||||
for _, field := range embed.Fields {
|
||||
switch {
|
||||
case field.Name != "" && field.Value != "":
|
||||
appendUnique(field.Name, embedLineFieldName, "")
|
||||
appendUnique(field.Value, embedLineFieldValue, "")
|
||||
case field.Name != "":
|
||||
appendUnique(field.Name, embedLineFieldName, "")
|
||||
default:
|
||||
appendUnique(field.Value, embedLineFieldValue, "")
|
||||
}
|
||||
}
|
||||
|
||||
if embed.Footer != nil {
|
||||
appendUnique(embed.Footer.Text, embedLineFooter, "")
|
||||
}
|
||||
|
||||
// Prefer media URLs after textual fields so previews read top-to-bottom before jumping to link targets.
|
||||
// When a title exists, embed.URL is represented by title Style.Url metadata instead of a separate URL row.
|
||||
if embed.Title == "" {
|
||||
appendURL(embed.URL)
|
||||
}
|
||||
if embed.Image != nil {
|
||||
appendURL(embed.Image.URL)
|
||||
}
|
||||
if embed.Video != nil {
|
||||
appendURL(embed.Video.URL)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func linkDisplayText(raw string) string {
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Host == "" {
|
||||
return raw
|
||||
}
|
||||
|
||||
path := strings.TrimSpace(parsed.EscapedPath())
|
||||
switch {
|
||||
case path == "", path == "/":
|
||||
return parsed.Host
|
||||
case len(path) > 48:
|
||||
return parsed.Host + path[:45] + "..."
|
||||
default:
|
||||
return parsed.Host + path
|
||||
}
|
||||
}
|
||||
|
||||
func unescapeMarkdownEscapes(s string) string {
|
||||
if !strings.ContainsRune(s, '\\') {
|
||||
return s
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' && i+1 < len(s) && isMarkdownEscapable(s[i+1]) {
|
||||
continue
|
||||
}
|
||||
b.WriteByte(s[i])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isMarkdownEscapable(c byte) bool {
|
||||
switch c {
|
||||
case '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!', '|', '>', '~':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) drawForwardedMessage(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
||||
dimStyle := baseStyle.Dim(true)
|
||||
ml.drawTimestamps(builder, message.Timestamp, baseStyle)
|
||||
|
|
@ -740,10 +1055,7 @@ func (ml *messagesList) open() {
|
|||
return
|
||||
}
|
||||
|
||||
var urls []string
|
||||
if msg.Content != "" {
|
||||
urls = extractURLs(msg.Content)
|
||||
}
|
||||
urls := messageURLs(*msg)
|
||||
|
||||
if len(urls) == 0 && len(msg.Attachments) == 0 {
|
||||
return
|
||||
|
|
@ -788,6 +1100,42 @@ func extractURLs(content string) []string {
|
|||
return urls
|
||||
}
|
||||
|
||||
func extractEmbedURLs(embeds []discord.Embed) []string {
|
||||
urls := make([]string, 0, len(embeds)*3)
|
||||
for _, embed := range embeds {
|
||||
if embed.URL != "" {
|
||||
urls = append(urls, string(embed.URL))
|
||||
}
|
||||
if embed.Image != nil && embed.Image.URL != "" {
|
||||
urls = append(urls, string(embed.Image.URL))
|
||||
}
|
||||
if embed.Video != nil && embed.Video.URL != "" {
|
||||
urls = append(urls, string(embed.Video.URL))
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func messageURLs(msg discord.Message) []string {
|
||||
combined := extractURLs(msg.Content)
|
||||
combined = append(combined, extractEmbedURLs(msg.Embeds)...)
|
||||
|
||||
urls := make([]string, 0, len(combined))
|
||||
seen := make(map[string]struct{}, len(combined))
|
||||
for _, u := range combined {
|
||||
u = strings.TrimSpace(u)
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[u]; ok {
|
||||
continue
|
||||
}
|
||||
seen[u] = struct{}{}
|
||||
urls = append(urls, u)
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord.Attachment) {
|
||||
var items []attachmentItem
|
||||
for _, a := range attachments {
|
||||
|
|
@ -1047,7 +1395,7 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|||
canOpen := false
|
||||
if msg, err := ml.selectedMessage(); err == nil {
|
||||
canSelectReply = msg.ReferencedMessage != nil
|
||||
canOpen = len(extractURLs(msg.Content)) != 0 || len(msg.Attachments) != 0
|
||||
canOpen = len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0
|
||||
|
||||
me, _ := ml.chatView.state.Cabinet.Me()
|
||||
canReply = msg.Author.ID != me.ID
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue