refactor(ui): segregate widgets into individual files (#189)
This commit is contained in:
parent
119772a983
commit
0689e21870
12 changed files with 610 additions and 574 deletions
20
go.mod
20
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
|
||||
)
|
||||
|
|
|
|||
40
go.sum
40
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
219
ui/message_actions_list.go
Normal file
219
ui/message_actions_list.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
138
ui/message_input.go
Normal file
138
ui/message_input.go
Normal file
|
|
@ -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
|
||||
}
|
||||
463
ui/messages.go
463
ui/messages.go
|
|
@ -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
|
||||
}
|
||||
128
ui/messages_panel.go
Normal file
128
ui/messages_panel.go
Normal file
|
|
@ -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
|
||||
}
|
||||
28
ui/util.go
28
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue