2016-09-11 17:55:19 +02:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
2017-12-03 20:40:46 +01:00
|
|
|
"fmt"
|
2018-10-27 14:44:20 +02:00
|
|
|
"log"
|
2017-07-15 15:57:59 +02:00
|
|
|
"os"
|
2018-09-01 15:57:26 +02:00
|
|
|
"regexp"
|
2016-10-29 17:57:58 +02:00
|
|
|
"strconv"
|
2018-09-01 15:57:26 +02:00
|
|
|
"strings"
|
2016-10-29 22:48:27 +02:00
|
|
|
"time"
|
2016-10-27 18:53:37 +02:00
|
|
|
|
2018-04-01 13:03:28 +02:00
|
|
|
"github.com/0xAX/notificator"
|
2017-09-23 13:56:45 +02:00
|
|
|
"github.com/erroneousboat/termui"
|
2016-10-05 21:20:46 +02:00
|
|
|
"github.com/nlopes/slack"
|
2016-10-15 20:55:05 +02:00
|
|
|
termbox "github.com/nsf/termbox-go"
|
2016-09-11 17:55:19 +02:00
|
|
|
|
2018-09-01 15:57:26 +02:00
|
|
|
"github.com/erroneousboat/slack-term/components"
|
2018-04-06 13:42:12 +02:00
|
|
|
"github.com/erroneousboat/slack-term/config"
|
2016-10-19 09:08:31 +02:00
|
|
|
"github.com/erroneousboat/slack-term/context"
|
|
|
|
"github.com/erroneousboat/slack-term/views"
|
2016-09-11 17:55:19 +02:00
|
|
|
)
|
|
|
|
|
2018-04-01 13:03:28 +02:00
|
|
|
var scrollTimer *time.Timer
|
|
|
|
var notifyTimer *time.Timer
|
2016-10-29 22:48:27 +02:00
|
|
|
|
2016-10-29 17:57:58 +02:00
|
|
|
// actionMap binds specific action names to the function counterparts,
|
|
|
|
// these action names can then be used to bind them to specific keys
|
|
|
|
// in the Config.
|
2016-10-27 18:51:33 +02:00
|
|
|
var actionMap = map[string]func(*context.AppContext){
|
2017-12-17 13:48:20 +01:00
|
|
|
"space": actionSpace,
|
|
|
|
"backspace": actionBackSpace,
|
|
|
|
"delete": actionDelete,
|
|
|
|
"cursor-right": actionMoveCursorRight,
|
|
|
|
"cursor-left": actionMoveCursorLeft,
|
|
|
|
"send": actionSend,
|
|
|
|
"quit": actionQuit,
|
|
|
|
"mode-insert": actionInsertMode,
|
|
|
|
"mode-command": actionCommandMode,
|
|
|
|
"mode-search": actionSearchMode,
|
|
|
|
"clear-input": actionClearInput,
|
|
|
|
"channel-up": actionMoveCursorUpChannels,
|
|
|
|
"channel-down": actionMoveCursorDownChannels,
|
|
|
|
"channel-top": actionMoveCursorTopChannels,
|
|
|
|
"channel-bottom": actionMoveCursorBottomChannels,
|
|
|
|
"channel-search-next": actionSearchNextChannels,
|
|
|
|
"channel-search-prev": actionSearchPrevChannels,
|
2019-05-18 12:40:33 +02:00
|
|
|
"channel-jump": actionJumpChannels,
|
2019-04-06 21:08:33 +02:00
|
|
|
"thread-up": actionMoveCursorUpThreads,
|
|
|
|
"thread-down": actionMoveCursorDownThreads,
|
2017-12-17 13:48:20 +01:00
|
|
|
"chat-up": actionScrollUpChat,
|
|
|
|
"chat-down": actionScrollDownChat,
|
|
|
|
"help": actionHelp,
|
2016-10-27 18:51:33 +02:00
|
|
|
}
|
|
|
|
|
2019-05-18 12:16:27 +02:00
|
|
|
// Initialize will start a combination of event handlers and 'background tasks'
|
|
|
|
func Initialize(ctx *context.AppContext) {
|
2018-12-25 14:00:39 +01:00
|
|
|
|
|
|
|
// Keyboard events
|
2017-07-30 15:23:47 +02:00
|
|
|
eventHandler(ctx)
|
2018-12-25 14:00:39 +01:00
|
|
|
|
|
|
|
// RTM incoming events
|
2017-08-05 11:46:15 +02:00
|
|
|
messageHandler(ctx)
|
2018-12-25 14:00:39 +01:00
|
|
|
|
|
|
|
// User presence
|
|
|
|
go actionSetPresenceAll(ctx)
|
2016-10-29 17:57:58 +02:00
|
|
|
}
|
|
|
|
|
2018-03-24 13:56:55 +01:00
|
|
|
// eventHandler will handle events created by the user
|
2017-07-30 15:23:47 +02:00
|
|
|
func eventHandler(ctx *context.AppContext) {
|
2016-10-15 20:55:05 +02:00
|
|
|
go func() {
|
|
|
|
for {
|
2017-07-30 15:23:47 +02:00
|
|
|
ctx.EventQueue <- termbox.PollEvent()
|
|
|
|
}
|
|
|
|
}()
|
2016-10-15 20:55:05 +02:00
|
|
|
|
2017-07-30 15:23:47 +02:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
ev := <-ctx.EventQueue
|
2017-08-05 11:46:15 +02:00
|
|
|
handleTermboxEvents(ctx, ev)
|
|
|
|
handleMoreTermboxEvents(ctx, ev)
|
2017-12-02 15:24:31 +01:00
|
|
|
|
|
|
|
// Place your debugging statements here
|
|
|
|
if ctx.Debug {
|
2017-12-16 15:10:16 +01:00
|
|
|
ctx.View.Debug.Println(
|
|
|
|
"event received",
|
|
|
|
)
|
2017-12-02 15:24:31 +01:00
|
|
|
}
|
2017-08-05 11:46:15 +02:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleTermboxEvents(ctx *context.AppContext, ev termbox.Event) bool {
|
|
|
|
switch ev.Type {
|
|
|
|
case termbox.EventKey:
|
|
|
|
actionKeyEvent(ctx, ev)
|
|
|
|
case termbox.EventResize:
|
|
|
|
actionResizeEvent(ctx, ev)
|
|
|
|
}
|
2016-10-27 18:53:37 +02:00
|
|
|
|
2017-08-05 11:46:15 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleMoreTermboxEvents(ctx *context.AppContext, ev termbox.Event) bool {
|
|
|
|
for {
|
|
|
|
select {
|
2017-09-23 13:56:45 +02:00
|
|
|
case ev := <-ctx.EventQueue:
|
2017-08-05 11:46:15 +02:00
|
|
|
ok := handleTermboxEvents(ctx, ev)
|
|
|
|
if !ok {
|
|
|
|
return false
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
2017-08-05 11:46:15 +02:00
|
|
|
default:
|
|
|
|
return true
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
2017-08-05 11:46:15 +02:00
|
|
|
}
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
|
2018-03-24 13:56:55 +01:00
|
|
|
// messageHandler will handle events created by the service
|
2017-08-05 11:46:15 +02:00
|
|
|
func messageHandler(ctx *context.AppContext) {
|
2016-09-27 17:12:38 +02:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
2019-10-05 12:02:40 +02:00
|
|
|
case rtmEvent := <-ctx.Service.RTM.IncomingEvents:
|
|
|
|
switch ev := rtmEvent.Data.(type) {
|
2016-09-27 17:12:38 +02:00
|
|
|
case *slack.MessageEvent:
|
2018-03-24 13:56:55 +01:00
|
|
|
|
2016-09-29 21:19:09 +02:00
|
|
|
// Construct message
|
2018-12-25 15:09:03 +01:00
|
|
|
msg, err := ctx.Service.CreateMessageFromMessageEvent(ev, ev.Channel)
|
2018-03-24 13:56:55 +01:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2016-09-27 17:12:38 +02:00
|
|
|
|
|
|
|
// Add message to the selected channel
|
2018-09-01 15:57:26 +02:00
|
|
|
if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID {
|
2016-10-11 18:50:25 +02:00
|
|
|
|
2019-10-05 12:02:40 +02:00
|
|
|
// Get the thread timestamp of the event, we need to
|
|
|
|
// check the previous message as well, because edited
|
|
|
|
// message don't have the thread timestamp
|
|
|
|
var threadTimestamp string
|
|
|
|
if ev.ThreadTimestamp != "" {
|
|
|
|
threadTimestamp = ev.ThreadTimestamp
|
|
|
|
} else if ev.PreviousMessage.ThreadTimestamp != "" {
|
|
|
|
threadTimestamp = ev.PreviousMessage.ThreadTimestamp
|
|
|
|
} else {
|
|
|
|
threadTimestamp = ""
|
|
|
|
}
|
|
|
|
|
2019-02-16 22:45:36 +01:00
|
|
|
// When timestamp isn't set this is a thread reply,
|
|
|
|
// handle as such
|
2019-10-05 12:02:40 +02:00
|
|
|
if threadTimestamp != "" {
|
|
|
|
ctx.View.Debug.Println(
|
|
|
|
fmt.Sprint("here"),
|
|
|
|
)
|
|
|
|
ctx.View.Chat.AddReply(threadTimestamp, msg)
|
|
|
|
} else if threadTimestamp == "" && ctx.Focus == context.ChatFocus {
|
2018-12-25 15:56:27 +01:00
|
|
|
ctx.View.Chat.AddMessage(msg)
|
|
|
|
}
|
|
|
|
|
2016-09-27 22:05:44 +02:00
|
|
|
termui.Render(ctx.View.Chat)
|
2016-09-30 16:36:41 +02:00
|
|
|
|
2016-10-01 12:48:15 +02:00
|
|
|
// TODO: set Chat.Offset to 0, to automatically scroll
|
|
|
|
// down?
|
2016-09-27 17:12:38 +02:00
|
|
|
}
|
2016-09-30 12:09:03 +02:00
|
|
|
|
2016-10-01 12:48:15 +02:00
|
|
|
// Set new message indicator for channel, I'm leaving
|
|
|
|
// this here because I also want to be notified when
|
|
|
|
// I'm currently in a channel but not in the terminal
|
2016-10-05 21:57:43 +02:00
|
|
|
// window (tmux). But only create a notification when
|
|
|
|
// it comes from someone else but the current user.
|
2018-10-13 15:51:30 +02:00
|
|
|
if ev.User != ctx.Service.CurrentUserID {
|
|
|
|
actionNewMessage(ctx, ev)
|
|
|
|
}
|
2017-07-15 21:06:49 +02:00
|
|
|
case *slack.PresenceChangeEvent:
|
|
|
|
actionSetPresence(ctx, ev.User, ev.Presence)
|
2018-07-21 13:17:37 +02:00
|
|
|
case *slack.RTMError:
|
|
|
|
ctx.View.Debug.Println(
|
|
|
|
ev.Error(),
|
|
|
|
)
|
2016-09-27 17:12:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2017-07-30 15:23:47 +02:00
|
|
|
func actionKeyEvent(ctx *context.AppContext, ev termbox.Event) {
|
|
|
|
|
|
|
|
keyStr := getKeyString(ev)
|
|
|
|
|
|
|
|
// Get the action name (actionStr) from the key that
|
|
|
|
// has been pressed. If this is found try to uncover
|
|
|
|
// the associated function with this key and execute
|
|
|
|
// it.
|
|
|
|
actionStr, ok := ctx.Config.KeyMap[ctx.Mode][keyStr]
|
|
|
|
if ok {
|
|
|
|
action, ok := actionMap[actionStr]
|
|
|
|
if ok {
|
|
|
|
action(ctx)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ctx.Mode == context.InsertMode && ev.Ch != 0 {
|
|
|
|
actionInput(ctx.View, ev.Ch)
|
|
|
|
} else if ctx.Mode == context.SearchMode && ev.Ch != 0 {
|
|
|
|
actionSearch(ctx, ev.Ch)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) {
|
2018-03-23 13:08:42 +01:00
|
|
|
// When terminal window is too small termui will panic, here
|
|
|
|
// we won't resize when the terminal window is too small.
|
|
|
|
if termui.TermWidth() < 25 || termui.TermHeight() < 5 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-09-11 17:55:19 +02:00
|
|
|
termui.Body.Width = termui.TermWidth()
|
2018-01-27 12:03:10 +01:00
|
|
|
|
|
|
|
// Vertical resize components
|
|
|
|
ctx.View.Channels.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
|
|
|
|
ctx.View.Chat.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
|
|
|
|
ctx.View.Debug.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
|
|
|
|
|
2016-09-11 17:55:19 +02:00
|
|
|
termui.Body.Align()
|
|
|
|
termui.Render(termui.Body)
|
|
|
|
}
|
|
|
|
|
2019-06-15 15:16:08 +02:00
|
|
|
func actionRedrawGrid(ctx *context.AppContext, threads bool, debug bool) {
|
|
|
|
termui.Clear()
|
|
|
|
termui.Body = termui.NewGrid()
|
|
|
|
termui.Body.X = 0
|
|
|
|
termui.Body.Y = 0
|
|
|
|
termui.Body.BgColor = termui.ThemeAttr("bg")
|
|
|
|
termui.Body.Width = termui.TermWidth()
|
|
|
|
|
|
|
|
columns := []*termui.Row{
|
|
|
|
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Channels),
|
|
|
|
}
|
|
|
|
|
|
|
|
if threads && debug {
|
|
|
|
columns = append(
|
|
|
|
columns,
|
|
|
|
[]*termui.Row{
|
|
|
|
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth-3, 0, ctx.View.Chat),
|
|
|
|
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
|
|
|
|
termui.NewCol(3, 0, ctx.View.Debug),
|
|
|
|
}...,
|
|
|
|
)
|
|
|
|
} else if threads {
|
|
|
|
columns = append(
|
|
|
|
columns,
|
|
|
|
[]*termui.Row{
|
|
|
|
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth, 0, ctx.View.Chat),
|
|
|
|
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
|
|
|
|
}...,
|
|
|
|
)
|
|
|
|
} else if debug {
|
|
|
|
columns = append(
|
|
|
|
columns,
|
|
|
|
[]*termui.Row{
|
|
|
|
termui.NewCol(ctx.Config.MainWidth-5, 0, ctx.View.Chat),
|
|
|
|
termui.NewCol(ctx.Config.MainWidth-6, 0, ctx.View.Debug),
|
|
|
|
}...,
|
|
|
|
)
|
2019-06-29 12:24:51 +02:00
|
|
|
} else {
|
|
|
|
columns = append(
|
|
|
|
columns,
|
|
|
|
[]*termui.Row{
|
|
|
|
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Chat),
|
|
|
|
}...,
|
|
|
|
)
|
2019-06-15 15:16:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
termui.Body.AddRows(
|
|
|
|
termui.NewRow(columns...),
|
|
|
|
termui.NewRow(
|
|
|
|
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Mode),
|
|
|
|
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Input),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
termui.Body.Align()
|
|
|
|
termui.Render(termui.Body)
|
|
|
|
}
|
|
|
|
|
2016-10-21 21:23:25 +02:00
|
|
|
func actionInput(view *views.View, key rune) {
|
2016-09-11 17:55:19 +02:00
|
|
|
view.Input.Insert(key)
|
|
|
|
termui.Render(view.Input)
|
|
|
|
}
|
|
|
|
|
2017-07-15 23:56:39 +02:00
|
|
|
func actionClearInput(ctx *context.AppContext) {
|
|
|
|
// Clear input
|
|
|
|
ctx.View.Input.Clear()
|
|
|
|
ctx.View.Refresh()
|
|
|
|
|
|
|
|
// Set command mode
|
|
|
|
actionCommandMode(ctx)
|
|
|
|
}
|
|
|
|
|
2016-10-29 17:57:58 +02:00
|
|
|
func actionSpace(ctx *context.AppContext) {
|
|
|
|
actionInput(ctx.View, ' ')
|
|
|
|
}
|
|
|
|
|
2016-10-27 18:51:06 +02:00
|
|
|
func actionBackSpace(ctx *context.AppContext) {
|
|
|
|
ctx.View.Input.Backspace()
|
|
|
|
termui.Render(ctx.View.Input)
|
2016-10-09 14:59:48 +02:00
|
|
|
}
|
|
|
|
|
2016-10-27 18:51:06 +02:00
|
|
|
func actionDelete(ctx *context.AppContext) {
|
|
|
|
ctx.View.Input.Delete()
|
|
|
|
termui.Render(ctx.View.Input)
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
|
2016-10-27 18:51:06 +02:00
|
|
|
func actionMoveCursorRight(ctx *context.AppContext) {
|
|
|
|
ctx.View.Input.MoveCursorRight()
|
|
|
|
termui.Render(ctx.View.Input)
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
|
2016-10-27 18:51:06 +02:00
|
|
|
func actionMoveCursorLeft(ctx *context.AppContext) {
|
|
|
|
ctx.View.Input.MoveCursorLeft()
|
|
|
|
termui.Render(ctx.View.Input)
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func actionSend(ctx *context.AppContext) {
|
|
|
|
if !ctx.View.Input.IsEmpty() {
|
2016-10-14 17:04:22 +02:00
|
|
|
|
|
|
|
// Clear message before sending, to combat
|
|
|
|
// quick succession of actionSend
|
2016-10-21 21:23:25 +02:00
|
|
|
message := ctx.View.Input.GetText()
|
2016-10-14 17:04:22 +02:00
|
|
|
ctx.View.Input.Clear()
|
2019-09-21 13:39:23 +02:00
|
|
|
termui.Render(ctx.View.Input)
|
2016-10-14 17:04:22 +02:00
|
|
|
|
2019-02-16 22:45:36 +01:00
|
|
|
// Send slash command
|
|
|
|
isCmd, err := ctx.Service.SendCommand(
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
2016-10-14 17:04:22 +02:00
|
|
|
message,
|
2016-09-27 22:05:44 +02:00
|
|
|
)
|
2018-07-21 13:17:37 +02:00
|
|
|
if err != nil {
|
|
|
|
ctx.View.Debug.Println(
|
|
|
|
err.Error(),
|
|
|
|
)
|
|
|
|
}
|
2018-01-26 12:58:31 +01:00
|
|
|
|
2019-02-16 22:45:36 +01:00
|
|
|
// Send message
|
|
|
|
if !isCmd {
|
2019-06-29 11:58:39 +02:00
|
|
|
if ctx.Focus == context.ChatFocus {
|
|
|
|
err := ctx.Service.SendMessage(
|
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
|
|
|
message,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
ctx.View.Debug.Println(
|
|
|
|
err.Error(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.Focus == context.ThreadFocus {
|
|
|
|
err := ctx.Service.SendReply(
|
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
|
|
|
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
|
|
|
|
message,
|
2019-02-16 22:45:36 +01:00
|
|
|
)
|
2019-06-29 11:58:39 +02:00
|
|
|
if err != nil {
|
|
|
|
ctx.View.Debug.Println(
|
|
|
|
err.Error(),
|
|
|
|
)
|
|
|
|
}
|
2019-02-16 22:45:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 12:58:31 +01:00
|
|
|
// Clear notification icon if there is any
|
2018-09-01 15:57:26 +02:00
|
|
|
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
|
|
|
|
if channelItem.Notification {
|
2019-05-11 12:20:52 +02:00
|
|
|
ctx.Service.MarkAsRead(channelItem)
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
|
|
|
|
}
|
2018-01-26 12:58:31 +01:00
|
|
|
termui.Render(ctx.View.Channels)
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-23 12:26:09 +01:00
|
|
|
// actionSearch will search through the channels based on the users
|
|
|
|
// input. A time is implemented to make sure the actual searching
|
|
|
|
// and changing of channels is done when the user's typing is paused.
|
2017-07-15 23:56:39 +02:00
|
|
|
func actionSearch(ctx *context.AppContext, key rune) {
|
2018-03-23 12:26:09 +01:00
|
|
|
actionInput(ctx.View, key)
|
|
|
|
|
2017-07-15 23:56:39 +02:00
|
|
|
go func() {
|
2018-04-01 13:03:28 +02:00
|
|
|
if scrollTimer != nil {
|
|
|
|
scrollTimer.Stop()
|
2017-07-15 23:56:39 +02:00
|
|
|
}
|
|
|
|
|
2018-04-01 13:03:28 +02:00
|
|
|
scrollTimer = time.NewTimer(time.Second / 4)
|
|
|
|
<-scrollTimer.C
|
2017-07-15 23:56:39 +02:00
|
|
|
|
2018-03-23 12:26:09 +01:00
|
|
|
// Only actually search when the time expires
|
2017-07-15 23:56:39 +02:00
|
|
|
term := ctx.View.Input.GetText()
|
|
|
|
ctx.View.Channels.Search(term)
|
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2017-07-15 15:57:59 +02:00
|
|
|
// actionQuit will exit the program by using os.Exit, this is
|
|
|
|
// done because we are using a custom termui EvtStream. Which
|
|
|
|
// we won't be able to call termui.StopLoop() on. See main.go
|
|
|
|
// for the customEvtStream and why this is done.
|
|
|
|
func actionQuit(ctx *context.AppContext) {
|
2017-07-30 15:23:47 +02:00
|
|
|
termbox.Close()
|
2017-07-15 15:57:59 +02:00
|
|
|
os.Exit(0)
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func actionInsertMode(ctx *context.AppContext) {
|
|
|
|
ctx.Mode = context.InsertMode
|
2017-12-02 11:09:01 +01:00
|
|
|
ctx.View.Mode.SetInsertMode()
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func actionCommandMode(ctx *context.AppContext) {
|
|
|
|
ctx.Mode = context.CommandMode
|
2017-12-02 11:09:01 +01:00
|
|
|
ctx.View.Mode.SetCommandMode()
|
2016-09-11 17:55:19 +02:00
|
|
|
}
|
2016-09-25 22:34:02 +02:00
|
|
|
|
2017-07-15 23:56:39 +02:00
|
|
|
func actionSearchMode(ctx *context.AppContext) {
|
|
|
|
ctx.Mode = context.SearchMode
|
2017-12-02 11:09:01 +01:00
|
|
|
ctx.View.Mode.SetSearchMode()
|
2017-07-15 23:56:39 +02:00
|
|
|
}
|
|
|
|
|
2016-09-25 22:34:02 +02:00
|
|
|
func actionGetMessages(ctx *context.AppContext) {
|
2019-03-16 14:42:27 +01:00
|
|
|
msgs, _, err := ctx.Service.GetMessages(
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
2017-12-01 23:52:25 +01:00
|
|
|
ctx.View.Chat.GetMaxItems(),
|
2016-09-27 22:05:44 +02:00
|
|
|
)
|
2018-10-27 14:44:20 +02:00
|
|
|
if err != nil {
|
|
|
|
termbox.Close()
|
|
|
|
log.Println(err)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
2017-12-03 21:43:33 +01:00
|
|
|
|
2018-08-11 12:37:58 +02:00
|
|
|
ctx.View.Chat.SetMessages(msgs)
|
2016-09-27 22:05:44 +02:00
|
|
|
|
2016-09-25 22:34:02 +02:00
|
|
|
termui.Render(ctx.View.Chat)
|
|
|
|
}
|
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// actionMoveCursorUpChannels will execute the actionChangeChannel
|
2018-03-23 12:26:09 +01:00
|
|
|
// function. A timer is implemented to support fast scrolling through
|
2017-12-01 23:52:25 +01:00
|
|
|
// the list without executing the actionChangeChannel event
|
2016-09-27 22:05:44 +02:00
|
|
|
func actionMoveCursorUpChannels(ctx *context.AppContext) {
|
2016-10-29 22:48:27 +02:00
|
|
|
go func() {
|
2018-04-01 13:03:28 +02:00
|
|
|
if scrollTimer != nil {
|
|
|
|
scrollTimer.Stop()
|
2016-10-29 22:48:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx.View.Channels.MoveCursorUp()
|
|
|
|
termui.Render(ctx.View.Channels)
|
|
|
|
|
2018-04-01 13:03:28 +02:00
|
|
|
scrollTimer = time.NewTimer(time.Second / 4)
|
|
|
|
<-scrollTimer.C
|
2016-10-29 22:48:27 +02:00
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// Only actually change channel when the timer expires
|
2016-10-29 22:48:27 +02:00
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}()
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// actionMoveCursorDownChannels will execute the actionChangeChannel
|
2018-03-23 12:26:09 +01:00
|
|
|
// function. A timer is implemented to support fast scrolling through
|
2017-12-01 23:52:25 +01:00
|
|
|
// the list without executing the actionChangeChannel event
|
2016-09-27 22:05:44 +02:00
|
|
|
func actionMoveCursorDownChannels(ctx *context.AppContext) {
|
2016-10-29 22:48:27 +02:00
|
|
|
go func() {
|
2018-04-01 13:03:28 +02:00
|
|
|
if scrollTimer != nil {
|
|
|
|
scrollTimer.Stop()
|
2016-10-29 22:48:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx.View.Channels.MoveCursorDown()
|
|
|
|
termui.Render(ctx.View.Channels)
|
|
|
|
|
2018-04-01 13:03:28 +02:00
|
|
|
scrollTimer = time.NewTimer(time.Second / 4)
|
|
|
|
<-scrollTimer.C
|
2016-10-29 22:48:27 +02:00
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// Only actually change channel when the timer expires
|
2016-10-29 22:48:27 +02:00
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}()
|
2016-09-29 19:09:30 +02:00
|
|
|
}
|
|
|
|
|
2016-10-18 21:11:29 +02:00
|
|
|
func actionMoveCursorTopChannels(ctx *context.AppContext) {
|
|
|
|
ctx.View.Channels.MoveCursorTop()
|
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func actionMoveCursorBottomChannels(ctx *context.AppContext) {
|
|
|
|
ctx.View.Channels.MoveCursorBottom()
|
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}
|
|
|
|
|
2017-12-17 13:48:20 +01:00
|
|
|
func actionSearchNextChannels(ctx *context.AppContext) {
|
|
|
|
ctx.View.Channels.SearchNext()
|
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func actionSearchPrevChannels(ctx *context.AppContext) {
|
|
|
|
ctx.View.Channels.SearchPrev()
|
2016-10-18 21:11:29 +02:00
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}
|
|
|
|
|
2019-05-18 12:40:33 +02:00
|
|
|
func actionJumpChannels(ctx *context.AppContext) {
|
|
|
|
ctx.View.Channels.Jump()
|
|
|
|
actionChangeChannel(ctx)
|
|
|
|
}
|
|
|
|
|
2016-09-29 19:09:30 +02:00
|
|
|
func actionChangeChannel(ctx *context.AppContext) {
|
|
|
|
// Clear messages from Chat pane
|
|
|
|
ctx.View.Chat.ClearMessages()
|
2016-09-27 22:05:44 +02:00
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// Get messages of the SelectedChannel, and get the count of messages
|
|
|
|
// that fit into the Chat component
|
2019-03-16 14:42:27 +01:00
|
|
|
msgs, threads, err := ctx.Service.GetMessages(
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
2017-12-01 23:52:25 +01:00
|
|
|
ctx.View.Chat.GetMaxItems(),
|
2016-09-27 22:05:44 +02:00
|
|
|
)
|
2018-10-27 14:44:20 +02:00
|
|
|
if err != nil {
|
|
|
|
termbox.Close()
|
|
|
|
log.Println(err)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
2016-09-27 22:05:44 +02:00
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// Set messages for the channel
|
2018-08-11 12:37:58 +02:00
|
|
|
ctx.View.Chat.SetMessages(msgs)
|
2017-12-01 23:52:25 +01:00
|
|
|
|
2019-06-29 11:58:39 +02:00
|
|
|
// Set the threads identifiers in the threads pane
|
|
|
|
var haveThreads bool
|
|
|
|
if len(threads) > 0 {
|
|
|
|
haveThreads = true
|
2019-09-07 14:07:27 +02:00
|
|
|
|
|
|
|
// Make the first thread the current Channel
|
|
|
|
ctx.View.Threads.SetChannels(
|
|
|
|
append(
|
|
|
|
[]components.ChannelItem{ctx.View.Channels.GetSelectedChannel()},
|
|
|
|
threads...,
|
|
|
|
),
|
|
|
|
)
|
2019-09-07 14:17:18 +02:00
|
|
|
|
|
|
|
// Reset position of SelectedChannel
|
|
|
|
ctx.View.Threads.MoveCursorTop()
|
2019-06-29 11:58:39 +02:00
|
|
|
}
|
|
|
|
|
2016-09-29 19:09:30 +02:00
|
|
|
// Set channel name for the Chat pane
|
2016-09-28 22:10:04 +02:00
|
|
|
ctx.View.Chat.SetBorderLabel(
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].GetChannelName(),
|
2016-09-28 22:10:04 +02:00
|
|
|
)
|
|
|
|
|
2017-12-01 23:52:25 +01:00
|
|
|
// Clear notification icon if there is any
|
2018-09-01 15:57:26 +02:00
|
|
|
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
|
|
|
|
if channelItem.Notification {
|
2019-05-11 12:20:52 +02:00
|
|
|
ctx.Service.MarkAsRead(channelItem)
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
|
|
|
|
}
|
2016-10-29 23:59:16 +02:00
|
|
|
|
2019-09-07 13:38:19 +02:00
|
|
|
// Redraw grid, necessary when threads and/or debug is set. We will redraw
|
|
|
|
// the grid when there are threads, or we just came from a thread and went
|
|
|
|
// to a channel without threads. Hence the clearing of ChannelItems of
|
|
|
|
// Threads.
|
|
|
|
if haveThreads {
|
|
|
|
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
|
|
|
|
} else if !haveThreads && len(ctx.View.Threads.ChannelItems) > 0 {
|
|
|
|
ctx.View.Threads.SetChannels([]components.ChannelItem{})
|
|
|
|
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
|
|
|
|
} else {
|
2019-09-14 12:34:53 +02:00
|
|
|
termui.Render(ctx.View.Threads)
|
2019-09-07 13:38:19 +02:00
|
|
|
termui.Render(ctx.View.Channels)
|
|
|
|
termui.Render(ctx.View.Chat)
|
|
|
|
}
|
2019-06-15 15:16:08 +02:00
|
|
|
|
2019-06-29 11:58:39 +02:00
|
|
|
// Set focus, necessary to know when replying to thread or chat
|
|
|
|
ctx.Focus = context.ChatFocus
|
2016-09-27 22:05:44 +02:00
|
|
|
}
|
2016-09-30 12:09:03 +02:00
|
|
|
|
2019-04-06 21:08:33 +02:00
|
|
|
func actionChangeThread(ctx *context.AppContext) {
|
|
|
|
// Clear messages from Chat pane
|
|
|
|
ctx.View.Chat.ClearMessages()
|
|
|
|
|
2019-09-07 14:07:27 +02:00
|
|
|
// The first channel in the Thread list is current Channel. Set context
|
|
|
|
// Focus and messages accordingly.
|
|
|
|
var err error
|
|
|
|
msgs := []components.Message{}
|
|
|
|
if ctx.View.Threads.SelectedChannel == 0 {
|
|
|
|
ctx.Focus = context.ChatFocus
|
|
|
|
|
|
|
|
msgs, _, err = ctx.Service.GetMessages(
|
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
|
|
|
ctx.View.Chat.GetMaxItems(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
termbox.Close()
|
|
|
|
log.Println(err)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ctx.Focus = context.ThreadFocus
|
|
|
|
|
|
|
|
msgs, err = ctx.Service.GetMessageByID(
|
|
|
|
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
|
|
|
|
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
termbox.Close()
|
|
|
|
log.Println(err)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
2019-04-06 21:08:33 +02:00
|
|
|
|
|
|
|
// Set messages for the channel
|
|
|
|
ctx.View.Chat.SetMessages(msgs)
|
|
|
|
|
|
|
|
termui.Render(ctx.View.Channels)
|
|
|
|
termui.Render(ctx.View.Threads)
|
|
|
|
termui.Render(ctx.View.Chat)
|
|
|
|
}
|
|
|
|
|
|
|
|
func actionMoveCursorUpThreads(ctx *context.AppContext) {
|
|
|
|
go func() {
|
|
|
|
if scrollTimer != nil {
|
|
|
|
scrollTimer.Stop()
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.View.Threads.MoveCursorUp()
|
|
|
|
termui.Render(ctx.View.Threads)
|
|
|
|
|
|
|
|
scrollTimer = time.NewTimer(time.Second / 4)
|
|
|
|
<-scrollTimer.C
|
|
|
|
|
|
|
|
// Only actually change channel when the timer expires
|
|
|
|
actionChangeThread(ctx)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func actionMoveCursorDownThreads(ctx *context.AppContext) {
|
|
|
|
go func() {
|
|
|
|
if scrollTimer != nil {
|
|
|
|
scrollTimer.Stop()
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.View.Threads.MoveCursorDown()
|
2019-09-07 14:11:51 +02:00
|
|
|
termui.Render(ctx.View.Threads)
|
2019-04-06 21:08:33 +02:00
|
|
|
|
|
|
|
scrollTimer = time.NewTimer(time.Second / 4)
|
|
|
|
<-scrollTimer.C
|
|
|
|
|
|
|
|
// Only actually change thread when the timer expires
|
|
|
|
actionChangeThread(ctx)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2018-04-06 13:42:12 +02:00
|
|
|
// actionNewMessage will set the new message indicator for a channel, and
|
|
|
|
// if configured will also display a desktop notification
|
|
|
|
func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) {
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.MarkAsUnread(ev.Channel)
|
2017-07-15 21:06:49 +02:00
|
|
|
termui.Render(ctx.View.Channels)
|
2018-04-01 13:03:28 +02:00
|
|
|
|
|
|
|
// Terminal bell
|
2017-12-03 20:40:46 +01:00
|
|
|
fmt.Print("\a")
|
2018-04-01 13:03:28 +02:00
|
|
|
|
|
|
|
// Desktop notification
|
2018-04-06 13:42:12 +02:00
|
|
|
if ctx.Config.Notify == config.NotifyMention {
|
2018-09-01 15:57:26 +02:00
|
|
|
if isMention(ctx, ev) {
|
2018-04-06 13:42:12 +02:00
|
|
|
createNotifyMessage(ctx, ev)
|
|
|
|
}
|
|
|
|
} else if ctx.Config.Notify == config.NotifyAll {
|
|
|
|
createNotifyMessage(ctx, ev)
|
2018-04-01 13:03:28 +02:00
|
|
|
}
|
2017-07-15 21:06:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func actionSetPresence(ctx *context.AppContext, channelID string, presence string) {
|
2018-09-01 15:57:26 +02:00
|
|
|
ctx.View.Channels.SetPresence(channelID, presence)
|
2016-09-30 12:09:03 +02:00
|
|
|
termui.Render(ctx.View.Channels)
|
|
|
|
}
|
2016-09-30 16:36:41 +02:00
|
|
|
|
2018-12-25 14:00:39 +01:00
|
|
|
// actionPresenceAll will set the presence of the user list. Because the
|
|
|
|
// requests to the endpoint are rate limited we implement a timeout here.
|
|
|
|
func actionSetPresenceAll(ctx *context.AppContext) {
|
|
|
|
for _, chn := range ctx.Service.Conversations {
|
|
|
|
if chn.IsIM {
|
|
|
|
|
|
|
|
presence, err := ctx.Service.GetUserPresence(chn.User)
|
|
|
|
if err != nil {
|
|
|
|
presence = "away"
|
|
|
|
}
|
|
|
|
ctx.View.Channels.SetPresence(chn.ID, presence)
|
|
|
|
|
|
|
|
termui.Render(ctx.View.Channels)
|
|
|
|
time.Sleep(1200 * time.Millisecond)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-30 16:36:41 +02:00
|
|
|
func actionScrollUpChat(ctx *context.AppContext) {
|
|
|
|
ctx.View.Chat.ScrollUp()
|
|
|
|
termui.Render(ctx.View.Chat)
|
|
|
|
}
|
|
|
|
|
|
|
|
func actionScrollDownChat(ctx *context.AppContext) {
|
|
|
|
ctx.View.Chat.ScrollDown()
|
|
|
|
termui.Render(ctx.View.Chat)
|
|
|
|
}
|
2016-10-29 17:57:58 +02:00
|
|
|
|
2016-10-30 14:26:12 +01:00
|
|
|
func actionHelp(ctx *context.AppContext) {
|
2019-05-29 10:21:51 +02:00
|
|
|
ctx.View.Chat.ClearMessages()
|
2018-10-13 18:34:18 +02:00
|
|
|
ctx.View.Chat.Help(ctx.Usage, ctx.Config)
|
2016-10-30 14:26:12 +01:00
|
|
|
termui.Render(ctx.View.Chat)
|
|
|
|
}
|
|
|
|
|
2016-10-29 17:57:58 +02:00
|
|
|
// GetKeyString will return a string that resembles the key event from
|
|
|
|
// termbox. This is blatanly copied from termui because it is an unexported
|
|
|
|
// function.
|
|
|
|
//
|
|
|
|
// See:
|
|
|
|
// - https://github.com/gizak/termui/blob/a7e3aeef4cdf9fa2edb723b1541cb69b7bb089ea/events.go#L31-L72
|
|
|
|
// - https://github.com/nsf/termbox-go/blob/master/api_common.go
|
|
|
|
func getKeyString(e termbox.Event) string {
|
|
|
|
var ek string
|
|
|
|
|
|
|
|
k := string(e.Ch)
|
|
|
|
pre := ""
|
|
|
|
mod := ""
|
|
|
|
|
|
|
|
if e.Mod == termbox.ModAlt {
|
|
|
|
mod = "M-"
|
|
|
|
}
|
|
|
|
if e.Ch == 0 {
|
|
|
|
if e.Key > 0xFFFF-12 {
|
|
|
|
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
|
|
|
|
} else if e.Key > 0xFFFF-25 {
|
|
|
|
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
|
|
|
|
k = ks[0xFFFF-int(e.Key)-12]
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Key <= 0x7F {
|
|
|
|
pre = "C-"
|
|
|
|
k = string('a' - 1 + int(e.Key))
|
|
|
|
kmap := map[termbox.Key][2]string{
|
|
|
|
termbox.KeyCtrlSpace: {"C-", "<space>"},
|
|
|
|
termbox.KeyBackspace: {"", "<backspace>"},
|
|
|
|
termbox.KeyTab: {"", "<tab>"},
|
|
|
|
termbox.KeyEnter: {"", "<enter>"},
|
|
|
|
termbox.KeyEsc: {"", "<escape>"},
|
|
|
|
termbox.KeyCtrlBackslash: {"C-", "\\"},
|
|
|
|
termbox.KeyCtrlSlash: {"C-", "/"},
|
|
|
|
termbox.KeySpace: {"", "<space>"},
|
|
|
|
termbox.KeyCtrl8: {"C-", "8"},
|
|
|
|
}
|
|
|
|
if sk, ok := kmap[e.Key]; ok {
|
|
|
|
pre = sk[0]
|
|
|
|
k = sk[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ek = pre + mod + k
|
|
|
|
return ek
|
|
|
|
}
|
2018-04-06 13:42:12 +02:00
|
|
|
|
2018-09-01 15:57:26 +02:00
|
|
|
// isMention check if the message event either contains a
|
|
|
|
// mention or is posted on an IM channel.
|
|
|
|
func isMention(ctx *context.AppContext, ev *slack.MessageEvent) bool {
|
|
|
|
channel := ctx.View.Channels.ChannelItems[ctx.View.Channels.FindChannel(ev.Channel)]
|
|
|
|
|
|
|
|
if channel.Type == components.ChannelTypeIM {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mentions have the following format:
|
|
|
|
// <@U12345|erroneousboat>
|
|
|
|
// <@U12345>
|
|
|
|
r := regexp.MustCompile(`\<@(\w+\|*\w+)\>`)
|
|
|
|
matches := r.FindAllString(ev.Text, -1)
|
|
|
|
for _, match := range matches {
|
|
|
|
if strings.Contains(match, ctx.Service.CurrentUserID) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-04-06 13:42:12 +02:00
|
|
|
func createNotifyMessage(ctx *context.AppContext, ev *slack.MessageEvent) {
|
|
|
|
go func() {
|
|
|
|
if notifyTimer != nil {
|
|
|
|
notifyTimer.Stop()
|
|
|
|
}
|
|
|
|
|
2018-09-02 10:14:34 +02:00
|
|
|
// Only actually notify when time expires
|
2018-04-06 13:42:12 +02:00
|
|
|
notifyTimer = time.NewTimer(time.Second * 2)
|
|
|
|
<-notifyTimer.C
|
|
|
|
|
2018-09-01 15:57:26 +02:00
|
|
|
var message string
|
|
|
|
channel := ctx.View.Channels.ChannelItems[ctx.View.Channels.FindChannel(ev.Channel)]
|
|
|
|
switch channel.Type {
|
|
|
|
case components.ChannelTypeChannel:
|
|
|
|
message = fmt.Sprintf("Message received on channel: %s", channel.Name)
|
|
|
|
case components.ChannelTypeGroup:
|
|
|
|
message = fmt.Sprintf("Message received in group: %s", channel.Name)
|
|
|
|
case components.ChannelTypeIM:
|
|
|
|
message = fmt.Sprintf("Message received from: %s", channel.Name)
|
|
|
|
default:
|
|
|
|
message = fmt.Sprintf("Message received from: %s", channel.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Notify.Push("slack-term", message, "", notificator.UR_NORMAL)
|
2018-04-06 13:42:12 +02:00
|
|
|
}()
|
|
|
|
}
|