|
@@ -3,11 +3,13 @@ package chat
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
"errors"
|
|
"errors"
|
|
|
|
|
+ "fmt"
|
|
|
"io"
|
|
"io"
|
|
|
"log/slog"
|
|
"log/slog"
|
|
|
"net/http"
|
|
"net/http"
|
|
|
"net/url"
|
|
"net/url"
|
|
|
"os"
|
|
"os"
|
|
|
|
|
+ "os/exec"
|
|
|
"path/filepath"
|
|
"path/filepath"
|
|
|
"slices"
|
|
"slices"
|
|
|
"strings"
|
|
"strings"
|
|
@@ -497,7 +499,9 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d
|
|
|
for _, a := range message.Attachments {
|
|
for _, a := range message.Attachments {
|
|
|
builder.NewLine()
|
|
builder.NewLine()
|
|
|
if ml.cfg.ShowAttachmentLinks {
|
|
if ml.cfg.ShowAttachmentLinks {
|
|
|
- builder.Write(a.Filename+":\n"+a.URL, attachmentStyle)
|
|
|
|
|
|
|
+ builder.Write(a.Filename+":", attachmentStyle)
|
|
|
|
|
+ builder.NewLine()
|
|
|
|
|
+ builder.Write(a.URL, attachmentStyle.Url(a.URL))
|
|
|
} else {
|
|
} else {
|
|
|
builder.Write(a.Filename, attachmentStyle)
|
|
builder.Write(a.Filename, attachmentStyle)
|
|
|
}
|
|
}
|
|
@@ -874,6 +878,9 @@ func (ml *messagesList) HandleEvent(event tview.Event) tview.Command {
|
|
|
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Open.Keybind):
|
|
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Open.Keybind):
|
|
|
ml.open()
|
|
ml.open()
|
|
|
return nil
|
|
return nil
|
|
|
|
|
+ case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SaveImage.Keybind):
|
|
|
|
|
+ ml.saveImage()
|
|
|
|
|
+ return nil
|
|
|
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
|
|
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
|
|
|
ml.reply(false)
|
|
ml.reply(false)
|
|
|
return nil
|
|
return nil
|
|
@@ -1180,37 +1187,146 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|
|
ml.chatView.app.SetFocus(ml.attachmentsPicker)
|
|
ml.chatView.app.SetFocus(ml.attachmentsPicker)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (ml *messagesList) openAttachment(attachment discord.Attachment) {
|
|
|
|
|
|
|
+func (ml *messagesList) downloadToCache(attachment discord.Attachment) (string, error) {
|
|
|
resp, err := http.Get(attachment.URL)
|
|
resp, err := http.Get(attachment.URL)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- slog.Error("failed to fetch the attachment", "err", err, "url", attachment.URL)
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ return "", fmt.Errorf("failed to fetch attachment: %w", err)
|
|
|
}
|
|
}
|
|
|
defer resp.Body.Close()
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
- path := filepath.Join(consts.CacheDir(), "attachments")
|
|
|
|
|
- if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
|
|
|
|
- slog.Error("failed to create attachments dir", "err", err, "path", path)
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ dir := filepath.Join(consts.CacheDir(), "attachments")
|
|
|
|
|
+ if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
|
|
|
|
+ return "", fmt.Errorf("failed to create attachments dir: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- path = filepath.Join(path, attachment.Filename)
|
|
|
|
|
|
|
+ path := filepath.Join(dir, attachment.Filename)
|
|
|
file, err := os.Create(path)
|
|
file, err := os.Create(path)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- slog.Error("failed to create attachment file", "err", err, "path", path)
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ return "", fmt.Errorf("failed to create attachment file: %w", err)
|
|
|
}
|
|
}
|
|
|
defer file.Close()
|
|
defer file.Close()
|
|
|
|
|
|
|
|
if _, err := io.Copy(file, resp.Body); err != nil {
|
|
if _, err := io.Copy(file, resp.Body); err != nil {
|
|
|
- slog.Error("failed to copy attachment to file", "err", err)
|
|
|
|
|
|
|
+ return "", fmt.Errorf("failed to write attachment file: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return path, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+var supportedImageTypes = map[string]bool{
|
|
|
|
|
+ "image/jpeg": true,
|
|
|
|
|
+ "image/png": true,
|
|
|
|
|
+ "image/webp": true,
|
|
|
|
|
+ "image/gif": true,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (ml *messagesList) openAttachment(attachment discord.Attachment) {
|
|
|
|
|
+ path, err := ml.downloadToCache(attachment)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("failed to download attachment", "err", err, "url", attachment.URL)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ viewer := ml.cfg.ImageViewer
|
|
|
|
|
+ if viewer != "" && supportedImageTypes[attachment.ContentType] {
|
|
|
|
|
+ cmd := exec.Command(viewer, path)
|
|
|
|
|
+ cmd.Stdin = os.Stdin
|
|
|
|
|
+ cmd.Stdout = os.Stdout
|
|
|
|
|
+ cmd.Stderr = os.Stderr
|
|
|
|
|
+ ml.chatView.app.Suspend(func() {
|
|
|
|
|
+ if err := cmd.Run(); err != nil {
|
|
|
|
|
+ slog.Error("failed to run image viewer", "viewer", viewer, "err", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if err := open.Start(path); err != nil {
|
|
if err := open.Start(path); err != nil {
|
|
|
slog.Error("failed to open attachment file", "err", err, "path", path)
|
|
slog.Error("failed to open attachment file", "err", err, "path", path)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (ml *messagesList) saveImage() {
|
|
|
|
|
+ msg, err := ml.selectedMessage()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("failed to get selected message", "err", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var images []discord.Attachment
|
|
|
|
|
+ for _, a := range msg.Attachments {
|
|
|
|
|
+ if supportedImageTypes[a.ContentType] {
|
|
|
|
|
+ images = append(images, a)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if len(images) == 0 {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ if len(images) == 1 {
|
|
|
|
|
+ go ml.saveAttachmentImage(images[0])
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var items []attachmentItem
|
|
|
|
|
+ for _, a := range images {
|
|
|
|
|
+ attachment := a
|
|
|
|
|
+ items = append(items, attachmentItem{
|
|
|
|
|
+ label: attachment.Filename,
|
|
|
|
|
+ open: func() { go ml.saveAttachmentImage(attachment) },
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ ml.attachmentsPicker.SetItems(items)
|
|
|
|
|
+ ml.chatView.
|
|
|
|
|
+ AddLayer(
|
|
|
|
|
+ ui.Centered(ml.attachmentsPicker, ml.cfg.Picker.Width, ml.cfg.Picker.Height),
|
|
|
|
|
+ layers.WithName(attachmentsListLayerName),
|
|
|
|
|
+ layers.WithResize(true),
|
|
|
|
|
+ layers.WithVisible(true),
|
|
|
|
|
+ layers.WithOverlay(),
|
|
|
|
|
+ ).
|
|
|
|
|
+ SendToFront(attachmentsListLayerName)
|
|
|
|
|
+ ml.chatView.app.SetFocus(ml.attachmentsPicker)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (ml *messagesList) saveAttachmentImage(attachment discord.Attachment) {
|
|
|
|
|
+ path, err := ml.downloadToCache(attachment)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("failed to download attachment", "err", err, "url", attachment.URL)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ saveDir := ml.cfg.ImageSaveDir
|
|
|
|
|
+ if saveDir == "" {
|
|
|
|
|
+ saveDir = "."
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := os.MkdirAll(saveDir, os.ModePerm); err != nil {
|
|
|
|
|
+ slog.Error("failed to create save directory", "err", err, "path", saveDir)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ destPath := filepath.Join(saveDir, attachment.Filename)
|
|
|
|
|
+ src, err := os.Open(path)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("failed to open cached file", "err", err, "path", path)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ defer src.Close()
|
|
|
|
|
+
|
|
|
|
|
+ dst, err := os.Create(destPath)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("failed to create save file", "err", err, "path", destPath)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ defer dst.Close()
|
|
|
|
|
+
|
|
|
|
|
+ if _, err := io.Copy(dst, src); err != nil {
|
|
|
|
|
+ slog.Error("failed to save image", "err", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ slog.Info("image saved", "path", destPath)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (ml *messagesList) openURL(url string) {
|
|
func (ml *messagesList) openURL(url string) {
|
|
@@ -1431,7 +1547,7 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|
|
manage = append(manage, cfg.DeleteConfirm.Keybind, cfg.Delete.Keybind)
|
|
manage = append(manage, cfg.DeleteConfirm.Keybind, cfg.Delete.Keybind)
|
|
|
}
|
|
}
|
|
|
if canOpen {
|
|
if canOpen {
|
|
|
- manage = append(manage, cfg.Open.Keybind)
|
|
|
|
|
|
|
+ manage = append(manage, cfg.Open.Keybind, cfg.SaveImage.Keybind)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return [][]keybind.Keybind{
|
|
return [][]keybind.Keybind{
|