diff --git a/README.md b/README.md index 8abaf43..0857c8d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,13 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t ## Currently supported commands * `:q` `:quit` exit * `:timeline` home, local, federated, direct, notifications -* `:tl` h, l, f, d, n (a shorter form of the former) + * `:tl` h, l, f, d, n (shorter form) +* `:blocking` lists users that you have blocked +* `:boosts` lists users that boosted the toot +* `:compose` compose a new toot +* `:favorites` lists users that favorited the toot +* `:muting` lists users that you have muted +* `:profile` go to your profile * `:tag` followed by the hashtag e.g. `:tag linux` * `:user` followed by a username e.g. `:user rasmus` to narrow a search include the instance like this `:user rasmus@mastodon.acc.sunet.se`. diff --git a/api.go b/api.go index 6e5f19e..f98e900 100644 --- a/api.go +++ b/api.go @@ -17,6 +17,18 @@ const ( TimelineFederated ) +type UserListType uint + +const ( + UserListSearch UserListType = iota + UserListBoosts + UserListFavorites + UserListFollowers + UserListFollowing + UserListBlocking + UserListMuting +) + type API struct { Client *mastodon.Client } @@ -147,13 +159,13 @@ func (api *API) GetNotificationsNewer(n *mastodon.Notification) ([]*mastodon.Not return api.Client.GetNotifications(context.Background(), pg) } -type UserSearchData struct { +type UserData struct { User *mastodon.Account Relationship *mastodon.Relationship } -func (api *API) GetUsers(s string) ([]*UserSearchData, error) { - var ud []*UserSearchData +func (api *API) GetUsers(s string) ([]*UserData, error) { + var ud []*UserData users, err := api.Client.AccountsSearch(context.Background(), s, 10) if err != nil { return nil, err @@ -163,12 +175,72 @@ func (api *API) GetUsers(s string) ([]*UserSearchData, error) { if err != nil { return ud, err } - ud = append(ud, &UserSearchData{User: u, Relationship: r}) + ud = append(ud, &UserData{User: u, Relationship: r}) + } + + return ud, nil +} + +func (api *API) getUserList(t UserListType, id string, pg *mastodon.Pagination) ([]*UserData, error) { + + var ud []*UserData + var users []*mastodon.Account + var err error + + switch t { + case UserListSearch: + users, err = api.Client.AccountsSearch(context.Background(), id, 10) + case UserListBoosts: + users, err = api.Client.GetRebloggedBy(context.Background(), mastodon.ID(id), pg) + case UserListFavorites: + users, err = api.Client.GetFavouritedBy(context.Background(), mastodon.ID(id), pg) + case UserListFollowers: + users, err = api.Client.GetAccountFollowers(context.Background(), mastodon.ID(id), pg) + case UserListFollowing: + users, err = api.Client.GetAccountFollowing(context.Background(), mastodon.ID(id), pg) + case UserListBlocking: + users, err = api.Client.GetBlocks(context.Background(), pg) + case UserListMuting: + users, err = api.Client.GetMutes(context.Background(), pg) + } + if err != nil { + return ud, err } + for _, u := range users { + r, err := api.UserRelation(*u) + if err != nil { + return ud, err + } + ud = append(ud, &UserData{User: u, Relationship: r}) + } return ud, nil } +func (api *API) GetUserList(t UserListType, id string) ([]*UserData, error) { + return api.getUserList(t, id, nil) +} + +func (api *API) GetUserListOlder(t UserListType, id string, user *mastodon.Account) ([]*UserData, error) { + if t == UserListSearch { + return []*UserData{}, nil + } + pg := &mastodon.Pagination{ + MaxID: user.ID, + } + return api.getUserList(t, id, pg) +} + +func (api *API) GetUserListNewer(t UserListType, id string, user *mastodon.Account) ([]*UserData, error) { + if t == UserListSearch { + return []*UserData{}, nil + } + pg := &mastodon.Pagination{ + MinID: user.ID, + } + return api.getUserList(t, id, pg) +} + func (api *API) GetUserByID(id mastodon.ID) (*mastodon.Account, error) { a, err := api.Client.GetAccount(context.Background(), id) return a, err diff --git a/feed.go b/feed.go index 359d92f..a2cd12a 100644 --- a/feed.go +++ b/feed.go @@ -16,6 +16,7 @@ const ( TimelineFeedType FeedType = iota ThreadFeedType UserFeedType + UserListFeedType UserSearchFeedType NotificationFeedType TagFeedType @@ -29,6 +30,8 @@ type Feed interface { DrawToot() DrawSpoiler() RedrawControls() + GetCurrentUser() *mastodon.Account + GetCurrentStatus() *mastodon.Status FeedType() FeedType GetSavedIndex() int GetDesc() string @@ -53,9 +56,9 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str strippedContent, urls = cleanTootHTML(status.Content) - subtleColor := fmt.Sprintf("[#%x]", app.Config.Style.Subtle.Hex()) - special1 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial1.Hex()) - special2 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial2.Hex()) + subtleColor := ColorMark(app.Config.Style.Subtle) + special1 := ColorMark(app.Config.Style.TextSpecial1) + special2 := ColorMark(app.Config.Style.TextSpecial2) if status.Sensitive { strippedSpoiler, u = cleanTootHTML(status.SpoilerText) @@ -167,7 +170,7 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str if len(urls)+len(status.Mentions)+len(status.Tags) > 0 { info = append(info, ColorKey(app.Config.Style, "", "O", "pen")) } - + info = append(info, ColorKey(app.Config.Style, "", "A", "vatar")) if status.Account.ID == app.Me.ID { info = append(info, ColorKey(app.Config.Style, "", "D", "elete")) } @@ -180,9 +183,9 @@ func showUser(app *App, user *mastodon.Account, relation *mastodon.Relationship, var text string var controls string - n := fmt.Sprintf("[#%x]", app.Config.Style.Text.Hex()) - s1 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial1.Hex()) - s2 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial2.Hex()) + n := ColorMark(app.Config.Style.Text) + s1 := ColorMark(app.Config.Style.TextSpecial1) + s2 := ColorMark(app.Config.Style.TextSpecial2) if user.DisplayName != "" { text = fmt.Sprintf(s2+"%s\n", user.DisplayName) @@ -227,6 +230,7 @@ func showUser(app *App, user *mastodon.Account, relation *mastodon.Relationship, if showUserControl { controlItems = append(controlItems, ColorKey(app.Config.Style, "", "U", "ser")) } + controlItems = append(controlItems, ColorKey(app.Config.Style, "", "A", "vatar")) controls = strings.Join(controlItems, " ") return text, controls @@ -417,6 +421,16 @@ func inputSimple(app *App, event *tcell.EventKey, controls ControlItem, return } +func userFromStatus(s *mastodon.Status) *mastodon.Account { + if s == nil { + return nil + } + if s.Reblog != nil { + s = s.Reblog + } + return &s.Account +} + func NewTimelineFeed(app *App, tl TimelineType) *TimelineFeed { t := &TimelineFeed{ app: app, @@ -461,7 +475,11 @@ func (t *TimelineFeed) GetCurrentStatus() *mastodon.Status { if index >= len(t.statuses) { return nil } - return t.statuses[t.app.UI.StatusView.GetCurrentItem()] + return t.statuses[index] +} + +func (t *TimelineFeed) GetCurrentUser() *mastodon.Account { + return userFromStatus(t.GetCurrentStatus()) } func (t *TimelineFeed) GetFeedList() <-chan string { @@ -607,6 +625,10 @@ func (t *ThreadFeed) GetCurrentStatus() *mastodon.Status { return t.statuses[t.app.UI.StatusView.GetCurrentItem()] } +func (t *ThreadFeed) GetCurrentUser() *mastodon.Account { + return userFromStatus(t.GetCurrentStatus()) +} + func (t *ThreadFeed) GetFeedList() <-chan string { return drawStatusList(t.statuses) } @@ -732,6 +754,10 @@ func (u *UserFeed) GetCurrentStatus() *mastodon.Status { return u.statuses[index-1] } +func (u *UserFeed) GetCurrentUser() *mastodon.Account { + return &u.user +} + func (u *UserFeed) GetFeedList() <-chan string { ch := make(chan string) go func() { @@ -905,6 +931,22 @@ func (n *NotificationsFeed) GetCurrentNotification() *mastodon.Notification { return n.notifications[index] } +func (n *NotificationsFeed) GetCurrentStatus() *mastodon.Status { + notification := n.GetCurrentNotification() + if notification == nil { + return nil + } + return notification.Status +} + +func (n *NotificationsFeed) GetCurrentUser() *mastodon.Account { + notification := n.GetCurrentNotification() + if notification == nil { + return nil + } + return ¬ification.Account +} + func (n *NotificationsFeed) GetFeedList() <-chan string { ch := make(chan string) notifications := n.notifications @@ -1097,6 +1139,10 @@ func (t *TagFeed) GetCurrentStatus() *mastodon.Status { return t.statuses[t.app.UI.StatusView.GetCurrentItem()] } +func (t *TagFeed) GetCurrentUser() *mastodon.Account { + return userFromStatus(t.GetCurrentStatus()) +} + func (t *TagFeed) GetFeedList() <-chan string { return drawStatusList(t.statuses) } @@ -1202,11 +1248,13 @@ func (t *TagFeed) Input(event *tcell.EventKey) { } } -func NewUserSearchFeed(app *App, s string) *UserSearchFeed { - u := &UserSearchFeed{ - app: app, +func NewUserListFeed(app *App, t UserListType, s string) *UserListFeed { + u := &UserListFeed{ + app: app, + listType: t, + input: s, } - users, err := app.API.GetUsers(s) + users, err := app.API.GetUserList(t, s) if err != nil { u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't load users. Error: %v\n", err)) return u @@ -1215,30 +1263,60 @@ func NewUserSearchFeed(app *App, s string) *UserSearchFeed { return u } -type UserSearchFeed struct { - app *App - users []*UserSearchData - index int - search string +type UserListFeed struct { + app *App + users []*UserData + index int + input string + listType UserListType +} + +func (u *UserListFeed) FeedType() FeedType { + return UserListFeedType } -func (u *UserSearchFeed) FeedType() FeedType { - return UserSearchFeedType +func (u *UserListFeed) GetDesc() string { + var output string + switch u.listType { + case UserListSearch: + output = "User search: " + u.input + case UserListBoosts: + output = "Boosts" + case UserListFavorites: + output = "Favorites" + case UserListFollowers: + output = "Followers" + case UserListFollowing: + output = "Following" + case UserListBlocking: + output = "Blocking" + case UserListMuting: + output = "Muting" + } + return output } -func (u *UserSearchFeed) GetDesc() string { - return "User search: " + u.search +func (u *UserListFeed) GetCurrentStatus() *mastodon.Status { + return nil } -func (u *UserSearchFeed) GetCurrentUser() *UserSearchData { +func (u *UserListFeed) GetCurrentUser() *mastodon.Account { + ud := u.GetCurrentUserData() + if ud == nil { + return nil + } + return ud.User +} + +func (u *UserListFeed) GetCurrentUserData() *UserData { index := u.app.UI.app.UI.StatusView.GetCurrentItem() - if index > 0 && index-1 >= len(u.users) { + if len(u.users) == 0 || index > len(u.users)-1 { return nil } return u.users[index-1] } -func (u *UserSearchFeed) GetFeedList() <-chan string { +func (u *UserListFeed) GetFeedList() <-chan string { ch := make(chan string) users := u.users go func() { @@ -1256,27 +1334,58 @@ func (u *UserSearchFeed) GetFeedList() <-chan string { return ch } -func (u *UserSearchFeed) LoadNewer() int { - return 0 +func (u *UserListFeed) LoadNewer() int { + var users []*UserData + var err error + if len(u.users) == 0 { + users, err = u.app.API.GetUserList(u.listType, u.input) + } else { + users, err = u.app.API.GetUserListNewer(u.listType, u.input, u.users[0].User) + } + if err != nil { + u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't load new users. Error: %v\n", err)) + return 0 + } + if len(users) == 0 { + return 0 + } + old := u.users + u.users = append(users, old...) + return len(users) } -func (u *UserSearchFeed) LoadOlder() int { - return 0 +func (u *UserListFeed) LoadOlder() int { + var users []*UserData + var err error + if len(u.users) == 0 { + users, err = u.app.API.GetUserList(u.listType, u.input) + } else { + users, err = u.app.API.GetUserListOlder(u.listType, u.input, u.users[len(u.users)-1].User) + } + if err != nil { + u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't load more users. Error: %v\n", err)) + return 0 + } + if len(users) == 0 { + return 0 + } + u.users = append(u.users, users...) + return len(users) } -func (u *UserSearchFeed) DrawList() { +func (u *UserListFeed) DrawList() { u.app.UI.StatusView.SetList(u.GetFeedList()) } -func (u *UserSearchFeed) RedrawControls() { +func (u *UserListFeed) RedrawControls() { //Does not implement } -func (u *UserSearchFeed) DrawSpoiler() { +func (u *UserListFeed) DrawSpoiler() { //Does not implement } -func (u *UserSearchFeed) DrawToot() { +func (u *UserListFeed) DrawToot() { u.index = u.app.UI.StatusView.GetCurrentItem() index := u.index if index > len(u.users)-1 || len(u.users) == 0 { @@ -1290,11 +1399,11 @@ func (u *UserSearchFeed) DrawToot() { u.app.UI.StatusView.SetControls(controls) } -func (u *UserSearchFeed) GetSavedIndex() int { +func (u *UserListFeed) GetSavedIndex() int { return u.index } -func (u *UserSearchFeed) Input(event *tcell.EventKey) { +func (u *UserListFeed) Input(event *tcell.EventKey) { index := u.GetSavedIndex() if index > len(u.users)-1 || len(u.users) == 0 { return diff --git a/main.go b/main.go index 5e88dec..add6a07 100644 --- a/main.go +++ b/main.go @@ -206,7 +206,7 @@ func main() { ) app.UI.CmdBar.Input.SetAutocompleteFunc(func(currentText string) (entries []string) { - words := strings.Split(":compose,:tag,:timeline,:tl,:user,:quit,:q", ",") + words := strings.Split(":blocking,:boosts,:compose,:favorites,:muting,:profile,:tag,:timeline,:tl,:user,:quit,:q", ",") if currentText == "" { return } @@ -243,6 +243,62 @@ func main() { case ":compose": app.UI.NewToot() app.UI.CmdBar.ClearInput() + case ":blocking": + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListBlocking, "")) + app.UI.SetFocus(LeftPaneFocus) + app.UI.CmdBar.ClearInput() + case ":boosts": + app.UI.CmdBar.ClearInput() + status := app.UI.StatusView.GetCurrentStatus() + if status == nil { + return + } + + if status.Reblog != nil { + status = status.Reblog + } + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListBoosts, string(status.ID))) + app.UI.SetFocus(LeftPaneFocus) + case ":favorites": + app.UI.CmdBar.ClearInput() + status := app.UI.StatusView.GetCurrentStatus() + if status == nil { + return + } + if status.Reblog != nil { + status = status.Reblog + } + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListFavorites, string(status.ID))) + app.UI.SetFocus(LeftPaneFocus) + /* + case ":followers": + app.UI.CmdBar.ClearInput() + user := app.UI.StatusView.GetCurrentUser() + if user == nil { + return + } + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListFollowers, string(user.ID))) + app.UI.SetFocus(LeftPaneFocus) + case ":following": + app.UI.CmdBar.ClearInput() + user := app.UI.StatusView.GetCurrentUser() + if user == nil { + return + } + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListFollowing, string(user.ID))) + app.UI.SetFocus(LeftPaneFocus) + */ + case ":muting": + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListMuting, "")) + app.UI.SetFocus(LeftPaneFocus) + app.UI.CmdBar.ClearInput() + case ":profile": + app.UI.CmdBar.ClearInput() + if app.Me == nil { + return + } + app.UI.StatusView.AddFeed(NewUserFeed(app, *app.Me)) + app.UI.SetFocus(LeftPaneFocus) case ":timeline", ":tl": if len(parts) < 2 { break @@ -288,7 +344,7 @@ func main() { if len(user) == 0 { break } - app.UI.StatusView.AddFeed(NewUserSearchFeed(app, user)) + app.UI.StatusView.AddFeed(NewUserListFeed(app, UserListSearch, user)) app.UI.SetFocus(LeftPaneFocus) app.UI.CmdBar.ClearInput() } diff --git a/messagebox.go b/messagebox.go index d225731..bd41da2 100644 --- a/messagebox.go +++ b/messagebox.go @@ -134,8 +134,8 @@ func (m *MessageBox) Draw() { var outputHead string var output string - subtleColor := fmt.Sprintf("[#%x]", m.app.Config.Style.Subtle.Hex()) - warningColor := fmt.Sprintf("[#%x]", m.app.Config.Style.WarningText.Hex()) + subtleColor := ColorMark(m.app.Config.Style.Subtle) + warningColor := ColorMark(m.app.Config.Style.WarningText) if m.currentToot.Status != nil { var acct string if m.currentToot.Status.Account.DisplayName != "" { diff --git a/statusview.go b/statusview.go index 2db412f..06adce2 100644 --- a/statusview.go +++ b/statusview.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gdamore/tcell" + "github.com/mattn/go-mastodon" "github.com/rivo/tview" ) @@ -100,6 +101,20 @@ func (t *StatusView) GetCurrentItem() int { return t.list.GetCurrentItem() } +func (t *StatusView) GetCurrentStatus() *mastodon.Status { + if len(t.feeds) == 0 { + return nil + } + return t.feeds[len(t.feeds)-1].GetCurrentStatus() +} + +func (t *StatusView) GetCurrentUser() *mastodon.Account { + if len(t.feeds) == 0 { + return nil + } + return t.feeds[len(t.feeds)-1].GetCurrentUser() +} + func (t *StatusView) ScrollToBeginning() { t.text.ScrollToBeginning() } diff --git a/util.go b/util.go index 7f89631..a901106 100644 --- a/util.go +++ b/util.go @@ -12,6 +12,7 @@ import ( "regexp" "strings" + "github.com/gdamore/tcell" "github.com/mattn/go-mastodon" "github.com/microcosm-cc/bluemonday" "github.com/rivo/tview" @@ -242,13 +243,17 @@ func FindFiles(s string) []string { } func ColorKey(style StyleConfig, pre, key, end string) string { - color := fmt.Sprintf("[#%x]", style.TextSpecial2.Hex()) - normal := fmt.Sprintf("[#%x]", style.Text.Hex()) + color := ColorMark(style.TextSpecial2) + normal := ColorMark(style.Text) key = tview.Escape("[" + key + "]") text := fmt.Sprintf("%s%s%s%s%s", pre, color, key, normal, end) return text } +func ColorMark(color tcell.Color) string { + return fmt.Sprintf("[#%x]", color.Hex()) +} + func FormatUsername(a mastodon.Account) string { if a.DisplayName != "" { return fmt.Sprintf("%s (%s)", a.DisplayName, a.Acct) @@ -257,6 +262,6 @@ func FormatUsername(a mastodon.Account) string { } func SublteText(style StyleConfig, text string) string { - subtle := fmt.Sprintf("[#%x]", style.Subtle.Hex()) + subtle := ColorMark(style.Subtle) return fmt.Sprintf("%s%s", subtle, text) }