From 9f2d372ea097e9af3e09151f656dbd3518ece1c5 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Sun, 18 Dec 2022 19:42:12 +0100 Subject: [PATCH] 1.0.27 (#227) * bump version * filter * show boosted instead of booster * add :refetch * add special-* * update example config --- README.md | 5 +- api/feed.go | 20 ++-- api/item.go | 239 +++++++++++++++++++++++++-------------- api/status.go | 4 + config.example.ini | 40 +++++-- config/config.go | 111 ++++++++++-------- config/default_config.go | 40 +++++-- config/help.tmpl | 7 +- config/toot.tmpl | 4 +- feed/feed.go | 25 +++- go.mod | 6 +- go.sum | 8 +- main.go | 2 +- ui/cmdbar.go | 18 ++- ui/commands.go | 18 +++ ui/composeview.go | 28 ++--- ui/feed.go | 14 +++ ui/input.go | 44 ++++--- ui/item.go | 11 +- ui/item_notification.go | 12 +- ui/item_status.go | 21 ++-- ui/styled_elements.go | 8 ++ ui/timeline.go | 4 + 23 files changed, 459 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index da429b0..da33b8a 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t ## Currently supported commands * `:q` `:quit` exit -* `:timeline` home, local, federated, direct, notifications, favorited - * `:tl` h, l, f, d, n, fav (shorter form) +* `:timeline` home, local, federated, direct, notifications, favorited, special-all, special-boosts, special-replies + * `:tl` h, l, f, d, n, fav, sa, sb, sr (shorter form) * `:blocking` lists users that you have blocked * `:boosts` lists users that boosted the toot * `:bookmarks` lists all your bookmarks @@ -63,6 +63,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:preferences` update your profile and some other settings * `:profile` go to your profile * `:proportions` [int] [int], where the first integer is the list and the other content, e.g. `:proportions 1 3` +* `:refetch` refetches the current item * `:requests` see following requests * `:saved` alias for bookmarks * `:stick-to-top` toggle the stick-to-top setting. diff --git a/api/feed.go b/api/feed.go index ca633a0..ce5a756 100644 --- a/api/feed.go +++ b/api/feed.go @@ -10,14 +10,14 @@ import ( type TimelineType uint -func (ac *AccountClient) getStatusSimilar(fn func() ([]*mastodon.Status, error), filter string) ([]Item, error) { +func (ac *AccountClient) getStatusSimilar(fn func() ([]*mastodon.Status, error), timeline string) ([]Item, error) { var items []Item statuses, err := fn() if err != nil { return items, err } for _, s := range statuses { - item := NewStatusItem(s, ac.Filters, filter, false) + item := NewStatusItem(s, false) items = append(items, item) } return items, nil @@ -96,7 +96,7 @@ func (ac *AccountClient) GetNotifications(nth []config.NotificationToHide, pg *m if n.Account.ID == r.ID { item := NewNotificationItem(n, &User{ Data: &n.Account, Relation: r, - }, ac.Filters) + }) items = append(items, item) break } @@ -124,11 +124,11 @@ func (ac *AccountClient) GetThread(status *mastodon.Status) ([]Item, error) { return items, err } for _, s := range statuses.Ancestors { - items = append(items, NewStatusItem(s, ac.Filters, "thread", false)) + items = append(items, NewStatusItem(s, false)) } - items = append(items, NewStatusItem(status, ac.Filters, "thread", false)) + items = append(items, NewStatusItem(status, false)) for _, s := range statuses.Descendants { - items = append(items, NewStatusItem(s, ac.Filters, "thread", false)) + items = append(items, NewStatusItem(s, false)) } return items, nil } @@ -154,7 +154,7 @@ func (ac *AccountClient) GetConversations(pg *mastodon.Pagination) ([]Item, erro return items, err } for _, c := range conversations { - item := NewStatusItem(c.LastStatus, ac.Filters, "thread", false) + item := NewStatusItem(c.LastStatus, false) items = append(items, item) } return items, nil @@ -251,7 +251,7 @@ func (ac *AccountClient) GetUser(pg *mastodon.Pagination, id mastodon.ID) ([]Ite return items, err } for _, s := range statuses { - item := NewStatusItem(s, ac.Filters, "account", false) + item := NewStatusItem(s, false) items = append(items, item) } return items, nil @@ -264,7 +264,7 @@ func (ac *AccountClient) GetUserPinned(id mastodon.ID) ([]Item, error) { return items, err } for _, s := range statuses { - item := NewStatusItem(s, ac.Filters, "account", true) + item := NewStatusItem(s, true) items = append(items, item) } return items, nil @@ -301,7 +301,7 @@ func (ac *AccountClient) GetListStatuses(pg *mastodon.Pagination, id mastodon.ID return items, err } for _, s := range statuses { - item := NewStatusItem(s, ac.Filters, "home", false) + item := NewStatusItem(s, false) items = append(items, item) } return items, nil diff --git a/api/item.go b/api/item.go index 9f92139..3abf57d 100644 --- a/api/item.go +++ b/api/item.go @@ -3,10 +3,11 @@ package api import ( "strings" "sync" - "unicode" "github.com/RasmusLindroth/go-mastodon" + "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" + "golang.org/x/exp/slices" ) var id uint = 0 @@ -22,17 +23,25 @@ func newID() uint { type Item interface { ID() uint Type() MastodonType - ToggleSpoiler() - ShowSpoiler() bool + ToggleCW() + ShowCW() bool Raw() interface{} URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) - Filtered() (bool, string) + Filtered(config.FeedType) (bool, string, string, bool) + ForceViewFilter() Pinned() bool + Refetch(*AccountClient) bool } type filtered struct { - inUse bool - name string + InUse bool + Filters []filter +} + +type filter struct { + Values []string + Where []string + Type string } func getUrlsStatus(status *mastodon.Status) ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) { @@ -72,72 +81,36 @@ func getUrlsUser(user *mastodon.Account) ([]util.URL, []mastodon.Mention, []mast return urls, []mastodon.Mention{}, []mastodon.Tag{}, len(urls) } -func NewStatusItem(item *mastodon.Status, filters []*mastodon.Filter, timeline string, pinned bool) (sitem Item) { - filtered := filtered{inUse: false} +func NewStatusItem(item *mastodon.Status, pinned bool) (sitem Item) { + filtered := filtered{InUse: false} if item == nil { return &StatusItem{id: newID(), item: item, showSpoiler: false, filtered: filtered, pinned: pinned} } s := util.StatusOrReblog(item) - content := s.Content - if s.Sensitive { - content += "\n" + s.SpoilerText - } - content = strings.ToLower(content) - for _, f := range filters { - apply := false - for _, c := range f.Context { - if timeline == c { - apply = true - break - } - } - if !apply { - continue - } - if f.WholeWord { - lines := strings.Split(content, "\n") - var stripped []string - for _, l := range lines { - var words []string - words = append(words, strings.Split(l, " ")...) - for _, w := range words { - ns := strings.TrimSpace(w) - ns = strings.TrimFunc(ns, func(r rune) bool { - return !unicode.IsLetter(r) && !unicode.IsNumber(r) - }) - stripped = append(stripped, ns) - } - } - filter := strings.Split(strings.ToLower(f.Phrase), " ") - for i := 0; i+len(filter)-1 < len(stripped); i++ { - if strings.ToLower(f.Phrase) == strings.Join(stripped[i:i+len(filter)], " ") { - filtered.inUse = true - filtered.name = f.Phrase - break - } - } - } else { - if strings.Contains(s.Content, strings.ToLower(f.Phrase)) { - filtered.inUse = true - filtered.name = f.Phrase - } - if strings.Contains(s.SpoilerText, strings.ToLower(f.Phrase)) { - filtered.inUse = true - filtered.name = f.Phrase - } - } - if filtered.inUse { - break - } + for _, f := range s.Filtered { + filtered.InUse = true + filtered.Filters = append(filtered.Filters, + filter{ + Type: f.Filter.FilterAction, + Values: f.KeywordMatches, + Where: f.Filter.Context, + }) } sitem = &StatusItem{id: newID(), item: item, showSpoiler: false, filtered: filtered, pinned: pinned} return sitem } +func NewStatusItemID(item *mastodon.Status, pinned bool, id uint) (sitem Item) { + sitem = NewStatusItem(item, pinned) + sitem.(*StatusItem).id = id + return sitem +} + type StatusItem struct { id uint item *mastodon.Status showSpoiler bool + forceView bool filtered filtered pinned bool } @@ -150,11 +123,11 @@ func (s *StatusItem) Type() MastodonType { return StatusType } -func (s *StatusItem) ToggleSpoiler() { +func (s *StatusItem) ToggleCW() { s.showSpoiler = !s.showSpoiler } -func (s *StatusItem) ShowSpoiler() bool { +func (s *StatusItem) ShowCW() bool { return s.showSpoiler } @@ -166,14 +139,80 @@ func (s *StatusItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int return getUrlsStatus(s.item) } -func (s *StatusItem) Filtered() (bool, string) { - return s.filtered.inUse, s.filtered.name +func (s *StatusItem) Filtered(tl config.FeedType) (bool, string, string, bool) { + if !s.filtered.InUse || s.forceView { + return false, "", "", true + } + words := []string{} + t := "" + for _, f := range s.filtered.Filters { + used := false + for _, w := range f.Where { + switch w { + case "home", "special": + if tl == config.TimelineHome || tl == config.List { + used = true + } + case "thread": + if tl == config.Thread || tl == config.Conversations { + used = true + } + case "notifications": + if tl == config.Notifications { + used = true + } + + case "account": + if tl == config.User { + used = true + } + case "public": + where := []config.FeedType{ + config.Favorites, + config.Favorited, + config.Boosts, + config.Tag, + config.Notifications, + config.TimelineHome, + config.TimelineHomeSpecial, + config.Conversations, + config.User, + config.List, + } + if !slices.Contains(where, tl) { + used = true + } + } + if used { + words = append(words, f.Values...) + if t == "" || t == "warn" { + t = f.Type + } + break + } + } + } + return len(words) > 0, t, strings.Join(words, ", "), s.forceView +} + +func (s *StatusItem) ForceViewFilter() { + s.forceView = true } func (s *StatusItem) Pinned() bool { return s.pinned } +func (s *StatusItem) Refetch(ac *AccountClient) bool { + ns, err := ac.GetStatus(s.item.ID) + if err != nil { + return false + } + nsi := NewStatusItemID(ns, s.pinned, s.id) + *s = *nsi.(*StatusItem) + return true +} + func NewStatusHistoryItem(item *mastodon.StatusHistory) (sitem Item) { return &StatusHistoryItem{id: newID(), item: item, showSpoiler: false} } @@ -192,11 +231,11 @@ func (s *StatusHistoryItem) Type() MastodonType { return StatusHistoryType } -func (s *StatusHistoryItem) ToggleSpoiler() { +func (s *StatusHistoryItem) ToggleCW() { s.showSpoiler = !s.showSpoiler } -func (s *StatusHistoryItem) ShowSpoiler() bool { +func (s *StatusHistoryItem) ShowCW() bool { return s.showSpoiler } @@ -217,14 +256,20 @@ func (s *StatusHistoryItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.T return getUrlsStatus(&status) } -func (s *StatusHistoryItem) Filtered() (bool, string) { - return false, "" +func (s *StatusHistoryItem) Filtered(config.FeedType) (bool, string, string, bool) { + return false, "", "", true } +func (t *StatusHistoryItem) ForceViewFilter() {} + func (s *StatusHistoryItem) Pinned() bool { return false } +func (s *StatusHistoryItem) Refetch(ac *AccountClient) bool { + return false +} + func NewUserItem(item *User, profile bool) Item { return &UserItem{id: newID(), item: item, profile: profile} } @@ -246,10 +291,10 @@ func (u *UserItem) Type() MastodonType { return UserType } -func (u *UserItem) ToggleSpoiler() { +func (u *UserItem) ToggleCW() { } -func (u *UserItem) ShowSpoiler() bool { +func (u *UserItem) ShowCW() bool { return false } @@ -261,16 +306,22 @@ func (u *UserItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) return getUrlsUser(u.item.Data) } -func (s *UserItem) Filtered() (bool, string) { - return false, "" +func (u *UserItem) Filtered(config.FeedType) (bool, string, string, bool) { + return false, "", "", true } +func (u *UserItem) ForceViewFilter() {} + func (u *UserItem) Pinned() bool { return false } -func NewNotificationItem(item *mastodon.Notification, user *User, filters []*mastodon.Filter) (nitem Item) { - status := NewStatusItem(item.Status, filters, "notifications", false) +func (u *UserItem) Refetch(ac *AccountClient) bool { + return false +} + +func NewNotificationItem(item *mastodon.Notification, user *User) (nitem Item) { + status := NewStatusItem(item.Status, false) nitem = &NotificationItem{ id: newID(), item: item, @@ -304,11 +355,11 @@ func (n *NotificationItem) Type() MastodonType { return NotificationType } -func (n *NotificationItem) ToggleSpoiler() { +func (n *NotificationItem) ToggleCW() { n.showSpoiler = !n.showSpoiler } -func (n *NotificationItem) ShowSpoiler() bool { +func (n *NotificationItem) ShowCW() bool { return n.showSpoiler } @@ -344,14 +395,20 @@ func (n *NotificationItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Ta } } -func (n *NotificationItem) Filtered() (bool, string) { - return false, "" +func (n *NotificationItem) Filtered(config.FeedType) (bool, string, string, bool) { + return false, "", "", true } +func (n *NotificationItem) ForceViewFilter() {} + func (n *NotificationItem) Pinned() bool { return false } +func (n *NotificationItem) Refetch(ac *AccountClient) bool { + return false +} + func NewListsItem(item *mastodon.List) Item { return &ListItem{id: newID(), item: item, showSpoiler: true} } @@ -370,10 +427,10 @@ func (s *ListItem) Type() MastodonType { return ListsType } -func (s *ListItem) ToggleSpoiler() { +func (s *ListItem) ToggleCW() { } -func (s *ListItem) ShowSpoiler() bool { +func (s *ListItem) ShowCW() bool { return true } @@ -385,14 +442,20 @@ func (s *ListItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) return nil, nil, nil, 0 } -func (s *ListItem) Filtered() (bool, string) { - return false, "" +func (s *ListItem) Filtered(config.FeedType) (bool, string, string, bool) { + return false, "", "", true } +func (l *ListItem) ForceViewFilter() {} + func (n *ListItem) Pinned() bool { return false } +func (l *ListItem) Refetch(ac *AccountClient) bool { + return false +} + func NewTagItem(item *mastodon.Tag) Item { return &TagItem{id: newID(), item: item, showSpoiler: true} } @@ -411,10 +474,10 @@ func (t *TagItem) Type() MastodonType { return TagType } -func (t *TagItem) ToggleSpoiler() { +func (t *TagItem) ToggleCW() { } -func (t *TagItem) ShowSpoiler() bool { +func (t *TagItem) ShowCW() bool { return true } @@ -426,10 +489,16 @@ func (t *TagItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) { return nil, nil, nil, 0 } -func (t *TagItem) Filtered() (bool, string) { - return false, "" +func (t *TagItem) Filtered(config.FeedType) (bool, string, string, bool) { + return false, "", "", true } +func (t *TagItem) ForceViewFilter() {} + func (t *TagItem) Pinned() bool { return false } + +func (t *TagItem) Refetch(ac *AccountClient) bool { + return false +} diff --git a/api/status.go b/api/status.go index 282e732..925f5ca 100644 --- a/api/status.go +++ b/api/status.go @@ -90,3 +90,7 @@ func (ac *AccountClient) Unbookmark(s *mastodon.Status) (*mastodon.Status, error func (ac *AccountClient) DeleteStatus(s *mastodon.Status) error { return ac.Client.DeleteStatus(context.Background(), util.StatusOrReblog(s).ID) } + +func (ac *AccountClient) GetStatus(id mastodon.ID) (*mastodon.Status, error) { + return ac.Client.GetStatus(context.Background(), id) +} diff --git a/config.example.ini b/config.example.ini index eba1710..f924539 100644 --- a/config.example.ini +++ b/config.example.ini @@ -13,8 +13,12 @@ mouse-support=false # Timelines adds windows of feeds. You can customize the number of feeds, what # they should show and the key to activate them. # -# Available timelines: home, direct, local, federated, bookmarks, saved, -# favorited, notifications, lists, tag +# Available timelines: home, direct, local, federated, special-all, +# special-boosts, special-replies, bookmarks, saved, favorited, notifications, +# lists, tag +# +# The ones named special-* are the home timeline with only boosts and/or +# replies. All contains both, -boosts only boosts and -replies only replies. # # Tag is special as you need to add the tag after, see the example below. # @@ -134,6 +138,11 @@ show-help=true # default=false stick-to-top=false +# If you want to display the username of the person being boosted instead of the +# person that boosted. +# default=false +show-boosted-user=false + # 0 = No terminal title # 1 = Show title in terminal and top bar # 2 = Only show terminal title, and no top bar in tut. @@ -164,11 +173,14 @@ leader-timeout=1000 # of two parts first the action then the shortcut. And they're separated by a # comma. # -# Available commands: home, direct, local, federated, clear-notifications, -# compose, edit, history, blocking, bookmarks, saved, favorited, boosts, -# favorites, following, followers, muting, newer, preferences, profile, -# notifications, lists, stick-to-top, tag, tags, window, list-placement, -# list-split, proportions +# Available commands: home, direct, local, federated, special-all, +# special-boosts, special-replies, clear-notifications, compose, edit, history, +# blocking, bookmarks, refetch, saved, favorited, boosts, favorites, following, +# followers, muting, newer, preferences, profile, notifications, lists, +# stick-to-top, tag, tags, window, list-placement, list-split, proportions +# +# The ones named special-* are the home timeline with only boosts and/or +# replies. All contains both, -boosts only boosts and -replies only replies. # # The shortcuts are up to you, but keep them quite short and make sure they # don't collide. If you have one shortcut that is "f" and an other one that is @@ -622,9 +634,13 @@ status-view-focus="[V]iew",'v','V' # default="[Y]ank",'y','Y' status-yank="[Y]ank",'y','Y' -# Remove the spoiler -# default="Press [Z] to toggle spoiler",'z','Z' -status-toggle-spoiler="Press [Z] to toggle spoiler",'z','Z' +# Show the content in a content warning +# default="Press [Z] to toggle cw",'z','Z' +status-toggle-cw="Press [Z] to toggle cw",'z','Z' + +# Show the content of a filtered toot +# default="Press [Z] to view filtered toot",'z','Z' +status-show-filtered="Press [Z] to view filtered toot",'z','Z' # View avatar # default="[A]vatar",'a','A' @@ -694,9 +710,9 @@ tag-open-feed="[O]pen",'o','O' # default="[F]ollow","Un[F]ollow",'f','F' tag-follow="[F]ollow","Un[F]ollow",'f','F' -# Edit spoiler text on new toot +# Edit content warning text on new toot # default="[C]W text",'c','C' -compose-edit-spoiler="[C]W text",'c','C' +compose-edit-cw="[C]W text",'c','C' # Edit the text on new toot # default="[E]dit text",'e','E' diff --git a/config/config.go b/config/config.go index 9d4bd1a..241e268 100644 --- a/config/config.go +++ b/config/config.go @@ -55,6 +55,9 @@ const ( LeaderDirect LeaderLocal LeaderFederated + LeaderSpecialAll + LeaderSpecialBoosts + LeaderSpecialReplies LeaderClearNotifications LeaderCompose LeaderEdit @@ -74,6 +77,7 @@ const ( LeaderProportions LeaderNotifications LeaderLists + LeaderRefetch LeaderTag LeaderTags LeaderStickToTop @@ -103,6 +107,7 @@ const ( Thread TimelineFederated TimelineHome + TimelineHomeSpecial TimelineLocal Conversations User @@ -142,7 +147,6 @@ type General struct { DateFormat string DateRelative int MaxWidth int - StartTimeline FeedType NotificationFeed bool QuoteReply bool CharLimit int @@ -163,6 +167,7 @@ type General struct { Timelines []Timeline StickToTop bool NotificationsToHide []NotificationToHide + ShowBoostedUser bool } type Style struct { @@ -407,21 +412,22 @@ type Input struct { MainNextWindow Key MainCompose Key - StatusAvatar Key - StatusBoost Key - StatusDelete Key - StatusEdit Key - StatusFavorite Key - StatusMedia Key - StatusLinks Key - StatusPoll Key - StatusReply Key - StatusBookmark Key - StatusThread Key - StatusUser Key - StatusViewFocus Key - StatusYank Key - StatusToggleSpoiler Key + StatusAvatar Key + StatusBoost Key + StatusDelete Key + StatusEdit Key + StatusFavorite Key + StatusMedia Key + StatusLinks Key + StatusPoll Key + StatusReply Key + StatusBookmark Key + StatusThread Key + StatusUser Key + StatusViewFocus Key + StatusYank Key + StatusToggleCW Key + StatusShowFiltered Key UserAvatar Key UserBlock Key @@ -444,7 +450,7 @@ type Input struct { LinkOpen Key LinkYank Key - ComposeEditSpoiler Key + ComposeEditCW Key ComposeEditText Key ComposeIncludeQuote Key ComposeMediaFocus Key @@ -831,18 +837,6 @@ func parseGeneral(cfg *ini.File) General { } general.DateRelative = dateRelative - tl := cfg.Section("general").Key("timeline").In("home", []string{"home", "direct", "local", "federated"}) - switch tl { - case "direct": - general.StartTimeline = Conversations - case "local": - general.StartTimeline = TimelineLocal - case "federated": - general.StartTimeline = TimelineFederated - default: - general.StartTimeline = TimelineHome - } - general.NotificationFeed = cfg.Section("general").Key("notification-feed").MustBool(true) general.QuoteReply = cfg.Section("general").Key("quote-reply").MustBool(false) general.CharLimit = cfg.Section("general").Key("char-limit").MustInt(500) @@ -853,6 +847,7 @@ func parseGeneral(cfg *ini.File) General { general.ShowHelp = cfg.Section("general").Key("show-help").MustBool(true) general.RedrawUI = cfg.Section("general").Key("redraw-ui").MustBool(true) general.StickToTop = cfg.Section("general").Key("stick-to-top").MustBool(false) + general.ShowBoostedUser = cfg.Section("general").Key("show-boosted-user").MustBool(false) lp := cfg.Section("general").Key("list-placement").In("left", []string{"left", "right", "top", "bottom"}) switch lp { @@ -926,6 +921,12 @@ func parseGeneral(cfg *ini.File) General { la.Command = LeaderLocal case "federated": la.Command = LeaderFederated + case "special-all": + la.Command = LeaderSpecialAll + case "special-boosts": + la.Command = LeaderSpecialBoosts + case "special-replies": + la.Command = LeaderSpecialReplies case "clear-notifications": la.Command = LeaderClearNotifications case "compose": @@ -962,6 +963,8 @@ func parseGeneral(cfg *ini.File) General { la.Command = LeaderLists case "stick-to-top": la.Command = LeaderStickToTop + case "refetch": + la.Command = LeaderRefetch case "tag": la.Command = LeaderTag la.Subaction = subaction @@ -1017,6 +1020,8 @@ func parseGeneral(cfg *ini.File) General { switch cmd { case "home": tl.FeedType = TimelineHome + case "special": + tl.FeedType = TimelineHomeSpecial case "direct": tl.FeedType = Conversations case "local": @@ -1372,21 +1377,22 @@ func parseInput(cfg *ini.File) Input { MainNextWindow: inputStrOrErr([]string{"\"\"", "\"Tab\""}, false), MainCompose: inputStrOrErr([]string{"\"\"", "'c'", "'C'"}, false), - StatusAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), - StatusBoost: inputStrOrErr([]string{"\"[B]oost\"", "\"Un[B]oost\"", "'b'", "'B'"}, true), - StatusDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), - StatusEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false), - StatusFavorite: inputStrOrErr([]string{"\"[F]avorite\"", "\"Un[F]avorite\"", "'f'", "'F'"}, true), - StatusMedia: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false), - StatusLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), - StatusPoll: inputStrOrErr([]string{"\"[P]oll\"", "'p'", "'P'"}, false), - StatusReply: inputStrOrErr([]string{"\"[R]eply\"", "'r'", "'R'"}, false), - StatusBookmark: inputStrOrErr([]string{"\"[S]ave\"", "\"Un[S]ave\"", "'s'", "'S'"}, true), - StatusThread: inputStrOrErr([]string{"\"[T]hread\"", "'t'", "'T'"}, false), - StatusUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false), - StatusViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), - StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), - StatusToggleSpoiler: inputStrOrErr([]string{"\"Press [Z] to toggle spoiler\"", "'z'", "'Z'"}, false), + StatusAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), + StatusBoost: inputStrOrErr([]string{"\"[B]oost\"", "\"Un[B]oost\"", "'b'", "'B'"}, true), + StatusDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), + StatusEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false), + StatusFavorite: inputStrOrErr([]string{"\"[F]avorite\"", "\"Un[F]avorite\"", "'f'", "'F'"}, true), + StatusMedia: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false), + StatusLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), + StatusPoll: inputStrOrErr([]string{"\"[P]oll\"", "'p'", "'P'"}, false), + StatusReply: inputStrOrErr([]string{"\"[R]eply\"", "'r'", "'R'"}, false), + StatusBookmark: inputStrOrErr([]string{"\"[S]ave\"", "\"Un[S]ave\"", "'s'", "'S'"}, true), + StatusThread: inputStrOrErr([]string{"\"[T]hread\"", "'t'", "'T'"}, false), + StatusUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false), + StatusViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), + StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), + StatusToggleCW: inputStrOrErr([]string{"\"Press [Z] to toggle CW\"", "'z'", "'Z'"}, false), + StatusShowFiltered: inputStrOrErr([]string{"\"Press [Z] to view filtered toot\"", "'z'", "'Z'"}, false), UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true), @@ -1409,7 +1415,7 @@ func parseInput(cfg *ini.File) Input { LinkOpen: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), LinkYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), - ComposeEditSpoiler: inputStrOrErr([]string{"\"[C]W Text\"", "'c'", "'C'"}, false), + ComposeEditCW: inputStrOrErr([]string{"\"[C]W Text\"", "'c'", "'C'"}, false), ComposeEditText: inputStrOrErr([]string{"\"[E]dit text\"", "'e'", "'E'"}, false), ComposeIncludeQuote: inputStrOrErr([]string{"\"[I]nclude quote\"", "'i'", "'I'"}, false), ComposeMediaFocus: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false), @@ -1467,7 +1473,13 @@ func parseInput(cfg *ini.File) Input { ic.StatusUser = inputOrErr(cfg, "status-user", false, ic.StatusUser) ic.StatusViewFocus = inputOrErr(cfg, "status-view-focus", false, ic.StatusViewFocus) ic.StatusYank = inputOrErr(cfg, "status-yank", false, ic.StatusYank) - ic.StatusToggleSpoiler = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleSpoiler) + ic.StatusToggleCW = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleCW) + ts := cfg.Section("input").Key("status-toggle-spoiler").MustString("") + if ts != "" { + ic.StatusToggleCW = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleCW) + } else { + ic.StatusToggleCW = inputOrErr(cfg, "status-toggle-cw", false, ic.StatusToggleCW) + } ic.UserAvatar = inputOrErr(cfg, "user-avatar", false, ic.UserAvatar) ic.UserBlock = inputOrErr(cfg, "user-block", true, ic.UserBlock) @@ -1490,7 +1502,12 @@ func parseInput(cfg *ini.File) Input { ic.LinkOpen = inputOrErr(cfg, "link-open", false, ic.LinkOpen) ic.LinkYank = inputOrErr(cfg, "link-yank", false, ic.LinkYank) - ic.ComposeEditSpoiler = inputOrErr(cfg, "compose-edit-spoiler", false, ic.ComposeEditSpoiler) + es := cfg.Section("input").Key("compose-edit-spoiler").MustString("") + if es != "" { + ic.ComposeEditCW = inputOrErr(cfg, "compose-edit-spoiler", false, ic.ComposeEditCW) + } else { + ic.ComposeEditCW = inputOrErr(cfg, "compose-edit-cw", false, ic.ComposeEditCW) + } ic.ComposeEditText = inputOrErr(cfg, "compose-edit-text", false, ic.ComposeEditText) ic.ComposeIncludeQuote = inputOrErr(cfg, "compose-include-quote", false, ic.ComposeIncludeQuote) ic.ComposeMediaFocus = inputOrErr(cfg, "compose-media-focus", false, ic.ComposeMediaFocus) diff --git a/config/default_config.go b/config/default_config.go index e02fbd4..5c37d0d 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -15,8 +15,12 @@ mouse-support=false # Timelines adds windows of feeds. You can customize the number of feeds, what # they should show and the key to activate them. # -# Available timelines: home, direct, local, federated, bookmarks, saved, -# favorited, notifications, lists, tag +# Available timelines: home, direct, local, federated, special-all, +# special-boosts, special-replies, bookmarks, saved, favorited, notifications, +# lists, tag +# +# The ones named special-* are the home timeline with only boosts and/or +# replies. All contains both, -boosts only boosts and -replies only replies. # # Tag is special as you need to add the tag after, see the example below. # @@ -136,6 +140,11 @@ show-help=true # default=false stick-to-top=false +# If you want to display the username of the person being boosted instead of the +# person that boosted. +# default=false +show-boosted-user=false + # 0 = No terminal title # 1 = Show title in terminal and top bar # 2 = Only show terminal title, and no top bar in tut. @@ -166,11 +175,14 @@ leader-timeout=1000 # of two parts first the action then the shortcut. And they're separated by a # comma. # -# Available commands: home, direct, local, federated, clear-notifications, -# compose, edit, history, blocking, bookmarks, saved, favorited, boosts, -# favorites, following, followers, muting, newer, preferences, profile, -# notifications, lists, stick-to-top, tag, tags, window, list-placement, -# list-split, proportions +# Available commands: home, direct, local, federated, special-all, +# special-boosts, special-replies, clear-notifications, compose, edit, history, +# blocking, bookmarks, refetch, saved, favorited, boosts, favorites, following, +# followers, muting, newer, preferences, profile, notifications, lists, +# stick-to-top, tag, tags, window, list-placement, list-split, proportions +# +# The ones named special-* are the home timeline with only boosts and/or +# replies. All contains both, -boosts only boosts and -replies only replies. # # The shortcuts are up to you, but keep them quite short and make sure they # don't collide. If you have one shortcut that is "f" and an other one that is @@ -624,9 +636,13 @@ status-view-focus="[V]iew",'v','V' # default="[Y]ank",'y','Y' status-yank="[Y]ank",'y','Y' -# Remove the spoiler -# default="Press [Z] to toggle spoiler",'z','Z' -status-toggle-spoiler="Press [Z] to toggle spoiler",'z','Z' +# Show the content in a content warning +# default="Press [Z] to toggle cw",'z','Z' +status-toggle-cw="Press [Z] to toggle cw",'z','Z' + +# Show the content of a filtered toot +# default="Press [Z] to view filtered toot",'z','Z' +status-show-filtered="Press [Z] to view filtered toot",'z','Z' # View avatar # default="[A]vatar",'a','A' @@ -696,9 +712,9 @@ tag-open-feed="[O]pen",'o','O' # default="[F]ollow","Un[F]ollow",'f','F' tag-follow="[F]ollow","Un[F]ollow",'f','F' -# Edit spoiler text on new toot +# Edit content warning text on new toot # default="[C]W text",'c','C' -compose-edit-spoiler="[C]W text",'c','C' +compose-edit-cw="[C]W text",'c','C' # Edit the text on new toot # default="[E]dit text",'e','E' diff --git a/config/help.tmpl b/config/help.tmpl index 1d7ac1f..5123c01 100644 --- a/config/help.tmpl +++ b/config/help.tmpl @@ -30,10 +30,10 @@ Here's a list of supported commands. {{- Color .Style.TextSpecial2 }}{{ Flags "b" }} :quit{{ Flags "-" }}{{ Color .Style.Text }} Exit the program -{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:timeline{{ Flags "-" }}{{ Color .Style.Text }} home|local|federated|direct|notifications|favorited +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:timeline{{ Flags "-" }}{{ Color .Style.Text }} home|local|federated|direct|notifications|favorited|special-all|special-boosts|special-replies Open selected timeline -{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tl{{ Flags "-" }}{{ Color .Style.Text }} h|l|f|d|n|fav +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tl{{ Flags "-" }}{{ Color .Style.Text }} h|l|f|d|n|fav|sa|sb|sr Shorter form of the former command *:timeline* {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:blocking{{ Flags "-" }}{{ Color .Style.Text }} @@ -97,6 +97,9 @@ Here's a list of supported commands. {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:proportions{{ Flags "-" }}{{ Color .Style.Text }} [int] [int] Set proportions for list and content with an int. First list proportion then content proportion +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:refetch{{ Flags "-" }}{{ Color .Style.Text }} + Refetch the current item + {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:requests{{ Flags "-" }}{{ Color .Style.Text }} See following requests diff --git a/config/toot.tmpl b/config/toot.tmpl index a5eda86..dacb537 100644 --- a/config/toot.tmpl +++ b/config/toot.tmpl @@ -20,9 +20,9 @@ {{- end }} {{- if .Toot.Edited -}}{{ Color .Style.Subtle }} (edited toot){{ end }} {{ if .Toot.Spoiler -}} -{{ Color .Style.Text }}{{ .Toot.SpoilerText }} +{{ Color .Style.Text }}{{ .Toot.CWText }} {{ if not .Toot.ShowSpoiler }} -{{ Color .Style.Subtle }}Press [z[] to show hidden text{{ Color .Style.Text }} +{{ Color .Style.Subtle }}{{ .Toot.CWlabel }}{{ Color .Style.Text }} {{ end }} {{ end -}} {{- if or (not .Toot.Spoiler) (.Toot.ShowSpoiler) -}} diff --git a/feed/feed.go b/feed/feed.go index 758b847..cc865ab 100644 --- a/feed/feed.go +++ b/feed/feed.go @@ -78,6 +78,9 @@ func (f *Feed) filteredList() []api.Item { for _, fd := range f.items { switch x := fd.Raw().(type) { case *mastodon.Status: + if f.Type() == config.TimelineHomeSpecial && x.Reblog == nil && x.InReplyToID == nil { + continue + } if x.Reblog != nil && !f.showBoosts { continue } @@ -85,6 +88,10 @@ func (f *Feed) filteredList() []api.Item { continue } } + inUse, fType, _, _ := fd.Filtered(f.feedType) + if inUse && fType == "hide" { + continue + } filtered = append(filtered, fd) } r := f.sticky @@ -684,7 +691,7 @@ func (f *Feed) startStream(rec *api.Receiver, timeline string, err error) { for e := range rec.Ch { switch t := e.(type) { case *mastodon.UpdateEvent: - s := api.NewStatusItem(t.Status, f.accountClient.Filters, timeline, false) + s := api.NewStatusItem(t.Status, false) f.itemsMux.Lock() found := false if len(f.streams) > 0 { @@ -766,7 +773,7 @@ func (f *Feed) startStreamNotification(rec *api.Receiver, timeline string, err e &api.User{ Data: &t.Notification.Account, Relation: rel[0], - }, f.accountClient.Filters) + }) f.itemsMux.Lock() f.items = append([]api.Item{s}, f.items...) nft := DesktopNotificationNone @@ -831,6 +838,20 @@ func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, showBoosts bool, return feed } +func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { + feed := newFeed(ac, config.TimelineHomeSpecial, cnf, showBoosts, showReplies) + feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) } + feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) } + feed.startStream(feed.accountClient.NewHomeStream()) + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveHomeReceiver(s) + } + } + + return feed +} + func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { feed := newFeed(ac, config.TimelineFederated, cnf, showBoosts, showReplies) feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineFederated) } diff --git a/go.mod b/go.mod index 6dc8c96..47cba5e 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.16 + github.com/RasmusLindroth/go-mastodon v0.0.17 github.com/atotto/clipboard v0.1.4 github.com/gdamore/tcell/v2 v2.5.3 github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 @@ -11,10 +11,10 @@ require ( github.com/icza/gox v0.0.0-20221026131554-a08a8cdc726a github.com/microcosm-cc/bluemonday v1.0.21 github.com/pelletier/go-toml/v2 v2.0.6 - github.com/rivo/tview v0.0.0-20221212150847-19d943d59543 + github.com/rivo/tview v0.0.0-20221217182043-ccce554c3803 github.com/rivo/uniseg v0.4.3 github.com/spf13/pflag v1.0.5 - golang.org/x/exp v0.0.0-20221212164502-fae10dda9338 + golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 golang.org/x/net v0.4.0 gopkg.in/ini.v1 v1.67.0 ) diff --git a/go.sum b/go.sum index 1564665..2fbe9c3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/RasmusLindroth/go-mastodon v0.0.16 h1:87lm4+TE6cNguL/gbfHDYqbOd1Jblnpy8J0l1O3/1wo= -github.com/RasmusLindroth/go-mastodon v0.0.16/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE= +github.com/RasmusLindroth/go-mastodon v0.0.17 h1:PUR4YS9ORe62ZSabvZVwxROZvrcMuNVC/8Y/D/d6dFQ= +github.com/RasmusLindroth/go-mastodon v0.0.17/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= @@ -40,6 +40,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/tview v0.0.0-20221212150847-19d943d59543 h1:qu4/1SXI23subKkH50FN7t6r0tPg7i7jI48M5kZ2qEE= github.com/rivo/tview v0.0.0-20221212150847-19d943d59543/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= +github.com/rivo/tview v0.0.0-20221217182043-ccce554c3803 h1:gaknGRzW4g4I+5sGu4X81BZbROJ0j96ap9xnEbcZhXA= +github.com/rivo/tview v0.0.0-20221217182043-ccce554c3803/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -58,6 +60,8 @@ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= golang.org/x/exp v0.0.0-20221212164502-fae10dda9338 h1:OvjRkcNHnf6/W5FZXSxODbxwD+X7fspczG7Jn/xQVD4= golang.org/x/exp v0.0.0-20221212164502-fae10dda9338/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go index 903635a..37a7c80 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.26" +const version = "1.0.27" func main() { util.SetTerminalTitle("tut") diff --git a/ui/cmdbar.go b/ui/cmdbar.go index e684966..3116d0e 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -162,6 +162,15 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { case "federated", "f": c.tutView.FederatedCommand() c.Back() + case "special-all", "sa": + c.tutView.SpecialCommand(true, true) + c.Back() + case "special-boosts", "sb": + c.tutView.SpecialCommand(true, false) + c.Back() + case "special-replies", "sr": + c.tutView.SpecialCommand(false, true) + c.Back() case "direct", "d": c.tutView.DirectCommand() c.Back() @@ -206,6 +215,9 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { NewUserSearchFeed(c.tutView, user), ) c.Back() + case ":refetch": + c.tutView.RefetchCommand() + c.Back() case ":stick-to-top": c.tutView.ToggleStickToTop() c.Back() @@ -242,16 +254,16 @@ 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,:follow-tag,:followers,:following,:help,:h,:history,:lists,:list-placement,:list-split,:muting,:newer,:preferences,:profile,:proportions,:requests,:saved,:stick-to-top,:tag,:timeline,:tl,:unfollow-tag,: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,:refetch,:requests,:saved,:stick-to-top,:tag,:timeline,:tl,:unfollow-tag,:user,:window,:quit,:q", ",") if curr == "" { return entries } if len(curr) > 2 && curr[:3] == ":tl" { - words = strings.Split(":tl home,:tl notifications,:tl local,:tl federated,:tl direct,:tl favorited", ",") + words = strings.Split(":tl home,:tl notifications,:tl local,:tl federated,:tl direct,:tl favorited,:tl special-all,:tl special-boosts,:tl-special-replies", ",") } if len(curr) > 8 && curr[:9] == ":timeline" { - words = strings.Split(":timeline home,:timeline notifications,:timeline local,:timeline federated,:timeline direct,:timeline favorited", ",") + words = strings.Split(":timeline home,:timeline notifications,:timeline local,:timeline federated,:timeline direct,:timeline favorited,:timeline special-all,:timeline special-boosts,:timeline special-replies", ",") } if len(curr) > 14 && curr[:15] == ":list-placement" { words = strings.Split(":list-placement top,:list-placement right,:list-placement bottom,:list-placement left", ",") diff --git a/ui/commands.go b/ui/commands.go index 6deb933..f4fb91a 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -71,6 +71,12 @@ func (tv *TutView) FederatedCommand() { ) } +func (tv *TutView) SpecialCommand(boosts, replies bool) { + tv.Timeline.AddFeed( + NewHomeSpecialFeed(tv, boosts, replies), + ) +} + func (tv *TutView) DirectCommand() { tv.Timeline.AddFeed( NewConversationsFeed(tv), @@ -269,3 +275,15 @@ func (tv *TutView) ClearNotificationsCommand() { func (tv *TutView) ToggleStickToTop() { tv.tut.Config.General.StickToTop = !tv.tut.Config.General.StickToTop } + +func (tv *TutView) RefetchCommand() { + item, itemErr := tv.GetCurrentItem() + f := tv.GetCurrentFeed() + if itemErr != nil { + return + } + update := item.Refetch(tv.tut.Client) + if update { + f.DrawContent() + } +} diff --git a/ui/composeview.go b/ui/composeview.go index 45d336d..990870b 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -24,7 +24,7 @@ type msgToot struct { Edit *mastodon.Status MediaIDs []mastodon.ID Sensitive bool - SpoilerText string + CWText string ScheduledAt *time.Time QuoteIncluded bool Visibility string @@ -105,7 +105,7 @@ const ( func (cv *ComposeView) msgLength() int { m := cv.msg charCount := uniseg.GraphemeClusterCount(m.Text) - spoilerCount := uniseg.GraphemeClusterCount(m.SpoilerText) + spoilerCount := uniseg.GraphemeClusterCount(m.CWText) totalCount := charCount if m.Sensitive { totalCount += spoilerCount @@ -122,7 +122,7 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) { items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeEditText, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeVisibility, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeToggleContentWarning, true)) - items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeEditSpoiler, true)) + items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeEditCW, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeMediaFocus, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposePoll, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeLanguage, true)) @@ -165,7 +165,7 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) msg.Reply = reply if reply.Sensitive { msg.Sensitive = true - msg.SpoilerText = reply.SpoilerText + msg.CWText = reply.SpoilerText } if visibilities[reply.Visibility] > visibilities[visibility] { visibility = reply.Visibility @@ -188,7 +188,7 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) msg.Edit = edit msg.ID = source.ID msg.Text = source.Text - msg.SpoilerText = source.SpoilerText + msg.CWText = source.SpoilerText for _, mid := range edit.MediaAttachments { msg.MediaIDs = append(msg.MediaIDs, mid.ID) } @@ -268,14 +268,14 @@ func (cv *ComposeView) EditText() { } func (cv *ComposeView) EditSpoiler() { - text, err := OpenEditor(cv.tutView, cv.msg.SpoilerText) + text, err := OpenEditor(cv.tutView, cv.msg.CWText) if err != nil { cv.tutView.ShowError( fmt.Sprintf("Couldn't open editor. Error: %v", err), ) return } - cv.msg.SpoilerText = text + cv.msg.CWText = text cv.UpdateContent() } @@ -285,7 +285,7 @@ func (cv *ComposeView) ToggleCW() { } func (cv *ComposeView) UpdateContent() { - cv.info.SetText(fmt.Sprintf("Chars left: %d\nSpoiler: %t\nHas poll: %t\n", cv.msgLength(), cv.msg.Sensitive, cv.tutView.PollView.HasPoll())) + cv.info.SetText(fmt.Sprintf("Chars left: %d\nCW: %t\nHas poll: %t\n", cv.msgLength(), cv.msg.Sensitive, cv.tutView.PollView.HasPoll())) normal := config.ColorMark(cv.tutView.tut.Config.Style.Text) subtleColor := config.ColorMark(cv.tutView.tut.Config.Style.Subtle) warningColor := config.ColorMark(cv.tutView.tut.Config.Style.WarningText) @@ -302,17 +302,17 @@ func (cv *ComposeView) UpdateContent() { } outputHead += subtleColor + "Replying to " + tview.Escape(acct) + "\n" + normal } - if cv.msg.SpoilerText != "" && !cv.msg.Sensitive { - outputHead += warningColor + "You have entered spoiler text, but haven't set an content warning. Do it by pressing " + tview.Escape("[T]") + "\n\n" + normal + if cv.msg.CWText != "" && !cv.msg.Sensitive { + outputHead += warningColor + "You have entered content warning text, but haven't set an content warning. Do it by pressing " + tview.Escape("[T]") + "\n\n" + normal } - if cv.msg.Sensitive && cv.msg.SpoilerText == "" { + if cv.msg.Sensitive && cv.msg.CWText == "" { outputHead += warningColor + "You have added an content warning, but haven't set any text above the hidden text. Do it by pressing " + tview.Escape("[C]") + "\n\n" + normal } - if cv.msg.Sensitive && cv.msg.SpoilerText != "" { + if cv.msg.Sensitive && cv.msg.CWText != "" { outputHead += subtleColor + "Content warning\n\n" + normal - outputHead += tview.Escape(cv.msg.SpoilerText) + outputHead += tview.Escape(cv.msg.CWText) outputHead += "\n\n" + subtleColor + "---hidden content below---\n\n" + normal } output = outputHead + normal + tview.Escape(cv.msg.Text) @@ -425,7 +425,7 @@ func (cv *ComposeView) Post() { } if toot.Sensitive { send.Sensitive = true - send.SpoilerText = toot.SpoilerText + send.SpoilerText = toot.CWText } if cv.HasMedia() { diff --git a/ui/feed.go b/ui/feed.go index 9e1ceb7..a8e3082 100644 --- a/ui/feed.go +++ b/ui/feed.go @@ -153,6 +153,20 @@ func NewHomeFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { return fd } +func NewHomeSpecialFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { + f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) + f.LoadNewer() + fd := &Feed{ + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), + } + go fd.update() + + return fd +} + func NewFederatedFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { f := feed.NewTimelineFederated(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) f.LoadNewer() diff --git a/ui/input.go b/ui/input.go index 549e298..affbd3c 100644 --- a/ui/input.go +++ b/ui/input.go @@ -114,6 +114,12 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey { tv.LocalCommand() case config.LeaderFederated: tv.FederatedCommand() + case config.LeaderSpecialAll: + tv.SpecialCommand(true, true) + case config.LeaderSpecialBoosts: + tv.SpecialCommand(true, false) + case config.LeaderSpecialReplies: + tv.SpecialCommand(false, true) case config.LeaderClearNotifications: tv.ClearNotificationsCommand() case config.LeaderCompose: @@ -150,6 +156,8 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey { tv.ListsCommand() case config.LeaderStickToTop: tv.ToggleStickToTop() + case config.LeaderRefetch: + tv.RefetchCommand() case config.LeaderTag: tv.TagCommand(subaction) case config.LeaderTags: @@ -305,7 +313,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { } switch item.Type() { case api.StatusType: - return tv.InputStatus(event, item, item.Raw().(*mastodon.Status), nil) + return tv.InputStatus(event, item, item.Raw().(*mastodon.Status), nil, fd.Data.Type()) case api.StatusHistoryType: return tv.InputStatusHistory(event, item, item.Raw().(*mastodon.StatusHistory), nil) case api.UserType, api.ProfileType: @@ -326,18 +334,18 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { 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) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data, config.Notifications) case "reblog": user := nd.User.Raw().(*api.User) - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data, config.Notifications) case "mention": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications) case "update": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications) case "status": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications) case "poll": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications) case "follow_request": return tv.InputUser(event, nd.User.Raw().(*api.User), InputUserFollowRequest) } @@ -351,7 +359,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { return event } -func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mastodon.Status, nAcc *mastodon.Account) *tcell.EventKey { +func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mastodon.Status, nAcc *mastodon.Account, fd config.FeedType) *tcell.EventKey { sr := util.StatusOrReblog(status) hasMedia := len(sr.MediaAttachments) > 0 @@ -501,12 +509,18 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas copyToClipboard(sr.URL) return nil } - if tv.tut.Config.Input.StatusToggleSpoiler.Match(event.Key(), event.Rune()) { + if tv.tut.Config.Input.StatusToggleCW.Match(event.Key(), event.Rune()) { + filtered, _, _, forceView := item.Filtered(fd) + if filtered && !forceView { + item.ForceViewFilter() + tv.RedrawContent() + return nil + } if !hasSpoiler { return nil } - if !item.ShowSpoiler() { - item.ToggleSpoiler() + if !item.ShowCW() { + item.ToggleCW() tv.RedrawContent() } return nil @@ -551,12 +565,12 @@ func (tv *TutView) InputStatusHistory(event *tcell.EventKey, item api.Item, sr * tv.SetPage(ViewFocus) return nil } - if tv.tut.Config.Input.StatusToggleSpoiler.Match(event.Key(), event.Rune()) { + if tv.tut.Config.Input.StatusToggleCW.Match(event.Key(), event.Rune()) { if !hasSpoiler { return nil } - if !item.ShowSpoiler() { - item.ToggleSpoiler() + if !item.ShowCW() { + item.ToggleCW() tv.RedrawContent() } return nil @@ -811,7 +825,7 @@ func (tv *TutView) InputLinkView(event *tcell.EventKey) *tcell.EventKey { } func (tv *TutView) InputComposeView(event *tcell.EventKey) *tcell.EventKey { - if tv.tut.Config.Input.ComposeEditSpoiler.Match(event.Key(), event.Rune()) { + if tv.tut.Config.Input.ComposeEditCW.Match(event.Key(), event.Rune()) { tv.ComposeView.EditSpoiler() return nil } diff --git a/ui/item.go b/ui/item.go index 2a20438..d7ba959 100644 --- a/ui/item.go +++ b/ui/item.go @@ -28,6 +28,9 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) { symbol = " ! " } acc := strings.TrimSpace(s.Account.Acct) + if cfg.General.ShowBoostedUser && s.Reblog != nil { + acc = strings.TrimSpace(s.Reblog.Account.Acct) + } if s.Reblog != nil && cfg.General.ShowIcons { acc = fmt.Sprintf("♺ %s", acc) } @@ -78,7 +81,7 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) { func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview.Flex, ft config.FeedType) { switch item.Type() { case api.StatusType: - drawStatus(tv, item, item.Raw().(*mastodon.Status), main, controls, false, "") + drawStatus(tv, item, item.Raw().(*mastodon.Status), main, controls, ft, false, "") case api.StatusHistoryType: s := item.Raw().(*mastodon.StatusHistory) status := mastodon.Status{ @@ -91,7 +94,7 @@ func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview. MediaAttachments: s.MediaAttachments, Visibility: mastodon.VisibilityPublic, } - drawStatus(tv, item, &status, main, controls, true, "") + drawStatus(tv, item, &status, main, controls, ft, true, "") case api.UserType, api.ProfileType: switch ft { case config.FollowRequests: @@ -115,7 +118,7 @@ func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview. func DrawItemControls(tv *TutView, item api.Item, controls *tview.Flex, ft config.FeedType) { switch item.Type() { case api.StatusType: - drawStatus(tv, item, item.Raw().(*mastodon.Status), nil, controls, false, "") + drawStatus(tv, item, item.Raw().(*mastodon.Status), nil, controls, ft, false, "") case api.StatusHistoryType: s := item.Raw().(*mastodon.StatusHistory) status := mastodon.Status{ @@ -128,7 +131,7 @@ func DrawItemControls(tv *TutView, item api.Item, controls *tview.Flex, ft confi MediaAttachments: s.MediaAttachments, Visibility: mastodon.VisibilityPublic, } - drawStatus(tv, item, &status, nil, controls, true, "") + drawStatus(tv, item, &status, nil, controls, ft, true, "") case api.UserType, api.ProfileType: if ft == config.FollowRequests { drawUser(tv, item.Raw().(*api.User), nil, controls, "", InputUserFollowRequest) diff --git a/ui/item_notification.go b/ui/item_notification.go index c18ea1f..d6b29f4 100644 --- a/ui/item_notification.go +++ b/ui/item_notification.go @@ -16,27 +16,27 @@ func drawNotification(tv *TutView, item api.Item, notification *api.Notification fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), InputUserNormal, ) case "favourite": - drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, + drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false, fmt.Sprintf("%s favorited your toot", util.FormatUsername(notification.Item.Account)), ) case "reblog": - drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, + drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false, fmt.Sprintf("%s boosted your toot", util.FormatUsername(notification.Item.Account)), ) case "mention": - drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, + drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false, fmt.Sprintf("%s mentioned you", util.FormatUsername(notification.Item.Account)), ) case "update": - drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, + drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false, fmt.Sprintf("%s updated their toot", util.FormatUsername(notification.Item.Account)), ) case "status": - drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, + drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false, fmt.Sprintf("%s posted a new toot", util.FormatUsername(notification.Item.Account)), ) case "poll": - drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false, + drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false, "A poll of yours or one you participated in has ended", ) case "follow_request": diff --git a/ui/item_status.go b/ui/item_status.go index da53af8..2180710 100644 --- a/ui/item_status.go +++ b/ui/item_status.go @@ -22,8 +22,9 @@ type Toot struct { AccountDisplayName string Account string Spoiler bool - SpoilerText string + CWText string ShowSpoiler bool + CWlabel string ContentText string Width int HasExtra bool @@ -71,15 +72,18 @@ type DisplayTootData struct { Style config.Style } -func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview.TextView, controls *tview.Flex, isHistory bool, additional string) { - filtered, phrase := item.Filtered() +func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview.TextView, controls *tview.Flex, ft config.FeedType, isHistory bool, additional string) { + controls.Clear() + filtered, _, phrase, _ := item.Filtered(ft) if filtered { var output string if tv.tut.Config.General.ShowFilterPhrase { - output = fmt.Sprintf("Filtered by phrase: %s", tview.Escape(phrase)) + output = fmt.Sprintf("Filtered by phrase: %s\n\n", tview.Escape(phrase)) } else { - output = "Filtered." + output = "Filtered.\n\n" } + ctrl := NewControl(tv.tut.Config, tv.tut.Config.Input.StatusShowFiltered, true) + output += ctrl.Label if main != nil { if additional != "" { additional = fmt.Sprintf("%s\n\n", config.SublteText(tv.tut.Config, additional)) @@ -89,7 +93,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview return } - showSensitive := item.ShowSpoiler() + showSensitive := item.ShowCW() var strippedContent string var strippedSpoiler string @@ -106,6 +110,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview if main != nil { _, _, width, _ = main.GetInnerRect() } + cwToggle := NewControl(tv.tut.Config, tv.tut.Config.Input.StatusToggleCW, true) toot := Toot{ Width: width, ContentText: strippedContent, @@ -113,6 +118,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview BoostedDisplayName: tview.Escape(so.Account.DisplayName), BoostedAcct: tview.Escape(so.Account.Acct), ShowSpoiler: showSensitive, + CWlabel: cwToggle.Label, } toot.AccountDisplayName = tview.Escape(status.Account.DisplayName) @@ -156,7 +162,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview strippedSpoiler = tview.Escape(strippedSpoiler) } - toot.SpoilerText = strippedSpoiler + toot.CWText = strippedSpoiler media := []Media{} for _, att := range status.MediaAttachments { @@ -230,7 +236,6 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview info = append(info, NewControl(tv.tut.Config, tv.tut.Config.Input.StatusYank, true)) } - controls.Clear() for i, item := range info { if i < len(info)-1 { controls.AddItem(NewControlButton(tv, item), item.Len+1, 0, false) diff --git a/ui/styled_elements.go b/ui/styled_elements.go index d3de03f..8f28ab8 100644 --- a/ui/styled_elements.go +++ b/ui/styled_elements.go @@ -34,7 +34,15 @@ func NewControlView(cnf *config.Config) *tview.Flex { func NewControlButton(tv *TutView, control Control) *tview.Button { btn := tview.NewButton(control.Label) + style := tcell.Style{} + style = style.Foreground(tv.tut.Config.Style.Text) + style = style.Background(tv.tut.Config.Style.Background) + btn.SetActivatedStyle(style) + btn.SetStyle(style) btn.SetBackgroundColor(tv.tut.Config.Style.Background) + btn.SetBackgroundColorActivated(tv.tut.Config.Style.Background) + btn.SetLabelColor(tv.tut.Config.Style.Background) + btn.SetLabelColorActivated(tv.tut.Config.Style.Background) btn.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { if !btn.InRect(event.Position()) { return action, event diff --git a/ui/timeline.go b/ui/timeline.go index a2fee0e..c290e64 100644 --- a/ui/timeline.go +++ b/ui/timeline.go @@ -33,6 +33,8 @@ func NewTimeline(tv *TutView, update chan bool) *Timeline { switch f.FeedType { case config.TimelineHome: nf = NewHomeFeed(tv, f.ShowBoosts, f.ShowReplies) + case config.TimelineHomeSpecial: + nf = NewHomeSpecialFeed(tv, f.ShowBoosts, f.ShowReplies) case config.Conversations: nf = NewConversationsFeed(tv) case config.TimelineLocal: @@ -162,6 +164,8 @@ func (tl *Timeline) GetTitle() string { ct = "federated" case config.TimelineHome: ct = "home" + case config.TimelineHomeSpecial: + ct = "special" case config.TimelineLocal: ct = "local" case config.Saved: