2016-09-25 22:34:02 +02:00
|
|
|
package components
|
|
|
|
|
|
|
|
import (
|
2016-09-30 12:09:03 +02:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
2016-09-25 22:34:02 +02:00
|
|
|
"github.com/gizak/termui"
|
2016-10-01 14:07:42 +02:00
|
|
|
|
2016-10-19 09:08:31 +02:00
|
|
|
"github.com/erroneousboat/slack-term/service"
|
2016-09-25 22:34:02 +02:00
|
|
|
)
|
|
|
|
|
2017-07-15 21:06:49 +02:00
|
|
|
const (
|
|
|
|
IconOnline = "●"
|
|
|
|
IconOffline = "○"
|
|
|
|
IconChannel = "#"
|
|
|
|
IconGroup = "☰"
|
|
|
|
IconIM = "●"
|
|
|
|
IconNotification = "🞷"
|
|
|
|
|
|
|
|
PresenceAway = "away"
|
|
|
|
PresenceActive = "active"
|
|
|
|
)
|
|
|
|
|
2016-10-02 14:53:00 +02:00
|
|
|
// Channels is the definition of a Channels component
|
2016-09-25 22:34:02 +02:00
|
|
|
type Channels struct {
|
2016-09-27 22:05:44 +02:00
|
|
|
List *termui.List
|
2016-10-18 21:11:29 +02:00
|
|
|
SelectedChannel int // index of which channel is selected from the List
|
|
|
|
Offset int // from what offset are channels rendered
|
|
|
|
CursorPosition int // the y position of the 'cursor'
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2016-10-01 12:48:15 +02:00
|
|
|
// CreateChannels is the constructor for the Channels component
|
2016-09-25 22:34:02 +02:00
|
|
|
func CreateChannels(svc *service.SlackService, inputHeight int) *Channels {
|
|
|
|
channels := &Channels{
|
|
|
|
List: termui.NewList(),
|
|
|
|
}
|
|
|
|
|
|
|
|
channels.List.BorderLabel = "Channels"
|
|
|
|
channels.List.Height = termui.TermHeight() - inputHeight
|
|
|
|
|
2016-09-28 22:10:04 +02:00
|
|
|
channels.SelectedChannel = 0
|
2016-10-01 12:48:15 +02:00
|
|
|
channels.Offset = 0
|
|
|
|
channels.CursorPosition = channels.List.InnerBounds().Min.Y
|
2016-09-27 22:05:44 +02:00
|
|
|
|
2016-09-25 22:34:02 +02:00
|
|
|
channels.GetChannels(svc)
|
2017-07-15 21:06:49 +02:00
|
|
|
channels.SetPresenceForIMChannels(svc)
|
2016-09-25 22:34:02 +02:00
|
|
|
|
|
|
|
return channels
|
|
|
|
}
|
|
|
|
|
|
|
|
// Buffer implements interface termui.Bufferer
|
|
|
|
func (c *Channels) Buffer() termui.Buffer {
|
|
|
|
buf := c.List.Buffer()
|
2016-09-27 22:05:44 +02:00
|
|
|
|
2016-10-01 12:48:15 +02:00
|
|
|
for i, item := range c.List.Items[c.Offset:] {
|
|
|
|
|
|
|
|
y := c.List.InnerBounds().Min.Y + i
|
|
|
|
|
|
|
|
if y > c.List.InnerBounds().Max.Y-1 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2016-09-27 22:05:44 +02:00
|
|
|
var cells []termui.Cell
|
2016-10-01 12:48:15 +02:00
|
|
|
if y == c.CursorPosition {
|
2016-09-27 22:05:44 +02:00
|
|
|
cells = termui.DefaultTxBuilder.Build(
|
2016-10-11 19:28:37 +02:00
|
|
|
item, c.List.ItemBgColor, c.List.ItemFgColor)
|
2016-09-27 22:05:44 +02:00
|
|
|
} else {
|
|
|
|
cells = termui.DefaultTxBuilder.Build(
|
|
|
|
item, c.List.ItemFgColor, c.List.ItemBgColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
|
|
|
|
|
|
|
|
x := 0
|
|
|
|
for _, cell := range cells {
|
|
|
|
width := cell.Width()
|
2016-10-01 12:48:15 +02:00
|
|
|
buf.Set(c.List.InnerBounds().Min.X+x, y, cell)
|
2016-09-27 22:05:44 +02:00
|
|
|
x += width
|
|
|
|
}
|
2016-10-01 12:48:15 +02:00
|
|
|
|
|
|
|
// When not at the end of the pane fill it up empty characters
|
|
|
|
for x < c.List.InnerBounds().Max.X-1 {
|
|
|
|
if y == c.CursorPosition {
|
|
|
|
buf.Set(x+1, y,
|
|
|
|
termui.Cell{
|
2016-10-11 19:28:37 +02:00
|
|
|
Ch: ' ',
|
|
|
|
Fg: c.List.ItemBgColor,
|
|
|
|
Bg: c.List.ItemFgColor,
|
2016-10-01 12:48:15 +02:00
|
|
|
},
|
|
|
|
)
|
|
|
|
} else {
|
2016-10-11 19:28:37 +02:00
|
|
|
buf.Set(
|
|
|
|
x+1, y,
|
|
|
|
termui.Cell{
|
|
|
|
Ch: ' ',
|
|
|
|
Fg: c.List.ItemFgColor,
|
|
|
|
Bg: c.List.ItemBgColor,
|
|
|
|
},
|
|
|
|
)
|
2016-10-01 12:48:15 +02:00
|
|
|
}
|
|
|
|
x++
|
|
|
|
}
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2016-09-25 22:34:02 +02:00
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetHeight implements interface termui.GridBufferer
|
|
|
|
func (c *Channels) GetHeight() int {
|
|
|
|
return c.List.Block.GetHeight()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetWidth implements interface termui.GridBufferer
|
|
|
|
func (c *Channels) SetWidth(w int) {
|
|
|
|
c.List.SetWidth(w)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetX implements interface termui.GridBufferer
|
|
|
|
func (c *Channels) SetX(x int) {
|
|
|
|
c.List.SetX(x)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetY implements interface termui.GridBufferer
|
|
|
|
func (c *Channels) SetY(y int) {
|
|
|
|
c.List.SetY(y)
|
|
|
|
}
|
|
|
|
|
2016-09-30 12:09:03 +02:00
|
|
|
// GetChannels will get all available channels from the SlackService
|
2016-09-25 22:34:02 +02:00
|
|
|
func (c *Channels) GetChannels(svc *service.SlackService) {
|
|
|
|
for _, slackChan := range svc.GetChannels() {
|
2017-07-15 21:06:49 +02:00
|
|
|
label := setChannelLabel(slackChan, false)
|
|
|
|
c.List.Items = append(c.List.Items, label)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPresenceForIMChannels this will set the correct icon for
|
|
|
|
// IM channels for when they're online of away
|
|
|
|
func (c *Channels) SetPresenceForIMChannels(svc *service.SlackService) {
|
|
|
|
for _, slackChan := range svc.GetChannels() {
|
|
|
|
if slackChan.Type == service.ChannelTypeIM {
|
|
|
|
presence, err := svc.GetUserPresence(slackChan.UserID)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
c.SetPresence(svc, slackChan.UserID, presence)
|
|
|
|
}
|
2016-09-25 22:34:02 +02:00
|
|
|
}
|
|
|
|
}
|
2016-09-27 22:05:44 +02:00
|
|
|
|
2016-09-30 12:09:03 +02:00
|
|
|
// SetSelectedChannel sets the SelectedChannel given the index
|
|
|
|
func (c *Channels) SetSelectedChannel(index int) {
|
|
|
|
c.SelectedChannel = index
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2016-09-30 12:09:03 +02:00
|
|
|
// MoveCursorUp will decrease the SelectedChannel by 1
|
2016-09-27 22:05:44 +02:00
|
|
|
func (c *Channels) MoveCursorUp() {
|
|
|
|
if c.SelectedChannel > 0 {
|
|
|
|
c.SetSelectedChannel(c.SelectedChannel - 1)
|
2016-10-01 12:48:15 +02:00
|
|
|
c.ScrollUp()
|
2016-09-30 12:09:03 +02:00
|
|
|
c.ClearNewMessageIndicator()
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-30 12:09:03 +02:00
|
|
|
// MoveCursorDown will increase the SelectedChannel by 1
|
2016-09-27 22:05:44 +02:00
|
|
|
func (c *Channels) MoveCursorDown() {
|
|
|
|
if c.SelectedChannel < len(c.List.Items)-1 {
|
|
|
|
c.SetSelectedChannel(c.SelectedChannel + 1)
|
2016-10-01 12:48:15 +02:00
|
|
|
c.ScrollDown()
|
2016-09-30 12:09:03 +02:00
|
|
|
c.ClearNewMessageIndicator()
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-18 21:11:29 +02:00
|
|
|
// MoveCursorTop will move the cursor to the top of the channels
|
|
|
|
func (c *Channels) MoveCursorTop() {
|
|
|
|
c.SetSelectedChannel(0)
|
|
|
|
c.CursorPosition = c.List.InnerBounds().Min.Y
|
|
|
|
c.Offset = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// MoveCursorBottom will move the cursor to the bottom of the channels
|
|
|
|
func (c *Channels) MoveCursorBottom() {
|
|
|
|
c.SetSelectedChannel(len(c.List.Items) - 1)
|
|
|
|
|
|
|
|
offset := len(c.List.Items) - (c.List.InnerBounds().Max.Y - 1)
|
|
|
|
|
|
|
|
if offset < 0 {
|
|
|
|
c.Offset = 0
|
|
|
|
c.CursorPosition = c.SelectedChannel + 1
|
|
|
|
} else {
|
|
|
|
c.Offset = offset
|
|
|
|
c.CursorPosition = c.List.InnerBounds().Max.Y - 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-02 16:07:35 +02:00
|
|
|
// ScrollUp enables us to scroll through the channel list when it overflows
|
2016-10-01 12:48:15 +02:00
|
|
|
func (c *Channels) ScrollUp() {
|
2017-07-15 23:56:39 +02:00
|
|
|
// Is cursor at the top of the channel view?
|
2016-10-01 12:48:15 +02:00
|
|
|
if c.CursorPosition == c.List.InnerBounds().Min.Y {
|
|
|
|
if c.Offset > 0 {
|
|
|
|
c.Offset--
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.CursorPosition--
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-02 16:07:35 +02:00
|
|
|
// ScrollDown enables us to scroll through the channel list when it overflows
|
2016-10-01 12:48:15 +02:00
|
|
|
func (c *Channels) ScrollDown() {
|
2017-07-15 23:56:39 +02:00
|
|
|
// Is the cursor at the bottom of the channel view?
|
2016-10-01 12:48:15 +02:00
|
|
|
if c.CursorPosition == c.List.InnerBounds().Max.Y-1 {
|
|
|
|
if c.Offset < len(c.List.Items)-1 {
|
|
|
|
c.Offset++
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.CursorPosition++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-15 23:56:39 +02:00
|
|
|
// Search will search through the channels to find a channel,
|
|
|
|
// when a match has been found the selected channel will then
|
|
|
|
// be the channel that has been found
|
|
|
|
func (c *Channels) Search(term string) {
|
|
|
|
for i, item := range c.List.Items {
|
|
|
|
if strings.Contains(item, term) {
|
|
|
|
|
|
|
|
// The new position
|
|
|
|
newPos := i
|
|
|
|
|
|
|
|
// Is the new position in range of the current view?
|
|
|
|
minRange := c.Offset
|
2017-07-17 09:42:26 +02:00
|
|
|
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
|
2017-07-15 23:56:39 +02:00
|
|
|
|
|
|
|
if newPos < minRange {
|
|
|
|
// newPos is above, we need to scroll up.
|
|
|
|
c.SetSelectedChannel(i)
|
|
|
|
|
|
|
|
// How much do we need to scroll to get it into range?
|
|
|
|
c.Offset = c.Offset - (minRange - newPos)
|
|
|
|
} else if newPos > maxRange {
|
|
|
|
// newPos is below, we need to scroll down
|
|
|
|
c.SetSelectedChannel(i)
|
|
|
|
|
|
|
|
// How much do we need to scroll to get it into range?
|
2017-07-17 09:42:26 +02:00
|
|
|
c.Offset = c.Offset + (newPos - maxRange)
|
2017-07-15 23:56:39 +02:00
|
|
|
} else {
|
|
|
|
// newPos is inside range
|
|
|
|
c.SetSelectedChannel(i)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set cursor to correct position
|
2017-07-17 09:42:26 +02:00
|
|
|
c.CursorPosition = (newPos - c.Offset) + 1
|
2017-07-15 23:56:39 +02:00
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-15 21:06:49 +02:00
|
|
|
// SetNotification will be called when a new message arrives and will
|
|
|
|
// render an notification icon in front of the channel name
|
|
|
|
func (c *Channels) SetNotification(svc *service.SlackService, channelID string) {
|
2016-10-01 12:48:15 +02:00
|
|
|
// Get the correct Channel from svc.Channels
|
2017-07-15 21:06:49 +02:00
|
|
|
var index int
|
2016-09-30 23:52:26 +02:00
|
|
|
for i, channel := range svc.Channels {
|
2016-09-30 12:09:03 +02:00
|
|
|
if channelID == channel.ID {
|
|
|
|
index = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-15 21:06:49 +02:00
|
|
|
if !strings.Contains(c.List.Items[index], IconNotification) {
|
2016-10-01 12:48:15 +02:00
|
|
|
// The order of svc.Channels relates to the order of
|
2016-09-30 16:36:41 +02:00
|
|
|
// List.Items, index will be the index of the channel
|
2017-07-15 21:06:49 +02:00
|
|
|
c.List.Items[index] = fmt.Sprintf(
|
|
|
|
"%s %s", IconNotification, strings.TrimSpace(c.List.Items[index]),
|
|
|
|
)
|
2016-09-30 16:36:41 +02:00
|
|
|
}
|
2016-09-30 12:09:03 +02:00
|
|
|
|
|
|
|
// Play terminal bell sound
|
|
|
|
fmt.Print("\a")
|
|
|
|
}
|
|
|
|
|
2017-07-15 21:06:49 +02:00
|
|
|
// ClearNewMessageIndicator will remove the notification icon in front of
|
|
|
|
// a channel that received a new message. This will happen as one will
|
|
|
|
// move up or down the cursor for Channels
|
2016-09-30 12:09:03 +02:00
|
|
|
func (c *Channels) ClearNewMessageIndicator() {
|
2017-07-15 21:06:49 +02:00
|
|
|
channelName := strings.Split(
|
|
|
|
c.List.Items[c.SelectedChannel],
|
|
|
|
fmt.Sprintf("%s ", IconNotification),
|
|
|
|
)
|
|
|
|
|
2016-09-30 12:09:03 +02:00
|
|
|
if len(channelName) > 1 {
|
|
|
|
c.List.Items[c.SelectedChannel] = fmt.Sprintf(" %s", channelName[1])
|
|
|
|
} else {
|
|
|
|
c.List.Items[c.SelectedChannel] = channelName[0]
|
|
|
|
}
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
2016-10-29 23:59:16 +02:00
|
|
|
|
2017-07-15 21:06:49 +02:00
|
|
|
// SetReadMark will send the ReadMark event on the service
|
2016-10-29 23:59:16 +02:00
|
|
|
func (c *Channels) SetReadMark(svc *service.SlackService) {
|
|
|
|
svc.SetChannelReadMark(svc.SlackChannels[c.SelectedChannel])
|
|
|
|
}
|
2017-07-15 21:06:49 +02:00
|
|
|
|
|
|
|
// SetPresence will set the correct icon for a IM Channel
|
|
|
|
func (c *Channels) SetPresence(svc *service.SlackService, userID string, presence string) {
|
|
|
|
// Get the correct Channel from svc.Channels
|
|
|
|
var index int
|
|
|
|
for i, channel := range svc.Channels {
|
|
|
|
if userID == channel.UserID {
|
|
|
|
index = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch presence {
|
|
|
|
case PresenceActive:
|
|
|
|
c.List.Items[index] = strings.Replace(
|
|
|
|
c.List.Items[index], IconOffline, IconOnline, 1,
|
|
|
|
)
|
|
|
|
case PresenceAway:
|
|
|
|
c.List.Items[index] = strings.Replace(
|
|
|
|
c.List.Items[index], IconOnline, IconOffline, 1,
|
|
|
|
)
|
|
|
|
default:
|
|
|
|
c.List.Items[index] = strings.Replace(
|
|
|
|
c.List.Items[index], IconOnline, IconOffline, 1,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// setChannelLabel will set the label of the channel, meaning, how it
|
|
|
|
// is displayed on screen. Based on the type, different icons are
|
|
|
|
// shown, as well as an optional notification icon.
|
|
|
|
func setChannelLabel(channel service.Channel, notification bool) string {
|
|
|
|
var prefix string
|
|
|
|
if notification {
|
|
|
|
prefix = IconNotification
|
|
|
|
} else {
|
|
|
|
prefix = " "
|
|
|
|
}
|
|
|
|
|
|
|
|
var label string
|
|
|
|
switch channel.Type {
|
|
|
|
case service.ChannelTypeChannel:
|
|
|
|
label = fmt.Sprintf("%s %s %s", prefix, IconChannel, channel.Name)
|
|
|
|
case service.ChannelTypeGroup:
|
|
|
|
label = fmt.Sprintf("%s %s %s", prefix, IconGroup, channel.Name)
|
|
|
|
case service.ChannelTypeIM:
|
|
|
|
label = fmt.Sprintf("%s %s %s", prefix, IconIM, channel.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return label
|
|
|
|
}
|