Skip to content
This repository has been archived by the owner on May 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #9 from derricw/feature/send_attachments
Browse files Browse the repository at this point in the history
Minimal attachment sending
  • Loading branch information
derricw authored Jun 25, 2020
2 parents de77c69 + edf3031 commit af3e108
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
bin
examples.json
dist
cov.cov
coverage.html
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ bin/siggo
* `k` - Scroll Up
* `J` - Next Contact
* `K` - Previous Contact
* `a` - Attach file (sent with next message)
* `i` - Insert Mode
* `CTRL+L` - Clear input field
* `I` - Compose (opens $EDITOR and lets you make a fancy message)
Expand Down Expand Up @@ -107,7 +108,6 @@ Here is a list of things that are currently broken.

Here is a list of features I'd like to add soonish.
* Better Attachments Support
* Sending attachments
* Opening attachments (besides the most recent)
* gui configuration
* colors and border styles
Expand Down
49 changes: 38 additions & 11 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ func (m *Message) String(color string) string {
return data
}

// AddAttachments currently only is used to track attachments we sent to other people, so that
// they show up in the GUI.
func (m *Message) AddAttachments(paths []string) {
if m.Attachments == nil {
m.Attachments = make([]*signal.Attachment, 0)
}
for _, path := range paths {
m.Attachments = append(m.Attachments, &signal.Attachment{Filename: path})
}
}

// Coversation is a contact and its associated messages
type Conversation struct {
Contact *Contact
Expand All @@ -125,8 +136,9 @@ type Conversation struct {
HasNewMessage bool
// hasNewData tracks whether new data has been added
// since the last save to disk
color string
hasNewData bool
color string
hasNewData bool
stagedAttachments []string
}

// String renders the conversation to a single string
Expand Down Expand Up @@ -173,14 +185,24 @@ func (c *Conversation) LastMessage() *Message {
return nil
}

// PopMessage just removes and returns the last message, if it exists
func (c *Conversation) PopMessage() {
nMessage := len(c.MessageOrder)
if nMessage > 0 {
lastMsg := c.MessageOrder[nMessage-1]
c.MessageOrder = c.MessageOrder[:nMessage-1]
delete(c.Messages, lastMsg)
// StageAttachment attaches a file to be sent in the next message
func (c *Conversation) AddAttachment(path string) error {
if _, err := os.Stat(path); err != nil {
// no file there...
return err
}
c.stagedAttachments = append(c.stagedAttachments, path)
return nil
}

// ClearAttachments removes any staged attachments
func (c *Conversation) ClearAttachments() {
c.stagedAttachments = []string{}
}

// NumAttachments returns the number of staged attachments
func (c *Conversation) NumAttachments() int {
return len(c.stagedAttachments)
}

// CaughtUp iterates back through the messages of the conversation marking the un-read ones
Expand Down Expand Up @@ -256,12 +278,14 @@ func NewConversation(contact *Contact) *Conversation {
Messages: make(map[int64]*Message),
MessageOrder: make([]int64, 0),
HasNewMessage: false,

stagedAttachments: make([]string, 0),
}
}

type SignalAPI interface {
Send(string, string) (int64, error)
SendDbus(string, string) (int64, error)
SendDbus(string, string, ...string) (int64, error)
Receive() error
ReceiveForever()
OnReceived(signal.ReceivedCallback)
Expand Down Expand Up @@ -290,14 +314,15 @@ func (s *Siggo) Send(msg string, contact *Contact) error {
IsDelivered: false,
IsRead: false,
FromSelf: true,
Attachments: make([]*signal.Attachment, 0),
}
conv, ok := s.conversations[contact]
if !ok {
log.Infof("new conversation for contact: %v", contact)
conv = s.newConversation(contact)
}
// finally send the message
ID, err := s.signal.SendDbus(contact.Number, msg)
ID, err := s.signal.SendDbus(contact.Number, msg, conv.stagedAttachments...)
if err != nil {
message.Content = fmt.Sprintf("FAILED TO SEND: %s ERROR: %v", message.Content, err)
s.NewInfo(conv)
Expand All @@ -306,6 +331,8 @@ func (s *Siggo) Send(msg string, contact *Contact) error {
// use the official timestamp on success
message.Timestamp = ID
conv.CaughtUp()
message.AddAttachments(conv.stagedAttachments)
conv.ClearAttachments()
conv.AddMessage(message)
s.NewInfo(conv)
log.Infof("successfully sent message %s with timestamp: %d", message.Content, message.Timestamp)
Expand Down
5 changes: 5 additions & 0 deletions signal/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ type Attachment struct {

// Path returns the full path to an attachment file
func (a *Attachment) Path() (string, error) {
if a.ID == "" {
// TODO: save our own copy of the attachment with our own ID
// for now, just return the path where we attached it
return a.Filename, nil
}
folder, err := GetSignalFolder()
if err != nil {
return "", err
Expand Down
2 changes: 1 addition & 1 deletion signal/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (ms *MockSignal) Send(dest, msg string) (int64, error) {
return timestamp, nil
}

func (ms *MockSignal) SendDbus(dest, msg string) (int64, error) {
func (ms *MockSignal) SendDbus(dest, msg string, attachments ...string) (int64, error) {
return ms.Send(dest, msg)
}

Expand Down
10 changes: 8 additions & 2 deletions signal/signal.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,17 @@ func (s *Signal) Send(dest, msg string) (int64, error) {
}

// SendDbus does the same thing as Send but it goes through a running daemon.
func (s *Signal) SendDbus(dest, msg string) (int64, error) {
func (s *Signal) SendDbus(dest, msg string, attachments ...string) (int64, error) {
if !strings.HasPrefix(dest, "+") {
dest = fmt.Sprintf("+%s", dest)
}
cmd := exec.Command("signal-cli", "--dbus", "send", dest, "-m", msg)
args := []string{"--dbus", "send", dest, "-m", msg}
if len(attachments) > 0 {
// how do I do this in one line?
args = append(args, "-a")
args = append(args, attachments...)
}
cmd := exec.Command("signal-cli", args...)
out, err := cmd.Output()
if err != nil {
s.publishError(err)
Expand Down
154 changes: 151 additions & 3 deletions widgets/widgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -45,6 +47,7 @@ type ChatWindow struct {
contactsPanel *ContactListPanel
conversationPanel *ConversationPanel
searchPanel tview.Primitive
commandPanel tview.Primitive
statusBar *StatusBar
app *tview.Application
normalKeybinds func(*tcell.EventKey) *tcell.EventKey
Expand Down Expand Up @@ -153,7 +156,7 @@ func (c *ChatWindow) YankLastLink() {
}

// OpenLastLink opens the last link that is finds in the conversation
// TODO: solution for browsing/opening any attachment
// TODO: solution for browsing/opening any link
func (c *ChatWindow) OpenLastLink() {
c.NormalMode()
links := c.getLinks()
Expand Down Expand Up @@ -213,6 +216,24 @@ func (c *ChatWindow) HideSearch() {
c.app.SetFocus(c)
}

// ShowAttachInput opens a commandPanel to choose a file to attach
func (c *ChatWindow) ShowAttachInput() {
log.Debug("SHOWING CONTACT SEARCH")
p := NewAttachInput(c)
c.commandPanel = p
c.SetRows(0, 3, 1)
c.AddItem(p, 2, 0, 1, 2, 0, 0, false)
c.app.SetFocus(p)
}

// HideCommandInput hides any current CommandInput panel
func (c *ChatWindow) HideCommandInput() {
log.Debug("HIDING COMMAND INPUT")
c.RemoveItem(c.commandPanel)
c.SetRows(0, 3)
c.app.SetFocus(c)
}

// ShowStatusBar shows the bottom status bar
func (c *ChatWindow) ShowStatusBar() {
c.SetRows(0, 3, 1)
Expand Down Expand Up @@ -260,6 +281,7 @@ func (c *ChatWindow) SetCurrentContact(contact *model.Contact) error {
}
c.conversationPanel.Update(conv)
conv.CaughtUp()
c.sendPanel.Update()
c.conversationPanel.ScrollToEnd()
return nil
}
Expand Down Expand Up @@ -380,16 +402,39 @@ func (s *SendPanel) Send() {
go s.siggo.Send(msg, contact)
log.Infof("sent message: %s to contact: %s", msg, contact)
s.SetText("")
s.SetLabel("")
}

func (s *SendPanel) Clear() {
s.SetText("")
conv, err := s.parent.currentConversation()
if err != nil {
return
}
conv.ClearAttachments()
s.SetLabel("")
}

func (s *SendPanel) Defocus() {
s.parent.NormalMode()
}

func (s *SendPanel) Update() {
conv, err := s.parent.currentConversation()
if err != nil {
return
}
nAttachments := conv.NumAttachments()
if nAttachments > 0 {
s.SetLabel(fmt.Sprintf("📎(%d): ", nAttachments))
} else {
s.SetLabel("")
}
}

// emojify is a custom input change handler that provides emoji support
func (s *SendPanel) emojify(input string) {
if strings.HasSuffix(input, ":") {
//log.Printf("emojify: %s", input)
emojified := emoji.Sprint(input)
if emojified != input {
s.SetText(emojified)
Expand Down Expand Up @@ -420,7 +465,7 @@ func NewSendPanel(parent *ChatWindow, siggo *model.Siggo) *SendPanel {
case tcell.KeyCtrlQ:
s.parent.Quit()
case tcell.KeyCtrlL:
s.SetText("")
s.Clear()
return nil
}
return event
Expand Down Expand Up @@ -601,6 +646,57 @@ func NewSearchInput(parent *SearchPanel) *SearchInput {
return si
}

// CommandInput is an input field that appears at the bottom of the window and allows for various
// commands
type CommandInput struct {
*tview.InputField
parent *ChatWindow
}

// AttachInput is a command input that selects an attachment and attaches it to the current
// conversation to be sent in the next message.
func NewAttachInput(parent *ChatWindow) *CommandInput {
ci := &CommandInput{
InputField: tview.NewInputField(),
parent: parent,
}
ci.SetLabel("📎: ")
ci.SetText("~/")
ci.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
ci.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
// Setup keys
log.Debugf("Key Event <ATTACH>: %v mods: %v rune: %v", event.Key(), event.Modifiers(), event.Rune())
switch event.Key() {
case tcell.KeyESC:
ci.parent.HideCommandInput()
return nil
case tcell.KeyTAB:
ci.SetText(CompletePath(ci.GetText()))
return nil
case tcell.KeyEnter:
path := ci.GetText()
ci.parent.HideCommandInput()
if path == "" {
return nil
}
conv, err := ci.parent.currentConversation()
if err != nil {
ci.parent.SetErrorStatus(fmt.Errorf("couldn't find conversation: %v", err))
return nil
}
err = conv.AddAttachment(path)
if err != nil {
ci.parent.SetErrorStatus(fmt.Errorf("failed to attach: %s - %v", path, err))
return nil
}
ci.parent.sendPanel.Update()
return nil
}
return event
})
return ci
}

type StatusBar struct {
*tview.TextView
parent *ChatWindow
Expand Down Expand Up @@ -659,6 +755,9 @@ func NewChatWindow(siggo *model.Siggo, app *tview.Application) *ChatWindow {
case 111: // o
w.OpenMode()
return nil
case 97: // o
w.ShowAttachInput()
return nil
}
// pass some events on to the conversation panel
case tcell.KeyCtrlQ:
Expand Down Expand Up @@ -794,3 +893,52 @@ func FancyCompose() (string, error) {
}
return string(b), nil
}

// CompletePath autocompletes a path stub
func CompletePath(path string) string {
if path == "" {
return ""
}
if path[0] == '~' {
usr, err := user.Current()
if err != nil {
return ""
}
path = usr.HomeDir + path[1:]
}
matches, err := filepath.Glob(path + "*")
if err != nil || matches == nil || len(matches) == 0 {
return path
}
if len(matches) == 1 {
path = matches[0]
} else if !strings.HasSuffix(path, "/") {
path = GetSharedPrefix(matches...)
}
stat, err := os.Stat(path)
if err != nil {
return path
}
if stat.IsDir() {
if !strings.HasSuffix(path, "/") {
return path + "/"
}
}
return path
}

// GetSharedPrefix finds the prefix shared by any number of strings
// Is there a more efficient way to do this?
func GetSharedPrefix(s ...string) string {
var out strings.Builder
for i := 0; i < len(s[0]); i++ {
c := s[0][i]
for _, str := range s {
if str[i] != c {
return out.String()
}
}
out.WriteByte(c)
}
return out.String()
}

0 comments on commit af3e108

Please sign in to comment.