From bcddfa234f71971db53b2c81379ef2c7f691f030 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 10 Apr 2020 09:19:24 +0200 Subject: [PATCH] Support user search and display more info --- README.md | 2 + api.go | 30 +++++- config.go | 4 +- feed.go | 248 ++++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 16 +++- statusview.go | 23 +++-- ui.go | 10 +- 7 files changed, 311 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2d11c88..8abaf43 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:timeline` home, local, federated, direct, notifications * `:tl` h, l, f, d, n (a shorter form of the former) * `: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`. Explanation of the non obvious keys when viewing a toot * `V` = view. In this mode you can scroll throught the text of the toot if it doesn't fit the screen diff --git a/api.go b/api.go index da167c2..6e5f19e 100644 --- a/api.go +++ b/api.go @@ -38,7 +38,13 @@ func (api *API) getStatuses(tl TimelineType, pg *mastodon.Pagination) ([]*mastod case TimelineHome: statuses, err = api.Client.GetTimelineHome(context.Background(), pg) case TimelineDirect: - statuses, err = api.Client.GetTimelineDirect(context.Background(), pg) + var conv []*mastodon.Conversation + conv, err = api.Client.GetConversations(context.Background(), pg) + var cStatuses []*mastodon.Status + for _, c := range conv { + cStatuses = append(cStatuses, c.LastStatus) + } + statuses = cStatuses case TimelineLocal: statuses, err = api.Client.GetTimelinePublic(context.Background(), true, pg) case TimelineFederated: @@ -141,6 +147,28 @@ func (api *API) GetNotificationsNewer(n *mastodon.Notification) ([]*mastodon.Not return api.Client.GetNotifications(context.Background(), pg) } +type UserSearchData struct { + User *mastodon.Account + Relationship *mastodon.Relationship +} + +func (api *API) GetUsers(s string) ([]*UserSearchData, error) { + var ud []*UserSearchData + users, err := api.Client.AccountsSearch(context.Background(), s, 10) + if err != nil { + return nil, err + } + for _, u := range users { + r, err := api.UserRelation(*u) + if err != nil { + return ud, err + } + ud = append(ud, &UserSearchData{User: u, Relationship: r}) + } + + return ud, nil +} + func (api *API) GetUserByID(id mastodon.ID) (*mastodon.Account, error) { a, err := api.Client.GetAccount(context.Background(), id) return a, err diff --git a/config.go b/config.go index 2237f4a..679d059 100644 --- a/config.go +++ b/config.go @@ -164,7 +164,9 @@ func parseMedia(cfg *ini.File) MediaConfig { } func ParseConfig(filepath string) (Config, error) { - cfg, err := ini.Load(filepath) + cfg, err := ini.LoadSources(ini.LoadOptions{ + SpaceBeforeInlineComment: true, + }, filepath) conf := Config{} if err != nil { return conf, err diff --git a/feed.go b/feed.go index 66f9f07..6e6dc91 100644 --- a/feed.go +++ b/feed.go @@ -16,6 +16,7 @@ const ( TimelineFeedType FeedType = iota ThreadFeedType UserFeedType + UserSearchFeedType NotificationFeedType TagFeedType ) @@ -28,6 +29,7 @@ type Feed interface { DrawToot() FeedType() FeedType GetSavedIndex() int + GetDesc() string Input(event *tcell.EventKey) } @@ -49,27 +51,29 @@ 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()) + if status.Sensitive { strippedSpoiler, u = cleanTootHTML(status.SpoilerText) + strippedSpoiler = tview.Escape(strippedSpoiler) urls = append(urls, u...) } if status.Sensitive && !showSensitive { - strippedSpoiler += "\n" + line - strippedSpoiler += "Press [s] to show hidden text" + strippedSpoiler += "\n" + subtleColor + line + strippedSpoiler += subtleColor + tview.Escape("Press [s] to show hidden text") stripped = strippedSpoiler } if status.Sensitive && showSensitive { - stripped = strippedSpoiler + "\n\n" + strippedContent + stripped = strippedSpoiler + "\n\n" + tview.Escape(strippedContent) } if !status.Sensitive { - stripped = strippedContent + stripped = tview.Escape(strippedContent) } app.UI.LinkOverlay.SetLinks(urls, status) - 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()) var head string if status.Reblog != nil { if status.Account.DisplayName != "" { @@ -87,7 +91,7 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str } head += fmt.Sprintf(special1+"%s\n\n", status.Account.Acct) output := head - content := tview.Escape(stripped) + content := stripped if content != "" { output += content + "\n\n" } @@ -135,6 +139,9 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str if shouldDisplay { output += poll + media + card } + output += "\n" + subtleColor + line + output += fmt.Sprintf("%sReplies %s%d %sBoosts %s%d %sFavorites %s%d\n\n", + subtleColor, special1, status.RepliesCount, subtleColor, special1, status.ReblogsCount, subtleColor, special1, status.FavouritesCount) app.UI.StatusView.ScrollToBeginning() var info []string @@ -213,6 +220,20 @@ func (t *TimelineFeed) FeedType() FeedType { return TimelineFeedType } +func (t *TimelineFeed) GetDesc() string { + switch t.timelineType { + case TimelineHome: + return "Timeline home" + case TimelineDirect: + return "Timeline direct" + case TimelineLocal: + return "Timeline local" + case TimelineFederated: + return "Timeline federated" + } + return "Timeline" +} + func (t *TimelineFeed) GetCurrentStatus() *mastodon.Status { index := t.app.UI.StatusView.GetCurrentItem() if index >= len(t.statuses) { @@ -383,6 +404,10 @@ func (t *ThreadFeed) FeedType() FeedType { return ThreadFeedType } +func (t *ThreadFeed) GetDesc() string { + return "Thread" +} + func (t *ThreadFeed) GetCurrentStatus() *mastodon.Status { index := t.app.UI.StatusView.GetCurrentItem() if index >= len(t.statuses) { @@ -534,6 +559,10 @@ func (u *UserFeed) FeedType() FeedType { return UserFeedType } +func (u *UserFeed) GetDesc() string { + return "User " + u.user.Acct +} + func (u *UserFeed) GetCurrentStatus() *mastodon.Status { index := u.app.UI.app.UI.StatusView.GetCurrentItem() if index > 0 && index-1 >= len(u.statuses) { @@ -821,6 +850,10 @@ func (n *NotificationsFeed) FeedType() FeedType { return NotificationFeedType } +func (n *NotificationsFeed) GetDesc() string { + return "Notifications" +} + func (n *NotificationsFeed) GetCurrentNotification() *mastodon.Notification { index := n.app.UI.StatusView.GetCurrentItem() if index >= len(n.notifications) { @@ -1044,6 +1077,10 @@ func (t *TagFeed) FeedType() FeedType { return TagFeedType } +func (t *TagFeed) GetDesc() string { + return "Tag #" + t.tag +} + func (t *TagFeed) GetCurrentStatus() *mastodon.Status { index := t.app.UI.StatusView.GetCurrentItem() if index >= len(t.statuses) { @@ -1187,3 +1224,198 @@ func (t *TagFeed) Input(event *tcell.EventKey) { } } } + +func NewUserSearchFeed(app *App, s string) *UserSearchFeed { + u := &UserSearchFeed{ + app: app, + } + users, err := app.API.GetUsers(s) + if err != nil { + u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't load users. Error: %v\n", err)) + return u + } + u.users = users + return u +} + +type UserSearchFeed struct { + app *App + users []*UserSearchData + index int + search string +} + +func (u *UserSearchFeed) FeedType() FeedType { + return UserSearchFeedType +} + +func (u *UserSearchFeed) GetDesc() string { + return "User search: " + u.search +} + +func (u *UserSearchFeed) GetCurrentUser() *UserSearchData { + index := u.app.UI.app.UI.StatusView.GetCurrentItem() + if index > 0 && index-1 >= len(u.users) { + return nil + } + return u.users[index-1] +} + +func (u *UserSearchFeed) GetFeedList() <-chan string { + ch := make(chan string) + users := u.users + go func() { + for _, user := range users { + var username string + if user.User.DisplayName == "" { + username = user.User.Acct + } else { + username = fmt.Sprintf("%s (%s)", user.User.DisplayName, user.User.Acct) + } + ch <- username + } + close(ch) + }() + return ch +} + +func (u *UserSearchFeed) LoadNewer() int { + return 0 +} + +func (u *UserSearchFeed) LoadOlder() int { + return 0 +} + +func (u *UserSearchFeed) DrawList() { + u.app.UI.StatusView.SetList(u.GetFeedList()) +} + +func (u *UserSearchFeed) DrawToot() { + u.index = u.app.UI.StatusView.GetCurrentItem() + index := u.index + if index > len(u.users)-1 || len(u.users) == 0 { + return + } + user := u.users[index] + + var text string + var controls string + + n := fmt.Sprintf("[#%x]", u.app.Config.Style.Text.Hex()) + s1 := fmt.Sprintf("[#%x]", u.app.Config.Style.TextSpecial1.Hex()) + s2 := fmt.Sprintf("[#%x]", u.app.Config.Style.TextSpecial2.Hex()) + + if user.User.DisplayName != "" { + text = fmt.Sprintf(s2+"%s\n", user.User.DisplayName) + } + text += fmt.Sprintf(s1+"%s\n\n", user.User.Acct) + + text += fmt.Sprintf("Toots %s%d %sFollowers %s%d %sFollowing %s%d\n\n", + s2, user.User.StatusesCount, n, s2, user.User.FollowersCount, n, s2, user.User.FollowingCount) + + note, urls := cleanTootHTML(user.User.Note) + text += note + "\n\n" + + for _, f := range user.User.Fields { + value, fu := cleanTootHTML(f.Value) + text += fmt.Sprintf("%s%s: %s%s\n", s2, f.Name, n, value) + urls = append(urls, fu...) + } + + u.app.UI.LinkOverlay.SetLinks(urls, nil) + + var controlItems []string + if u.app.Me.ID != user.User.ID { + if user.Relationship.Following { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "F", "ollow")) + } else { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "F", "ollow")) + } + if user.Relationship.Blocking { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "B", "lock")) + } else { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "B", "lock")) + } + if user.Relationship.Muting { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "M", "ute")) + } else { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "M", "ute")) + } + if len(urls) > 0 { + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "O", "pen")) + } + } + controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "U", "ser")) + controls = strings.Join(controlItems, " ") + + u.app.UI.StatusView.SetText(text) + u.app.UI.StatusView.SetControls(controls) +} + +func (u *UserSearchFeed) GetSavedIndex() int { + return u.index +} + +func (u *UserSearchFeed) Input(event *tcell.EventKey) { + index := u.GetSavedIndex() + if index > len(u.users)-1 || len(u.users) == 0 { + return + } + user := u.users[index] + + if event.Key() == tcell.KeyRune { + switch event.Rune() { + case 'f', 'F': + var relation *mastodon.Relationship + var err error + if user.Relationship.Following { + relation, err = u.app.API.UnfollowUser(*user.User) + } else { + relation, err = u.app.API.FollowUser(*user.User) + } + if err != nil { + u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't follow/unfollow user. Error: %v\n", err)) + return + } + user.Relationship = relation + u.DrawToot() + case 'b', 'B': + var relation *mastodon.Relationship + var err error + if user.Relationship.Blocking { + relation, err = u.app.API.UnblockUser(*user.User) + } else { + relation, err = u.app.API.BlockUser(*user.User) + } + if err != nil { + u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't block/unblock user. Error: %v\n", err)) + return + } + user.Relationship = relation + u.DrawToot() + case 'm', 'M': + var relation *mastodon.Relationship + var err error + if user.Relationship.Muting { + relation, err = u.app.API.UnmuteUser(*user.User) + } else { + relation, err = u.app.API.MuteUser(*user.User) + } + if err != nil { + u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't mute/unmute user. Error: %v\n", err)) + return + } + user.Relationship = relation + u.DrawToot() + case 'r', 'R': + //toots and replies? + case 'o', 'O': + u.app.UI.ShowLinks() + case 'u', 'U': + u.app.UI.StatusView.AddFeed( + NewUserFeed(u.app, *user.User), + ) + } + } +} diff --git a/main.go b/main.go index 40f8b84..5e88dec 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(":tag,:timeline,:tl,:quit,:q", ",") + words := strings.Split(":compose,:tag,:timeline,:tl,:user,:quit,:q", ",") if currentText == "" { return } @@ -240,6 +240,9 @@ func main() { fallthrough case ":quit": app.UI.Root.Stop() + case ":compose": + app.UI.NewToot() + app.UI.CmdBar.ClearInput() case ":timeline", ":tl": if len(parts) < 2 { break @@ -277,6 +280,17 @@ func main() { app.UI.StatusView.AddFeed(NewTagFeed(app, tag)) app.UI.SetFocus(LeftPaneFocus) app.UI.CmdBar.ClearInput() + case ":user": + if len(parts) < 2 { + break + } + user := strings.TrimSpace(parts[1]) + if len(user) == 0 { + break + } + app.UI.StatusView.AddFeed(NewUserSearchFeed(app, user)) + app.UI.SetFocus(LeftPaneFocus) + app.UI.CmdBar.ClearInput() } }) diff --git a/statusview.go b/statusview.go index a90692a..2db412f 100644 --- a/statusview.go +++ b/statusview.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "time" "github.com/gdamore/tcell" @@ -27,12 +28,6 @@ func NewStatusView(app *App, tl TimelineType) *StatusView { t.list.ShowSecondaryText(false) t.list.SetHighlightFullLine(true) - t.list.SetChangedFunc(func(i int, _ string, _ string, _ rune) { - if app.HaveAccount { - t.showToot(i) - } - }) - t.text.SetWordWrap(true).SetDynamicColors(true) t.text.SetBackgroundColor(app.Config.Style.Background) t.text.SetTextColor(app.Config.Style.Text) @@ -71,6 +66,7 @@ func (t *StatusView) AddFeed(f Feed) { f.DrawList() t.list.SetCurrentItem(f.GetSavedIndex()) f.DrawToot() + t.drawDesc() } func (t *StatusView) RemoveLatestFeed() { @@ -79,6 +75,7 @@ func (t *StatusView) RemoveLatestFeed() { feed.DrawList() t.list.SetCurrentItem(feed.GetSavedIndex()) feed.DrawToot() + t.drawDesc() } func (t *StatusView) GetLeftView() tview.Primitive { @@ -206,10 +203,16 @@ func (t *StatusView) SetControls(text string) { t.controls.SetText(text) } -func (t *StatusView) showToot(index int) { -} - -func (t *StatusView) showTootOptions(index int, showSensitive bool) { +func (t *StatusView) drawDesc() { + if len(t.feeds) == 0 { + t.app.UI.SetTopText("") + return + } + l := len(t.feeds) + f := t.feeds[l-1] + t.app.UI.SetTopText( + fmt.Sprintf("%s (%d/%d)", f.GetDesc(), l, l), + ) } func (t *StatusView) prev() { diff --git a/ui.go b/ui.go index b6a2a42..665a094 100644 --- a/ui.go +++ b/ui.go @@ -51,6 +51,7 @@ func (ui *UI) Init() { } return 0, 0, 0, 0 }) + ui.SetTopText("") ui.Pages.AddPage("main", tview.NewFlex(). AddItem(tview.NewFlex().SetDirection(tview.FlexRow). @@ -222,6 +223,14 @@ func (ui *UI) OpenMedia(status *mastodon.Status) { } } +func (ui *UI) SetTopText(s string) { + if s == "" { + ui.Top.Text.SetText("tut") + } else { + ui.Top.Text.SetText(fmt.Sprintf("tut - %s", s)) + } +} + func (ui *UI) LoggedIn() { ui.StatusView = NewStatusView(ui.app, ui.Timeline) @@ -249,7 +258,6 @@ func (ui *UI) LoggedIn() { ui.Pages.SendToBack("main") ui.SetFocus(LeftPaneFocus) - fmt.Fprint(ui.Top.Text, "tut\n") me, err := ui.app.API.Client.GetAccountCurrentUser(context.Background()) if err != nil {