package ui import ( "fmt" "strconv" "github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/feed" "github.com/RasmusLindroth/tut/util" "github.com/gdamore/tcell/v2" "github.com/gen2brain/beeep" "github.com/rivo/tview" ) type FeedList struct { Text *tview.List Symbol *tview.List stickyCount int } func (fl *FeedList) InFocus(style config.Style) { inFocus(fl.Text, style) inFocus(fl.Symbol, style) } func inFocus(l *tview.List, style config.Style) { l.SetBackgroundColor(style.Background) l.SetMainTextColor(style.Text) l.SetSelectedBackgroundColor(style.ListSelectedBackground) l.SetSelectedTextColor(style.ListSelectedText) } func (fl *FeedList) OutFocus(style config.Style) { outFocus(fl.Text, style) outFocus(fl.Symbol, style) } func outFocus(l *tview.List, style config.Style) { l.SetBackgroundColor(style.Background) l.SetMainTextColor(style.Text) l.SetSelectedBackgroundColor(style.ListSelectedInactiveBackground) l.SetSelectedTextColor(style.ListSelectedInactiveText) } type Feed struct { tutView *TutView Data *feed.Feed List *FeedList Content *FeedContent Timeline *config.Timeline } func (f *Feed) ListInFocus() { f.List.InFocus(f.tutView.tut.Config.Style) } func (f *Feed) ListOutFocus() { f.List.OutFocus(f.tutView.tut.Config.Style) } func (f *Feed) LoadOlder() { f.Data.LoadOlder() } func (f *Feed) LoadNewer(force bool) { if f.Data.HasStream() && !force { return } f.Data.LoadNewer() } func (f *Feed) Delete() { id := f.List.GetCurrentID() f.Data.Delete(id) } func (f *Feed) DrawContent() { id := f.List.GetCurrentID() for _, item := range f.Data.List() { if id != item.ID() { continue } DrawItem(f.tutView, item, f.Content.Main, f.Content.Controls, f.Data.Type()) f.tutView.ShouldSync() } } func (f *Feed) update() { for nft := range f.Data.Update { switch nft.Type { case feed.DesktopNotificationFollower: if f.tutView.tut.Config.NotificationConfig.NotificationFollower { beeep.Notify(fmt.Sprintf("%s follows you", nft.Data), "", "") } case feed.DesktopNotificationFavorite: if f.tutView.tut.Config.NotificationConfig.NotificationFavorite { beeep.Notify(fmt.Sprintf("%s favorited your toot", nft.Data), "", "") } case feed.DesktopNotificationMention: if f.tutView.tut.Config.NotificationConfig.NotificationMention { beeep.Notify(fmt.Sprintf("%s mentioned you", nft.Data), "", "") } case feed.DesktopNotificationUpdate: if f.tutView.tut.Config.NotificationConfig.NotificationUpdate { beeep.Notify(fmt.Sprintf("%s changed their toot", nft.Data), "", "") } case feed.DesktopNotificationBoost: if f.tutView.tut.Config.NotificationConfig.NotificationBoost { beeep.Notify(fmt.Sprintf("%s boosted your toot", nft.Data), "", "") } case feed.DesktopNotificationPoll: if f.tutView.tut.Config.NotificationConfig.NotificationPoll { beeep.Notify("Poll has ended", "", "") } case feed.DesktopNotificationPost: if f.tutView.tut.Config.NotificationConfig.NotificationPost { beeep.Notify("New post", "", "") } } f.tutView.tut.App.QueueUpdateDraw(func() { lLen := f.List.GetItemCount() curr := f.List.GetCurrentID() f.List.Clear() for _, item := range f.Data.List() { main, symbol := DrawListItem(f.tutView.tut.Config, item) f.List.AddItem(main, symbol, item.ID()) } if f.tutView.tut.Config.General.StickToTop { f.List.SetCurrentItem(f.List.stickyCount) f.DrawContent() } else { f.List.SetByID(curr) } if lLen == 0 { f.DrawContent() } }) } } func NewHomeFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewTimelineHome(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewHomeSpecialFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFederatedFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewTimelineFederated(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewLocalFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewTimelineLocal(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewNotificationFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewNotifications(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewNotificatioMentionsFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewNotificationsMentions(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewThreadFeed(tv *TutView, item api.Item, tl *config.Timeline) *Feed { status := util.StatusOrReblog(item.Raw().(*mastodon.Status)) f := feed.NewThread(tv.tut.Client, tv.tut.Config, status) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } for i, s := range f.List() { main, symbol := DrawListItem(tv.tut.Config, s) fd.List.AddItem(main, symbol, s.ID()) if s.Raw().(*mastodon.Status).ID == item.Raw().(*mastodon.Status).ID { fd.List.SetCurrentItem(i) } } fd.DrawContent() return fd } func NewHistoryFeed(tv *TutView, item api.Item, tl *config.Timeline) *Feed { status := util.StatusOrReblog(item.Raw().(*mastodon.Status)) f := feed.NewHistory(tv.tut.Client, tv.tut.Config, status) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } for _, s := range f.List() { main, symbol := DrawListItem(tv.tut.Config, s) fd.List.AddItem(main, symbol, s.ID()) } fd.List.SetCurrentItem(0) fd.DrawContent() return fd } func NewConversationsFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewConversations(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewUserFeed(tv *TutView, item api.Item, tl *config.Timeline) *Feed { if item.Type() != api.UserType && item.Type() != api.ProfileType { panic("Can't open user. Wrong type.\n") } u := item.Raw().(*api.User) f := feed.NewUserProfile(tv.tut.Client, tv.tut.Config, u) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewUserSearchFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewUserSearch(tv.tut.Client, tv.tut.Config, tl.Subaction) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } for _, s := range f.List() { main, symbol := DrawListItem(tv.tut.Config, s) fd.List.AddItem(main, symbol, s.ID()) } fd.DrawContent() return fd } func NewTagFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewTag(tv.tut.Client, tv.tut.Config, tl.Subaction, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewTagsFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewTags(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewListsFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewListList(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed { f := feed.NewList(tv.tut.Client, tv.tut.Config, l, tl.HideBoosts, tl.HideReplies) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewUsersInListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed { f := feed.NewUsersInList(tv.tut.Client, tv.tut.Config, l) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewUsersAddListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed { f := feed.NewUsersAddList(tv.tut.Client, tv.tut.Config, l) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFavoritedFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewFavorites(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewBookmarksFeed(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewBookmarks(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFavoritesStatus(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed { f := feed.NewFavoritesStatus(tv.tut.Client, tv.tut.Config, id) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewBoosts(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed { f := feed.NewBoosts(tv.tut.Client, tv.tut.Config, id) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFollowers(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed { f := feed.NewFollowers(tv.tut.Client, tv.tut.Config, id) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFollowing(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed { f := feed.NewFollowing(tv.tut.Client, tv.tut.Config, id) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewBlocking(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewBlocking(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewMuting(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewMuting(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFollowRequests(tv *TutView, tl *config.Timeline) *Feed { f := feed.NewFollowRequests(tv.tut.Client, tv.tut.Config) f.LoadNewer() fd := &Feed{ tutView: tv, Data: f, List: NewFeedList(tv.tut, f.StickyCount()), Content: NewFeedContent(tv.tut), Timeline: tl, } go fd.update() return fd } func NewFeedList(t *Tut, stickyCount int) *FeedList { fl := &FeedList{ Text: NewList(t.Config), Symbol: NewList(t.Config), stickyCount: stickyCount, } return fl } func (fl *FeedList) AddItem(text string, symbols string, id uint) { fl.Text.AddItem(text, fmt.Sprintf("%d", id), 0, nil) fl.Symbol.AddItem(symbols, fmt.Sprintf("%d", id), 0, nil) } func (fl *FeedList) Set(index int) (loadOlder bool, loadNewer bool) { ni := index if ni >= fl.Text.GetItemCount() { ni = fl.Text.GetItemCount() - 1 } if ni < 0 { ni = 0 } fl.Text.SetCurrentItem(ni) fl.Symbol.SetCurrentItem(ni) return fl.Text.GetItemCount()-(ni+1) < 5, ni-fl.stickyCount < 4 } func (fl *FeedList) Next() (loadOlder bool) { ni := fl.Text.GetCurrentItem() + 1 if ni >= fl.Text.GetItemCount() { ni = fl.Text.GetItemCount() - 1 if ni < 0 { ni = 0 } } fl.Text.SetCurrentItem(ni) fl.Symbol.SetCurrentItem(ni) return fl.Text.GetItemCount()-(ni+1) < 5 } func (fl *FeedList) Prev() (loadNewer bool) { ni := fl.Text.GetCurrentItem() - 1 if ni < 0 { ni = 0 } fl.Text.SetCurrentItem(ni) fl.Symbol.SetCurrentItem(ni) return ni-fl.stickyCount < 4 } func (fl *FeedList) Clear() { fl.Text.Clear() fl.Symbol.Clear() } func (fl *FeedList) GetItemCount() int { return fl.Text.GetItemCount() } func (fl *FeedList) SetCurrentItem(index int) { fl.Text.SetCurrentItem(index) fl.Symbol.SetCurrentItem(index) } func (fl *FeedList) GetCurrentID() uint { if fl.GetItemCount() == 0 { return 0 } i := fl.Text.GetCurrentItem() _, sec := fl.Text.GetItemText(i) id, err := strconv.ParseUint(sec, 10, 32) if err != nil { return 0 } return uint(id) } func (fl *FeedList) SetByID(id uint) { if fl.Text.GetItemCount() == 0 { return } s := fmt.Sprintf("%d", id) items := fl.Text.FindItems("", s, false, false) for _, i := range items { _, sec := fl.Text.GetItemText(i) if sec == s { fl.Text.SetCurrentItem(i) fl.Symbol.SetCurrentItem(i) break } } } type FeedContent struct { Main *tview.TextView Controls *tview.Flex } func NewFeedContent(t *Tut) *FeedContent { m := NewTextView(t.Config) m.SetWordWrap(true) if t.Config.General.MaxWidth > 0 { mw := t.Config.General.MaxWidth m.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { rWidth := width if rWidth > mw { rWidth = mw } return x, y, rWidth, height }) } c := NewControlView(t.Config) fc := &FeedContent{ Main: m, Controls: c, } return fc }