From 0689e21870ad4ecfa0361b9f7c5dd47da80f535d Mon Sep 17 00:00:00 2001 From: ayntgl <90600421+ayntgl@users.noreply.github.com> Date: Thu, 25 Aug 2022 22:39:04 +0530 Subject: [PATCH] refactor(ui): segregate widgets into individual files (#189) --- go.mod | 20 +- go.sum | 40 ++- ui/app.go | 9 +- ui/{channels.go => channels_tree.go} | 59 +++- ui/{guilds.go => guilds_tree.go} | 60 +--- ui/{login.go => login_form.go} | 0 ui/markdown.go | 20 -- ui/message_actions_list.go | 219 +++++++++++++ ui/message_input.go | 138 ++++++++ ui/messages.go | 463 --------------------------- ui/messages_panel.go | 128 ++++++++ ui/util.go | 28 +- 12 files changed, 610 insertions(+), 574 deletions(-) rename ui/{channels.go => channels_tree.go} (50%) rename ui/{guilds.go => guilds_tree.go} (50%) rename ui/{login.go => login_form.go} (100%) delete mode 100644 ui/markdown.go create mode 100644 ui/message_actions_list.go create mode 100644 ui/message_input.go delete mode 100644 ui/messages.go create mode 100644 ui/messages_panel.go diff --git a/go.mod b/go.mod index 4da79cf..80bbecc 100644 --- a/go.mod +++ b/go.mod @@ -3,29 +3,29 @@ module github.com/ayntgl/discordo go 1.18 require ( - github.com/BurntSushi/toml v1.1.0 + github.com/BurntSushi/toml v1.2.0 github.com/alecthomas/kong v0.6.1 github.com/atotto/clipboard v0.1.4 github.com/diamondburned/arikawa/v3 v3.0.0 - github.com/gdamore/tcell/v2 v2.5.1 - github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 + github.com/gdamore/tcell/v2 v2.5.3 + github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/zalando/go-keyring v0.2.1 ) require ( github.com/alessio/shellescape v1.4.1 // indirect - github.com/danieljoos/wincred v1.1.0 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect github.com/gdamore/encoding v1.0.0 // indirect - github.com/godbus/dbus/v5 v5.0.6 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gorilla/schema v1.2.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect - golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect + github.com/rivo/uniseg v0.3.4 // indirect + golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect + golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect ) diff --git a/go.sum b/go.sum index 527d59c..8c16520 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA= github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I= github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= @@ -8,8 +8,9 @@ github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVK github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,15 +18,16 @@ github.com/diamondburned/arikawa/v3 v3.0.0 h1:VbdX1DtrBLE752IJftZHInVy6v8I3T8vhN github.com/diamondburned/arikawa/v3 v3.0.0/go.mod h1:5jBSNnp82Z/EhsKa6Wk9FsOqSxfVkNZDTDBPOj47LpY= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= -github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= -github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= -github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0= +github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -34,15 +36,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc= -github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96 h1:O435d1KIgG6KxpP7NDdmj7SdaLIzq4F+PG8ZB/BHC4c= +github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96/go.mod h1:hyzpnqn4KWzZopTEjL1AxvlzOLMH1IuKo4lTw6vyOQc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= +github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= @@ -50,25 +54,27 @@ github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4es golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 h1:TyKJRhyo17yWxOMCTHKWrc5rddHORMlnZ/j57umaUd8= +golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= +golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ui/app.go b/ui/app.go index bbe74a8..70f74f4 100644 --- a/ui/app.go +++ b/ui/app.go @@ -21,10 +21,8 @@ type App struct { MessagesPanel *MessagesPanel MessageInputField *MessageInput - Config *config.Config - State *state.State - SelectedChannel *discord.Channel - SelectedMessage int + Config *config.Config + State *state.State } func NewApp(token string, c *config.Config) *App { @@ -43,7 +41,6 @@ func NewApp(token string, c *config.Config) *App { // The official client sets the compress field as false. Compress: false, })), - SelectedMessage: -1, } app.GuildsTree = NewGuildsTree(app) @@ -193,7 +190,7 @@ func (app *App) onStateGuildDelete(g *gateway.GuildDeleteEvent) { } func (app *App) onStateMessageCreate(m *gateway.MessageCreateEvent) { - if app.SelectedChannel != nil && app.SelectedChannel.ID == m.ChannelID { + if app.ChannelsTree.SelectedChannel != nil && app.ChannelsTree.SelectedChannel.ID == m.ChannelID { _, err := app.MessagesPanel.Write(buildMessage(app, m.Message)) if err != nil { return diff --git a/ui/channels.go b/ui/channels_tree.go similarity index 50% rename from ui/channels.go rename to ui/channels_tree.go index 4c15269..b9a6fb2 100644 --- a/ui/channels.go +++ b/ui/channels_tree.go @@ -8,6 +8,8 @@ import ( type ChannelsTree struct { *tview.TreeView app *App + + SelectedChannel *discord.Channel } func NewChannelsTree(app *App) *ChannelsTree { @@ -29,8 +31,8 @@ func NewChannelsTree(app *App) *ChannelsTree { } func (ct *ChannelsTree) onSelected(node *tview.TreeNode) { - ct.app.SelectedChannel = nil - ct.app.SelectedMessage = -1 + ct.SelectedChannel = nil + ct.app.MessagesPanel.SelectedMessage = -1 ct.app.MessagesPanel. Highlight(). Clear(). @@ -49,7 +51,7 @@ func (ct *ChannelsTree) onSelected(node *tview.TreeNode) { return } - ct.app.SelectedChannel = c + ct.SelectedChannel = c ct.app.SetFocus(ct.app.MessageInputField) title := channelToString(*c) @@ -75,3 +77,54 @@ func (ct *ChannelsTree) onSelected(node *tview.TreeNode) { ct.app.MessagesPanel.ScrollToEnd() }() } + +func (ct *ChannelsTree) createChannelNode(c discord.Channel) *tview.TreeNode { + channelNode := tview.NewTreeNode(channelToString(c)) + channelNode.SetReference(c.ID) + + return channelNode +} + +func (ct *ChannelsTree) createOrphanChannelNodes(rootNode *tview.TreeNode, cs []discord.Channel) { + for _, c := range cs { + if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (!c.ParentID.IsValid()) { + rootNode.AddChild(ct.createChannelNode(c)) + } + } +} + +func (ct *ChannelsTree) createCategoryChannelNodes(rootNode *tview.TreeNode, cs []discord.Channel) { +CATEGORY: + for _, c := range cs { + if c.Type == discord.GuildCategory { + for _, nestedChannel := range cs { + if nestedChannel.ParentID == c.ID { + rootNode.AddChild(ct.createChannelNode(c)) + continue CATEGORY + } + } + + rootNode.AddChild(ct.createChannelNode(c)) + } + } +} + +func (ct *ChannelsTree) createChildrenChannelNodes(rootNode *tview.TreeNode, cs []discord.Channel) { + for _, c := range cs { + if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID.IsValid()) { + var parentNode *tview.TreeNode + rootNode.Walk(func(node, _ *tview.TreeNode) bool { + if node.GetReference() == c.ParentID { + parentNode = node + return false + } + + return true + }) + + if parentNode != nil { + parentNode.AddChild(ct.createChannelNode(c)) + } + } + } +} diff --git a/ui/guilds.go b/ui/guilds_tree.go similarity index 50% rename from ui/guilds.go rename to ui/guilds_tree.go index b1ea11f..7e489df 100644 --- a/ui/guilds.go +++ b/ui/guilds_tree.go @@ -3,7 +3,7 @@ package ui import ( "sort" - dsc "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/discord" "github.com/rivo/tview" ) @@ -34,8 +34,8 @@ func NewGuildsTree(app *App) *GuildsTree { } func (gt *GuildsTree) onSelected(node *tview.TreeNode) { - gt.app.SelectedChannel = nil - gt.app.SelectedMessage = -1 + gt.app.ChannelsTree.SelectedChannel = nil + gt.app.MessagesPanel.SelectedMessage = -1 rootNode := gt.app.ChannelsTree.GetRoot() rootNode.ClearChildren() gt.app.MessagesPanel. @@ -63,12 +63,10 @@ func (gt *GuildsTree) onSelected(node *tview.TreeNode) { }) for _, c := range cs { - channelNode := tview.NewTreeNode(c.Name) - channelNode.SetReference(c.ID) - rootNode.AddChild(channelNode) + rootNode.AddChild(gt.app.ChannelsTree.createChannelNode(c)) } } else { // Guild - cs, err := gt.app.State.Cabinet.Channels(ref.(dsc.GuildID)) + cs, err := gt.app.State.Cabinet.Channels(ref.(discord.GuildID)) if err != nil { return } @@ -77,51 +75,9 @@ func (gt *GuildsTree) onSelected(node *tview.TreeNode) { return cs[i].Position < cs[j].Position }) - for _, c := range cs { - if (c.Type == dsc.GuildText || c.Type == dsc.GuildNews) && (!c.ParentID.IsValid()) { - channelNode := tview.NewTreeNode(channelToString(c)) - channelNode.SetReference(c.ID) - rootNode.AddChild(channelNode) - } - } - - CATEGORY: - for _, c := range cs { - if c.Type == dsc.GuildCategory { - for _, nestedChannel := range cs { - if nestedChannel.ParentID == c.ID { - channelNode := tview.NewTreeNode(c.Name) - channelNode.SetReference(c.ID) - rootNode.AddChild(channelNode) - continue CATEGORY - } - } - - channelNode := tview.NewTreeNode(channelToString(c)) - channelNode.SetReference(c.ID) - rootNode.AddChild(channelNode) - } - } - - for _, c := range cs { - if (c.Type == dsc.GuildText || c.Type == dsc.GuildNews) && (c.ParentID.IsValid()) { - var parentNode *tview.TreeNode - rootNode.Walk(func(node, _ *tview.TreeNode) bool { - if node.GetReference() == c.ParentID { - parentNode = node - return false - } - - return true - }) - - if parentNode != nil { - channelNode := tview.NewTreeNode(channelToString(c)) - channelNode.SetReference(c.ID) - parentNode.AddChild(channelNode) - } - } - } + gt.app.ChannelsTree.createOrphanChannelNodes(rootNode, cs) + gt.app.ChannelsTree.createCategoryChannelNodes(rootNode, cs) + gt.app.ChannelsTree.createChildrenChannelNodes(rootNode, cs) } gt.app.ChannelsTree.SetCurrentNode(rootNode) diff --git a/ui/login.go b/ui/login_form.go similarity index 100% rename from ui/login.go rename to ui/login_form.go diff --git a/ui/markdown.go b/ui/markdown.go deleted file mode 100644 index d698f42..0000000 --- a/ui/markdown.go +++ /dev/null @@ -1,20 +0,0 @@ -package ui - -import "regexp" - -var ( - boldRegex = regexp.MustCompile(`(?ms)\*\*(.*?)\*\*`) - italicRegex = regexp.MustCompile(`(?ms)\*(.*?)\*`) - underlineRegex = regexp.MustCompile(`(?ms)__(.*?)__`) - strikeThroughRegex = regexp.MustCompile(`(?ms)~~(.*?)~~`) -) - -func parseMarkdown(md string) string { - var res string - res = boldRegex.ReplaceAllString(md, "[::b]$1[::-]") - res = italicRegex.ReplaceAllString(res, "[::i]$1[::-]") - res = underlineRegex.ReplaceAllString(res, "[::u]$1[::-]") - res = strikeThroughRegex.ReplaceAllString(res, "[::s]$1[::-]") - - return res -} diff --git a/ui/message_actions_list.go b/ui/message_actions_list.go new file mode 100644 index 0000000..d70871a --- /dev/null +++ b/ui/message_actions_list.go @@ -0,0 +1,219 @@ +package ui + +import ( + "io" + "net/http" + "os" + "path/filepath" + "regexp" + + "github.com/atotto/clipboard" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/rivo/tview" + "github.com/skratchdot/open-golang/open" +) + +var linkRegex = regexp.MustCompile("https?://.+") + +type MessageActionsList struct { + *tview.List + app *App + message *discord.Message +} + +func NewMessageActionsList(app *App, m *discord.Message) *MessageActionsList { + mal := &MessageActionsList{ + List: tview.NewList(), + app: app, + message: m, + } + + mal.ShowSecondaryText(false) + mal.SetDoneFunc(func() { + app. + SetRoot(app.MainFlex, true). + SetFocus(app.MessagesPanel) + }) + + // If the client user has the `SEND_MESSAGES` permission, add "Reply" and "Mention Reply" actions. + if hasPermission(app.State, app.ChannelsTree.SelectedChannel.ID, discord.PermissionSendMessages) { + mal.AddItem("Reply", "", 'r', mal.replyAction) + mal.AddItem("Mention Reply", "", 'R', mal.mentionReplyAction) + } + + // If the referenced message exists, add a new action to select the reply. + if m.ReferencedMessage != nil { + mal.AddItem("Select Reply", "", 'm', mal.selectReplyAction) + } + + // If the content of the message contains link(s), add the appropriate actions to the list. + links := linkRegex.FindAllString(m.Content, -1) + if len(links) != 0 { + mal.AddItem("Open Link", "", 'l', func() { + for _, l := range links { + go open.Run(l) + } + + app.SetRoot(app.MainFlex, true) + app.SetFocus(app.MessagesPanel) + }) + } + + // If the message contains attachments, add the appropriate actions to the actions list. + if len(m.Attachments) != 0 { + mal.AddItem("Open Attachment", "", 'o', mal.openAttachmentAction) + mal.AddItem("Download Attachment", "", 'd', mal.downloadAttachmentAction) + } + + // If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message. + if hasPermission(app.State, app.ChannelsTree.SelectedChannel.ID, discord.PermissionManageMessages) { + mal.AddItem("Delete", "", 'd', mal.deleteAction) + } + + mal.AddItem("Copy Content", "", 'c', mal.copyContentAction) + mal.AddItem("Copy ID", "", 'i', mal.copyIDAction) + + mal.SetTitle("Press the Escape key to close") + mal.SetTitleAlign(tview.AlignLeft) + mal.SetBorder(true) + mal.SetBorderPadding(0, 0, 1, 1) + + return mal +} + +func (mal *MessageActionsList) replyAction() { + mal.app.MessageInputField.SetTitle("Replying to " + mal.message.Author.Tag()) + + mal.app. + SetRoot(mal.app.MainFlex, true). + SetFocus(mal.app.MessageInputField) +} + +func (mal *MessageActionsList) mentionReplyAction() { + mal.app.MessageInputField.SetTitle("[@] Replying to " + mal.message.Author.Tag()) + + mal.app. + SetRoot(mal.app.MainFlex, true). + SetFocus(mal.app.MessageInputField) +} + +func (mal *MessageActionsList) selectReplyAction() { + ms, err := mal.app.State.Cabinet.Messages(mal.message.ChannelID) + if err != nil { + return + } + + mal.app.MessagesPanel.SelectedMessage, _ = findMessageByID(ms, mal.message.ReferencedMessage.ID) + mal.app.MessagesPanel. + Highlight(mal.message.ReferencedMessage.ID.String()). + ScrollToHighlight() + + mal.app. + SetRoot(mal.app.MainFlex, true). + SetFocus(mal.app.MessagesPanel) +} + +func (mal *MessageActionsList) openAttachmentAction() { + for _, a := range mal.message.Attachments { + cacheDirPath, _ := os.UserCacheDir() + f, err := os.Create(filepath.Join(cacheDirPath, a.Filename)) + if err != nil { + return + } + defer f.Close() + + resp, err := http.Get(a.URL) + if err != nil { + return + } + + d, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + f.Write(d) + go open.Run(f.Name()) + } + + mal.app. + SetRoot(mal.app.MainFlex, true). + SetFocus(mal.app.MessagesPanel) +} + +func (mal *MessageActionsList) downloadAttachmentAction() { + for _, a := range mal.message.Attachments { + f, err := os.Create(filepath.Join(mal.app.Config.AttachmentDownloadsDir, a.Filename)) + if err != nil { + return + } + defer f.Close() + + resp, err := http.Get(a.URL) + if err != nil { + return + } + + d, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + f.Write(d) + } + + mal.app. + SetRoot(mal.app.MainFlex, true). + SetFocus(mal.app.MessagesPanel) +} + +func (mal *MessageActionsList) deleteAction() { + mal.app.MessagesPanel.Clear() + + err := mal.app.State.MessageRemove(mal.message.ChannelID, mal.message.ID) + if err != nil { + return + } + + err = mal.app.State.DeleteMessage(mal.message.ChannelID, mal.message.ID, "Unknown") + if err != nil { + return + } + + // The returned slice will be sorted from latest to oldest. + ms, err := mal.app.State.Cabinet.Messages(mal.message.ChannelID) + if err != nil { + return + } + + for i := len(ms) - 1; i >= 0; i-- { + _, err = mal.app.MessagesPanel.Write(buildMessage(mal.app, ms[i])) + if err != nil { + return + } + } + + mal.app. + SetRoot(mal.app.MainFlex, true). + SetFocus(mal.app.MessagesPanel) +} + +func (mal *MessageActionsList) copyContentAction() { + err := clipboard.WriteAll(mal.message.Content) + if err != nil { + return + } + + mal.app.SetRoot(mal.app.MainFlex, true) + mal.app.SetFocus(mal.app.MessagesPanel) +} + +func (mal *MessageActionsList) copyIDAction() { + err := clipboard.WriteAll(mal.message.ID.String()) + if err != nil { + return + } + + mal.app.SetRoot(mal.app.MainFlex, true) + mal.app.SetFocus(mal.app.MessagesPanel) +} diff --git a/ui/message_input.go b/ui/message_input.go new file mode 100644 index 0000000..d92859b --- /dev/null +++ b/ui/message_input.go @@ -0,0 +1,138 @@ +package ui + +import ( + "io" + "os" + "os/exec" + "strings" + + "github.com/atotto/clipboard" + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/utils/json/option" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type MessageInput struct { + *tview.InputField + app *App +} + +func NewMessageInput(app *App) *MessageInput { + mi := &MessageInput{ + InputField: tview.NewInputField(), + app: app, + } + + mi.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor) + mi.SetPlaceholder("Message...") + mi.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor)) + mi.SetInputCapture(mi.onInputCapture) + + mi.SetTitleAlign(tview.AlignLeft) + mi.SetBorder(true) + mi.SetBorderPadding(0, 0, 1, 1) + + return mi +} + +func (mi *MessageInput) onInputCapture(e *tcell.EventKey) *tcell.EventKey { + switch e.Name() { + case "Enter": + if mi.app.ChannelsTree.SelectedChannel == nil { + return nil + } + + t := strings.TrimSpace(mi.app.MessageInputField.GetText()) + if t == "" { + return nil + } + + ms, err := mi.app.State.Messages(mi.app.ChannelsTree.SelectedChannel.ID, mi.app.Config.MessagesLimit) + if err != nil { + return nil + } + + if len(mi.app.MessagesPanel.GetHighlights()) != 0 { + mID, err := discord.ParseSnowflake(mi.app.MessagesPanel.GetHighlights()[0]) + if err != nil { + return nil + } + + _, m := findMessageByID(ms, discord.MessageID(mID)) + d := api.SendMessageData{ + Content: t, + Reference: m.Reference, + AllowedMentions: &api.AllowedMentions{RepliedUser: option.False}, + } + + // If the title of the message InputField widget has "[@]" as a prefix, send the message as a reply and mention the replied user. + if strings.HasPrefix(mi.app.MessageInputField.GetTitle(), "[@]") { + d.AllowedMentions.RepliedUser = option.True + } + + go mi.app.State.SendMessageComplex(m.ChannelID, d) + + mi.app.MessagesPanel.SelectedMessage = -1 + mi.app.MessagesPanel.Highlight() + + mi.app.MessageInputField.SetTitle("") + } else { + go mi.app.State.SendMessage(mi.app.ChannelsTree.SelectedChannel.ID, t) + } + + mi.app.MessageInputField.SetText("") + + return nil + case "Ctrl+V": + text, _ := clipboard.ReadAll() + text = mi.app.MessageInputField.GetText() + text + mi.app.MessageInputField.SetText(text) + + return nil + case "Esc": + mi.app.MessageInputField. + SetText(""). + SetTitle("") + mi.app.SetFocus(mi.app.MainFlex) + + mi.app.MessagesPanel.SelectedMessage = -1 + mi.app.MessagesPanel.Highlight() + + return nil + case mi.app.Config.Keys.OpenExternalEditor: + e := os.Getenv("EDITOR") + if e == "" { + return nil + } + + f, err := os.CreateTemp(os.TempDir(), "discordo-*.md") + if err != nil { + return nil + } + defer os.Remove(f.Name()) + + cmd := exec.Command(e, f.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + + mi.app.Suspend(func() { + err = cmd.Run() + if err != nil { + return + } + }) + + b, err := io.ReadAll(f) + if err != nil { + return nil + } + + mi.app.MessageInputField.SetText(string(b)) + + return nil + } + + return e +} diff --git a/ui/messages.go b/ui/messages.go deleted file mode 100644 index 14cd887..0000000 --- a/ui/messages.go +++ /dev/null @@ -1,463 +0,0 @@ -package ui - -import ( - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - - "github.com/atotto/clipboard" - "github.com/diamondburned/arikawa/v3/api" - "github.com/diamondburned/arikawa/v3/discord" - "github.com/diamondburned/arikawa/v3/utils/json/option" - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" - "github.com/skratchdot/open-golang/open" -) - -var linkRegex = regexp.MustCompile("https?://.+") - -type MessagesPanel struct { - *tview.TextView - app *App -} - -func NewMessagesPanel(app *App) *MessagesPanel { - mtv := &MessagesPanel{ - TextView: tview.NewTextView(), - app: app, - } - - mtv.SetDynamicColors(true) - mtv.SetRegions(true) - mtv.SetWordWrap(true) - mtv.SetInputCapture(mtv.onInputCapture) - mtv.SetChangedFunc(func() { - mtv.app.Draw() - }) - - mtv.SetTitleAlign(tview.AlignLeft) - mtv.SetBorder(true) - mtv.SetBorderPadding(0, 0, 1, 1) - - return mtv -} - -func (mp *MessagesPanel) onInputCapture(e *tcell.EventKey) *tcell.EventKey { - if mp.app.SelectedChannel == nil { - return nil - } - - // Messages should return messages ordered from latest to earliest. - ms, err := mp.app.State.Cabinet.Messages(mp.app.SelectedChannel.ID) - if err != nil || len(ms) == 0 { - return nil - } - - switch e.Name() { - case mp.app.Config.Keys.SelectPreviousMessage: - // If there are no highlighted regions, select the latest (last) message in the messages panel. - if len(mp.app.MessagesPanel.GetHighlights()) == 0 { - mp.app.SelectedMessage = 0 - } else { - // If the selected message is the oldest (first) message, select the latest (last) message in the messages panel. - if mp.app.SelectedMessage == len(ms)-1 { - mp.app.SelectedMessage = 0 - } else { - mp.app.SelectedMessage++ - } - } - - mp.app.MessagesPanel. - Highlight(ms[mp.app.SelectedMessage].ID.String()). - ScrollToHighlight() - return nil - case mp.app.Config.Keys.SelectNextMessage: - // If there are no highlighted regions, select the latest (last) message in the messages panel. - if len(mp.app.MessagesPanel.GetHighlights()) == 0 { - mp.app.SelectedMessage = 0 - } else { - // If the selected message is the latest (last) message, select the oldest (first) message in the messages panel. - if mp.app.SelectedMessage == 0 { - mp.app.SelectedMessage = len(ms) - 1 - } else { - mp.app.SelectedMessage-- - } - } - - mp.app.MessagesPanel. - Highlight(ms[mp.app.SelectedMessage].ID.String()). - ScrollToHighlight() - return nil - case mp.app.Config.Keys.SelectFirstMessage: - mp.app.SelectedMessage = len(ms) - 1 - mp.app.MessagesPanel. - Highlight(ms[mp.app.SelectedMessage].ID.String()). - ScrollToHighlight() - return nil - case mp.app.Config.Keys.SelectLastMessage: - mp.app.SelectedMessage = 0 - mp.app.MessagesPanel. - Highlight(ms[mp.app.SelectedMessage].ID.String()). - ScrollToHighlight() - return nil - case mp.app.Config.Keys.OpenMessageActionsList: - hs := mp.app.MessagesPanel.GetHighlights() - if len(hs) == 0 { - return nil - } - - mID, err := discord.ParseSnowflake(hs[0]) - if err != nil { - return nil - } - - _, m := findMessageByID(ms, discord.MessageID(mID)) - if m == nil { - return nil - } - - actionsList := NewMessageActionsList(mp.app, m) - mp.app.SetRoot(actionsList, true) - return nil - case "Esc": - mp.app.SelectedMessage = -1 - mp.app.SetFocus(mp.app.MainFlex) - mp.app.MessagesPanel. - Clear(). - Highlight(). - SetTitle("") - return nil - } - - return e -} - -type MessageActionsList struct { - *tview.List - app *App - message *discord.Message -} - -func NewMessageActionsList(app *App, m *discord.Message) *MessageActionsList { - mal := &MessageActionsList{ - List: tview.NewList(), - app: app, - message: m, - } - - mal.ShowSecondaryText(false) - mal.SetDoneFunc(func() { - app. - SetRoot(app.MainFlex, true). - SetFocus(app.MessagesPanel) - }) - - // If the client user has the `SEND_MESSAGES` permission, add "Reply" and "Mention Reply" actions. - if hasPermission(app.State, app.SelectedChannel.ID, discord.PermissionSendMessages) { - mal.AddItem("Reply", "", 'r', mal.replyAction) - mal.AddItem("Mention Reply", "", 'R', mal.mentionReplyAction) - } - - // If the referenced message exists, add a new action to select the reply. - if m.ReferencedMessage != nil { - mal.AddItem("Select Reply", "", 'm', mal.selectReplyAction) - } - - // If the content of the message contains link(s), add the appropriate actions to the list. - links := linkRegex.FindAllString(m.Content, -1) - if len(links) != 0 { - mal.AddItem("Open Link", "", 'l', func() { - for _, l := range links { - go open.Run(l) - } - - app.SetRoot(app.MainFlex, true) - app.SetFocus(app.MessagesPanel) - }) - } - - // If the message contains attachments, add the appropriate actions to the actions list. - if len(m.Attachments) != 0 { - mal.AddItem("Open Attachment", "", 'o', mal.openAttachmentAction) - mal.AddItem("Download Attachment", "", 'd', mal.downloadAttachmentAction) - } - - // If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message. - if hasPermission(app.State, app.SelectedChannel.ID, discord.PermissionManageMessages) { - mal.AddItem("Delete", "", 'd', mal.deleteAction) - } - - mal.AddItem("Copy Content", "", 'c', mal.copyContentAction) - mal.AddItem("Copy ID", "", 'i', mal.copyIDAction) - - mal.SetTitle("Press the Escape key to close") - mal.SetTitleAlign(tview.AlignLeft) - mal.SetBorder(true) - mal.SetBorderPadding(0, 0, 1, 1) - - return mal -} - -func (mal *MessageActionsList) replyAction() { - mal.app.MessageInputField.SetTitle("Replying to " + mal.message.Author.Tag()) - - mal.app. - SetRoot(mal.app.MainFlex, true). - SetFocus(mal.app.MessageInputField) -} - -func (mal *MessageActionsList) mentionReplyAction() { - mal.app.MessageInputField.SetTitle("[@] Replying to " + mal.message.Author.Tag()) - - mal.app. - SetRoot(mal.app.MainFlex, true). - SetFocus(mal.app.MessageInputField) -} - -func (mal *MessageActionsList) selectReplyAction() { - ms, err := mal.app.State.Cabinet.Messages(mal.message.ChannelID) - if err != nil { - return - } - - mal.app.SelectedMessage, _ = findMessageByID(ms, mal.message.ReferencedMessage.ID) - mal.app.MessagesPanel. - Highlight(mal.message.ReferencedMessage.ID.String()). - ScrollToHighlight() - - mal.app. - SetRoot(mal.app.MainFlex, true). - SetFocus(mal.app.MessagesPanel) -} - -func (mal *MessageActionsList) openAttachmentAction() { - for _, a := range mal.message.Attachments { - cacheDirPath, _ := os.UserCacheDir() - f, err := os.Create(filepath.Join(cacheDirPath, a.Filename)) - if err != nil { - return - } - defer f.Close() - - resp, err := http.Get(a.URL) - if err != nil { - return - } - - d, err := io.ReadAll(resp.Body) - if err != nil { - return - } - - f.Write(d) - go open.Run(f.Name()) - } - - mal.app. - SetRoot(mal.app.MainFlex, true). - SetFocus(mal.app.MessagesPanel) -} - -func (mal *MessageActionsList) downloadAttachmentAction() { - for _, a := range mal.message.Attachments { - f, err := os.Create(filepath.Join(mal.app.Config.AttachmentDownloadsDir, a.Filename)) - if err != nil { - return - } - defer f.Close() - - resp, err := http.Get(a.URL) - if err != nil { - return - } - - d, err := io.ReadAll(resp.Body) - if err != nil { - return - } - - f.Write(d) - } - - mal.app. - SetRoot(mal.app.MainFlex, true). - SetFocus(mal.app.MessagesPanel) -} - -func (mal *MessageActionsList) deleteAction() { - mal.app.MessagesPanel.Clear() - - err := mal.app.State.MessageRemove(mal.message.ChannelID, mal.message.ID) - if err != nil { - return - } - - err = mal.app.State.DeleteMessage(mal.message.ChannelID, mal.message.ID, "Unknown") - if err != nil { - return - } - - // The returned slice will be sorted from latest to oldest. - ms, err := mal.app.State.Cabinet.Messages(mal.message.ChannelID) - if err != nil { - return - } - - for i := len(ms) - 1; i >= 0; i-- { - _, err = mal.app.MessagesPanel.Write(buildMessage(mal.app, ms[i])) - if err != nil { - return - } - } - - mal.app. - SetRoot(mal.app.MainFlex, true). - SetFocus(mal.app.MessagesPanel) -} - -func (mal *MessageActionsList) copyContentAction() { - err := clipboard.WriteAll(mal.message.Content) - if err != nil { - return - } - - mal.app.SetRoot(mal.app.MainFlex, true) - mal.app.SetFocus(mal.app.MessagesPanel) -} - -func (mal *MessageActionsList) copyIDAction() { - err := clipboard.WriteAll(mal.message.ID.String()) - if err != nil { - return - } - - mal.app.SetRoot(mal.app.MainFlex, true) - mal.app.SetFocus(mal.app.MessagesPanel) -} - -type MessageInput struct { - *tview.InputField - app *App -} - -func NewMessageInput(app *App) *MessageInput { - mi := &MessageInput{ - InputField: tview.NewInputField(), - app: app, - } - - mi.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor) - mi.SetPlaceholder("Message...") - mi.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor)) - - mi.SetTitleAlign(tview.AlignLeft) - mi.SetBorder(true) - mi.SetBorderPadding(0, 0, 1, 1) - mi.SetInputCapture(mi.onInputCapture) - - return mi -} - -func (mi *MessageInput) onInputCapture(e *tcell.EventKey) *tcell.EventKey { - switch e.Name() { - case "Enter": - if mi.app.SelectedChannel == nil { - return nil - } - - t := strings.TrimSpace(mi.app.MessageInputField.GetText()) - if t == "" { - return nil - } - - ms, err := mi.app.State.Messages(mi.app.SelectedChannel.ID, mi.app.Config.MessagesLimit) - if err != nil { - return nil - } - - if len(mi.app.MessagesPanel.GetHighlights()) != 0 { - mID, err := discord.ParseSnowflake(mi.app.MessagesPanel.GetHighlights()[0]) - if err != nil { - return nil - } - - _, m := findMessageByID(ms, discord.MessageID(mID)) - d := api.SendMessageData{ - Content: t, - Reference: m.Reference, - AllowedMentions: &api.AllowedMentions{RepliedUser: option.False}, - } - - // If the title of the message InputField widget has "[@]" as a prefix, send the message as a reply and mention the replied user. - if strings.HasPrefix(mi.app.MessageInputField.GetTitle(), "[@]") { - d.AllowedMentions.RepliedUser = option.True - } - - go mi.app.State.SendMessageComplex(m.ChannelID, d) - - mi.app.SelectedMessage = -1 - mi.app.MessagesPanel.Highlight() - - mi.app.MessageInputField.SetTitle("") - } else { - go mi.app.State.SendMessage(mi.app.SelectedChannel.ID, t) - } - - mi.app.MessageInputField.SetText("") - - return nil - case "Ctrl+V": - text, _ := clipboard.ReadAll() - text = mi.app.MessageInputField.GetText() + text - mi.app.MessageInputField.SetText(text) - - return nil - case "Esc": - mi.app.MessageInputField. - SetText(""). - SetTitle("") - mi.app.SetFocus(mi.app.MainFlex) - - mi.app.SelectedMessage = -1 - mi.app.MessagesPanel.Highlight() - - return nil - case mi.app.Config.Keys.OpenExternalEditor: - e := os.Getenv("EDITOR") - if e == "" { - return nil - } - - f, err := os.CreateTemp(os.TempDir(), "discordo-*.md") - if err != nil { - return nil - } - defer os.Remove(f.Name()) - - cmd := exec.Command(e, f.Name()) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - - mi.app.Suspend(func() { - err = cmd.Run() - if err != nil { - return - } - }) - - b, err := io.ReadAll(f) - if err != nil { - return nil - } - - mi.app.MessageInputField.SetText(string(b)) - - return nil - } - - return e -} diff --git a/ui/messages_panel.go b/ui/messages_panel.go new file mode 100644 index 0000000..902eaab --- /dev/null +++ b/ui/messages_panel.go @@ -0,0 +1,128 @@ +package ui + +import ( + "github.com/diamondburned/arikawa/v3/discord" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type MessagesPanel struct { + *tview.TextView + app *App + // The index of the currently selected message. + SelectedMessage int +} + +func NewMessagesPanel(app *App) *MessagesPanel { + mp := &MessagesPanel{ + TextView: tview.NewTextView(), + app: app, + // Negative index indicates that there is no currently selected message. + SelectedMessage: -1, + } + + mp.SetDynamicColors(true) + mp.SetRegions(true) + mp.SetWordWrap(true) + mp.SetInputCapture(mp.onInputCapture) + mp.SetChangedFunc(func() { + mp.app.Draw() + }) + + mp.SetTitle("Messages") + mp.SetTitleAlign(tview.AlignLeft) + mp.SetBorder(true) + mp.SetBorderPadding(0, 0, 1, 1) + + return mp +} + +func (mp *MessagesPanel) onInputCapture(e *tcell.EventKey) *tcell.EventKey { + if mp.app.ChannelsTree.SelectedChannel == nil { + return nil + } + + // Messages should return messages ordered from latest to earliest. + ms, err := mp.app.State.Cabinet.Messages(mp.app.ChannelsTree.SelectedChannel.ID) + if err != nil || len(ms) == 0 { + return nil + } + + switch e.Name() { + case mp.app.Config.Keys.SelectPreviousMessage: + // If there are no highlighted regions, select the latest (last) message in the messages panel. + if len(mp.app.MessagesPanel.GetHighlights()) == 0 { + mp.SelectedMessage = 0 + } else { + // If the selected message is the oldest (first) message, select the latest (last) message in the messages panel. + if mp.SelectedMessage == len(ms)-1 { + mp.SelectedMessage = 0 + } else { + mp.SelectedMessage++ + } + } + + mp.app.MessagesPanel. + Highlight(ms[mp.SelectedMessage].ID.String()). + ScrollToHighlight() + return nil + case mp.app.Config.Keys.SelectNextMessage: + // If there are no highlighted regions, select the latest (last) message in the messages panel. + if len(mp.app.MessagesPanel.GetHighlights()) == 0 { + mp.SelectedMessage = 0 + } else { + // If the selected message is the latest (last) message, select the oldest (first) message in the messages panel. + if mp.SelectedMessage == 0 { + mp.SelectedMessage = len(ms) - 1 + } else { + mp.SelectedMessage-- + } + } + + mp.app.MessagesPanel. + Highlight(ms[mp.SelectedMessage].ID.String()). + ScrollToHighlight() + return nil + case mp.app.Config.Keys.SelectFirstMessage: + mp.SelectedMessage = len(ms) - 1 + mp.app.MessagesPanel. + Highlight(ms[mp.SelectedMessage].ID.String()). + ScrollToHighlight() + return nil + case mp.app.Config.Keys.SelectLastMessage: + mp.SelectedMessage = 0 + mp.app.MessagesPanel. + Highlight(ms[mp.SelectedMessage].ID.String()). + ScrollToHighlight() + return nil + case mp.app.Config.Keys.OpenMessageActionsList: + hs := mp.app.MessagesPanel.GetHighlights() + if len(hs) == 0 { + return nil + } + + mID, err := discord.ParseSnowflake(hs[0]) + if err != nil { + return nil + } + + _, m := findMessageByID(ms, discord.MessageID(mID)) + if m == nil { + return nil + } + + actionsList := NewMessageActionsList(mp.app, m) + mp.app.SetRoot(actionsList, true) + return nil + case "Esc": + mp.SelectedMessage = -1 + mp.app.SetFocus(mp.app.MainFlex) + mp.app.MessagesPanel. + Clear(). + Highlight(). + SetTitle("") + return nil + } + + return e +} diff --git a/ui/util.go b/ui/util.go index 37ed281..e0a73ca 100644 --- a/ui/util.go +++ b/ui/util.go @@ -1,26 +1,48 @@ package ui import ( + "regexp" "strings" "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/state" ) +var ( + boldRegex = regexp.MustCompile(`(?ms)\*\*(.*?)\*\*`) + italicRegex = regexp.MustCompile(`(?ms)\*(.*?)\*`) + underlineRegex = regexp.MustCompile(`(?ms)__(.*?)__`) + strikeThroughRegex = regexp.MustCompile(`(?ms)~~(.*?)~~`) +) + +func parseMarkdown(md string) string { + var res string + res = boldRegex.ReplaceAllString(md, "[::b]$1[::-]") + res = italicRegex.ReplaceAllString(res, "[::i]$1[::-]") + res = underlineRegex.ReplaceAllString(res, "[::u]$1[::-]") + res = strikeThroughRegex.ReplaceAllString(res, "[::s]$1[::-]") + + return res +} + func channelToString(c discord.Channel) string { var repr string - if c.Name != "" { + + switch c.Type { + case discord.GuildText: repr = "#" + c.Name - } else if len(c.DMRecipients) == 1 { + case discord.DirectMessage: rp := c.DMRecipients[0] repr = rp.Username + "#" + rp.Discriminator - } else { + case discord.GroupDM: rps := make([]string, len(c.DMRecipients)) for i, r := range c.DMRecipients { rps[i] = r.Username + "#" + r.Discriminator } repr = strings.Join(rps, ", ") + default: + repr = c.Name } return repr