package main import ( "context" "crypto/ecdsa" "encoding/hex" "fmt" "log" "strings" "time" "github.com/ethereum/go-ethereum/crypto" "github.com/fatih/color" "github.com/jroimartin/gocui" "github.com/status-im/status-console-client/protocol/client" "github.com/status-im/status-console-client/protocol/v1" ) // ChatViewController manages chat view. type ChatViewController struct { *ViewController contact client.Chat identity *ecdsa.PrivateKey messenger *client.Messenger onError func(error) cancel chan struct{} // cancel the current chat loop done chan struct{} // wait for the current chat loop to finish changeChat chan client.Chat } // NewChatViewController returns a new chat view controller. func NewChatViewController(vc *ViewController, id Identity, m *client.Messenger, onError func(error)) *ChatViewController { if onError == nil { onError = func(error) {} } return &ChatViewController{ ViewController: vc, identity: id, messenger: m, onError: onError, changeChat: make(chan client.Chat, 1), } } func (c *ChatViewController) readEventsLoop(contact client.Chat) { c.done = make(chan struct{}) defer close(c.done) var ( messages = []*protocol.Message{} clock int64 inorder bool events = make(chan client.Event) sub = c.messenger.Subscribe(events) // We use a ticker in order to buffer storm of received events. t = time.NewTicker(time.Second) ) defer sub.Unsubscribe() defer t.Stop() for { select { case <-t.C: if !inorder { // messages are sorted by clock value // TODO draw messages only after offset (if possible) all, err := c.messenger.Messages(c.contact, 0) if err != nil { c.onError(err) continue } if len(all) != 0 { clock = all[len(all)-1].Clock } log.Printf("[ChatViewController::readEventsLoop] retrieved %d messages", len(messages)) c.printMessages(true, all...) inorder = true } else { if len(messages) != 0 { c.printMessages(false, messages...) } } messages = []*protocol.Message{} case err := <-sub.Err(): if err == nil { return } c.onError(err) return case event := <-events: log.Printf("[ChatViewController::readEventsLoop] received an event: %+v", event) switch ev := event.Interface.(type) { case client.EventWithError: c.onError(ev.GetError()) case client.EventWithChat: if !ev.GetChat().Equal(contact) { log.Printf("[ChatViewController::readEventsLoop] selected and received message contact are not equal: %s, %s", contact, ev.GetChat()) continue } msgev, ok := ev.(client.EventWithMessage) if !ok { log.Printf("[ChatViewController::readEventsLoop] can not convert to EventWithMessage") continue } if !inorder { log.Printf("[ChatViewController::readEventsLoop] not in order; skipping") continue } msg := msgev.GetMessage() log.Printf("[ChatViewController::readEventsLoop] received message %v", msg) if msg.Clock < clock { inorder = false log.Printf("[ChatViewController::readEventsLoop] received message is out of order") continue } messages = append(messages, msg) } case contact = <-c.changeChat: inorder = false clock = 0 messages = []*protocol.Message{} case <-c.cancel: return } } } // Select informs the chat view controller about a selected contact. // The chat view controller setup subscribers and request recent messages. func (c *ChatViewController) Select(contact client.Chat) error { log.Printf("[ChatViewController::Select] contact %s", contact.Name) if c.cancel == nil { c.cancel = make(chan struct{}) go c.readEventsLoop(contact) } c.changeChat <- contact c.contact = contact ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() return c.messenger.Join(ctx, contact) } // RequestOptions returns the RequestOptions for the next request call. // Newest param when true means that we are interested in the most recent messages. func (c *ChatViewController) RequestOptions(newest bool) (protocol.RequestOptions, error) { return protocol.DefaultRequestOptions(), nil } // RequestMessages sends a request fro historical messages. func (c *ChatViewController) RequestMessages(params protocol.RequestOptions) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() return c.messenger.Request(ctx, c.contact, params) } // Send sends a payload as a message. func (c *ChatViewController) Send(data []byte) error { log.Printf("[ChatViewController::Send]") _, err := c.messenger.Send(c.contact, data) return err } func (c *ChatViewController) printMessages(clear bool, messages ...*protocol.Message) { log.Printf("[ChatViewController::printMessages] printing %d messages", len(messages)) c.g.Update(func(*gocui.Gui) error { if clear { if err := c.Clear(); err != nil { return err } } for _, message := range messages { if err := c.writeMessage(message); err != nil { return err } } return nil }) } func (c *ChatViewController) writeMessage(message *protocol.Message) error { myPubKey := c.identity.PublicKey pubKey := message.SigPubKey line := formatMessageLine( pubKey, message.ID, int64(message.Clock), message.Timestamp.Time(), message.Text, ) println := fmt.Fprintln // TODO: extract if pubKey.X.Cmp(myPubKey.X) == 0 && pubKey.Y.Cmp(myPubKey.Y) == 0 { println = color.New(color.FgGreen).Fprintln } if _, err := println(c.ViewController, line); err != nil { return err } return nil } func formatMessageLine(id *ecdsa.PublicKey, hash []byte, clock int64, t time.Time, text string) string { author := "" if id != nil { author = "0x" + hex.EncodeToString(crypto.CompressPubkey(id))[:7] } return fmt.Sprintf( "%s | %#+x | %d | %s | %s", author, hash[:3], clock, t.Format(time.RFC822), strings.TrimSpace(text), ) }