diff --git a/README.md b/README.md index a20f81b..3865c30 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:lists` show a list of your lists * `:muting` lists users that you have muted * `:profile` go to your profile +* `:requests` see following requests * `:saved` alias for bookmarks * `:tag` followed by the hashtag e.g. `:tag linux` * `:user` followed by a username e.g. `:user rasmus` to narrow a search include diff --git a/api/feed.go b/api/feed.go index 2a65403..7fcd906 100644 --- a/api/feed.go +++ b/api/feed.go @@ -223,6 +223,13 @@ func (ac *AccountClient) GetMuting(pg *mastodon.Pagination) ([]Item, error) { return ac.getUserSimilar(fn) } +func (ac *AccountClient) GetFollowRequests(pg *mastodon.Pagination) ([]Item, error) { + fn := func() ([]*mastodon.Account, error) { + return ac.Client.GetFollowRequests(context.Background(), pg) + } + return ac.getUserSimilar(fn) +} + func (ac *AccountClient) getUserSimilar(fn func() ([]*mastodon.Account, error)) ([]Item, error) { var items []Item users, err := fn() diff --git a/api/user.go b/api/user.go index 7d5400b..14a0b2e 100644 --- a/api/user.go +++ b/api/user.go @@ -71,3 +71,11 @@ func (ac *AccountClient) MuteUser(u *mastodon.Account) (*mastodon.Relationship, func (ac *AccountClient) UnmuteUser(u *mastodon.Account) (*mastodon.Relationship, error) { return ac.Client.AccountUnmute(context.Background(), u.ID) } + +func (ac *AccountClient) FollowRequestAccept(u *mastodon.Account) error { + return ac.Client.FollowRequestAuthorize(context.Background(), u.ID) +} + +func (ac *AccountClient) FollowRequestDeny(u *mastodon.Account) error { + return ac.Client.FollowRequestReject(context.Background(), u.ID) +} diff --git a/auth/add.go b/auth/add.go index 3191833..ef4aeaf 100644 --- a/auth/add.go +++ b/auth/add.go @@ -82,7 +82,7 @@ func AddAccount(ad *AccountData) *mastodon.Client { } me, err := client.GetAccountCurrentUser(context.Background()) if err != nil { - fmt.Printf("\nCouldnät get user. Error: %v\nExiting...\n", err) + fmt.Printf("\nCouldn't get user. Error: %v\nExiting...\n", err) os.Exit(1) } acc := Account{ diff --git a/auth/file.go b/auth/file.go index 341afbd..f526795 100644 --- a/auth/file.go +++ b/auth/file.go @@ -48,7 +48,7 @@ func (ad *AccountData) Save(filepath string) error { if err != nil { return err } - f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 0600) + f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } diff --git a/auth/load.go b/auth/load.go index 8595117..29db199 100644 --- a/auth/load.go +++ b/auth/load.go @@ -16,7 +16,11 @@ func StartAuth(newUser bool) *AccountData { accs, err = GetAccounts(path) } if err != nil || accs == nil || len(accs.Accounts) == 0 || newUser { - AddAccount(nil) + if err == nil && accs != nil { + AddAccount(accs) + } else { + AddAccount(nil) + } return StartAuth(false) } return accs diff --git a/config.example.ini b/config.example.ini index 33f2b49..20e45b9 100644 --- a/config.example.ini +++ b/config.example.ini @@ -6,6 +6,42 @@ # default=true confirmation=true +# 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 +# +# Tag is special as you need to add the tag after, see the example below. +# +# The syntax is: +# timelines=feed,[name],[keys...] +# +# Tha values in brackets are optional. You can see the syntax for keys under the +# [input] section. +# +# Some examples: +# +# home timeline with the name Home +# timelines=home,Home +# +# local timeline with the name Local and it gets focus when you press 2 +# timelines=local,Local,'2' +# +# notification timeline with the name [N]otifications and it gets focus when you +# press n or N +# timelines=notifications,[N]otifications,'n','N' +# +# tag timeline for #linux with the name Linux and it gets focus when you press +# timelines=tag linux,Linux,"F2" +# +# +# If you don't set any timelines it will default to this: +# timelines=home +# timelines=notifications,[N]otifications,'n','N' +# + + # The date format to be used. See https://godoc.org/time#Time.Format # default=2006-01-02 15:04 date-format=2006-01-02 15:04 @@ -27,20 +63,11 @@ date-today-format=15:04 # default=-1 date-relative=-1 -# The timeline that opens up when you start tut. -# Valid values: home, direct, local, federated -# default=home -timeline=home - # The max width of text before it wraps when displaying toots. # 0 = no restriction. # default=0 max-width=0 -# If you want to display a list of notifications under your timeline feed. -# default=true -notification-feed=true - # Where do you want the list of toots to be placed? # Valid values: left, right, top, bottom. # default=left @@ -52,11 +79,6 @@ list-placement=left # default=row list-split=row -# Hide notification text above list in column split. It's displayed as -# [N]otifications. -# default=false -hide-notification-text=false - # You can change the proportions of the list view in relation to the content # view list-proportion=1 and content-proportoin=3 will result in the content # taking up 3 times more space. @@ -421,6 +443,14 @@ main-prev-feed="",'h','H',"Left" # default="",'l','L',"Right" main-next-feed="",'l','L',"Right" +# Focus on the previous feed window +# default="","Backtab" +main-prev-window="","Backtab" + +# Focus on the next feed window +# default="","Tab" +main-next-window="","Tab" + # Focus on the notification list # default="[N]otifications",'n','N' main-notification-focus="[N]otifications",'n','N' @@ -497,6 +527,10 @@ user-block="[B]lock","Un[B]lock",'b','B' # default="[F]ollow","Un[F]ollow",'f','F' user-follow="[F]ollow","Un[F]ollow",'f','F' +# Follow user +# default="Follow [R]equest","Follow [R]equest",'r','R' +user-follow-request-decide="Follow [R]equest","Follow [R]equest",'r','R' + # Mute user # default="[M]ute","Un[M]ute",'m','M' user-mute="[M]ute","Un[M]ute",'m','M' diff --git a/config/config.go b/config/config.go index 9e6dcc6..a5654ab 100644 --- a/config/config.go +++ b/config/config.go @@ -72,6 +72,13 @@ const ( LeaderUser ) +type Timeline struct { + FeedType feed.FeedType + Subaction string + Name string + Key Key +} + type General struct { Confirmation bool DateTodayFormat string @@ -94,6 +101,8 @@ type General struct { LeaderKey rune LeaderTimeout int64 LeaderActions []LeaderAction + TimelineName bool + Timelines []Timeline } type Style struct { @@ -306,12 +315,13 @@ type Input struct { GlobalBack Key GlobalExit Key - MainHome Key - MainEnd Key - MainPrevFeed Key - MainNextFeed Key - MainNotificationFocus Key - MainCompose Key + MainHome Key + MainEnd Key + MainPrevFeed Key + MainNextFeed Key + MainPrevWindow Key + MainNextWindow Key + MainCompose Key StatusAvatar Key StatusBoost Key @@ -328,14 +338,15 @@ type Input struct { StatusYank Key StatusToggleSpoiler Key - UserAvatar Key - UserBlock Key - UserFollow Key - UserMute Key - UserLinks Key - UserUser Key - UserViewFocus Key - UserYank Key + UserAvatar Key + UserBlock Key + UserFollow Key + UserFollowRequestDecide Key + UserMute Key + UserLinks Key + UserUser Key + UserViewFocus Key + UserYank Key ListOpenFeed Key @@ -599,6 +610,7 @@ func parseGeneral(cfg *ini.File) General { parts := strings.Split(l, ",") if len(parts) != 2 { fmt.Printf("leader-action must consist of two parts seperated by a comma. Your value is: %s\n", strings.Join(parts, ",")) + os.Exit(1) } for i, p := range parts { parts[i] = strings.TrimSpace(p) @@ -658,6 +670,81 @@ func parseGeneral(cfg *ini.File) General { } general.LeaderActions = las } + + general.TimelineName = cfg.Section("general").Key("timeline-show-name").MustBool(true) + var tls []Timeline + timelines := cfg.Section("general").Key("timelines").ValueWithShadows() + for _, l := range timelines { + parts := strings.Split(l, ",") + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + if len(parts) == 0 { + fmt.Printf("timelines must consist of atleast one part seperated by a comma. Your value is: %s\n", strings.Join(parts, ",")) + os.Exit(1) + } + if len(parts) == 1 { + parts = append(parts, "") + } + cmd := parts[0] + var subaction string + if strings.Contains(parts[0], " ") { + p := strings.Split(cmd, " ") + cmd = p[0] + subaction = strings.Join(p[1:], " ") + } + tl := Timeline{} + switch cmd { + case "home": + tl.FeedType = feed.TimelineHome + case "direct": + tl.FeedType = feed.Conversations + case "local": + tl.FeedType = feed.TimelineLocal + case "federated": + tl.FeedType = feed.TimelineFederated + case "bookmarks": + tl.FeedType = feed.Saved + case "saved": + tl.FeedType = feed.Saved + case "favorited": + tl.FeedType = feed.Favorited + case "notifications": + tl.FeedType = feed.Notification + case "lists": + tl.FeedType = feed.Lists + case "tag": + tl.FeedType = feed.Tag + tl.Subaction = subaction + default: + fmt.Printf("timeline %s is invalid\n", parts[0]) + os.Exit(1) + } + tl.Name = parts[1] + if len(parts) > 2 { + vals := []string{""} + vals = append(vals, parts[2:]...) + tl.Key = inputStrOrErr(vals, false) + } + tls = append(tls, tl) + } + if len(tls) == 0 { + tls = append(tls, + Timeline{ + FeedType: feed.TimelineHome, + Name: "", + }, + ) + tls = append(tls, + Timeline{ + FeedType: feed.Notification, + Name: "[N]otifications", + Key: inputStrOrErr([]string{"", "'n'", "'N'"}, false), + }, + ) + } + general.Timelines = tls + return general } @@ -893,12 +980,13 @@ func parseInput(cfg *ini.File) Input { GlobalBack: inputStrOrErr([]string{"\"[Esc]\"", "\"Esc\""}, false), GlobalExit: inputStrOrErr([]string{"\"[Q]uit\"", "'q'", "'Q'"}, false), - MainHome: inputStrOrErr([]string{"\"\"", "'g'", "\"Home\""}, false), - MainEnd: inputStrOrErr([]string{"\"\"", "'G'", "\"End\""}, false), - MainPrevFeed: inputStrOrErr([]string{"\"\"", "'h'", "'H'", "\"Left\""}, false), - MainNextFeed: inputStrOrErr([]string{"\"\"", "'l'", "'L'", "\"Right\""}, false), - MainNotificationFocus: inputStrOrErr([]string{"\"[N]otifications\"", "'n'", "'N'"}, false), - MainCompose: inputStrOrErr([]string{"\"\"", "'c'", "'C'"}, false), + MainHome: inputStrOrErr([]string{"\"\"", "'g'", "\"Home\""}, false), + MainEnd: inputStrOrErr([]string{"\"\"", "'G'", "\"End\""}, false), + MainPrevFeed: inputStrOrErr([]string{"\"\"", "'h'", "'H'", "\"Left\""}, false), + MainNextFeed: inputStrOrErr([]string{"\"\"", "'l'", "'L'", "\"Right\""}, false), + MainPrevWindow: inputStrOrErr([]string{"\"\"", "\"Backtab\""}, false), + 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), @@ -915,14 +1003,15 @@ func parseInput(cfg *ini.File) Input { StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), StatusToggleSpoiler: inputStrOrErr([]string{"\"Press [Z] to toggle spoiler\"", "'z'", "'Z'"}, false), - UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), - UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true), - UserFollow: inputStrOrErr([]string{"\"[F]ollow\"", "\"Un[F]ollow\"", "'f'", "'F'"}, true), - UserMute: inputStrOrErr([]string{"\"[M]ute\"", "\"Un[M]ute\"", "'m'", "'M'"}, true), - UserLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), - UserUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false), - UserViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), - UserYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), + UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), + UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true), + UserFollow: inputStrOrErr([]string{"\"[F]ollow\"", "\"Un[F]ollow\"", "'f'", "'F'"}, true), + UserFollowRequestDecide: inputStrOrErr([]string{"\"Follow [R]equest\"", "\"Follow [R]equest\"", "'r'", "'R'"}, true), + UserMute: inputStrOrErr([]string{"\"[M]ute\"", "\"Un[M]ute\"", "'m'", "'M'"}, true), + UserLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), + UserUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false), + UserViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false), + UserYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), ListOpenFeed: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), @@ -954,7 +1043,6 @@ func parseInput(cfg *ini.File) Input { ic.MainEnd = inputOrErr(cfg, "main-end", false, ic.MainEnd) ic.MainPrevFeed = inputOrErr(cfg, "main-prev-feed", false, ic.MainPrevFeed) ic.MainNextFeed = inputOrErr(cfg, "main-next-feed", false, ic.MainNextFeed) - ic.MainNotificationFocus = inputOrErr(cfg, "main-notification-focus", false, ic.MainNotificationFocus) ic.MainCompose = inputOrErr(cfg, "main-compose", false, ic.MainCompose) ic.StatusAvatar = inputOrErr(cfg, "status-avatar", false, ic.StatusAvatar) @@ -975,6 +1063,7 @@ func parseInput(cfg *ini.File) Input { ic.UserAvatar = inputOrErr(cfg, "user-avatar", false, ic.UserAvatar) ic.UserBlock = inputOrErr(cfg, "user-block", true, ic.UserBlock) ic.UserFollow = inputOrErr(cfg, "user-follow", true, ic.UserFollow) + ic.UserFollowRequestDecide = inputOrErr(cfg, "user-follow-request-decide", true, ic.UserFollowRequestDecide) ic.UserMute = inputOrErr(cfg, "user-mute", true, ic.UserMute) ic.UserLinks = inputOrErr(cfg, "user-links", false, ic.UserLinks) ic.UserUser = inputOrErr(cfg, "user-user", false, ic.UserUser) diff --git a/config/default_config.go b/config/default_config.go index 800f429..adf561c 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -8,6 +8,42 @@ var conftext = `# Configuration file for tut # default=true confirmation=true +# 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 +# +# Tag is special as you need to add the tag after, see the example below. +# +# The syntax is: +# timelines=feed,[name],[keys...] +# +# Tha values in brackets are optional. You can see the syntax for keys under the +# [input] section. +# +# Some examples: +# +# home timeline with the name Home +# timelines=home,Home +# +# local timeline with the name Local and it gets focus when you press 2 +# timelines=local,Local,'2' +# +# notification timeline with the name [N]otifications and it gets focus when you +# press n or N +# timelines=notifications,[N]otifications,'n','N' +# +# tag timeline for #linux with the name Linux and it gets focus when you press +# timelines=tag linux,Linux,"F2" +# +# +# If you don't set any timelines it will default to this: +# timelines=home +# timelines=notifications,[N]otifications,'n','N' +# + + # The date format to be used. See https://godoc.org/time#Time.Format # default=2006-01-02 15:04 date-format=2006-01-02 15:04 @@ -29,20 +65,11 @@ date-today-format=15:04 # default=-1 date-relative=-1 -# The timeline that opens up when you start tut. -# Valid values: home, direct, local, federated -# default=home -timeline=home - # The max width of text before it wraps when displaying toots. # 0 = no restriction. # default=0 max-width=0 -# If you want to display a list of notifications under your timeline feed. -# default=true -notification-feed=true - # Where do you want the list of toots to be placed? # Valid values: left, right, top, bottom. # default=left @@ -54,11 +81,6 @@ list-placement=left # default=row list-split=row -# Hide notification text above list in column split. It's displayed as -# [N]otifications. -# default=false -hide-notification-text=false - # You can change the proportions of the list view in relation to the content # view list-proportion=1 and content-proportoin=3 will result in the content # taking up 3 times more space. @@ -423,6 +445,14 @@ main-prev-feed="",'h','H',"Left" # default="",'l','L',"Right" main-next-feed="",'l','L',"Right" +# Focus on the previous feed window +# default="","Backtab" +main-prev-window="","Backtab" + +# Focus on the next feed window +# default="","Tab" +main-next-window="","Tab" + # Focus on the notification list # default="[N]otifications",'n','N' main-notification-focus="[N]otifications",'n','N' @@ -499,6 +529,10 @@ user-block="[B]lock","Un[B]lock",'b','B' # default="[F]ollow","Un[F]ollow",'f','F' user-follow="[F]ollow","Un[F]ollow",'f','F' +# Follow user +# default="Follow [R]equest","Follow [R]equest",'r','R' +user-follow-request-decide="Follow [R]equest","Follow [R]equest",'r','R' + # Mute user # default="[M]ute","Un[M]ute",'m','M' user-mute="[M]ute","Un[M]ute",'m','M' diff --git a/config/help.tmpl b/config/help.tmpl index 5c50a6f..c9ff514 100644 --- a/config/help.tmpl +++ b/config/help.tmpl @@ -67,6 +67,9 @@ Here's a list of supported commands. {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:profile{{ Flags "-" }}{{ Color .Style.Text }} Go to your own profile +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:requests{{ Flags "-" }}{{ Color .Style.Text }} + See following requests + {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:saved{{ Flags "-" }}{{ Color .Style.Text }} Alias for :bookmarks diff --git a/feed/feed.go b/feed/feed.go index 22561e6..1a9ff16 100644 --- a/feed/feed.go +++ b/feed/feed.go @@ -26,6 +26,7 @@ const ( Boosts Followers Following + FollowRequests Blocking Muting InvalidFeed @@ -87,6 +88,19 @@ func (f *Feed) List() []api.Item { return f.items } +func (f *Feed) Delete(id uint) { + f.itemsMux.Lock() + defer f.itemsMux.Unlock() + var items []api.Item + for _, item := range f.items { + if item.ID() != id { + items = append(items, item) + } + } + f.items = items + f.Updated(DekstopNotificationNone) +} + func (f *Feed) Item(index int) (api.Item, error) { f.itemsMux.RLock() defer f.itemsMux.RUnlock() @@ -744,6 +758,7 @@ func NewUserProfile(ac *api.AccountClient, user *api.User) *Feed { loadOlder: func() {}, apiData: &api.RequestData{}, Update: make(chan DesktopNotificationType, 1), + name: user.Data.Acct, loadingNewer: &LoadingLock{}, loadingOlder: &LoadingLock{}, } @@ -974,3 +989,26 @@ func NewMuting(ac *api.AccountClient) *Feed { return feed } + +func NewFollowRequests(ac *api.AccountClient) *Feed { + feed := &Feed{ + accountClient: ac, + feedType: FollowRequests, + items: make([]api.Item, 0), + apiData: &api.RequestData{}, + Update: make(chan DesktopNotificationType, 1), + loadingNewer: &LoadingLock{}, + loadingOlder: &LoadingLock{}, + } + + once := true + feed.loadNewer = func() { + if once { + feed.linkNewer(feed.accountClient.GetFollowRequests) + } + once = false + } + feed.loadOlder = func() { feed.linkOlder(feed.accountClient.GetFollowRequests) } + + return feed +} diff --git a/go.sum b/go.sum index 2bc90ee..053393a 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,6 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= @@ -38,8 +37,6 @@ github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDS github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/tview v0.0.0-20211129142845-821b2667c414 h1:8pLxYvjWizid9rNUDyWv9D4gti+/w+TK7P10eXnh+xA= -github.com/rivo/tview v0.0.0-20211129142845-821b2667c414/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc= github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -58,15 +55,12 @@ golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index b666e95..f2d618a 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.6" +const version = "1.0.7" func main() { util.MakeDirs() diff --git a/ui/cliview.go b/ui/cliview.go index 718fdc8..f2f6f9f 100644 --- a/ui/cliview.go +++ b/ui/cliview.go @@ -26,13 +26,13 @@ func CliView(version string) (newUser bool, selectedUser string) { } case "--help", "-h": fmt.Print("tut - a TUI for Mastodon with vim inspired keys.\n\n") - fmt.Print("Usage:\n\n") + fmt.Print("Usage:\n") fmt.Print("\tTo run the program you just have to write tut\n\n") - fmt.Print("Commands:\n\n") + fmt.Print("Commands:\n") fmt.Print("\texample-config - creates the default configuration file in the current directory and names it ./config.example.ini\n\n") - fmt.Print("Flags:\n\n") + fmt.Print("Flags:\n") fmt.Print("\t--help -h - prints this message\n") fmt.Print("\t--version -v - prints the version\n") fmt.Print("\t--new-user -n - add one more user to tut\n") @@ -40,16 +40,16 @@ func CliView(version string) (newUser bool, selectedUser string) { fmt.Print("\t\tDon't use a = between --user and the \n") fmt.Print("\t\tIf two users are named the same. Use full name like tut@fosstodon.org\n\n") - fmt.Print("Configuration:\n\n") + fmt.Print("Configuration:\n") fmt.Printf("\tThe config is located in XDG_CONFIG_HOME/tut/config.ini which usally equals to ~/.config/tut/config.ini.\n") fmt.Printf("\tThe program will generate the file the first time you run tut. The file has comments which exmplains what each configuration option does.\n\n") - fmt.Print("Contact info for issues or questions:\n\n") - fmt.Printf("\t@rasmus@mastodon.acc.sunet.se\n\trasmus@lindroth.xyz\n") + fmt.Print("Contact info for issues or questions:\n") + fmt.Printf("\t@tut@fosstodon.org\n\t@rasmus@mastodon.acc.sunet.se\n\trasmus@lindroth.xyz\n") fmt.Printf("\thttps://github.com/RasmusLindroth/tut\n") os.Exit(0) case "--version", "-v": - fmt.Printf("tut version %s\n\n", version) + fmt.Printf("tut version %s\n", version) fmt.Printf("https://github.com/RasmusLindroth/tut\n") os.Exit(0) } diff --git a/ui/cmdbar.go b/ui/cmdbar.go index 7ce58be..ae81419 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -94,6 +94,9 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { case ":muting": c.tutView.MutingCommand() c.Back() + case ":requests": + c.tutView.FollowRequestsCommand() + c.Back() case ":profile": c.tutView.ProfileCommand() c.Back() @@ -159,7 +162,7 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { func (c *CmdBar) Autocomplete(curr string) []string { var entries []string - words := strings.Split(":blocking,:boosts,:bookmarks,:compose,:favorites,:favorited,:followers,:following,:help,:h,:lists,:muting,:profile,:saved,:tag,:timeline,:tl,:user,:quit,:q", ",") + words := strings.Split(":blocking,:boosts,:bookmarks,:compose,:favorites,:favorited,:followers,:following,:help,:h,:lists,:muting,:profile,:requests,:saved,:tag,:timeline,:tl,:user,:quit,:q", ",") if curr == "" { return entries } diff --git a/ui/commands.go b/ui/commands.go index 6ee566f..f016294 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -35,6 +35,12 @@ func (tv *TutView) MutingCommand() { ) } +func (tv *TutView) FollowRequestsCommand() { + tv.Timeline.AddFeed( + NewFollowRequests(tv), + ) +} + func (tv *TutView) LocalCommand() { tv.Timeline.AddFeed( NewLocalFeed(tv), diff --git a/ui/feed.go b/ui/feed.go index 2d7bf63..d4c461d 100644 --- a/ui/feed.go +++ b/ui/feed.go @@ -69,14 +69,18 @@ func (f *Feed) LoadNewer() { 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.tut, item, f.Content.Main, f.Content.Controls) - f.tutView.LinkView.SetLinks(item) + DrawItem(f.tutView.tut, item, f.Content.Main, f.Content.Controls, f.Data.Type()) f.tutView.ShouldSync() } } @@ -424,6 +428,21 @@ func NewMuting(tv *TutView) *Feed { return fd } +func NewFollowRequests(tv *TutView) *Feed { + f := feed.NewFollowRequests(tv.tut.Client) + f.LoadNewer() + fd := &Feed{ + tutView: tv, + Data: f, + ListIndex: 0, + List: NewFeedList(tv.tut), + Content: NewFeedContent(tv.tut), + } + go fd.update() + + return fd +} + func NewFeedList(t *Tut) *FeedList { fl := &FeedList{ Text: NewList(t.Config), diff --git a/ui/input.go b/ui/input.go index c797278..924ad11 100644 --- a/ui/input.go +++ b/ui/input.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -147,55 +148,64 @@ func (tv *TutView) InputMainView(event *tcell.EventKey) *tcell.EventKey { } func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey { - mainFocus := tv.TimelineFocus == FeedFocus - if tv.tut.Config.Input.MainHome.Match(event.Key(), event.Rune()) { - tv.Timeline.HomeItemFeed(mainFocus) + tv.Timeline.HomeItemFeed() return nil } if tv.tut.Config.Input.MainEnd.Match(event.Key(), event.Rune()) { - tv.Timeline.EndItemFeed(mainFocus) + tv.Timeline.EndItemFeed() return nil } if tv.tut.Config.Input.MainPrevFeed.Match(event.Key(), event.Rune()) { - if mainFocus { - tv.Timeline.PrevFeed() - } + tv.Timeline.PrevFeed() return nil } if tv.tut.Config.Input.MainNextFeed.Match(event.Key(), event.Rune()) { - if mainFocus { - tv.Timeline.NextFeed() - } + tv.Timeline.NextFeed() return nil } if tv.tut.Config.Input.GlobalDown.Match(event.Key(), event.Rune()) { - tv.Timeline.NextItemFeed(mainFocus) + tv.Timeline.NextItemFeed() return nil } if tv.tut.Config.Input.GlobalUp.Match(event.Key(), event.Rune()) { - tv.Timeline.PrevItemFeed(mainFocus) + tv.Timeline.PrevItemFeed() + return nil + } + if tv.tut.Config.Input.MainPrevWindow.Match(event.Key(), event.Rune()) { + if tv.tut.Config.General.NotificationFeed { + tv.PrevFeed() + } return nil } - if tv.tut.Config.Input.MainNotificationFocus.Match(event.Key(), event.Rune()) { + if tv.tut.Config.Input.MainNextWindow.Match(event.Key(), event.Rune()) { if tv.tut.Config.General.NotificationFeed { - tv.FocusNotification() + tv.NextFeed() } return nil } if tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) { - if mainFocus { - tv.Timeline.RemoveCurrent(true) - } else { - tv.FocusFeed() + exiting := tv.Timeline.RemoveCurrent(false) + if exiting && tv.Timeline.FeedFocusIndex == 0 { + tv.ModalView.Run("Do you want to exit tut?", + func() { + tv.Timeline.RemoveCurrent(true) + }) + return nil + } else if exiting && tv.Timeline.FeedFocusIndex != 0 { + tv.FocusFeed(0) } return nil } + for i, tl := range tv.tut.Config.General.Timelines { + if tl.Key.Match(event.Key(), event.Rune()) { + tv.FocusFeed(i) + } + } if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) { - if mainFocus { - tv.Timeline.RemoveCurrent(false) - } else { - tv.FocusFeed() + exiting := tv.Timeline.RemoveCurrent(false) + if exiting && tv.Timeline.FeedFocusIndex != 0 { + tv.FocusFeed(0) } return nil } @@ -233,6 +243,8 @@ func (tv *TutView) InputViewItem(event *tcell.EventKey) *tcell.EventKey { } func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { + fd := tv.GetCurrentFeed() + ft := fd.Data.Type() item, err := tv.GetCurrentItem() if err != nil { return event @@ -245,12 +257,16 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { case api.StatusType: return tv.InputStatus(event, item, item.Raw().(*mastodon.Status)) case api.UserType, api.ProfileType: - return tv.InputUser(event, item.Raw().(*api.User)) + if ft == feed.FollowRequests { + return tv.InputUser(event, item.Raw().(*api.User), true) + } else { + return tv.InputUser(event, item.Raw().(*api.User), false) + } case api.NotificationType: nd := item.Raw().(*api.NotificationData) switch nd.Item.Type { case "follow": - return tv.InputUser(event, nd.User.Raw().(*api.User)) + return tv.InputUser(event, nd.User.Raw().(*api.User), false) case "favourite": return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) case "reblog": @@ -262,7 +278,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { case "poll": return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) case "follow_request": - return tv.InputUser(event, nd.User.Raw().(*api.User)) + return tv.InputUser(event, nd.User.Raw().(*api.User), true) } case api.ListsType: ld := item.Raw().(*mastodon.List) @@ -423,11 +439,39 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas return event } -func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User) *tcell.EventKey { +func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, fr bool) *tcell.EventKey { blocking := user.Relation.Blocking muting := user.Relation.Muting following := user.Relation.Following + if tv.tut.Config.Input.UserFollowRequestDecide.Match(event.Key(), event.Rune()) { + tv.ModalView.RunDecide("Do you want accept the follow request?", + func() { + err := tv.tut.Client.FollowRequestAccept(user.Data) + if err != nil { + tv.ShowError( + fmt.Sprintf("Couldn't accept follow request. Error: %v\n", err), + ) + return + } + f := tv.GetCurrentFeed() + f.Delete() + tv.RedrawContent() + }, + func() { + err := tv.tut.Client.FollowRequestDeny(user.Data) + if err != nil { + tv.ShowError( + fmt.Sprintf("Couldn't deny follow request. Error: %v\n", err), + ) + return + } + f := tv.GetCurrentFeed() + f.Delete() + tv.RedrawContent() + }) + return nil + } if tv.tut.Config.Input.UserAvatar.Match(event.Key(), event.Rune()) { openAvatar(tv, *user.Data) return nil diff --git a/ui/item.go b/ui/item.go index d54d994..3c7dd3c 100644 --- a/ui/item.go +++ b/ui/item.go @@ -8,6 +8,7 @@ import ( "github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" + "github.com/RasmusLindroth/tut/feed" "github.com/icza/gox/timex" "github.com/rivo/tview" ) @@ -58,12 +59,16 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) { } } -func DrawItem(tut *Tut, item api.Item, main *tview.TextView, controls *tview.TextView) { +func DrawItem(tut *Tut, item api.Item, main *tview.TextView, controls *tview.TextView, ft feed.FeedType) { switch item.Type() { case api.StatusType: drawStatus(tut, item, item.Raw().(*mastodon.Status), main, controls, "") case api.UserType, api.ProfileType: - drawUser(tut, item.Raw().(*api.User), main, controls, "") + if ft == feed.FollowRequests { + drawUser(tut, item.Raw().(*api.User), main, controls, "", true) + } else { + drawUser(tut, item.Raw().(*api.User), main, controls, "", false) + } case api.NotificationType: drawNotification(tut, item, item.Raw().(*api.NotificationData), main, controls) case api.ListsType: @@ -71,12 +76,16 @@ func DrawItem(tut *Tut, item api.Item, main *tview.TextView, controls *tview.Tex } } -func DrawItemControls(tut *Tut, item api.Item, controls *tview.TextView) { +func DrawItemControls(tut *Tut, item api.Item, controls *tview.TextView, ft feed.FeedType) { switch item.Type() { case api.StatusType: drawStatus(tut, item, item.Raw().(*mastodon.Status), nil, controls, "") case api.UserType, api.ProfileType: - drawUser(tut, item.Raw().(*api.User), nil, controls, "") + if ft == feed.FollowRequests { + drawUser(tut, item.Raw().(*api.User), nil, controls, "", true) + } else { + drawUser(tut, item.Raw().(*api.User), nil, controls, "", false) + } case api.NotificationType: drawNotification(tut, item, item.Raw().(*api.NotificationData), nil, controls) } diff --git a/ui/item_notification.go b/ui/item_notification.go index 1298edf..ebacc67 100644 --- a/ui/item_notification.go +++ b/ui/item_notification.go @@ -12,7 +12,7 @@ func drawNotification(tut *Tut, item api.Item, notification *api.NotificationDat switch notification.Item.Type { case "follow": drawUser(tut, notification.User.Raw().(*api.User), main, controls, - fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), + fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), false, ) case "favourite": drawStatus(tut, notification.Status, notification.Item.Status, main, controls, @@ -36,7 +36,8 @@ func drawNotification(tut *Tut, item api.Item, notification *api.NotificationDat ) case "follow_request": drawUser(tut, notification.User.Raw().(*api.User), main, controls, - fmt.Sprintf("%s wants to follow you. This is currently not implemented, so use another app to accept or reject the request.", util.FormatUsername(notification.Item.Account)), + fmt.Sprintf("%s wants to follow you.", util.FormatUsername(notification.Item.Account)), + true, ) } } diff --git a/ui/item_user.go b/ui/item_user.go index 7db93bb..5a405d7 100644 --- a/ui/item_user.go +++ b/ui/item_user.go @@ -44,7 +44,7 @@ type DisplayUserData struct { Style config.Style } -func drawUser(tut *Tut, data *api.User, main *tview.TextView, controls *tview.TextView, additional string) { +func drawUser(tut *Tut, data *api.User, main *tview.TextView, controls *tview.TextView, additional string, fr bool) { user := data.Data relation := data.Relation showUserControl := true @@ -82,6 +82,9 @@ func drawUser(tut *Tut, data *api.User, main *tview.TextView, controls *tview.Te u.Fields = fields var controlItems []string + if fr { + controlItems = append(controlItems, config.ColorFromKey(tut.Config, tut.Config.Input.UserFollowRequestDecide, false)) + } if tut.Client.Me.ID != user.ID { if relation.Following { controlItems = append(controlItems, config.ColorFromKey(tut.Config, tut.Config.Input.UserFollow, false)) diff --git a/ui/linkview.go b/ui/linkview.go index 43d63b8..5f22706 100644 --- a/ui/linkview.go +++ b/ui/linkview.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/rivo/tview" ) @@ -49,7 +48,12 @@ func linkViewUI(lv *LinkView) *tview.Flex { AddItem(lv.shared.Bottom.View, 2, 0, false) } -func (lv *LinkView) SetLinks(item api.Item) { +func (lv *LinkView) SetLinks() { + item, err := lv.tutView.GetCurrentItem() + if err != nil { + lv.list.Clear() + return + } lv.list.Clear() urls, mentions, tags, _ := item.URLs() diff --git a/ui/loginview.go b/ui/loginview.go index fa2b699..a0e2cad 100644 --- a/ui/loginview.go +++ b/ui/loginview.go @@ -15,6 +15,7 @@ type LoginView struct { } func NewLoginView(tv *TutView, accs *auth.AccountData) *LoginView { + tv.Shared.Top.SetText("select account") list := NewList(tv.tut.Config) for _, a := range accs.Accounts { list.AddItem(fmt.Sprintf("%s - %s", a.Name, a.Server), "", 0, nil) diff --git a/ui/mainview.go b/ui/mainview.go index dcc4774..e8c63bb 100644 --- a/ui/mainview.go +++ b/ui/mainview.go @@ -1,8 +1,6 @@ package ui import ( - "fmt" - "github.com/RasmusLindroth/tut/config" "github.com/rivo/tview" ) @@ -19,48 +17,28 @@ func NewMainView(tv *TutView, update chan bool) *MainView { for range update { tv.tut.App.QueueUpdateDraw(func() { *tv.MainView.View = *mainViewUI(tv) + tv.ShouldSync() }) } }() return mv } -func feedList(mv *TutView) *tview.Flex { - iw := 3 - if !mv.tut.Config.General.ShowIcons { - iw = 0 - } - return tview.NewFlex().SetDirection(tview.FlexColumn). - AddItem(mv.Timeline.GetFeedList().Text, 0, 1, false). - AddItem(mv.Timeline.GetFeedList().Symbol, iw, 0, false) //fix so you can hide -} -func notificationList(mv *TutView) *tview.Flex { +func feedList(mv *TutView, fh *FeedHolder) *tview.Flex { iw := 3 if !mv.tut.Config.General.ShowIcons { iw = 0 } return tview.NewFlex().SetDirection(tview.FlexColumn). - AddItem(mv.Timeline.Notifications.List.Text, 0, 1, false). - AddItem(mv.Timeline.Notifications.List.Symbol, iw, 0, false) //fix so you can hide + AddItem(fh.GetFeedList().Text, 0, 1, false). + AddItem(fh.GetFeedList().Symbol, iw, 0, false) } func mainViewUI(mv *TutView) *tview.Flex { - showMain := mv.TimelineFocus == FeedFocus vl := NewVerticalLine(mv.tut.Config) hl := NewHorizontalLine(mv.tut.Config) - nt := NewTextView(mv.tut.Config) lp := mv.tut.Config.General.ListProportion cp := mv.tut.Config.General.ContentProportion - nt.SetTextColor(mv.tut.Config.Style.Subtle) - parts := mv.tut.Config.Input.MainNotificationFocus.Hint - start, middle, end := "", "", "" - if len(parts) > 0 && len(parts[0]) == 3 { - start = parts[0][0] - middle = parts[0][1] - end = parts[0][2] - } - - nt.SetText(tview.Escape(fmt.Sprintf("%s[%s]%s", start, middle, end))) var list *tview.Flex if mv.tut.Config.General.ListSplit == config.ListColumn { list = tview.NewFlex().SetDirection(tview.FlexColumn) @@ -68,33 +46,37 @@ func mainViewUI(mv *TutView) *tview.Flex { list = tview.NewFlex().SetDirection(tview.FlexRow) } - if mv.tut.Config.General.NotificationFeed && !mv.tut.Config.General.HideNotificationText { - if mv.tut.Config.General.ListSplit == config.ListColumn { - list.AddItem(tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(tview.NewBox(), 1, 0, false). - AddItem(feedList(mv), 0, 1, false), 0, 1, false). - AddItem(tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(nt, 1, 0, false). - AddItem(notificationList(mv), 0, 1, false), 0, 1, false) - } else { - list.AddItem(feedList(mv), 0, 1, false). - AddItem(nt, 1, 0, false). - AddItem(notificationList(mv), 0, 1, false) + if mv.tut.Config.General.ListSplit == config.ListColumn { + feeds := tview.NewFlex() + for _, fh := range mv.Timeline.Feeds { + if mv.tut.Config.General.TimelineName && len(fh.Name) > 0 { + txt := NewTextView(mv.tut.Config) + txt.SetText(tview.Escape(fh.Name)) + txt.SetTextColor(mv.tut.Config.Style.Subtle) + feeds.AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(txt, 1, 0, false). + AddItem(feedList(mv, fh), 0, 1, false), 0, 1, false) + } else { + feeds.AddItem(feedList(mv, fh), 0, 1, false) + } } - - } else if mv.tut.Config.General.NotificationFeed && mv.tut.Config.General.HideNotificationText { - if mv.tut.Config.General.ListSplit == config.ListColumn { - list.AddItem(feedList(mv), 0, 1, false). - AddItem(notificationList(mv), 0, 1, false) - - } else { - list.AddItem(feedList(mv), 0, 1, false). - AddItem(notificationList(mv), 0, 1, false) + list.AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(feeds, 0, 1, false), 0, 1, false) + } else { + feeds := tview.NewFlex().SetDirection(tview.FlexRow) + for _, fh := range mv.Timeline.Feeds { + if mv.tut.Config.General.TimelineName && len(fh.Name) > 0 { + txt := NewTextView(mv.tut.Config) + txt.SetText(tview.Escape(fh.Name)) + txt.SetTextColor(mv.tut.Config.Style.Subtle) + feeds.AddItem(txt, 1, 0, false) + } + feeds.AddItem(feedList(mv, fh), 0, 1, false) } - } else if !mv.tut.Config.General.NotificationFeed { - list.AddItem(feedList(mv), 0, 1, false) + list.AddItem(feeds, 0, 1, false) } - fc := mv.Timeline.GetFeedContent(showMain) + + fc := mv.Timeline.GetFeedContent() content := fc.Main controls := fc.Controls r := tview.NewFlex().SetDirection(tview.FlexRow). diff --git a/ui/modalview.go b/ui/modalview.go index 223b5c2..e268be2 100644 --- a/ui/modalview.go +++ b/ui/modalview.go @@ -29,6 +29,7 @@ func NewModalView(tv *TutView) *ModalView { } func (mv *ModalView) run(text string) (chan bool, func()) { + mv.View.SetFocus(0) mv.View.SetText(text) mv.tutView.SetPage(ModalFocus) return mv.res, func() { @@ -54,3 +55,15 @@ func (mv *ModalView) Run(text string, fn func()) { func (mv *ModalView) Stop(fn func()) { fn() } + +func (mv *ModalView) RunDecide(text string, fnYes func(), fnNo func()) { + r, f := mv.run(text) + go func() { + if <-r { + fnYes() + } else { + fnNo() + } + f() + }() +} diff --git a/ui/open.go b/ui/open.go index 46cd4c2..bcf965a 100644 --- a/ui/open.go +++ b/ui/open.go @@ -1,7 +1,6 @@ package ui import ( - "io/ioutil" "log" "os" "os/exec" @@ -63,7 +62,7 @@ func OpenEditor(tv *TutView, content string) (string, error) { args = append(args, parts[1:]...) editor = parts[0] } - f, err := ioutil.TempFile("", "tut") + f, err := os.CreateTemp("", "tut") if err != nil { return "", err } @@ -73,7 +72,9 @@ func OpenEditor(tv *TutView, content string) (string, error) { return "", err } } - args = append(args, f.Name()) + fname := f.Name() + args = append(args, fname) + f.Close() cmd := exec.Command(editor, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -84,10 +85,9 @@ func OpenEditor(tv *TutView, content string) (string, error) { if err != nil { log.Fatalln(err) } - f.Seek(0, 0) - text, err = ioutil.ReadAll(f) + text, err = os.ReadFile(fname) }) - f.Close() + os.Remove(fname) if err != nil { return "", err } diff --git a/ui/timeline.go b/ui/timeline.go index e6fb909..f1a42ea 100644 --- a/ui/timeline.go +++ b/ui/timeline.go @@ -7,115 +7,139 @@ import ( "github.com/RasmusLindroth/tut/feed" ) +type FeedHolder struct { + Name string + Feeds []*Feed + FeedIndex int +} + type Timeline struct { - tutView *TutView - Feeds []*Feed - FeedIndex int - Notifications *Feed - update chan bool + tutView *TutView + Feeds []*FeedHolder + FeedFocusIndex int + update chan bool } func NewTimeline(tv *TutView, update chan bool) *Timeline { tl := &Timeline{ - tutView: tv, - Feeds: []*Feed{}, - FeedIndex: 0, - Notifications: nil, - update: update, + tutView: tv, + Feeds: []*FeedHolder{}, + update: update, } var nf *Feed - switch tv.tut.Config.General.StartTimeline { - case feed.TimelineFederated: - nf = NewFederatedFeed(tv) - case feed.TimelineLocal: - nf = NewLocalFeed(tv) - case feed.Conversations: - nf = NewConversationsFeed(tv) - default: - nf = NewHomeFeed(tv) + for _, f := range tv.tut.Config.General.Timelines { + switch f.FeedType { + case feed.TimelineHome: + nf = NewHomeFeed(tv) + case feed.Conversations: + nf = NewConversationsFeed(tv) + case feed.TimelineLocal: + nf = NewLocalFeed(tv) + case feed.TimelineFederated: + nf = NewFederatedFeed(tv) + case feed.Saved: + nf = NewBookmarksFeed(tv) + case feed.Favorited: + nf = NewFavoritedFeed(tv) + case feed.Notification: + nf = NewNotificationFeed(tv) + case feed.Lists: + nf = NewListsFeed(tv) + case feed.Tag: + nf = NewTagFeed(tv, f.Subaction) + default: + fmt.Println("Invalid feed") + os.Exit(1) + } + tl.Feeds = append(tl.Feeds, &FeedHolder{ + Feeds: []*Feed{nf}, + Name: f.Name, + }) + } + for i := 1; i < len(tl.Feeds); i++ { + for _, f := range tl.Feeds[i].Feeds { + f.ListOutFocus() + } } - tl.Feeds = append(tl.Feeds, nf) - tl.Notifications = NewNotificationFeed(tv) - tl.Notifications.ListOutFocus() return tl } func (tl *Timeline) AddFeed(f *Feed) { - tl.tutView.FocusFeed() - tl.Feeds = append(tl.Feeds, f) - tl.FeedIndex = tl.FeedIndex + 1 + fh := tl.Feeds[tl.FeedFocusIndex] + fh.Feeds = append(fh.Feeds, f) + fh.FeedIndex = fh.FeedIndex + 1 tl.tutView.Shared.Top.SetText(tl.GetTitle()) tl.update <- true } -func (tl *Timeline) RemoveCurrent(quit bool) { - if len(tl.Feeds) == 1 && !quit { - return +func (tl *Timeline) RemoveCurrent(quit bool) bool { + if len(tl.Feeds[tl.FeedFocusIndex].Feeds) == 1 && !quit { + return true } - if len(tl.Feeds) == 1 && quit { + if len(tl.Feeds[tl.FeedFocusIndex].Feeds) == 1 && quit { tl.tutView.tut.App.Stop() os.Exit(0) } - tl.Feeds[tl.FeedIndex].Data.Close() - tl.Feeds = append(tl.Feeds[:tl.FeedIndex], tl.Feeds[tl.FeedIndex+1:]...) - ni := tl.FeedIndex - 1 + + f := tl.Feeds[tl.FeedFocusIndex] + f.Feeds[f.FeedIndex].Data.Close() + f.Feeds = append(f.Feeds[:f.FeedIndex], f.Feeds[f.FeedIndex+1:]...) + ni := f.FeedIndex - 1 if ni < 0 { ni = 0 } - tl.FeedIndex = ni + f.FeedIndex = ni tl.tutView.Shared.Top.SetText(tl.GetTitle()) tl.update <- true + return false } func (tl *Timeline) NextFeed() { - l := len(tl.Feeds) - ni := tl.FeedIndex + 1 + f := tl.Feeds[tl.FeedFocusIndex] + l := len(f.Feeds) + ni := f.FeedIndex + 1 if ni >= l { ni = l - 1 } - tl.FeedIndex = ni + f.FeedIndex = ni tl.tutView.Shared.Top.SetText(tl.GetTitle()) tl.update <- true } func (tl *Timeline) PrevFeed() { - ni := tl.FeedIndex - 1 + f := tl.Feeds[tl.FeedFocusIndex] + ni := f.FeedIndex - 1 if ni < 0 { ni = 0 } - tl.FeedIndex = ni + f.FeedIndex = ni tl.tutView.Shared.Top.SetText(tl.GetTitle()) tl.update <- true } -func (tl *Timeline) DrawContent(main bool) { - var f *Feed - if main { - f = tl.Feeds[tl.FeedIndex] - } else { - f = tl.Notifications - } +func (tl *Timeline) DrawContent() { + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] f.DrawContent() } -func (tl *Timeline) GetFeedList() *FeedList { - return tl.Feeds[tl.FeedIndex].List +func (fh *FeedHolder) GetFeedList() *FeedList { + return fh.Feeds[fh.FeedIndex].List } -func (tl *Timeline) GetFeedContent(main bool) *FeedContent { - if main { - return tl.Feeds[tl.FeedIndex].Content - } else { - return tl.Notifications.Content - } +func (tl *Timeline) GetFeedContent() *FeedContent { + fh := tl.Feeds[tl.FeedFocusIndex] + return fh.Feeds[fh.FeedIndex].Content } func (tl *Timeline) GetTitle() string { - index := tl.FeedIndex - total := len(tl.Feeds) - current := tl.Feeds[index].Data.Type() - name := tl.Feeds[index].Data.Name() + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] + index := fh.FeedIndex + total := len(fh.Feeds) + current := f.Data.Type() + name := f.Data.Name() ct := "" switch current { case feed.Favorited: @@ -127,19 +151,19 @@ func (tl *Timeline) GetTitle() string { case feed.Thread: ct = "thread feed" case feed.TimelineFederated: - ct = "timeline federated" + ct = "federated" case feed.TimelineHome: - ct = "timeline home" + ct = "home" case feed.TimelineLocal: - ct = "timeline local" + ct = "local" case feed.Saved: ct = "saved/bookmarked toots" case feed.User: - ct = "timeline user" + ct = fmt.Sprintf("user %s", name) case feed.UserList: ct = fmt.Sprintf("user search %s", name) case feed.Conversations: - ct = "timeline direct" + ct = "direct" case feed.Lists: ct = "lists" case feed.List: @@ -152,6 +176,8 @@ func (tl *Timeline) GetTitle() string { ct = "followers" case feed.Following: ct = "following" + case feed.FollowRequests: + ct = "follow requests" case feed.Blocking: ct = "blocking" case feed.Muting: @@ -161,7 +187,8 @@ func (tl *Timeline) GetTitle() string { } func (tl *Timeline) ScrollUp() { - f := tl.Feeds[tl.FeedIndex] + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] row, _ := f.Content.Main.GetScrollOffset() if row > 0 { row = row - 1 @@ -170,62 +197,55 @@ func (tl *Timeline) ScrollUp() { } func (tl *Timeline) ScrollDown() { - f := tl.Feeds[tl.FeedIndex] + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] row, _ := f.Content.Main.GetScrollOffset() f.Content.Main.ScrollTo(row+1, 0) } -func (tl *Timeline) NextItemFeed(mainFocus bool) { - var f *Feed - if mainFocus { - f = tl.Feeds[tl.FeedIndex] - } else { - f = tl.Notifications - } +func (tl *Timeline) NextItemFeed() { + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] loadMore := f.List.Next() if loadMore { f.LoadOlder() } - tl.DrawContent(mainFocus) -} -func (tl *Timeline) PrevItemFeed(mainFocus bool) { - var f *Feed - if mainFocus { - f = tl.Feeds[tl.FeedIndex] - } else { - f = tl.Notifications - } + tl.DrawContent() +} +func (tl *Timeline) PrevItemFeed() { + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] loadMore := f.List.Prev() if loadMore { f.LoadNewer() } - tl.DrawContent(mainFocus) + tl.DrawContent() } -func (tl *Timeline) HomeItemFeed(mainFocus bool) { - var f *Feed - if mainFocus { - f = tl.Feeds[tl.FeedIndex] - } else { - f = tl.Notifications - } +func (tl *Timeline) HomeItemFeed() { + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] f.List.SetCurrentItem(0) f.LoadNewer() - tl.DrawContent(mainFocus) + tl.DrawContent() } -func (tl *Timeline) EndItemFeed(mainFocus bool) { - var f *Feed - if mainFocus { - f = tl.Feeds[tl.FeedIndex] - } else { - f = tl.Notifications - } +func (tl *Timeline) DeleteItemFeed() { + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] + f.List.GetCurrentID() + + tl.DrawContent() +} + +func (tl *Timeline) EndItemFeed() { + fh := tl.Feeds[tl.FeedFocusIndex] + f := fh.Feeds[fh.FeedIndex] ni := f.List.GetItemCount() - 1 if ni < 0 { return } f.List.SetCurrentItem(ni) f.LoadOlder() - tl.DrawContent(mainFocus) + tl.DrawContent() } diff --git a/ui/top.go b/ui/top.go index 668ac91..82b262c 100644 --- a/ui/top.go +++ b/ui/top.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "net/url" "github.com/rivo/tview" ) @@ -23,9 +24,23 @@ func NewTop(tv *TutView) *Top { } func (t *Top) SetText(s string) { - if s == "" { - t.View.SetText("tut") + if t.TutView.tut.Client != nil { + acct := t.TutView.tut.Client.Me + us := acct.Acct + u, err := url.Parse(acct.URL) + if err == nil { + us = fmt.Sprintf("%s@%s", us, u.Host) + } + if s == "" { + t.View.SetText(fmt.Sprintf("tut - %s", us)) + } else { + t.View.SetText(fmt.Sprintf("tut - %s - %s", s, us)) + } } else { - t.View.SetText(fmt.Sprintf("tut - %s", s)) + if s == "" { + t.View.SetText("tut") + } else { + t.View.SetText(fmt.Sprintf("tut - %s", s)) + } } } diff --git a/ui/tutview.go b/ui/tutview.go index 031f92f..4bbbdd5 100644 --- a/ui/tutview.go +++ b/ui/tutview.go @@ -40,7 +40,6 @@ type TutView struct { Timeline *Timeline PageFocus PageFocusAt PrevPageFocus PageFocusAt - TimelineFocus TimelineFocusAt SubFocus SubFocusAt Leader *Leader Shared *Shared @@ -116,6 +115,7 @@ func NewTutView(t *Tut, accs *auth.AccountData, selectedUser string) *TutView { if accName == selectedUser { tv.loggedIn(acc) found = true + break } } if !found { @@ -155,7 +155,6 @@ func (tv *TutView) loggedIn(acc auth.Account) { tv.tut.Client = ac update := make(chan bool, 1) - tv.TimelineFocus = FeedFocus tv.SubFocus = ListFocus tv.LinkView = NewLinkView(tv) tv.Timeline = NewTimeline(tv, update) @@ -174,20 +173,38 @@ func (tv *TutView) loggedIn(acc auth.Account) { tv.SetPage(MainFocus) } -func (tv *TutView) FocusNotification() { - tv.TimelineFocus = NotificationFocus - for _, f := range tv.Timeline.Feeds { - f.ListOutFocus() +func (tv *TutView) FocusFeed(index int) { + if index < 0 || index >= len(tv.Timeline.Feeds) { + return } - tv.Timeline.Notifications.ListInFocus() + tv.Timeline.FeedFocusIndex = index + for i := 0; i < len(tv.Timeline.Feeds); i++ { + if i == index { + for _, f := range tv.Timeline.Feeds[i].Feeds { + f.ListInFocus() + } + } else { + for _, f := range tv.Timeline.Feeds[i].Feeds { + f.ListOutFocus() + } + } + } + tv.Shared.Top.SetText(tv.Timeline.GetTitle()) tv.Timeline.update <- true } -func (tv *TutView) FocusFeed() { - tv.TimelineFocus = FeedFocus - for _, f := range tv.Timeline.Feeds { - f.ListInFocus() +func (tv *TutView) NextFeed() { + index := tv.Timeline.FeedFocusIndex + 1 + if index >= len(tv.Timeline.Feeds) { + index = 0 } - tv.Timeline.Notifications.ListOutFocus() - tv.Timeline.update <- true + tv.FocusFeed(index) +} + +func (tv *TutView) PrevFeed() { + index := tv.Timeline.FeedFocusIndex - 1 + if index < 0 { + index = len(tv.Timeline.Feeds) - 1 + } + tv.FocusFeed(index) } diff --git a/ui/view.go b/ui/view.go index 48643fd..362c2bb 100644 --- a/ui/view.go +++ b/ui/view.go @@ -22,11 +22,8 @@ const ( ) func (tv *TutView) GetCurrentFeed() *Feed { - foc := tv.TimelineFocus - if foc == FeedFocus { - return tv.Timeline.Feeds[tv.Timeline.FeedIndex] - } - return tv.Timeline.Notifications + fh := tv.Timeline.Feeds[tv.Timeline.FeedFocusIndex] + return fh.Feeds[fh.FeedIndex] } func (tv *TutView) GetCurrentItem() (api.Item, error) { @@ -38,9 +35,11 @@ func (tv *TutView) RedrawContent() { f := tv.GetCurrentFeed() item, err := f.Data.Item(f.List.Text.GetCurrentItem()) if err != nil { + f.Content.Main.SetText("") + f.Content.Controls.SetText("") return } - DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls) + DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls, f.Data.Type()) } func (tv *TutView) RedrawPoll(poll *mastodon.Poll) { f := tv.GetCurrentFeed() @@ -58,7 +57,7 @@ func (tv *TutView) RedrawPoll(poll *mastodon.Poll) { } else { so.Poll = poll } - DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls) + DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls, f.Data.Type()) } func (tv *TutView) RedrawControls() { f := tv.GetCurrentFeed() @@ -66,7 +65,7 @@ func (tv *TutView) RedrawControls() { if err != nil { return } - DrawItemControls(tv.tut, item, f.Content.Controls) + DrawItemControls(tv.tut, item, f.Content.Controls, f.Data.Type()) } func (tv *TutView) SetPage(f PageFocusAt) { @@ -96,6 +95,7 @@ func (tv *TutView) SetPage(f PageFocusAt) { tv.Shared.Bottom.StatusBar.SetMode(ScrollMode) tv.tut.App.SetFocus(f.Content.Main) case LinkFocus: + tv.LinkView.SetLinks() tv.PageFocus = LinkFocus tv.View.SwitchToPage("link") tv.Shared.Bottom.StatusBar.SetMode(ListMode)