parent
86c1fd6a77
commit
0509f47775
5 changed files with 143 additions and 63 deletions
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
type Item struct {
|
||||
Label string
|
||||
Open tview.Cmd
|
||||
Action tview.Cmd
|
||||
}
|
||||
|
||||
type Model struct{ *picker.Model }
|
||||
|
|
@ -23,7 +23,7 @@ func NewModel(cfg *config.Config) *Model {
|
|||
func (m *Model) SetItems(items []Item) {
|
||||
pickerItems := make(picker.Items, len(items))
|
||||
for i, item := range items {
|
||||
pickerItems[i] = picker.Item{Text: item.Label, Reference: item.Open}
|
||||
pickerItems[i] = picker.Item{Text: item.Label, Reference: item.Action}
|
||||
}
|
||||
m.Model.SetItems(pickerItems)
|
||||
}
|
||||
|
|
@ -31,11 +31,11 @@ func (m *Model) SetItems(items []Item) {
|
|||
func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
||||
switch msg := msg.(type) {
|
||||
case picker.SelectedMsg:
|
||||
open, ok := msg.Reference.(tview.Cmd)
|
||||
action, ok := msg.Reference.(tview.Cmd)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return func() tview.Msg { return SelectedMsg{Open: open} }
|
||||
return func() tview.Msg { return SelectedMsg{Action: action} }
|
||||
case picker.CancelMsg:
|
||||
return func() tview.Msg { return CancelMsg{} }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package attachmentspicker
|
|||
import "github.com/ayn2op/tview"
|
||||
|
||||
type SelectedMsg struct {
|
||||
Open tview.Cmd
|
||||
Action tview.Cmd
|
||||
}
|
||||
|
||||
type CancelMsg struct{}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ package chat
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -34,6 +37,7 @@ import (
|
|||
"github.com/ayn2op/tview/text"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/gdamore/tcell/v3/color"
|
||||
"github.com/ncruces/zenity"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
|
|
@ -807,6 +811,10 @@ func (ml *messagesList) Update(msg tview.Msg) tview.Cmd {
|
|||
return ml.yankURL()
|
||||
case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Open.Keybind):
|
||||
return ml.open()
|
||||
case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.OpenInBrowser.Keybind):
|
||||
return ml.openInBrowser()
|
||||
case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Download.Keybind):
|
||||
return ml.download()
|
||||
case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
|
||||
return ml.reply(false)
|
||||
case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.ReplyMention.Keybind):
|
||||
|
|
@ -995,6 +1003,14 @@ func (ml *messagesList) yankURL() tview.Cmd {
|
|||
}
|
||||
|
||||
func (ml *messagesList) open() tview.Cmd {
|
||||
return ml.openWith(ml.openAttachment)
|
||||
}
|
||||
|
||||
func (ml *messagesList) openInBrowser() tview.Cmd {
|
||||
return ml.openWith(func(attachment discord.Attachment) tview.Cmd { return openURL(attachment.URL) })
|
||||
}
|
||||
|
||||
func (ml *messagesList) openWith(openAttachment func(discord.Attachment) tview.Cmd) tview.Cmd {
|
||||
selectedMessage, ok := ml.selectedMessage()
|
||||
if !ok {
|
||||
return nil
|
||||
|
|
@ -1005,16 +1021,29 @@ func (ml *messagesList) open() tview.Cmd {
|
|||
case total == 0:
|
||||
return nil
|
||||
case total > 1:
|
||||
return ml.showAttachmentsList(urls, selectedMessage.Attachments)
|
||||
return ml.showAttachmentsList(urls, selectedMessage.Attachments, openAttachment)
|
||||
case len(urls) == 1:
|
||||
return openURL(urls[0])
|
||||
}
|
||||
|
||||
attachment := selectedMessage.Attachments[0]
|
||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||
return openAttachment(attachment)
|
||||
return openAttachment(selectedMessage.Attachments[0])
|
||||
}
|
||||
return openURL(attachment.URL)
|
||||
|
||||
func (ml *messagesList) download() tview.Cmd {
|
||||
selectedMessage, ok := ml.selectedMessage()
|
||||
if !ok || len(selectedMessage.Attachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(selectedMessage.Attachments) == 1 {
|
||||
attachment := selectedMessage.Attachments[0]
|
||||
return ml.confirmAttachment(attachment, saveAttachment(attachment))
|
||||
}
|
||||
|
||||
items := make([]attachmentspicker.Item, len(selectedMessage.Attachments))
|
||||
for i, attachment := range selectedMessage.Attachments {
|
||||
items[i] = attachmentspicker.Item{Label: attachment.Filename, Action: ml.confirmAttachment(attachment, saveAttachment(attachment))}
|
||||
}
|
||||
return ml.showAttachmentsPicker(items)
|
||||
}
|
||||
|
||||
func extractURLs(content string) []string {
|
||||
|
|
@ -1081,26 +1110,18 @@ func messageURLs(msg discord.Message) []string {
|
|||
return urls
|
||||
}
|
||||
|
||||
func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord.Attachment) tview.Cmd {
|
||||
func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord.Attachment, openAttachment func(discord.Attachment) tview.Cmd) tview.Cmd {
|
||||
var items []attachmentspicker.Item
|
||||
for _, a := range attachments {
|
||||
attachment := a
|
||||
open := openURL(attachment.URL)
|
||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||
open = openAttachment(attachment)
|
||||
for _, attachment := range attachments {
|
||||
items = append(items, attachmentspicker.Item{Label: attachment.Filename, Action: openAttachment(attachment)})
|
||||
}
|
||||
items = append(items, attachmentspicker.Item{
|
||||
Label: attachment.Filename,
|
||||
Open: open,
|
||||
})
|
||||
for _, url := range urls {
|
||||
items = append(items, attachmentspicker.Item{Label: url, Action: openURL(url)})
|
||||
}
|
||||
for _, u := range urls {
|
||||
url := u
|
||||
items = append(items, attachmentspicker.Item{
|
||||
Label: url,
|
||||
Open: openURL(url),
|
||||
})
|
||||
return ml.showAttachmentsPicker(items)
|
||||
}
|
||||
|
||||
func (ml *messagesList) showAttachmentsPicker(items []attachmentspicker.Item) tview.Cmd {
|
||||
ml.attachmentsPicker.SetItems(items)
|
||||
|
||||
ml.chat.
|
||||
|
|
@ -1115,46 +1136,87 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
return nil
|
||||
}
|
||||
|
||||
func openAttachment(attachment discord.Attachment) tview.Cmd {
|
||||
func (ml *messagesList) openAttachment(attachment discord.Attachment) tview.Cmd {
|
||||
return ml.confirmAttachment(attachment, openDownloadedAttachment(attachment))
|
||||
}
|
||||
|
||||
func (ml *messagesList) confirmAttachment(attachment discord.Attachment, action tview.Cmd) tview.Cmd {
|
||||
if !ml.cfg.AllowedMIMETypes.Allows(attachment.ContentType) {
|
||||
return ui.ShowModal(
|
||||
"This attachment type is not allowed and may be unsafe. Continue anyway?",
|
||||
ui.ModalButton{Label: "No"},
|
||||
ui.ModalButton{Label: "Yes", Result: attachmentActionMsg{action}},
|
||||
)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func openDownloadedAttachment(attachment discord.Attachment) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
mediaType, _, _ := mime.ParseMediaType(attachment.ContentType)
|
||||
extensions, _ := mime.ExtensionsByType(mediaType)
|
||||
extension := ""
|
||||
if len(extensions) != 0 {
|
||||
extension = extensions[0]
|
||||
}
|
||||
|
||||
dir := filepath.Join(consts.CacheDir(), "attachments")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
slog.Error("failed to create attachments directory", "err", err)
|
||||
return nil
|
||||
}
|
||||
file, err := os.CreateTemp(dir, "attachment-*"+extension)
|
||||
if err != nil {
|
||||
slog.Error("failed to create attachment file", "err", err)
|
||||
return nil
|
||||
}
|
||||
path := file.Name()
|
||||
file.Close()
|
||||
|
||||
if err := downloadAttachment(attachment, path); err != nil {
|
||||
os.Remove(path)
|
||||
slog.Error("failed to download attachment", "err", err)
|
||||
} else if err := open.Start(path); err != nil {
|
||||
slog.Error("failed to open attachment file", "err", err, "path", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func saveAttachment(attachment discord.Attachment) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
destination, err := zenity.SelectFileSave(zenity.Filename(filepath.Base(attachment.Filename)), zenity.ConfirmOverwrite())
|
||||
if errors.Is(err, zenity.ErrCanceled) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("failed to select attachment destination", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := downloadAttachment(attachment, destination); err != nil {
|
||||
slog.Error("failed to download attachment", "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func downloadAttachment(attachment discord.Attachment, destination string) error {
|
||||
resp, err := http.Get(attachment.URL)
|
||||
if err != nil {
|
||||
slog.Error("failed to fetch the attachment", "err", err, "url", attachment.URL)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
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 nil
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected HTTP status: %s", resp.Status)
|
||||
}
|
||||
|
||||
root, err := os.OpenRoot(path)
|
||||
file, err := os.Create(destination)
|
||||
if err != nil {
|
||||
slog.Error("failed to open attachments dir", "err", err, "path", path)
|
||||
return nil
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
file, err := root.Create(attachment.Filename)
|
||||
if err != nil {
|
||||
slog.Error("failed to create attachment file", "err", err, "filename", attachment.Filename)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := io.Copy(file, resp.Body); err != nil {
|
||||
slog.Error("failed to copy attachment to file", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := open.Start(file.Name()); err != nil {
|
||||
slog.Error("failed to open attachment file", "err", err, "path", file.Name())
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func openURL(url string) tview.Cmd {
|
||||
|
|
@ -1308,6 +1370,9 @@ func (ml *messagesList) ShortHelp() []keybind.Keybind {
|
|||
if !ml.chat.isMe(selectedMessage.Author.ID) {
|
||||
help = append(help, cfg.Reply.Keybind)
|
||||
}
|
||||
if len(selectedMessage.Attachments) != 0 || len(messageURLs(*selectedMessage)) != 0 {
|
||||
help = append(help, cfg.Open.Keybind)
|
||||
}
|
||||
}
|
||||
|
||||
return help
|
||||
|
|
@ -1321,9 +1386,11 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|||
canEdit := false
|
||||
canDelete := false
|
||||
canOpen := false
|
||||
canDownload := false
|
||||
if selectedMessage, ok := ml.selectedMessage(); ok {
|
||||
canSelectReply = selectedMessage.ReferencedMessage != nil
|
||||
canOpen = len(messageURLs(*selectedMessage)) != 0 || len(selectedMessage.Attachments) != 0
|
||||
canDownload = len(selectedMessage.Attachments) != 0
|
||||
|
||||
canEdit = ml.chat.isMe(selectedMessage.Author.ID)
|
||||
canReply = !canEdit
|
||||
|
|
@ -1344,10 +1411,18 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|||
manage = append(manage, cfg.Edit.Keybind)
|
||||
}
|
||||
if canDelete {
|
||||
manage = append(manage, cfg.DeleteConfirm.Keybind, cfg.Delete.Keybind)
|
||||
manage = append(manage, cfg.DeleteConfirm.Keybind)
|
||||
if len(cfg.Delete.Keys()) != 0 {
|
||||
manage = append(manage, cfg.Delete.Keybind)
|
||||
}
|
||||
}
|
||||
|
||||
attachments := make([]keybind.Keybind, 0, 3)
|
||||
if canOpen {
|
||||
manage = append(manage, cfg.Open.Keybind)
|
||||
attachments = append(attachments, cfg.Open.Keybind, cfg.OpenInBrowser.Keybind)
|
||||
}
|
||||
if canDownload {
|
||||
attachments = append(attachments, cfg.Download.Keybind)
|
||||
}
|
||||
|
||||
return [][]keybind.Keybind{
|
||||
|
|
@ -1355,6 +1430,7 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|||
{cfg.ScrollUp.Keybind, cfg.ScrollDown.Keybind, cfg.ScrollTop.Keybind, cfg.ScrollBottom.Keybind},
|
||||
actions,
|
||||
manage,
|
||||
attachments,
|
||||
{cfg.YankContent.Keybind, cfg.YankURL.Keybind, cfg.YankID.Keybind},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,12 +355,14 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
return nil
|
||||
case deleteMessageMsg:
|
||||
return m.messagesList.deleteMessageRequest(discord.Message(msg))
|
||||
case attachmentActionMsg:
|
||||
return msg.Action
|
||||
case channelspicker.SelectedMsg:
|
||||
return m.navigateToChannel(msg.ChannelID)
|
||||
case channelspicker.CancelMsg:
|
||||
return m.closePicker()
|
||||
case attachmentspicker.SelectedMsg:
|
||||
return tview.Sequence(msg.Open, m.closeAttachmentsPicker())
|
||||
return tview.Sequence(msg.Action, m.closeAttachmentsPicker())
|
||||
case attachmentspicker.CancelMsg:
|
||||
return m.closeAttachmentsPicker()
|
||||
case QuitMsg:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"github.com/ayn2op/tview"
|
||||
)
|
||||
|
||||
type attachmentActionMsg struct{ Action tview.Cmd }
|
||||
|
||||
func openState(state *ningen.State) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if err := state.Open(context.Background()); err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue