From 2c8ba2e6b6e4b45294ab228adb736c30e5953059 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Wed, 23 Nov 2022 19:07:42 +0100 Subject: [PATCH] 1.0.20 (#183) * update version * only load threads once * don't crash tut on error from openInTerminal(...) * add/remove users to list and follow/unfollow tags --- README.md | 4 ++ api/feed.go | 35 ++++++++++----- api/tags.go | 35 +++++++++++++++ api/types.go | 5 ++- api/user.go | 8 ++++ config.example.ini | 12 ++++++ config/config.go | 13 +++++- config/default_config.go | 12 ++++++ config/help.tmpl | 14 +++++- feed/feed.go | 92 +++++++++++++++++++++++++++++++++++++++- go.mod | 2 +- go.sum | 2 + main.go | 2 +- ui/cmdbar.go | 22 +++++++++- ui/commands.go | 16 +++++++ ui/feed.go | 30 +++++++++++++ ui/input.go | 77 +++++++++++++++++++++++++++++---- ui/item.go | 17 +++++--- ui/item_list.go | 14 +++++- ui/item_notification.go | 4 +- ui/item_user.go | 11 ++++- ui/open.go | 10 ++--- ui/timeline.go | 6 ++- 23 files changed, 397 insertions(+), 46 deletions(-) create mode 100644 api/tags.go diff --git a/README.md b/README.md index 4c10d03..070cab8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:compose` compose a new toot * `:favorited` lists toots you've favorited * `:favorites` lists users that favorited the toot +* `:follow-tag` followed by the hashtag to follow e.g. `:follow-tag tut` +* `:followers` list of people the account are following. It only works on profiles. +* `:following` list of people following the account. It only works on profiles. * `:h` `:help` view help * `:history` show edits of a toot * `:lists` show a list of your lists @@ -58,6 +61,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:requests` see following requests * `:saved` alias for bookmarks * `:tag` followed by the hashtag e.g. `:tag linux` +* `:unfollow-tag` followed by the hashtag to unfollow e.g. `:unfollow-tag tut` * `:user` followed by a username e.g. `:user rasmus` to narrow a search include * `:window` switch window by index (zero indexed) e.g. `:window 0` for the first window. diff --git a/api/feed.go b/api/feed.go index a31e54d..b869aab 100644 --- a/api/feed.go +++ b/api/feed.go @@ -21,7 +21,7 @@ func (ac *AccountClient) getStatusSimilar(fn func() ([]*mastodon.Status, error), return items, nil } -func (ac *AccountClient) getUserSimilar(fn func() ([]*mastodon.Account, error)) ([]Item, error) { +func (ac *AccountClient) getUserSimilar(fn func() ([]*mastodon.Account, error), data interface{}) ([]Item, error) { var items []Item users, err := fn() if err != nil { @@ -39,8 +39,9 @@ func (ac *AccountClient) getUserSimilar(fn func() ([]*mastodon.Account, error)) for _, r := range rel { if u.ID == r.ID { items = append(items, NewUserItem(&User{ - Data: u, - Relation: r, + Data: u, + Relation: r, + AdditionalData: data, }, false)) break } @@ -185,49 +186,49 @@ func (ac *AccountClient) GetBoostsStatus(pg *mastodon.Pagination, id mastodon.ID fn := func() ([]*mastodon.Account, error) { return ac.Client.GetRebloggedBy(context.Background(), id, pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetFavoritesStatus(pg *mastodon.Pagination, id mastodon.ID) ([]Item, error) { fn := func() ([]*mastodon.Account, error) { return ac.Client.GetFavouritedBy(context.Background(), id, pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetFollowers(pg *mastodon.Pagination, id mastodon.ID) ([]Item, error) { fn := func() ([]*mastodon.Account, error) { return ac.Client.GetAccountFollowers(context.Background(), id, pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetFollowing(pg *mastodon.Pagination, id mastodon.ID) ([]Item, error) { fn := func() ([]*mastodon.Account, error) { return ac.Client.GetAccountFollowing(context.Background(), id, pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetBlocking(pg *mastodon.Pagination) ([]Item, error) { fn := func() ([]*mastodon.Account, error) { return ac.Client.GetBlocks(context.Background(), pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetMuting(pg *mastodon.Pagination) ([]Item, error) { fn := func() ([]*mastodon.Account, error) { return ac.Client.GetMutes(context.Background(), pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetFollowRequests(pg *mastodon.Pagination) ([]Item, error) { fn := func() ([]*mastodon.Account, error) { return ac.Client.GetFollowRequests(context.Background(), pg) } - return ac.getUserSimilar(fn) + return ac.getUserSimilar(fn, nil) } func (ac *AccountClient) GetUser(pg *mastodon.Pagination, id mastodon.ID) ([]Item, error) { @@ -281,6 +282,20 @@ func (ac *AccountClient) GetListStatuses(pg *mastodon.Pagination, id mastodon.ID return items, nil } +func (ac *AccountClient) GetFollowingForList(pg *mastodon.Pagination, id mastodon.ID, data interface{}) ([]Item, error) { + fn := func() ([]*mastodon.Account, error) { + return ac.Client.GetAccountFollowing(context.Background(), id, pg) + } + return ac.getUserSimilar(fn, data) +} + +func (ac *AccountClient) GetListUsers(pg *mastodon.Pagination, id mastodon.ID, data interface{}) ([]Item, error) { + fn := func() ([]*mastodon.Account, error) { + return ac.Client.GetListAccounts(context.Background(), id) + } + return ac.getUserSimilar(fn, data) +} + func (ac *AccountClient) GetTag(pg *mastodon.Pagination, search string) ([]Item, error) { fn := func() ([]*mastodon.Status, error) { return ac.Client.GetTimelineHashtag(context.Background(), search, false, pg) diff --git a/api/tags.go b/api/tags.go new file mode 100644 index 0000000..348675c --- /dev/null +++ b/api/tags.go @@ -0,0 +1,35 @@ +package api + +import ( + "context" + "errors" +) + +func (ac *AccountClient) FollowTag(tag string) error { + t, err := ac.Client.TagFollow(context.Background(), tag) + if err != nil { + return err + } + if t.Following == nil { + return errors.New("following is set to nil") + } + if t.Following == false { + return errors.New("following is still set to false") + } + return nil +} + +func (ac *AccountClient) UnfollowTag(tag string) error { + t, err := ac.Client.TagUnfollow(context.Background(), tag) + if err != nil { + return err + } + + if t.Following == nil { + return errors.New("following is set to nil") + } + if t.Following == true { + return errors.New("following is still set to true") + } + return nil +} diff --git a/api/types.go b/api/types.go index 89dc9ae..0752ab4 100644 --- a/api/types.go +++ b/api/types.go @@ -15,6 +15,7 @@ type AccountClient struct { } type User struct { - Data *mastodon.Account - Relation *mastodon.Relationship + Data *mastodon.Account + Relation *mastodon.Relationship + AdditionalData interface{} } diff --git a/api/user.go b/api/user.go index a8b594b..744901e 100644 --- a/api/user.go +++ b/api/user.go @@ -87,3 +87,11 @@ func (ac *AccountClient) SavePreferences(p *mastodon.Profile) error { } return err } + +func (ac *AccountClient) AddUserToList(u *mastodon.Account, l *mastodon.List) error { + return ac.Client.AddToList(context.Background(), l.ID, u.ID) +} + +func (ac *AccountClient) DeleteUserFromList(u *mastodon.Account, l *mastodon.List) error { + return ac.Client.RemoveFromList(context.Background(), l.ID, u.ID) +} diff --git a/config.example.ini b/config.example.ini index eddd8b3..981de00 100644 --- a/config.example.ini +++ b/config.example.ini @@ -642,6 +642,18 @@ user-yank="[Y]ank",'y','Y' # default="[O]pen",'o','O' list-open-feed="[O]pen",'o','O' +# List all users in a list +# default="[U]sers",'u','U' +list-user-list="[U]sers",'u','U' + +# Add user to list +# default="[A]dd",'a','A' +list-user-add="[A]dd",'a','A' + +# Delete user from list +# default="[D]elete",'d','D' +list-user-delete="[D]elete",'d','D' + # Open URL # default="[O]pen",'o','O' link-open="[O]pen",'o','O' diff --git a/config/config.go b/config/config.go index ec4747d..431a148 100644 --- a/config/config.go +++ b/config/config.go @@ -380,7 +380,10 @@ type Input struct { UserViewFocus Key UserYank Key - ListOpenFeed Key + ListOpenFeed Key + ListUserList Key + ListUserAdd Key + ListUserDelete Key LinkOpen Key LinkYank Key @@ -1265,7 +1268,10 @@ func parseInput(cfg *ini.File) Input { UserViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), UserYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), - ListOpenFeed: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), + ListOpenFeed: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), + ListUserList: inputStrOrErr([]string{"\"[U]sers\"", "'u'", "'U'"}, false), + ListUserAdd: inputStrOrErr([]string{"\"[A]dd\"", "'a'", "'A'"}, false), + ListUserDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), LinkOpen: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), LinkYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), @@ -1340,6 +1346,9 @@ func parseInput(cfg *ini.File) Input { ic.UserYank = inputOrErr(cfg, "user-yank", false, ic.UserYank) ic.ListOpenFeed = inputOrErr(cfg, "list-open-feed", false, ic.ListOpenFeed) + ic.ListUserList = inputOrErr(cfg, "list-user-list", false, ic.ListUserList) + ic.ListUserAdd = inputOrErr(cfg, "list-user-add", false, ic.ListUserAdd) + ic.ListUserDelete = inputOrErr(cfg, "list-user-delete", false, ic.ListUserDelete) ic.LinkOpen = inputOrErr(cfg, "link-open", false, ic.LinkOpen) ic.LinkYank = inputOrErr(cfg, "link-yank", false, ic.LinkYank) diff --git a/config/default_config.go b/config/default_config.go index f988a52..5dec689 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -644,6 +644,18 @@ user-yank="[Y]ank",'y','Y' # default="[O]pen",'o','O' list-open-feed="[O]pen",'o','O' +# List all users in a list +# default="[U]sers",'u','U' +list-user-list="[U]sers",'u','U' + +# Add user to list +# default="[A]dd",'a','A' +list-user-add="[A]dd",'a','A' + +# Delete user from list +# default="[D]elete",'d','D' +list-user-delete="[D]elete",'d','D' + # Open URL # default="[O]pen",'o','O' link-open="[O]pen",'o','O' diff --git a/config/help.tmpl b/config/help.tmpl index 3a53ace..b3e7600 100644 --- a/config/help.tmpl +++ b/config/help.tmpl @@ -57,6 +57,15 @@ Here's a list of supported commands. {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:favorites{{ Flags "-" }}{{ Color .Style.Text }} Lists users that favorited the toot +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:follow-tag{{ Flags "-" }}{{ Color .Style.Text }} + Followed by the hashtag to follow e.g. :follow-tag tut + +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:followers{{ Flags "-" }}{{ Color .Style.Text }} + List of people the account are following. It only works on profiles. + +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:following{{ Flags "-" }}{{ Color .Style.Text }} + List of people following the account. It only works on profiles. + {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:h{{ Flags "-" }}{{ Color .Style.Text }} or {{- Color .Style.TextSpecial2 }}{{ Flags "b" }} :help{{ Flags "-" }}{{ Color .Style.Text }} View this help message @@ -92,7 +101,10 @@ Here's a list of supported commands. Alias for :bookmarks {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tag{{ Flags "-" }}{{ Color .Style.Text }} tagname - See toots for a tag. E.g. :tag linux + See toots for a tag e.g. :tag linux + +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:unfollow-tag{{ Flags "-" }}{{ Color .Style.Text }} + Followed by the hashtag to unfollow e.g. :unfollow-tag tut {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:user{{ Flags "-" }}{{ Color .Style.Text }} username Go to profile for . E.g. :user rasmus diff --git a/feed/feed.go b/feed/feed.go index abb108c..bd9c230 100644 --- a/feed/feed.go +++ b/feed/feed.go @@ -14,6 +14,7 @@ import ( type apiFunc func(pg *mastodon.Pagination) ([]api.Item, error) type apiEmptyFunc func() ([]api.Item, error) type apiIDFunc func(pg *mastodon.Pagination, id mastodon.ID) ([]api.Item, error) +type apiIDFuncData func(pg *mastodon.Pagination, id mastodon.ID, data interface{}) ([]api.Item, error) type apiSearchFunc func(search string) ([]api.Item, error) type apiSearchPGFunc func(pg *mastodon.Pagination, search string) ([]api.Item, error) type apiThreadFunc func(status *mastodon.Status) ([]api.Item, error) @@ -44,6 +45,8 @@ const ( UserList Lists List + ListUsersIn + ListUsersAdd ) type LoadingLock struct { @@ -556,6 +559,57 @@ func (f *Feed) linkOlderID(fn apiIDFunc, id mastodon.ID) { f.itemsMux.Unlock() } +func (f *Feed) linkNewerIDdata(fn apiIDFuncData, id mastodon.ID, data interface{}) { + f.apiDataMux.Lock() + pg := &mastodon.Pagination{} + pg.MinID = f.apiData.MinID + maxTmp := f.apiData.MaxID + + items, err := fn(pg, id, data) + if err != nil { + f.apiDataMux.Unlock() + return + } + f.apiData.MinID = pg.MinID + if pg.MaxID == "" { + f.apiData.MaxID = maxTmp + } else { + f.apiData.MaxID = pg.MaxID + } + f.apiDataMux.Unlock() + f.itemsMux.Lock() + if len(items) > 0 { + f.items = append(items, f.items...) + f.Updated(DeskstopNotificationNone) + } + f.itemsMux.Unlock() +} + +func (f *Feed) linkOlderIDdata(fn apiIDFuncData, id mastodon.ID, data interface{}) { + f.apiDataMux.Lock() + pg := &mastodon.Pagination{} + pg.MaxID = f.apiData.MaxID + if pg.MaxID == "" { + f.apiDataMux.Unlock() + return + } + + items, err := fn(pg, id, data) + if err != nil { + f.apiDataMux.Unlock() + return + } + f.apiData.MaxID = pg.MaxID + f.apiDataMux.Unlock() + + f.itemsMux.Lock() + if len(items) > 0 { + f.items = append(f.items, items...) + f.Updated(DeskstopNotificationNone) + } + f.itemsMux.Unlock() +} + func (f *Feed) startStream(rec *api.Receiver, timeline string, err error) { if err != nil { log.Fatalln("Couldn't open stream") @@ -731,7 +785,13 @@ func NewUserProfile(ac *api.AccountClient, user *api.User) *Feed { func NewThread(ac *api.AccountClient, status *mastodon.Status) *Feed { feed := newFeed(ac, Thread) - feed.loadNewer = func() { feed.singleThread(feed.accountClient.GetThread, status) } + once := true + feed.loadNewer = func() { + if once { + feed.singleThread(feed.accountClient.GetThread, status) + once = false + } + } return feed } @@ -783,6 +843,36 @@ func NewList(ac *api.AccountClient, list *mastodon.List) *Feed { return feed } +func NewUsersInList(ac *api.AccountClient, list *mastodon.List) *Feed { + feed := newFeed(ac, ListUsersIn) + feed.name = list.Title + once := true + feed.loadNewer = func() { + if once { + feed.linkNewerIDdata(feed.accountClient.GetListUsers, list.ID, list) + } + once = false + } + feed.loadOlder = func() { feed.linkOlderIDdata(feed.accountClient.GetListUsers, list.ID, list) } + + return feed +} + +func NewUsersAddList(ac *api.AccountClient, list *mastodon.List) *Feed { + feed := newFeed(ac, ListUsersAdd) + feed.name = list.Title + once := true + feed.loadNewer = func() { + if once { + feed.linkNewerIDdata(feed.accountClient.GetFollowingForList, ac.Me.ID, list) + } + once = false + } + feed.loadOlder = func() { feed.linkOlderIDdata(feed.accountClient.GetFollowingForList, ac.Me.ID, list) } + + return feed +} + func NewFavoritesStatus(ac *api.AccountClient, id mastodon.ID) *Feed { feed := newFeed(ac, Favorites) once := true diff --git a/go.mod b/go.mod index 2835924..6df0b6d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/RasmusLindroth/tut go 1.18 require ( - github.com/RasmusLindroth/go-mastodon v0.0.10 + github.com/RasmusLindroth/go-mastodon v0.0.11 github.com/atotto/clipboard v0.1.4 github.com/gdamore/tcell/v2 v2.5.3 github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 diff --git a/go.sum b/go.sum index 5a08ded..1135336 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/RasmusLindroth/go-mastodon v0.0.10 h1:huGNcPn5SASfJDhBL4drKL0PFJ29+hqjCroIrkf2R0E= github.com/RasmusLindroth/go-mastodon v0.0.10/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE= +github.com/RasmusLindroth/go-mastodon v0.0.11 h1:Qcad+urrDVrboo13ayoHG3DcwsGK/07qR6IfOPPMilY= +github.com/RasmusLindroth/go-mastodon v0.0.11/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= diff --git a/main.go b/main.go index 1ebc11d..72d38c9 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.19" +const version = "1.0.20" func main() { util.SetTerminalTitle("tut") diff --git a/ui/cmdbar.go b/ui/cmdbar.go index cdf14da..d452ea4 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -198,6 +198,26 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { NewUserSearchFeed(c.tutView, user), ) c.Back() + case ":follow-tag": + if len(parts) < 2 { + break + } + tag := strings.TrimSpace(parts[1]) + if len(tag) == 0 { + break + } + c.tutView.TagFollowCommand(parts[1]) + c.Back() + case ":unfollow-tag": + if len(parts) < 2 { + break + } + tag := strings.TrimSpace(parts[1]) + if len(tag) == 0 { + break + } + c.tutView.TagUnfollowCommand(parts[1]) + c.Back() case ":lists": c.tutView.ListsCommand() c.Back() @@ -211,7 +231,7 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { func (c *CmdBar) Autocomplete(curr string) []string { var entries []string - words := strings.Split(":blocking,:boosts,:bookmarks,:clear-notifications,:compose,:favorites,:favorited,:followers,:following,:help,:h,:history,:lists,:list-placement,:list-split,:muting,:newer,:preferences,:profile,:proportions,:requests,:saved,:tag,:timeline,:tl,:user,:window,:quit,:q", ",") + words := strings.Split(":blocking,:boosts,:bookmarks,:clear-notifications,:compose,:favorites,:favorited,:follow-tag,:followers,:following,:help,:h,:history,:lists,:list-placement,:list-split,:muting,:newer,:preferences,:profile,:proportions,:requests,:saved,:tag,:timeline,:tl,:unfollow-tag,:user,:window,:quit,:q", ",") if curr == "" { return entries } diff --git a/ui/commands.go b/ui/commands.go index 9380d88..295f0a9 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -86,6 +86,22 @@ func (tv *TutView) TagCommand(tag string) { ) } +func (tv *TutView) TagFollowCommand(tag string) { + err := tv.tut.Client.FollowTag(tag) + if err != nil { + tv.ShowError(fmt.Sprintf("Couldn't follow tag. Error: %v\n", err)) + return + } +} + +func (tv *TutView) TagUnfollowCommand(tag string) { + err := tv.tut.Client.UnfollowTag(tag) + if err != nil { + tv.ShowError(fmt.Sprintf("Couldn't unfollow tag. Error: %v\n", err)) + return + } +} + func (tv *TutView) WindowCommand(index string) { i, err := strconv.Atoi(index) if err != nil { diff --git a/ui/feed.go b/ui/feed.go index 269dfc6..6feb5e3 100644 --- a/ui/feed.go +++ b/ui/feed.go @@ -336,6 +336,36 @@ func NewListFeed(tv *TutView, l *mastodon.List) *Feed { return fd } +func NewUsersInListFeed(tv *TutView, l *mastodon.List) *Feed { + f := feed.NewUsersInList(tv.tut.Client, l) + f.LoadNewer() + fd := &Feed{ + tutView: tv, + Data: f, + ListIndex: 0, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), + } + go fd.update() + + return fd +} + +func NewUsersAddListFeed(tv *TutView, l *mastodon.List) *Feed { + f := feed.NewUsersAddList(tv.tut.Client, l) + f.LoadNewer() + fd := &Feed{ + tutView: tv, + Data: f, + ListIndex: 0, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), + } + go fd.update() + + return fd +} + func NewFavoritedFeed(tv *TutView) *Feed { f := feed.NewFavorites(tv.tut.Client) f.LoadNewer() diff --git a/ui/input.go b/ui/input.go index 2987456..036b4cd 100644 --- a/ui/input.go +++ b/ui/input.go @@ -304,16 +304,21 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { case api.StatusHistoryType: return tv.InputStatusHistory(event, item, item.Raw().(*mastodon.StatusHistory), nil) case api.UserType, api.ProfileType: - if ft == feed.FollowRequests { - return tv.InputUser(event, item.Raw().(*api.User), true) - } else { - return tv.InputUser(event, item.Raw().(*api.User), false) + switch ft { + case feed.FollowRequests: + return tv.InputUser(event, item.Raw().(*api.User), InputUserFollowRequest) + case feed.ListUsersAdd: + return tv.InputUser(event, item.Raw().(*api.User), InputUserListAdd) + case feed.ListUsersIn: + return tv.InputUser(event, item.Raw().(*api.User), InputUserListDelete) + default: + return tv.InputUser(event, item.Raw().(*api.User), InputUserNormal) } case api.NotificationType: nd := item.Raw().(*api.NotificationData) switch nd.Item.Type { case "follow": - return tv.InputUser(event, nd.User.Raw().(*api.User), false) + return tv.InputUser(event, nd.User.Raw().(*api.User), InputUserNormal) case "favourite": user := nd.User.Raw().(*api.User) return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data) @@ -329,7 +334,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { case "poll": return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) case "follow_request": - return tv.InputUser(event, nd.User.Raw().(*api.User), true) + return tv.InputUser(event, nd.User.Raw().(*api.User), InputUserFollowRequest) } case api.ListsType: ld := item.Raw().(*mastodon.List) @@ -548,12 +553,60 @@ func (tv *TutView) InputStatusHistory(event *tcell.EventKey, item api.Item, sr * return event } -func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, fr bool) *tcell.EventKey { +type InputUserType uint + +const ( + InputUserNormal = iota + InputUserFollowRequest + InputUserListAdd + InputUserListDelete +) + +func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, ut InputUserType) *tcell.EventKey { blocking := user.Relation.Blocking muting := user.Relation.Muting following := user.Relation.Following - if tv.tut.Config.Input.UserFollowRequestDecide.Match(event.Key(), event.Rune()) { + if ut == InputUserListAdd { + if tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) || + tv.tut.Config.Input.ListUserAdd.Match(event.Key(), event.Rune()) { + ad := user.AdditionalData + switch ad.(type) { + case *mastodon.List: + l := user.AdditionalData.(*mastodon.List) + err := tv.tut.Client.AddUserToList(user.Data, l) + if err != nil { + tv.ShowError(fmt.Sprintf("Couldn't add user to list. Error: %v", err)) + } + return nil + default: + return event + } + + } + return event + } + + if ut == InputUserListDelete { + if tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) || + tv.tut.Config.Input.ListUserDelete.Match(event.Key(), event.Rune()) { + ad := user.AdditionalData + switch ad.(type) { + case *mastodon.List: + l := user.AdditionalData.(*mastodon.List) + err := tv.tut.Client.DeleteUserFromList(user.Data, l) + if err != nil { + tv.ShowError(fmt.Sprintf("Couldn't remove user from list. Error: %v", err)) + } + return nil + default: + return event + } + } + return event + } + + if ut == InputUserFollowRequest && tv.tut.Config.Input.UserFollowRequestDecide.Match(event.Key(), event.Rune()) { tv.ModalView.RunDecide("Do you want accept the follow request?", func() { err := tv.tut.Client.FollowRequestAccept(user.Data) @@ -671,6 +724,14 @@ func (tv *TutView) InputList(event *tcell.EventKey, list *mastodon.List) *tcell. tv.Timeline.AddFeed(NewListFeed(tv, list)) return nil } + if tv.tut.Config.Input.ListUserList.Match(event.Key(), event.Rune()) { + tv.Timeline.AddFeed(NewUsersInListFeed(tv, list)) + return nil + } + if tv.tut.Config.Input.ListUserAdd.Match(event.Key(), event.Rune()) { + tv.Timeline.AddFeed(NewUsersAddListFeed(tv, list)) + return nil + } return event } diff --git a/ui/item.go b/ui/item.go index 102d048..773c3b5 100644 --- a/ui/item.go +++ b/ui/item.go @@ -91,10 +91,15 @@ func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview. } drawStatus(tv, item, &status, main, controls, true, "") case api.UserType, api.ProfileType: - if ft == feed.FollowRequests { - drawUser(tv, item.Raw().(*api.User), main, controls, "", true) - } else { - drawUser(tv, item.Raw().(*api.User), main, controls, "", false) + switch ft { + case feed.FollowRequests: + drawUser(tv, item.Raw().(*api.User), main, controls, "", InputUserFollowRequest) + case feed.ListUsersAdd: + drawUser(tv, item.Raw().(*api.User), main, controls, "", InputUserListAdd) + case feed.ListUsersIn: + drawUser(tv, item.Raw().(*api.User), main, controls, "", InputUserListDelete) + default: + drawUser(tv, item.Raw().(*api.User), main, controls, "", InputUserFollowRequest) } case api.NotificationType: drawNotification(tv, item, item.Raw().(*api.NotificationData), main, controls) @@ -122,9 +127,9 @@ func DrawItemControls(tv *TutView, item api.Item, controls *tview.Flex, ft feed. drawStatus(tv, item, &status, nil, controls, true, "") case api.UserType, api.ProfileType: if ft == feed.FollowRequests { - drawUser(tv, item.Raw().(*api.User), nil, controls, "", true) + drawUser(tv, item.Raw().(*api.User), nil, controls, "", InputUserFollowRequest) } else { - drawUser(tv, item.Raw().(*api.User), nil, controls, "", false) + drawUser(tv, item.Raw().(*api.User), nil, controls, "", InputUserNormal) } case api.NotificationType: drawNotification(tv, item, item.Raw().(*api.NotificationData), nil, controls) diff --git a/ui/item_list.go b/ui/item_list.go index 162264e..2e1becf 100644 --- a/ui/item_list.go +++ b/ui/item_list.go @@ -11,9 +11,19 @@ type List struct { } func drawList(tv *TutView, data *mastodon.List, main *tview.TextView, controls *tview.Flex) { - btn := NewControl(tv.tut.Config, tv.tut.Config.Input.ListOpenFeed, true) controls.Clear() - controls.AddItem(NewControlButton(tv, btn), btn.Len, 0, false) + var items []Control + items = append(items, NewControl(tv.tut.Config, tv.tut.Config.Input.ListOpenFeed, true)) + items = append(items, NewControl(tv.tut.Config, tv.tut.Config.Input.ListUserList, true)) + items = append(items, NewControl(tv.tut.Config, tv.tut.Config.Input.ListUserAdd, true)) + controls.Clear() + for i, item := range items { + if i < len(items)-1 { + controls.AddItem(NewControlButton(tv, item), item.Len+1, 0, false) + } else { + controls.AddItem(NewControlButton(tv, item), item.Len, 0, false) + } + } main.SetText(fmt.Sprintf("List %s", tview.Escape(data.Title))) } diff --git a/ui/item_notification.go b/ui/item_notification.go index 9f1b946..c18ea1f 100644 --- a/ui/item_notification.go +++ b/ui/item_notification.go @@ -13,7 +13,7 @@ func drawNotification(tv *TutView, item api.Item, notification *api.Notification switch notification.Item.Type { case "follow": drawUser(tv, notification.User.Raw().(*api.User), main, controls, - fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), false, + fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), InputUserNormal, ) case "favourite": drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, @@ -42,7 +42,7 @@ func drawNotification(tv *TutView, item api.Item, notification *api.Notification case "follow_request": drawUser(tv, notification.User.Raw().(*api.User), main, controls, fmt.Sprintf("%s wants to follow you.", util.FormatUsername(notification.Item.Account)), - true, + InputUserFollowRequest, ) default: controls.Clear() diff --git a/ui/item_user.go b/ui/item_user.go index c68575c..e7d854b 100644 --- a/ui/item_user.go +++ b/ui/item_user.go @@ -43,7 +43,7 @@ type DisplayUserData struct { Style config.Style } -func drawUser(tv *TutView, data *api.User, main *tview.TextView, controls *tview.Flex, additional string, fr bool) { +func drawUser(tv *TutView, data *api.User, main *tview.TextView, controls *tview.Flex, additional string, ut InputUserType) { user := data.Data relation := data.Relation showUserControl := true @@ -79,7 +79,7 @@ func drawUser(tv *TutView, data *api.User, main *tview.TextView, controls *tview u.Fields = fields var controlItems []Control - if fr { + if ut == InputUserFollowRequest { controlItems = append(controlItems, NewControl(tv.tut.Config, tv.tut.Config.Input.UserFollowRequestDecide, false)) } if tv.tut.Client.Me.ID != user.ID { @@ -108,6 +108,13 @@ func drawUser(tv *TutView, data *api.User, main *tview.TextView, controls *tview controlItems = append(controlItems, NewControl(tv.tut.Config, tv.tut.Config.Input.UserAvatar, true)) controlItems = append(controlItems, NewControl(tv.tut.Config, tv.tut.Config.Input.UserYank, true)) + // Clear controls and only have add and delete for lists. + if ut == InputUserListAdd { + controlItems = []Control{NewControl(tv.tut.Config, tv.tut.Config.Input.ListUserAdd, true)} + } else if ut == InputUserListDelete { + controlItems = []Control{NewControl(tv.tut.Config, tv.tut.Config.Input.ListUserDelete, true)} + } + controls.Clear() for i, item := range controlItems { if i < len(controlItems)-1 { diff --git a/ui/open.go b/ui/open.go index c527a89..7ecfc3f 100644 --- a/ui/open.go +++ b/ui/open.go @@ -1,7 +1,7 @@ package ui import ( - "log" + "fmt" "os" "os/exec" "strings" @@ -36,11 +36,11 @@ func openInTerminal(tv *TutView, command string, args ...string) error { var err error tv.tut.App.Suspend(func() { err = cmd.Run() - if err != nil { - log.Fatalln(err) - } }) - return err + if err != nil { + tv.ShowError(fmt.Sprintf("Eroror while opening: %v", err)) + } + return nil } func openCustom(tv *TutView, program string, args []string, terminal bool, url string) { diff --git a/ui/timeline.go b/ui/timeline.go index 67c7a6e..f18cb4e 100644 --- a/ui/timeline.go +++ b/ui/timeline.go @@ -183,8 +183,10 @@ func (tl *Timeline) GetTitle() string { ct = "follow requests" case feed.Blocking: ct = "blocking" - case feed.Muting: - ct = "muting" + case feed.ListUsersAdd: + ct = fmt.Sprintf("Add users to %s", name) + case feed.ListUsersIn: + ct = fmt.Sprintf("Delete users from %s", name) } return fmt.Sprintf("%s (%d/%d)", ct, index+1, total) }