diff --git a/README.md b/README.md
index 142eac4..6aa3406 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,8 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
### Currently supported commands
* `:q` `:quit` exit
-* `:timeline` home, local, federated, direct
+* `:timeline` home, local, federated, direct, notifications
+* `:tl` h, l, f, d, n (a shorter form of the former)
Explanation of the non obvious keys when viewing a toot
* `V` = view. In this mode you can scroll throught the text of the toot if it doesn't fit the screen
@@ -45,11 +46,9 @@ you will have to add `go/bin` to your `$PATH`.
### On my TODO-list:
* Support for config files (theme, default image/video viewer)
* Multiple accounts
-* View users profiles
* Support search
* Support tags
* Support lists
-* Notifications
* Better error handling (in other words, don't crash the whole program)
### Thanks to
diff --git a/api.go b/api.go
index 84e7c71..739bd4c 100644
--- a/api.go
+++ b/api.go
@@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
+ "fmt"
"github.com/mattn/go-mastodon"
)
@@ -98,27 +99,194 @@ func (api *API) GetThread(s *mastodon.Status) ([]*mastodon.Status, int, error) {
return thread, len(cont.Ancestors), nil
}
-func (api *API) Boost(s *mastodon.Status) error {
- _, err := api.Client.Reblog(context.Background(), s.ID)
- return err
+func (api *API) GetUserStatuses(u mastodon.Account) ([]*mastodon.Status, error) {
+ return api.Client.GetAccountStatuses(context.Background(), u.ID, nil)
}
-func (api *API) Unboost(s *mastodon.Status) error {
- _, err := api.Client.Unreblog(context.Background(), s.ID)
- return err
+func (api *API) GetUserStatusesOlder(u mastodon.Account, s *mastodon.Status) ([]*mastodon.Status, bool, error) {
+ pg := &mastodon.Pagination{
+ MaxID: s.ID,
+ }
+
+ statuses, err := api.Client.GetAccountStatuses(context.Background(), u.ID, pg)
+ if err != nil {
+ return statuses, false, err
+ }
+
+ if pg.MinID == "" {
+ return statuses, false, err
+ }
+
+ return statuses, true, err
}
-func (api *API) Favorite(s *mastodon.Status) error {
- _, err := api.Client.Favourite(context.Background(), s.ID)
- return err
+func (api *API) GetUserStatusesNewer(u mastodon.Account, s *mastodon.Status) ([]*mastodon.Status, bool, error) {
+ pg := &mastodon.Pagination{
+ MinID: s.ID,
+ }
+
+ statuses, err := api.Client.GetAccountStatuses(context.Background(), u.ID, pg)
+ if err != nil {
+ return statuses, false, err
+ }
+
+ if pg.MaxID == "" {
+ return statuses, false, err
+ }
+
+ return statuses, true, err
}
-func (api *API) Unfavorite(s *mastodon.Status) error {
- _, err := api.Client.Unfavourite(context.Background(), s.ID)
- return err
+func (api *API) GetNotifications() ([]*mastodon.Notification, error) {
+ return api.Client.GetNotifications(context.Background(), nil)
+}
+
+func (api *API) GetNotificationsOlder(n *mastodon.Notification) ([]*mastodon.Notification, bool, error) {
+ pg := &mastodon.Pagination{
+ MaxID: n.ID,
+ }
+
+ statuses, err := api.Client.GetNotifications(context.Background(), pg)
+ if err != nil {
+ return statuses, false, err
+ }
+
+ if pg.MinID == "" {
+ return statuses, false, err
+ }
+
+ return statuses, true, err
+}
+
+func (api *API) GetNotificationsNewer(n *mastodon.Notification) ([]*mastodon.Notification, bool, error) {
+ pg := &mastodon.Pagination{
+ MinID: n.ID,
+ }
+
+ statuses, err := api.Client.GetNotifications(context.Background(), pg)
+ if err != nil {
+ return statuses, false, err
+ }
+
+ if pg.MaxID == "" {
+ return statuses, false, err
+ }
+
+ return statuses, true, err
+}
+
+func (api *API) BoostToggle(s *mastodon.Status) (*mastodon.Status, error) {
+ if s == nil {
+ return nil, fmt.Errorf("No status")
+ }
+
+ if s.Reblogged == true {
+ return api.Unboost(s)
+ }
+ return api.Boost(s)
+}
+
+func (api *API) Boost(s *mastodon.Status) (*mastodon.Status, error) {
+ status, err := api.Client.Reblog(context.Background(), s.ID)
+ return status, err
+}
+
+func (api *API) Unboost(s *mastodon.Status) (*mastodon.Status, error) {
+ status, err := api.Client.Unreblog(context.Background(), s.ID)
+ return status, err
+}
+
+func (api *API) FavoriteToogle(s *mastodon.Status) (*mastodon.Status, error) {
+ if s == nil {
+ return nil, fmt.Errorf("No status")
+ }
+
+ if s.Favourited == true {
+ return api.Unfavorite(s)
+ }
+ return api.Favorite(s)
+}
+
+func (api *API) Favorite(s *mastodon.Status) (*mastodon.Status, error) {
+ status, err := api.Client.Favourite(context.Background(), s.ID)
+ return status, err
+}
+
+func (api *API) Unfavorite(s *mastodon.Status) (*mastodon.Status, error) {
+ status, err := api.Client.Unfavourite(context.Background(), s.ID)
+ return status, err
}
func (api *API) DeleteStatus(s *mastodon.Status) error {
//TODO: check user here?
return api.Client.DeleteStatus(context.Background(), s.ID)
}
+
+func (api *API) UserRelation(u mastodon.Account) (*mastodon.Relationship, error) {
+ relations, err := api.Client.GetAccountRelationships(context.Background(), []string{string(u.ID)})
+
+ if err != nil {
+ return nil, err
+ }
+ if len(relations) == 0 {
+ return nil, fmt.Errorf("no accounts found")
+ }
+ return relations[0], nil
+}
+
+func (api *API) FollowToggle(u mastodon.Account) (*mastodon.Relationship, error) {
+ relation, err := api.UserRelation(u)
+ if err != nil {
+ return nil, err
+ }
+ if relation.Following {
+ return api.UnfollowUser(u)
+ }
+ return api.FollowUser(u)
+}
+
+func (api *API) FollowUser(u mastodon.Account) (*mastodon.Relationship, error) {
+ return api.Client.AccountFollow(context.Background(), u.ID)
+}
+
+func (api *API) UnfollowUser(u mastodon.Account) (*mastodon.Relationship, error) {
+ return api.Client.AccountUnfollow(context.Background(), u.ID)
+}
+
+func (api *API) BlockToggle(u mastodon.Account) (*mastodon.Relationship, error) {
+ relation, err := api.UserRelation(u)
+ if err != nil {
+ return nil, err
+ }
+ if relation.Blocking {
+ return api.UnblockUser(u)
+ }
+ return api.BlockUser(u)
+}
+
+func (api *API) BlockUser(u mastodon.Account) (*mastodon.Relationship, error) {
+ return api.Client.AccountBlock(context.Background(), u.ID)
+}
+
+func (api *API) UnblockUser(u mastodon.Account) (*mastodon.Relationship, error) {
+ return api.Client.AccountUnblock(context.Background(), u.ID)
+}
+
+func (api *API) MuteToggle(u mastodon.Account) (*mastodon.Relationship, error) {
+ relation, err := api.UserRelation(u)
+ if err != nil {
+ return nil, err
+ }
+ if relation.Blocking {
+ return api.UnmuteUser(u)
+ }
+ return api.MuteUser(u)
+}
+
+func (api *API) MuteUser(u mastodon.Account) (*mastodon.Relationship, error) {
+ return api.Client.AccountMute(context.Background(), u.ID)
+}
+
+func (api *API) UnmuteUser(u mastodon.Account) (*mastodon.Relationship, error) {
+ return api.Client.AccountUnmute(context.Background(), u.ID)
+}
diff --git a/config.go b/config.go
index c82a152..0d2f011 100644
--- a/config.go
+++ b/config.go
@@ -8,8 +8,14 @@ import (
)
type Config struct {
- Style StyleConfig
- Media MediaConfig
+ General GeneralConfig
+ Style StyleConfig
+ Media MediaConfig
+}
+
+type GeneralConfig struct {
+ DateTodayFormat string
+ DateFormat string
}
type StyleConfig struct {
diff --git a/feed.go b/feed.go
new file mode 100644
index 0000000..a863d48
--- /dev/null
+++ b/feed.go
@@ -0,0 +1,948 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "strings"
+ "time"
+
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-mastodon"
+ "github.com/rivo/tview"
+)
+
+type FeedType uint
+
+const (
+ TimelineFeed FeedType = iota
+ ThreadFeed
+ UserFeed
+ NotificationFeed
+)
+
+type Feed interface {
+ GetFeedList() <-chan string
+ LoadNewer() int
+ LoadOlder() int
+ DrawList()
+ DrawToot()
+ FeedType() FeedType
+ GetSavedIndex() int
+ Input(event *tcell.EventKey)
+}
+
+func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (string, string) {
+ var line string
+ width := app.UI.StatusView.GetTextWidth()
+ for i := 0; i < width; i++ {
+ line += "-"
+ }
+ line += "\n"
+
+ shouldDisplay := !status.Sensitive || showSensitive
+
+ var stripped string
+ var urls []URL
+ var u []URL
+ if status.Sensitive && !showSensitive {
+ stripped, u = cleanTootHTML(status.SpoilerText)
+ urls = append(urls, u...)
+ stripped += "\n" + line
+ stripped += "Press [s] to show hidden text"
+
+ } else {
+ stripped, u = cleanTootHTML(status.Content)
+ urls = append(urls, u...)
+
+ if status.Sensitive {
+ sens, u := cleanTootHTML(status.SpoilerText)
+ urls = append(urls, u...)
+ stripped = sens + "\n\n" + stripped
+ }
+ }
+ app.UI.LinkOverlay.SetURLs(urls)
+
+ subtleColor := fmt.Sprintf("[#%x]", app.Config.Style.Subtle.Hex())
+ special1 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial1.Hex())
+ special2 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial2.Hex())
+ var head string
+ if status.Reblog != nil {
+ if status.Account.DisplayName != "" {
+ head += fmt.Sprintf(subtleColor+"%s (%s)\n", status.Account.DisplayName, status.Account.Acct)
+ } else {
+ head += fmt.Sprintf(subtleColor+"%s\n", status.Account.Acct)
+ }
+ head += subtleColor + "Boosted\n"
+ head += subtleColor + line
+ status = status.Reblog
+ }
+
+ if status.Account.DisplayName != "" {
+ head += fmt.Sprintf(special2+"%s\n", status.Account.DisplayName)
+ }
+ head += fmt.Sprintf(special1+"%s\n\n", status.Account.Acct)
+ output := head
+ content := tview.Escape(stripped)
+ if content != "" {
+ output += content + "\n\n"
+ }
+
+ var poll string
+ if status.Poll != nil {
+ poll += subtleColor + "Poll\n"
+ poll += subtleColor + line
+ poll += fmt.Sprintf("Number of votes: %d\n\n", status.Poll.VotesCount)
+ votes := float64(status.Poll.VotesCount)
+ for _, o := range status.Poll.Options {
+ res := 0.0
+ if votes != 0 {
+ res = float64(o.VotesCount) / votes * 100
+ }
+ poll += fmt.Sprintf("%s - %.2f%% (%d)\n", tview.Escape(o.Title), res, o.VotesCount)
+ }
+ poll += "\n"
+ }
+
+ var media string
+ for _, att := range status.MediaAttachments {
+ media += subtleColor + line
+ media += fmt.Sprintf(subtleColor+"Attached %s\n", att.Type)
+ media += fmt.Sprintf("%s\n", att.URL)
+ }
+ if len(status.MediaAttachments) > 0 {
+ media += "\n"
+ }
+
+ var card string
+ if status.Card != nil {
+ card += subtleColor + "Card type: " + status.Card.Type + "\n"
+ card += subtleColor + line
+ if status.Card.Title != "" {
+ card += status.Card.Title + "\n\n"
+ }
+ desc := strings.TrimSpace(status.Card.Description)
+ if desc != "" {
+ card += desc + "\n\n"
+ }
+ card += status.Card.URL
+ }
+
+ if shouldDisplay {
+ output += poll + media + card
+ }
+
+ app.UI.StatusView.ScrollToBeginning()
+ var info []string
+ if status.Favourited == true {
+ info = append(info, ColorKey(app.Config.Style, "Un", "F", "avorite"))
+ } else {
+ info = append(info, ColorKey(app.Config.Style, "", "F", "avorite"))
+ }
+ if status.Reblogged == true {
+ info = append(info, ColorKey(app.Config.Style, "Un", "B", "boost"))
+ } else {
+ info = append(info, ColorKey(app.Config.Style, "", "B", "boost"))
+ }
+ info = append(info, ColorKey(app.Config.Style, "", "T", "hread"))
+ info = append(info, ColorKey(app.Config.Style, "", "R", "eply"))
+ info = append(info, ColorKey(app.Config.Style, "", "V", "iew"))
+ info = append(info, ColorKey(app.Config.Style, "", "U", "ser"))
+ if len(status.MediaAttachments) > 0 {
+ info = append(info, ColorKey(app.Config.Style, "", "M", "edia"))
+ }
+ if len(urls) > 0 {
+ info = append(info, ColorKey(app.Config.Style, "", "O", "pen"))
+ }
+
+ if status.Account.ID == app.Me.ID {
+ info = append(info, ColorKey(app.Config.Style, "", "D", "elete"))
+ }
+
+ controls := strings.Join(info, " ")
+ return output, controls
+}
+
+func drawStatusList(statuses []*mastodon.Status) <-chan string {
+ ch := make(chan string)
+ go func() {
+ today := time.Now()
+ ty, tm, td := today.Date()
+ for _, s := range statuses {
+
+ sLocal := s.CreatedAt.Local()
+ sy, sm, sd := sLocal.Date()
+ format := "2006-01-02 15:04"
+ if ty == sy && tm == sm && td == sd {
+ format = "15:04"
+ }
+ content := fmt.Sprintf("%s %s", sLocal.Format(format), s.Account.Acct)
+ ch <- content
+ }
+ close(ch)
+ }()
+ return ch
+}
+
+func NewTimeline(app *App, tl TimelineType) *Timeline {
+ t := &Timeline{
+ app: app,
+ timelineType: tl,
+ }
+ t.statuses, _ = t.app.API.GetStatuses(t.timelineType)
+ return t
+}
+
+type Timeline struct {
+ app *App
+ timelineType TimelineType
+ statuses []*mastodon.Status
+ index int
+ showSpoiler bool
+}
+
+func (t *Timeline) FeedType() FeedType {
+ return TimelineFeed
+}
+
+func (t *Timeline) GetCurrentStatus() *mastodon.Status {
+ index := t.app.UI.StatusView.GetCurrentItem()
+ if index >= len(t.statuses) {
+ return nil
+ }
+ return t.statuses[t.app.UI.StatusView.GetCurrentItem()]
+}
+
+func (t *Timeline) GetFeedList() <-chan string {
+ return drawStatusList(t.statuses)
+}
+
+func (t *Timeline) LoadNewer() int {
+ var statuses []*mastodon.Status
+ var err error
+ if len(t.statuses) == 0 {
+ statuses, err = t.app.API.GetStatuses(t.timelineType)
+ } else {
+ statuses, _, err = t.app.API.GetStatusesNewer(t.timelineType, t.statuses[0])
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if len(statuses) == 0 {
+ return 0
+ }
+ old := t.statuses
+ t.statuses = append(statuses, old...)
+ return len(statuses)
+}
+
+func (t *Timeline) LoadOlder() int {
+ var statuses []*mastodon.Status
+ var err error
+ if len(t.statuses) == 0 {
+ statuses, err = t.app.API.GetStatuses(t.timelineType)
+ } else {
+ statuses, _, err = t.app.API.GetStatusesOlder(t.timelineType, t.statuses[len(t.statuses)-1])
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if len(statuses) == 0 {
+ return 0
+ }
+ t.statuses = append(t.statuses, statuses...)
+ return len(statuses)
+}
+
+func (t *Timeline) DrawList() {
+ t.app.UI.StatusView.SetList(t.GetFeedList())
+}
+
+func (t *Timeline) DrawToot() {
+ if len(t.statuses) == 0 {
+ t.app.UI.StatusView.SetText("")
+ t.app.UI.StatusView.SetControls("")
+ return
+ }
+ t.index = t.app.UI.StatusView.GetCurrentItem()
+ text, controls := showTootOptions(t.app, t.statuses[t.index], t.showSpoiler)
+ t.showSpoiler = false
+ t.app.UI.StatusView.SetText(text)
+ t.app.UI.StatusView.SetControls(controls)
+}
+
+func (t *Timeline) redrawControls() {
+ status := t.GetCurrentStatus()
+ if status == nil {
+ return
+ }
+ _, controls := showTootOptions(t.app, status, t.showSpoiler)
+ t.app.UI.StatusView.SetControls(controls)
+}
+
+func (t *Timeline) GetSavedIndex() int {
+ return t.index
+}
+
+func (t *Timeline) Input(event *tcell.EventKey) {
+ status := t.GetCurrentStatus()
+ if status == nil {
+ return
+ }
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 't', 'T':
+ t.app.UI.StatusView.AddFeed(
+ NewThread(t.app, status),
+ )
+ case 'u', 'U':
+ t.app.UI.StatusView.AddFeed(
+ NewUser(t.app, status.Account),
+ )
+ case 's', 'S':
+ t.showSpoiler = true
+ t.DrawToot()
+ case 'c', 'C':
+ t.app.UI.NewToot()
+ case 'o', 'O':
+ t.app.UI.ShowLinks()
+ case 'r', 'R':
+ t.app.UI.Reply(status)
+ case 'm', 'M':
+ t.app.UI.OpenMedia(status)
+ case 'f', 'F':
+ index := t.app.UI.StatusView.GetCurrentItem()
+ newStatus, err := t.app.API.FavoriteToogle(status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ t.statuses[index] = newStatus
+ t.redrawControls()
+
+ case 'b', 'B':
+ index := t.app.UI.StatusView.GetCurrentItem()
+ newStatus, err := t.app.API.BoostToggle(status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ t.statuses[index] = newStatus
+ t.redrawControls()
+ case 'd', 'D':
+ t.app.API.DeleteStatus(status)
+ }
+ }
+}
+
+func NewThread(app *App, s *mastodon.Status) *Thread {
+ t := &Thread{
+ app: app,
+ }
+ statuses, index, err := t.app.API.GetThread(s)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ t.statuses = statuses
+ t.status = s
+ t.index = index
+ return t
+}
+
+type Thread struct {
+ app *App
+ statuses []*mastodon.Status
+ status *mastodon.Status
+ index int
+ showSpoiler bool
+}
+
+func (t *Thread) FeedType() FeedType {
+ return ThreadFeed
+}
+
+func (t *Thread) GetCurrentStatus() *mastodon.Status {
+ index := t.app.UI.StatusView.GetCurrentItem()
+ if index >= len(t.statuses) {
+ return nil
+ }
+ return t.statuses[t.app.UI.StatusView.GetCurrentItem()]
+}
+
+func (t *Thread) GetFeedList() <-chan string {
+ return drawStatusList(t.statuses)
+}
+
+func (t *Thread) LoadNewer() int {
+ return 0
+}
+
+func (t *Thread) LoadOlder() int {
+ return 0
+}
+
+func (t *Thread) DrawList() {
+ t.app.UI.StatusView.SetList(t.GetFeedList())
+}
+
+func (t *Thread) DrawToot() {
+ status := t.GetCurrentStatus()
+ if status == nil {
+ t.app.UI.StatusView.SetText("")
+ t.app.UI.StatusView.SetControls("")
+ return
+ }
+ t.index = t.app.UI.StatusView.GetCurrentItem()
+ text, controls := showTootOptions(t.app, status, t.showSpoiler)
+ t.showSpoiler = false
+ t.app.UI.StatusView.SetText(text)
+ t.app.UI.StatusView.SetControls(controls)
+}
+
+func (t *Thread) redrawControls() {
+ status := t.GetCurrentStatus()
+ if status == nil {
+ t.app.UI.StatusView.SetText("")
+ t.app.UI.StatusView.SetControls("")
+ return
+ }
+ _, controls := showTootOptions(t.app, status, t.showSpoiler)
+ t.app.UI.StatusView.SetControls(controls)
+}
+
+func (t *Thread) GetSavedIndex() int {
+ return t.index
+}
+
+func (t *Thread) Input(event *tcell.EventKey) {
+ status := t.GetCurrentStatus()
+ if status == nil {
+ return
+ }
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 't', 'T':
+ if t.status.ID != status.ID {
+ t.app.UI.StatusView.AddFeed(
+ NewThread(t.app, status),
+ )
+ }
+ case 'u', 'U':
+ t.app.UI.StatusView.AddFeed(
+ NewUser(t.app, status.Account),
+ )
+ case 's', 'S':
+ t.showSpoiler = true
+ t.DrawToot()
+ case 'c', 'C':
+ t.app.UI.NewToot()
+ case 'o', 'O':
+ t.app.UI.ShowLinks()
+ case 'r', 'R':
+ t.app.UI.Reply(status)
+ case 'm', 'M':
+ t.app.UI.OpenMedia(status)
+ case 'f', 'F':
+ index := t.app.UI.StatusView.GetCurrentItem()
+ newStatus, err := t.app.API.FavoriteToogle(status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ t.statuses[index] = newStatus
+ t.redrawControls()
+
+ case 'b', 'B':
+ index := t.app.UI.StatusView.GetCurrentItem()
+ newStatus, err := t.app.API.BoostToggle(status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ t.statuses[index] = newStatus
+ t.redrawControls()
+ case 'd', 'D':
+ t.app.API.DeleteStatus(status)
+ }
+ }
+}
+
+func NewUser(app *App, a mastodon.Account) *User {
+ u := &User{
+ app: app,
+ }
+ statuses, err := app.API.GetUserStatuses(a)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.statuses = statuses
+ relation, err := app.API.UserRelation(a)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.relation = relation
+ u.user = a
+ return u
+}
+
+type User struct {
+ app *App
+ statuses []*mastodon.Status
+ user mastodon.Account
+ relation *mastodon.Relationship
+ index int
+ showSpoiler bool
+}
+
+func (u *User) FeedType() FeedType {
+ return UserFeed
+}
+
+func (u *User) GetCurrentStatus() *mastodon.Status {
+ index := u.app.UI.app.UI.StatusView.GetCurrentItem()
+ if index > 0 && index-1 >= len(u.statuses) {
+ return nil
+ }
+ return u.statuses[index-1]
+}
+
+func (u *User) GetFeedList() <-chan string {
+ ch := make(chan string)
+ go func() {
+ ch <- "Profile"
+ for s := range drawStatusList(u.statuses) {
+ ch <- s
+ }
+ close(ch)
+ }()
+ return ch
+}
+
+func (u *User) LoadNewer() int {
+ var statuses []*mastodon.Status
+ var err error
+ if len(u.statuses) == 0 {
+ statuses, err = u.app.API.GetUserStatuses(u.user)
+ } else {
+ statuses, _, err = u.app.API.GetUserStatusesNewer(u.user, u.statuses[0])
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if len(statuses) == 0 {
+ return 0
+ }
+ old := u.statuses
+ u.statuses = append(statuses, old...)
+ return len(statuses)
+}
+
+func (u *User) LoadOlder() int {
+ var statuses []*mastodon.Status
+ var err error
+ if len(u.statuses) == 0 {
+ statuses, err = u.app.API.GetUserStatuses(u.user)
+ } else {
+ statuses, _, err = u.app.API.GetUserStatusesOlder(u.user, u.statuses[len(u.statuses)-1])
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if len(statuses) == 0 {
+ return 0
+ }
+ u.statuses = append(u.statuses, statuses...)
+ return len(statuses)
+}
+
+func (u *User) DrawList() {
+ u.app.UI.StatusView.SetList(u.GetFeedList())
+}
+
+func (u *User) DrawToot() {
+ u.index = u.app.UI.StatusView.GetCurrentItem()
+
+ var text string
+ var controls string
+
+ if u.index == 0 {
+ n := fmt.Sprintf("[#%x]", u.app.Config.Style.Text.Hex())
+ s1 := fmt.Sprintf("[#%x]", u.app.Config.Style.TextSpecial1.Hex())
+ s2 := fmt.Sprintf("[#%x]", u.app.Config.Style.TextSpecial2.Hex())
+
+ if u.user.DisplayName != "" {
+ text = fmt.Sprintf(s2+"%s\n", u.user.DisplayName)
+ }
+ text += fmt.Sprintf(s1+"%s\n\n", u.user.Acct)
+
+ text += fmt.Sprintf("Toots %s%d %sFollowers %s%d %sFollowing %s%d\n\n",
+ s2, u.user.StatusesCount, n, s2, u.user.FollowersCount, n, s2, u.user.FollowingCount)
+
+ note, urls := cleanTootHTML(u.user.Note)
+ text += note + "\n\n"
+
+ for _, f := range u.user.Fields {
+ value, fu := cleanTootHTML(f.Value)
+ text += fmt.Sprintf("%s%s: %s%s\n", s2, f.Name, n, value)
+ urls = append(urls, fu...)
+ }
+
+ u.app.UI.LinkOverlay.SetURLs(urls)
+
+ var controlItems []string
+ if u.app.Me.ID != u.user.ID {
+ if u.relation.Following {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "F", "ollow"))
+ } else {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "F", "ollow"))
+ }
+ if u.relation.Blocking {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "B", "lock"))
+ } else {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "B", "lock"))
+ }
+ if u.relation.Muting {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "M", "ute"))
+ } else {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "M", "ute"))
+ }
+ if len(urls) > 0 {
+ controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "O", "pen"))
+ }
+ controls = strings.Join(controlItems, " ")
+ }
+
+ } else {
+ status := u.GetCurrentStatus()
+ if status == nil {
+ text = ""
+ controls = ""
+ } else {
+ text, controls = showTootOptions(u.app, status, u.showSpoiler)
+ }
+ u.showSpoiler = false
+ }
+
+ u.app.UI.StatusView.SetText(text)
+ u.app.UI.StatusView.SetControls(controls)
+}
+
+func (u *User) redrawControls() {
+ var controls string
+ status := u.GetCurrentStatus()
+ if status == nil {
+ controls = ""
+ } else {
+ _, controls = showTootOptions(u.app, status, u.showSpoiler)
+ }
+ u.app.UI.StatusView.SetControls(controls)
+}
+
+func (u *User) GetSavedIndex() int {
+ return u.index
+}
+
+func (u *User) Input(event *tcell.EventKey) {
+ index := u.GetSavedIndex()
+
+ if index == 0 {
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 'f', 'F':
+ var relation *mastodon.Relationship
+ var err error
+ if u.relation.Following {
+ relation, err = u.app.API.UnfollowUser(u.user)
+ } else {
+ relation, err = u.app.API.FollowUser(u.user)
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.relation = relation
+ u.DrawToot()
+ case 'b', 'B':
+ var relation *mastodon.Relationship
+ var err error
+ if u.relation.Blocking {
+ relation, err = u.app.API.UnblockUser(u.user)
+ } else {
+ relation, err = u.app.API.BlockUser(u.user)
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.relation = relation
+ u.DrawToot()
+ case 'm', 'M':
+ var relation *mastodon.Relationship
+ var err error
+ if u.relation.Muting {
+ relation, err = u.app.API.UnmuteUser(u.user)
+ } else {
+ relation, err = u.app.API.MuteUser(u.user)
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.relation = relation
+ u.DrawToot()
+ case 'r', 'R':
+ //toots and replies?
+ case 'o', 'O':
+ u.app.UI.ShowLinks()
+ }
+ }
+ return
+ }
+
+ if event.Key() == tcell.KeyRune {
+ status := u.GetCurrentStatus()
+ if status == nil {
+ return
+ }
+ switch event.Rune() {
+ case 't', 'T':
+ u.app.UI.StatusView.AddFeed(
+ NewThread(u.app, status),
+ )
+ case 'u', 'U':
+ if u.user.ID != status.Account.ID {
+ u.app.UI.StatusView.AddFeed(
+ NewUser(u.app, status.Account),
+ )
+ }
+ case 's', 'S':
+ u.showSpoiler = true
+ u.DrawToot()
+ case 'c', 'C':
+ u.app.UI.NewToot()
+ case 'o', 'O':
+ u.app.UI.ShowLinks()
+ case 'r', 'R':
+ u.app.UI.Reply(status)
+ case 'm', 'M':
+ u.app.UI.OpenMedia(status)
+ case 'f', 'F':
+ index := u.app.UI.StatusView.GetCurrentItem()
+ newStatus, err := u.app.API.FavoriteToogle(status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.statuses[index-1] = newStatus
+ u.redrawControls()
+
+ case 'b', 'B':
+ index := u.app.UI.StatusView.GetCurrentItem()
+ newStatus, err := u.app.API.BoostToggle(status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ u.statuses[index-1] = newStatus
+ u.redrawControls()
+ case 'd', 'D':
+ u.app.API.DeleteStatus(status)
+ }
+ }
+}
+
+func NewNoticifations(app *App) *Notifications {
+ n := &Notifications{
+ app: app,
+ }
+ n.notifications, _ = n.app.API.GetNotifications()
+ return n
+}
+
+type Notifications struct {
+ app *App
+ timelineType TimelineType
+ notifications []*mastodon.Notification
+ index int
+ showSpoiler bool
+}
+
+func (n *Notifications) FeedType() FeedType {
+ return NotificationFeed
+}
+
+func (n *Notifications) GetCurrentNotification() *mastodon.Notification {
+ index := n.app.UI.StatusView.GetCurrentItem()
+ if index >= len(n.notifications) {
+ return nil
+ }
+ return n.notifications[index]
+}
+
+func (n *Notifications) GetFeedList() <-chan string {
+ ch := make(chan string)
+ notifications := n.notifications
+ go func() {
+ today := time.Now()
+ ty, tm, td := today.Date()
+ for _, item := range notifications {
+ sLocal := item.CreatedAt.Local()
+ sy, sm, sd := sLocal.Date()
+ format := "2006-01-02 15:04"
+ if ty == sy && tm == sm && td == sd {
+ format = "15:04"
+ }
+ content := fmt.Sprintf("%s %s", sLocal.Format(format), item.Account.Acct)
+ ch <- content
+ }
+ close(ch)
+ }()
+ return ch
+}
+
+func (n *Notifications) LoadNewer() int {
+ var notifications []*mastodon.Notification
+ var err error
+ if len(n.notifications) == 0 {
+ notifications, err = n.app.API.GetNotifications()
+ } else {
+ notifications, _, err = n.app.API.GetNotificationsNewer(n.notifications[0])
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if len(notifications) == 0 {
+ return 0
+ }
+ old := n.notifications
+ n.notifications = append(notifications, old...)
+ return len(notifications)
+}
+
+func (n *Notifications) LoadOlder() int {
+ var notifications []*mastodon.Notification
+ var err error
+ if len(n.notifications) == 0 {
+ notifications, err = n.app.API.GetNotifications()
+ } else {
+ notifications, _, err = n.app.API.GetNotificationsOlder(n.notifications[len(n.notifications)-1])
+ }
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if len(notifications) == 0 {
+ return 0
+ }
+ n.notifications = append(n.notifications, notifications...)
+ return len(notifications)
+}
+
+func (n *Notifications) DrawList() {
+ n.app.UI.StatusView.SetList(n.GetFeedList())
+}
+
+func (n *Notifications) DrawToot() {
+ n.index = n.app.UI.StatusView.GetCurrentItem()
+ notification := n.GetCurrentNotification()
+ if notification == nil {
+ n.app.UI.StatusView.SetText("")
+ n.app.UI.StatusView.SetControls("")
+ return
+ }
+ var text string
+ var controls string
+ defer func() { n.showSpoiler = false }()
+
+ switch notification.Type {
+ case "follow":
+ text = SublteText(n.app.Config.Style, FormatUsername(notification.Account)+" started following you\n\n")
+ controls = ColorKey(n.app.Config.Style, "", "U", "ser")
+ case "favourite":
+ pre := SublteText(n.app.Config.Style, FormatUsername(notification.Account)+" favorited your toot") + "\n\n"
+ text, controls = showTootOptions(n.app, notification.Status, n.showSpoiler)
+ text = pre + text
+ case "reblog":
+ pre := SublteText(n.app.Config.Style, FormatUsername(notification.Account)+" boosted your toot") + "\n\n"
+ text, controls = showTootOptions(n.app, notification.Status, n.showSpoiler)
+ text = pre + text
+ case "mention":
+ pre := SublteText(n.app.Config.Style, FormatUsername(notification.Account)+" mentioned you") + "\n\n"
+ text, controls = showTootOptions(n.app, notification.Status, n.showSpoiler)
+ text = pre + text
+ case "poll":
+ pre := SublteText(n.app.Config.Style, "A poll of yours or one you participated in has ended") + "\n\n"
+ text, controls = showTootOptions(n.app, notification.Status, n.showSpoiler)
+ text = pre + text
+ }
+
+ n.app.UI.StatusView.SetText(text)
+ n.app.UI.StatusView.SetControls(controls)
+}
+
+func (n *Notifications) redrawControls() {
+ notification := n.GetCurrentNotification()
+ if notification == nil {
+ n.app.UI.StatusView.SetControls("")
+ return
+ }
+ switch notification.Type {
+ case "favourite", "reblog", "mention", "poll":
+ _, controls := showTootOptions(n.app, notification.Status, n.showSpoiler)
+ n.app.UI.StatusView.SetControls(controls)
+ }
+}
+
+func (n *Notifications) GetSavedIndex() int {
+ return n.index
+}
+
+func (n *Notifications) Input(event *tcell.EventKey) {
+ notification := n.GetCurrentNotification()
+ if notification == nil {
+ return
+ }
+ if notification.Type == "follow" {
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 'u', 'U':
+ n.app.UI.StatusView.AddFeed(
+ NewUser(n.app, notification.Account),
+ )
+ }
+ }
+ return
+ }
+
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 't', 'T':
+ n.app.UI.StatusView.AddFeed(
+ NewThread(n.app, notification.Status),
+ )
+ case 'u', 'U':
+ n.app.UI.StatusView.AddFeed(
+ NewUser(n.app, notification.Account),
+ )
+ case 's', 'S':
+ n.showSpoiler = true
+ n.DrawToot()
+ case 'c', 'C':
+ n.app.UI.NewToot()
+ case 'o', 'O':
+ n.app.UI.ShowLinks()
+ case 'r', 'R':
+ n.app.UI.Reply(notification.Status)
+ case 'm', 'M':
+ n.app.UI.OpenMedia(notification.Status)
+ case 'f', 'F':
+ index := n.app.UI.StatusView.GetCurrentItem()
+ status, err := n.app.API.FavoriteToogle(notification.Status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ n.notifications[index].Status = status
+ n.redrawControls()
+
+ case 'b', 'B':
+ index := n.app.UI.StatusView.GetCurrentItem()
+ status, err := n.app.API.BoostToggle(notification.Status)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ n.notifications[index].Status = status
+ n.redrawControls()
+ case 'd', 'D':
+ n.app.API.DeleteStatus(notification.Status)
+ }
+ }
+}
diff --git a/linkoverlay.go b/linkoverlay.go
index d860b02..b059e98 100644
--- a/linkoverlay.go
+++ b/linkoverlay.go
@@ -14,6 +14,7 @@ func NewLinkOverlay(app *App) *LinkOverlay {
}
l.TextBottom.SetBackgroundColor(app.Config.Style.Background)
+ l.TextBottom.SetDynamicColors(true)
l.List.SetBackgroundColor(app.Config.Style.Background)
l.List.SetMainTextColor(app.Config.Style.Text)
l.List.SetSelectedBackgroundColor(app.Config.Style.ListSelectedBackground)
@@ -21,8 +22,7 @@ func NewLinkOverlay(app *App) *LinkOverlay {
l.List.ShowSecondaryText(false)
l.List.SetHighlightFullLine(true)
l.Flex.SetDrawFunc(app.Config.ClearContent)
-
- l.TextBottom.SetText("[O]pen")
+ l.TextBottom.SetText(ColorKey(app.Config.Style, "", "O", "pen"))
return l
}
diff --git a/main.go b/main.go
index a0d6add..c7266a6 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "context"
"log"
"strings"
@@ -39,7 +40,6 @@ func main() {
HaveAccount: false,
Config: &config,
}
- app.UI = NewUI(app)
if exists {
accounts, err := GetAccounts(path)
@@ -50,12 +50,21 @@ func main() {
a := accounts.Accounts[0]
client, err := a.Login()
if err == nil {
- app.API.Client = client
+ app.API.SetClient(client)
app.HaveAccount = true
+
+ me, err := app.API.Client.GetAccountCurrentUser(context.Background())
+ if err != nil {
+ log.Fatalln(err)
+ }
+ app.Me = me
}
}
}
+ app.UI = NewUI(app)
+ app.UI.Init()
+
if !app.HaveAccount {
app.UI.SetFocus(AuthOverlayFocus)
} else {
@@ -169,53 +178,6 @@ func main() {
return event
}
- if app.UI.Focus == LeftPaneFocus {
- if event.Key() == tcell.KeyRune {
- switch event.Rune() {
- case 'v', 'V':
- app.UI.SetFocus(RightPaneFocus)
- return nil
- case 'k', 'K':
- app.UI.TootList.Prev()
- return nil
- case 'j', 'J':
- app.UI.TootList.Next()
- return nil
- case 'q', 'Q':
- if app.UI.TootList.Focus == TootListThreadFocus {
- app.UI.TootList.GoBack()
- } else {
- app.UI.Root.Stop()
- }
- return nil
- }
- } else {
- switch event.Key() {
- case tcell.KeyUp:
- app.UI.TootList.Prev()
- return nil
- case tcell.KeyDown:
- app.UI.TootList.Next()
- return nil
- case tcell.KeyEsc:
- app.UI.TootList.GoBack()
- return nil
- case tcell.KeyCtrlC:
- app.UI.Root.Stop()
- return nil
- }
- }
- }
-
- if app.UI.Focus == RightPaneFocus {
- if event.Key() != tcell.KeyRune {
- switch event.Key() {
- case tcell.KeyEsc:
- app.UI.SetFocus(LeftPaneFocus)
- }
- }
- }
-
if app.UI.Focus == LeftPaneFocus || app.UI.Focus == RightPaneFocus {
if event.Key() == tcell.KeyRune {
switch event.Rune() {
@@ -223,28 +185,9 @@ func main() {
app.UI.CmdBar.Input.SetText(":")
app.UI.SetFocus(CmdBarFocus)
return nil
- case 't', 'T':
- app.UI.ShowThread()
- case 's', 'S':
- app.UI.ShowSensetive()
- case 'c', 'C':
- app.UI.NewToot()
- case 'o', 'O':
- app.UI.ShowLinks()
- case 'r', 'R':
- app.UI.Reply()
- case 'm', 'M':
- app.UI.OpenMedia()
- case 'f', 'F':
- //TODO UPDATE TOOT IN LIST
- app.UI.FavoriteEvent()
- case 'b':
- //TODO UPDATE TOOT IN LIST
- app.UI.BoostEvent()
- case 'd':
- app.UI.DeleteStatus()
}
}
+ return app.UI.StatusView.Input(event)
}
return event
@@ -254,7 +197,7 @@ func main() {
app.UI.MediaOverlay.InputField.HandleChanges,
)
- words := strings.Split(":q,:quit,:timeline", ",")
+ words := strings.Split(":q,:quit,:timeline,:tl", ",")
app.UI.CmdBar.Input.SetAutocompleteFunc(func(currentText string) (entries []string) {
if currentText == "" {
return
@@ -281,25 +224,29 @@ func main() {
fallthrough
case ":quit":
app.UI.Root.Stop()
- case ":timeline":
+ case ":timeline", ":tl":
if len(parts) < 2 {
break
}
switch parts[1] {
- case "local":
- app.UI.SetTimeline(TimelineLocal)
+ case "local", "l":
+ app.UI.StatusView.AddFeed(NewTimeline(app, TimelineLocal))
+ app.UI.SetFocus(LeftPaneFocus)
+ app.UI.CmdBar.ClearInput()
+ case "federated", "f":
+ app.UI.StatusView.AddFeed(NewTimeline(app, TimelineFederated))
app.UI.SetFocus(LeftPaneFocus)
app.UI.CmdBar.ClearInput()
- case "federated":
- app.UI.SetTimeline(TimelineFederated)
+ case "direct", "d":
+ app.UI.StatusView.AddFeed(NewTimeline(app, TimelineDirect))
app.UI.SetFocus(LeftPaneFocus)
app.UI.CmdBar.ClearInput()
- case "direct":
- app.UI.SetTimeline(TimelineDirect)
+ case "home", "h":
+ app.UI.StatusView.AddFeed(NewTimeline(app, TimelineHome))
app.UI.SetFocus(LeftPaneFocus)
app.UI.CmdBar.ClearInput()
- case "home":
- app.UI.SetTimeline(TimelineHome)
+ case "notifications", "n":
+ app.UI.StatusView.AddFeed(NewNoticifations(app))
app.UI.SetFocus(LeftPaneFocus)
app.UI.CmdBar.ClearInput()
}
diff --git a/media.go b/media.go
index 317935b..aa934c0 100644
--- a/media.go
+++ b/media.go
@@ -3,6 +3,7 @@ package main
import (
"os"
"path/filepath"
+ "strings"
"github.com/rivo/tview"
)
@@ -38,6 +39,7 @@ func NewMediaOverlay(app *App) *MediaView {
m.TextBottom.SetBackgroundColor(app.Config.Style.Background)
m.TextBottom.SetTextColor(app.Config.Style.Text)
+ m.TextBottom.SetDynamicColors(true)
m.InputField.View.SetBackgroundColor(app.Config.Style.Background)
m.InputField.View.SetFieldBackgroundColor(app.Config.Style.Background)
@@ -75,7 +77,11 @@ func (m *MediaView) AddFile(f string) {
func (m *MediaView) Draw() {
m.TextTop.SetText("List of attached files:")
- m.TextBottom.SetText("[A]dd file [D]elete file [Esc] Done")
+ var items []string
+ items = append(items, ColorKey(m.app.Config.Style, "", "A", "dd file"))
+ items = append(items, ColorKey(m.app.Config.Style, "", "D", "elete file"))
+ items = append(items, ColorKey(m.app.Config.Style, "", "Esc", " Done"))
+ m.TextBottom.SetText(strings.Join(items, " "))
}
func (m *MediaView) SetFocus(f MediaFocus) {
diff --git a/messagebox.go b/messagebox.go
index 465277d..f263112 100644
--- a/messagebox.go
+++ b/messagebox.go
@@ -121,8 +121,13 @@ func (m *MessageBox) Post() {
}
func (m *MessageBox) Draw() {
- info := "\n[P]ost [E]dit text, [T]oggle CW, [C]ontent warning text [M]edia attachment"
- status := tview.Escape(info)
+ var items []string
+ items = append(items, ColorKey(m.app.Config.Style, "", "P", "ost"))
+ items = append(items, ColorKey(m.app.Config.Style, "", "E", "dit"))
+ items = append(items, ColorKey(m.app.Config.Style, "", "T", "oggle CW"))
+ items = append(items, ColorKey(m.app.Config.Style, "", "C", "ontent warning text"))
+ items = append(items, ColorKey(m.app.Config.Style, "", "M", "edia attachment"))
+ status := strings.Join(items, " ")
m.Controls.SetText(status)
var outputHead string
diff --git a/paneview.go b/paneview.go
new file mode 100644
index 0000000..9b1bf9e
--- /dev/null
+++ b/paneview.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+ "github.com/gdamore/tcell"
+ "github.com/rivo/tview"
+)
+
+type PaneView interface {
+ GetLeftView() tview.Primitive
+ GetRightView() tview.Primitive
+ Input(event *tcell.EventKey) *tcell.EventKey
+}
diff --git a/statusview.go b/statusview.go
new file mode 100644
index 0000000..d6a1ea8
--- /dev/null
+++ b/statusview.go
@@ -0,0 +1,268 @@
+package main
+
+import (
+ "github.com/gdamore/tcell"
+ "github.com/rivo/tview"
+)
+
+func NewStatusView(app *App, tl TimelineType) *StatusView {
+ t := &StatusView{
+ app: app,
+ timelineType: tl,
+ list: tview.NewList(),
+ text: tview.NewTextView(),
+ controls: tview.NewTextView(),
+ focus: LeftPaneFocus,
+ loadingNewer: false,
+ loadingOlder: false,
+ }
+ t.flex = tview.NewFlex().SetDirection(tview.FlexRow).
+ AddItem(t.text, 0, 9, false).
+ AddItem(t.controls, 1, 0, false)
+
+ t.list.SetBackgroundColor(app.Config.Style.Background)
+ t.list.SetSelectedTextColor(app.Config.Style.ListSelectedText)
+ t.list.SetSelectedBackgroundColor(app.Config.Style.ListSelectedBackground)
+ t.list.ShowSecondaryText(false)
+ t.list.SetHighlightFullLine(true)
+
+ t.list.SetChangedFunc(func(i int, _ string, _ string, _ rune) {
+ if app.HaveAccount {
+ t.showToot(i)
+ }
+ })
+
+ t.text.SetWordWrap(true).SetDynamicColors(true)
+ t.text.SetBackgroundColor(app.Config.Style.Background)
+ t.text.SetTextColor(app.Config.Style.Text)
+ t.controls.SetDynamicColors(true)
+ t.controls.SetBackgroundColor(app.Config.Style.Background)
+ return t
+}
+
+type StatusView struct {
+ app *App
+ timelineType TimelineType
+ list *tview.List
+ flex *tview.Flex
+ text *tview.TextView
+ controls *tview.TextView
+ feeds []Feed
+ focus FocusAt
+ loadingNewer bool
+ loadingOlder bool
+}
+
+func (t *StatusView) AddFeed(f Feed) {
+ t.feeds = append(t.feeds, f)
+ f.DrawList()
+ t.list.SetCurrentItem(f.GetSavedIndex())
+ f.DrawToot()
+}
+
+func (t *StatusView) RemoveLatestFeed() {
+ t.feeds = t.feeds[:len(t.feeds)-1]
+ feed := t.feeds[len(t.feeds)-1]
+ feed.DrawList()
+ t.list.SetCurrentItem(feed.GetSavedIndex())
+ feed.DrawToot()
+}
+
+func (t *StatusView) GetLeftView() tview.Primitive {
+ if len(t.feeds) > 0 {
+ feed := t.feeds[len(t.feeds)-1]
+ feed.DrawList()
+ feed.DrawToot()
+ }
+ return t.list
+}
+
+func (t *StatusView) GetRightView() tview.Primitive {
+ return t.flex
+}
+
+func (t *StatusView) GetTextWidth() int {
+ _, _, width, _ := t.text.GetInnerRect()
+ return width
+}
+
+func (t *StatusView) GetCurrentItem() int {
+ return t.list.GetCurrentItem()
+}
+
+func (t *StatusView) ScrollToBeginning() {
+ t.text.ScrollToBeginning()
+}
+
+func (t *StatusView) inputBoth(event *tcell.EventKey) {
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 'q', 'Q':
+ if len(t.feeds) > 1 {
+ t.RemoveLatestFeed()
+ } else {
+ t.app.UI.Root.Stop()
+ }
+ }
+ } else {
+ switch event.Key() {
+ case tcell.KeyCtrlC:
+ t.app.UI.Root.Stop()
+ }
+ }
+ if len(t.feeds) > 0 {
+ feed := t.feeds[len(t.feeds)-1]
+ feed.Input(event)
+ }
+}
+
+func (t *StatusView) inputLeft(event *tcell.EventKey) {
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+ case 'v', 'V':
+ t.app.UI.FocusAt(t.text, "--VIEW--")
+ t.focus = RightPaneFocus
+ case 'k', 'K':
+ t.prev()
+ case 'j', 'J':
+ t.next()
+ }
+ } else {
+ switch event.Key() {
+ case tcell.KeyUp:
+ t.prev()
+ case tcell.KeyDown:
+ t.next()
+ case tcell.KeyEsc:
+ if len(t.feeds) > 1 {
+ t.RemoveLatestFeed()
+ }
+ }
+ }
+}
+
+func (t *StatusView) inputRight(event *tcell.EventKey) {
+ if event.Key() == tcell.KeyRune {
+ switch event.Rune() {
+
+ }
+ } else {
+ switch event.Key() {
+ case tcell.KeyEsc:
+ t.app.UI.FocusAt(nil, "--LIST--")
+ t.focus = LeftPaneFocus
+ }
+ }
+}
+
+func (t *StatusView) Input(event *tcell.EventKey) *tcell.EventKey {
+ t.inputBoth(event)
+ if len(t.feeds) == 0 {
+ return event
+ }
+
+ if t.focus == LeftPaneFocus {
+ t.inputLeft(event)
+ return nil
+ } else {
+ t.inputRight(event)
+ }
+
+ return event
+}
+
+func (t *StatusView) SetList(items <-chan string) {
+ t.list.Clear()
+ for s := range items {
+ t.list.AddItem(s, "", 0, nil)
+ }
+}
+func (t *StatusView) SetText(text string) {
+ t.text.SetText(text)
+}
+
+func (t *StatusView) SetControls(text string) {
+ t.controls.SetText(text)
+}
+
+func (t *StatusView) showToot(index int) {
+}
+
+func (t *StatusView) showTootOptions(index int, showSensitive bool) {
+}
+
+func (t *StatusView) prev() {
+ current := t.list.GetCurrentItem()
+ if current-1 >= 0 {
+ current--
+ }
+ t.list.SetCurrentItem(current)
+ t.feeds[len(t.feeds)-1].DrawToot()
+
+ if current < 4 {
+ t.loadNewer()
+ }
+}
+
+func (t *StatusView) next() {
+ t.list.SetCurrentItem(
+ t.list.GetCurrentItem() + 1,
+ )
+ t.feeds[len(t.feeds)-1].DrawToot()
+
+ count := t.list.GetItemCount()
+ current := t.list.GetCurrentItem()
+ if (count - current + 1) < 5 {
+ t.loadOlder()
+ }
+}
+
+func (t *StatusView) loadNewer() {
+ if t.loadingNewer {
+ return
+ }
+ t.loadingNewer = true
+ feedIndex := len(t.feeds) - 1
+ go func() {
+ new := t.feeds[feedIndex].LoadNewer()
+ if new == 0 {
+ return
+ }
+ if feedIndex != len(t.feeds)-1 {
+ return
+ }
+ t.app.UI.Root.QueueUpdateDraw(func() {
+ index := t.list.GetCurrentItem()
+ t.feeds[feedIndex].DrawList()
+ newIndex := index + new
+ if index == 0 && t.feeds[feedIndex].FeedType() == UserFeed {
+ newIndex = 0
+ }
+ t.list.SetCurrentItem(newIndex)
+ t.loadingNewer = false
+ })
+ }()
+}
+
+func (t *StatusView) loadOlder() {
+ if t.loadingOlder {
+ return
+ }
+ t.loadingOlder = true
+ feedIndex := len(t.feeds) - 1
+ go func() {
+ new := t.feeds[feedIndex].LoadOlder()
+ if new == 0 {
+ return
+ }
+ if feedIndex != len(t.feeds)-1 {
+ return
+ }
+ t.app.UI.Root.QueueUpdateDraw(func() {
+ index := t.list.GetCurrentItem()
+ t.feeds[feedIndex].DrawList()
+ t.list.SetCurrentItem(index)
+ t.loadingOlder = false
+ })
+ }()
+}
diff --git a/tootlist.go b/tootlist.go
deleted file mode 100644
index ac16b5e..0000000
--- a/tootlist.go
+++ /dev/null
@@ -1,257 +0,0 @@
-package main
-
-import (
- "fmt"
- "log"
- "time"
-
- "github.com/mattn/go-mastodon"
- "github.com/rivo/tview"
-)
-
-type TootListFocus int
-
-const (
- TootListFeedFocus TootListFocus = iota
- TootListThreadFocus
-)
-
-type TootList struct {
- app *App
- Index int
- Statuses []*mastodon.Status
- Thread []*mastodon.Status
- ThreadIndex int
- List *tview.List
- Focus TootListFocus
- loadingFeedOld bool
- loadingFeedNew bool
-}
-
-func NewTootList(app *App) *TootList {
- t := &TootList{
- app: app,
- Index: 0,
- Focus: TootListFeedFocus,
- List: tview.NewList(),
- }
- t.List.SetBackgroundColor(app.Config.Style.Background)
- t.List.SetSelectedTextColor(app.Config.Style.ListSelectedText)
- t.List.SetSelectedBackgroundColor(app.Config.Style.ListSelectedBackground)
- t.List.ShowSecondaryText(false)
- t.List.SetHighlightFullLine(true)
-
- t.List.SetChangedFunc(func(index int, _ string, _ string, _ rune) {
- if app.HaveAccount {
- app.UI.TootView.ShowToot(index)
- }
- })
-
- return t
-}
-
-func (t *TootList) GetStatuses() []*mastodon.Status {
- if t.Focus == TootListThreadFocus {
- return t.GetThread()
- }
- return t.GetFeed()
-}
-
-func (t *TootList) GetStatus(index int) (*mastodon.Status, error) {
- if t.Focus == TootListThreadFocus {
- return t.GetThreadStatus(index)
- }
- return t.GetFeedStatus(index)
-}
-
-func (t *TootList) SetFeedStatuses(s []*mastodon.Status) {
- t.Statuses = s
- t.Draw()
-}
-
-func (t *TootList) PrependFeedStatuses(s []*mastodon.Status) {
- t.Statuses = append(s, t.Statuses...)
- t.SetFeedIndex(
- t.GetFeedIndex() + len(s),
- )
- t.List.SetCurrentItem(t.GetFeedIndex())
-}
-
-func (t *TootList) AppendFeedStatuses(s []*mastodon.Status) {
- t.Statuses = append(t.Statuses, s...)
-}
-
-func (t *TootList) GetFeed() []*mastodon.Status {
- return t.Statuses
-}
-
-func (t *TootList) GetFeedStatus(index int) (*mastodon.Status, error) {
- statuses := t.GetFeed()
- if index < len(statuses) {
- return statuses[index], nil
- }
- return nil, fmt.Errorf("no status with that index")
-}
-
-func (t *TootList) GetIndex() int {
- if t.Focus == TootListThreadFocus {
- return t.GetThreadIndex()
- }
- return t.GetFeedIndex()
-}
-
-func (t *TootList) SetIndex(index int) {
- switch t.Focus {
- case TootListFeedFocus:
- t.SetFeedIndex(index)
- case TootListThreadFocus:
- t.SetThreadIndex(index)
- }
-}
-
-func (t *TootList) GetFeedIndex() int {
- return t.Index
-}
-
-func (t *TootList) SetFeedIndex(index int) {
- t.Index = index
-}
-
-func (t *TootList) GetThreadIndex() int {
- return t.ThreadIndex
-}
-
-func (t *TootList) SetThreadIndex(index int) {
- t.ThreadIndex = index
-}
-
-func (t *TootList) Prev() {
- index := t.GetIndex()
- statuses := t.GetStatuses()
-
- if index-1 > -1 {
- index--
- }
-
- if index < 5 && t.Focus == TootListFeedFocus {
- go func() {
- if t.loadingFeedNew {
- return
- }
- t.loadingFeedNew = true
- t.app.UI.LoadNewer(statuses[0])
- t.app.UI.Root.QueueUpdateDraw(func() {
- t.Draw()
- t.loadingFeedNew = false
- })
- }()
- }
- t.SetIndex(index)
- t.List.SetCurrentItem(index)
-}
-
-func (t *TootList) Next() {
- index := t.GetIndex()
- statuses := t.GetStatuses()
-
- if index+1 < len(statuses) {
- index++
- }
-
- if (len(statuses)-index) < 10 && t.Focus == TootListFeedFocus {
- go func() {
- if t.loadingFeedOld || len(statuses) == 0 {
- return
- }
- t.loadingFeedOld = true
- t.app.UI.LoadOlder(statuses[len(statuses)-1])
- t.app.UI.Root.QueueUpdateDraw(func() {
- t.Draw()
- t.loadingFeedOld = false
- })
- }()
- }
- t.SetIndex(index)
- t.List.SetCurrentItem(index)
-}
-
-func (t *TootList) Draw() {
- t.List.Clear()
-
- var statuses []*mastodon.Status
- var index int
-
- switch t.Focus {
- case TootListFeedFocus:
- statuses = t.GetFeed()
- index = t.GetFeedIndex()
- case TootListThreadFocus:
- statuses = t.GetThread()
- index = t.GetThreadIndex()
- }
- if len(statuses) == 0 {
- return
- }
-
- today := time.Now()
- ty, tm, td := today.Date()
- currRow := 0
- for _, s := range statuses {
- sLocal := s.CreatedAt.Local()
- sy, sm, sd := sLocal.Date()
- format := "2006-01-02 15:04"
- if ty == sy && tm == sm && td == sd {
- format = "15:04"
- }
- content := fmt.Sprintf("%s %s", sLocal.Format(format), s.Account.Acct)
- t.List.InsertItem(currRow, content, "", 0, nil)
- currRow++
- }
- t.List.SetCurrentItem(index)
- t.app.UI.TootView.ShowToot(index)
-}
-
-func (t *TootList) SetThread(s []*mastodon.Status, index int) {
- t.Thread = s
- t.SetThreadIndex(index)
-}
-
-func (t *TootList) GetThread() []*mastodon.Status {
- return t.Thread
-}
-
-func (t *TootList) GetThreadStatus(index int) (*mastodon.Status, error) {
- statuses := t.GetThread()
- if index < len(statuses) {
- return statuses[index], nil
- }
- return nil, fmt.Errorf("no status with that index")
-}
-
-func (t *TootList) FocusFeed() {
- t.Focus = TootListFeedFocus
-}
-
-func (t *TootList) FocusThread() {
- t.Focus = TootListThreadFocus
-}
-
-func (t *TootList) GoBack() {
- t.Focus = TootListFeedFocus
- t.Draw()
-}
-
-func (t *TootList) Reply() {
- status, err := t.GetStatus(t.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
- if status.Reblog != nil {
- status = status.Reblog
- }
-
- users := []string{"@" + status.Account.Acct}
- for _, m := range status.Mentions {
- users = append(users, "@"+m.Acct)
- }
-}
diff --git a/tootview.go b/tootview.go
deleted file mode 100644
index c28563c..0000000
--- a/tootview.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package main
-
-import (
- "fmt"
- "log"
- "strings"
-
- "github.com/rivo/tview"
-)
-
-func NewTootView(app *App) *TootView {
- t := &TootView{
- app: app,
- Index: 0,
- Text: tview.NewTextView(),
- Controls: tview.NewTextView(),
- }
-
- t.Text.SetWordWrap(true).SetDynamicColors(true)
- t.Text.SetBackgroundColor(app.Config.Style.Background)
- t.Text.SetTextColor(app.Config.Style.Text)
- t.Controls.SetDynamicColors(true)
- t.Controls.SetBackgroundColor(app.Config.Style.Background)
-
- return t
-}
-
-type TootView struct {
- app *App
- Index int
- Text *tview.TextView
- Controls *tview.TextView
-}
-
-func (s *TootView) ShowToot(index int) {
- s.ShowTootOptions(index, false)
-}
-
-func (s *TootView) ShowTootOptions(index int, showSensitive bool) {
- status, err := s.app.UI.TootList.GetStatus(index)
- if err != nil {
- log.Fatalln(err)
- }
-
- var line string
- _, _, width, _ := s.Text.GetInnerRect()
- for i := 0; i < width; i++ {
- line += "-"
- }
- line += "\n"
-
- shouldDisplay := !status.Sensitive || showSensitive
-
- var stripped string
- var urls []URL
- var u []URL
- if status.Sensitive && !showSensitive {
- stripped, u = cleanTootHTML(status.SpoilerText)
- urls = append(urls, u...)
- stripped += "\n" + line
- stripped += "Press [s] to show hidden text"
-
- } else {
- stripped, u = cleanTootHTML(status.Content)
- urls = append(urls, u...)
-
- if status.Sensitive {
- sens, u := cleanTootHTML(status.SpoilerText)
- urls = append(urls, u...)
- stripped = sens + "\n\n" + stripped
- }
- }
- s.app.UI.LinkOverlay.SetURLs(urls)
-
- subtleColor := fmt.Sprintf("[#%x]", s.app.Config.Style.Subtle.Hex())
- special1 := fmt.Sprintf("[#%x]", s.app.Config.Style.TextSpecial1.Hex())
- special2 := fmt.Sprintf("[#%x]", s.app.Config.Style.TextSpecial2.Hex())
- var head string
- if status.Reblog != nil {
- if status.Account.DisplayName != "" {
- head += fmt.Sprintf(subtleColor+"%s (%s)\n", status.Account.DisplayName, status.Account.Acct)
- } else {
- head += fmt.Sprintf(subtleColor+"%s\n", status.Account.Acct)
- }
- head += subtleColor + "Boosted\n"
- head += subtleColor + line
- status = status.Reblog
- }
-
- if status.Account.DisplayName != "" {
- head += fmt.Sprintf(special2+"%s\n", status.Account.DisplayName)
- }
- head += fmt.Sprintf(special1+"%s\n\n", status.Account.Acct)
- output := head
- content := tview.Escape(stripped)
- if content != "" {
- output += content + "\n\n"
- }
-
- var poll string
- if status.Poll != nil {
- poll += subtleColor + "Poll\n"
- poll += subtleColor + line
- poll += fmt.Sprintf("Number of votes: %d\n\n", status.Poll.VotesCount)
- votes := float64(status.Poll.VotesCount)
- for _, o := range status.Poll.Options {
- res := 0.0
- if votes != 0 {
- res = float64(o.VotesCount) / votes * 100
- }
- poll += fmt.Sprintf("%s - %.2f%% (%d)\n", tview.Escape(o.Title), res, o.VotesCount)
- }
- poll += "\n"
- }
-
- var media string
- for _, att := range status.MediaAttachments {
- media += subtleColor + line
- media += fmt.Sprintf(subtleColor+"Attached %s\n", att.Type)
- media += fmt.Sprintf("%s\n", att.URL)
- }
-
- var card string
- if status.Card != nil {
- card += subtleColor + "Card type: " + status.Card.Type + "\n"
- card += subtleColor + line
- if status.Card.Title != "" {
- card += status.Card.Title + "\n\n"
- }
- desc := strings.TrimSpace(status.Card.Description)
- if desc != "" {
- card += desc + "\n\n"
- }
- card += status.Card.URL
- }
-
- if shouldDisplay {
- output += poll + media + card
- }
-
- s.Text.SetText(output)
- s.Text.ScrollToBeginning()
- var info []string
- if status.Favourited == true {
- info = append(info, "Un[F]avorite")
- } else {
- info = append(info, "[F]avorite")
- }
- if status.Reblogged == true {
- info = append(info, "Un[B]oost")
- } else {
- info = append(info, "[B]oost")
- }
- info = append(info, "[T]hread", "[R]eply", "[V]iew")
- if len(status.MediaAttachments) > 0 {
- info = append(info, "[M]edia")
- }
- if len(urls) > 0 {
- info = append(info, "[O]pen")
- }
-
- if status.Account.ID == s.app.Me.ID {
- info = append(info, "[D]elete")
- }
-
- s.Controls.SetText(tview.Escape(strings.Join(info, " ")))
-}
diff --git a/ui.go b/ui.go
index 2d1f764..1e72222 100644
--- a/ui.go
+++ b/ui.go
@@ -24,42 +24,44 @@ const (
func NewUI(app *App) *UI {
ui := &UI{
- app: app,
- Root: tview.NewApplication(),
- Top: NewTop(app),
- Pages: tview.NewPages(),
- Timeline: TimelineHome,
- TootList: NewTootList(app),
- TootView: NewTootView(app),
- CmdBar: NewCmdBar(app),
- StatusBar: NewStatusBar(app),
- MessageBox: NewMessageBox(app),
- LinkOverlay: NewLinkOverlay(app),
- AuthOverlay: NewAuthOverlay(app),
- MediaOverlay: NewMediaOverlay(app),
+ app: app,
+ Root: tview.NewApplication(),
}
- verticalLine := tview.NewBox().SetBackgroundColor(app.Config.Style.Background)
+ return ui
+}
+
+func (ui *UI) Init() {
+ ui.Top = NewTop(ui.app)
+ ui.Pages = tview.NewPages()
+ ui.Timeline = TimelineHome
+ ui.CmdBar = NewCmdBar(ui.app)
+ ui.StatusBar = NewStatusBar(ui.app)
+ ui.MessageBox = NewMessageBox(ui.app)
+ ui.LinkOverlay = NewLinkOverlay(ui.app)
+ ui.AuthOverlay = NewAuthOverlay(ui.app)
+ ui.MediaOverlay = NewMediaOverlay(ui.app)
+ ui.StatusView = NewStatusView(ui.app, ui.Timeline)
+
+ verticalLine := tview.NewBox().SetBackgroundColor(ui.app.Config.Style.Background)
verticalLine.SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) {
for cy := y; cy < y+height; cy++ {
- screen.SetContent(x, cy, tview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Foreground(app.Config.Style.Subtle))
+ screen.SetContent(x, cy, tview.BoxDrawingsLightVertical, nil, tcell.StyleDefault.Foreground(ui.app.Config.Style.Subtle))
}
return 0, 0, 0, 0
})
- ui.Pages.SetBackgroundColor(app.Config.Style.Background)
+ ui.Pages.SetBackgroundColor(ui.app.Config.Style.Background)
ui.Pages.AddPage("main",
tview.NewFlex().
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(ui.Top.Text, 1, 0, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
- AddItem(ui.TootList.List, 0, 2, false).
+ AddItem(ui.StatusView.GetLeftView(), 0, 2, false).
AddItem(verticalLine, 1, 0, false).
- AddItem(tview.NewBox().SetBackgroundColor(app.Config.Style.Background), 1, 0, false).
- AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
- AddItem(ui.TootView.Text, 0, 9, false).
- AddItem(ui.TootView.Controls, 1, 0, false),
+ AddItem(tview.NewBox().SetBackgroundColor(ui.app.Config.Style.Background), 1, 0, false).
+ AddItem(ui.StatusView.GetRightView(),
0, 4, false),
0, 1, false).
AddItem(ui.StatusBar.Text, 1, 1, false).
@@ -111,8 +113,6 @@ func NewUI(app *App) *UI {
screen.Clear()
return false
})
-
- return ui
}
type UI struct {
@@ -120,8 +120,6 @@ type UI struct {
Root *tview.Application
Focus FocusAt
Top *Top
- TootView *TootView
- TootList *TootList
MessageBox *MessageBox
CmdBar *CmdBar
StatusBar *StatusBar
@@ -130,6 +128,18 @@ type UI struct {
AuthOverlay *AuthOverlay
MediaOverlay *MediaView
Timeline TimelineType
+ StatusView *StatusView
+}
+
+func (ui *UI) FocusAt(p tview.Primitive, s string) {
+ if p == nil {
+ ui.Root.SetFocus(ui.Pages)
+ } else {
+ ui.Root.SetFocus(p)
+ }
+ if s != "" {
+ ui.StatusBar.SetText(s)
+ }
}
func (ui *UI) SetFocus(f FocusAt) {
@@ -137,7 +147,6 @@ func (ui *UI) SetFocus(f FocusAt) {
switch f {
case RightPaneFocus:
ui.StatusBar.SetText("-- VIEW --")
- ui.Root.SetFocus(ui.TootView.Text)
case CmdBarFocus:
ui.StatusBar.SetText("-- CMD --")
ui.Root.SetFocus(ui.CmdBar.Input)
@@ -166,40 +175,6 @@ func (ui *UI) SetFocus(f FocusAt) {
}
}
-func (ui *UI) SetTimeline(tl TimelineType) {
- ui.Timeline = tl
- statuses, err := ui.app.API.GetStatuses(tl)
- if err != nil {
- log.Fatalln(err)
- }
- ui.TootList.SetFeedStatuses(statuses)
-}
-
-func (ui *UI) ShowThread() {
- status, err := ui.TootList.GetStatus(ui.TootList.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
-
- if status.Reblog != nil {
- status = status.Reblog
- }
-
- thread, index, err := ui.app.API.GetThread(status)
- if err != nil {
- log.Fatalln(err)
- }
-
- ui.TootList.SetThread(thread, index)
- ui.TootList.FocusThread()
- ui.SetFocus(LeftPaneFocus)
- ui.TootList.Draw()
-}
-
-func (ui *UI) ShowSensetive() {
- ui.TootView.ShowTootOptions(ui.TootList.GetIndex(), true)
-}
-
func (ui *UI) NewToot() {
ui.Root.SetFocus(ui.MessageBox.View)
ui.MediaOverlay.Reset()
@@ -208,11 +183,7 @@ func (ui *UI) NewToot() {
ui.SetFocus(MessageFocus)
}
-func (ui *UI) Reply() {
- status, err := ui.TootList.GetStatus(ui.TootList.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
+func (ui *UI) Reply(status *mastodon.Status) {
if status.Reblog != nil {
status = status.Reblog
}
@@ -226,11 +197,7 @@ func (ui *UI) ShowLinks() {
ui.SetFocus(LinkOverlayFocus)
}
-func (ui *UI) OpenMedia() {
- status, err := ui.TootList.GetStatus(ui.TootList.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
+func (ui *UI) OpenMedia(status *mastodon.Status) {
if status.Reblog != nil {
status = status.Reblog
}
@@ -267,64 +234,9 @@ func (ui *UI) LoggedIn() {
log.Fatalln(err)
}
ui.app.Me = me
-
- ui.SetTimeline(ui.Timeline)
-}
-
-func (ui *UI) LoadNewer(status *mastodon.Status) int {
- statuses, _, err := ui.app.API.GetStatusesNewer(ui.Timeline, status)
- if err != nil {
- log.Fatalln(err)
- }
- ui.TootList.PrependFeedStatuses(statuses)
- return len(statuses)
-}
-
-func (ui *UI) LoadOlder(status *mastodon.Status) int {
- statuses, _, err := ui.app.API.GetStatusesOlder(ui.Timeline, status)
- if err != nil {
- log.Fatalln(err)
- }
- ui.TootList.AppendFeedStatuses(statuses)
- return len(statuses)
-}
-
-func (ui *UI) FavoriteEvent() {
- status, err := ui.TootList.GetStatus(ui.TootList.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
- if status.Favourited == true {
- err = ui.app.API.Unfavorite(status)
- } else {
- err = ui.app.API.Favorite(status)
- }
-}
-
-func (ui *UI) BoostEvent() {
- status, err := ui.TootList.GetStatus(ui.TootList.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
- if status.Reblogged == true {
- err = ui.app.API.Unboost(status)
- } else {
- err = ui.app.API.Boost(status)
- }
- if err != nil {
- log.Fatalln(err)
- }
-}
-
-func (ui *UI) DeleteStatus() {
- status, err := ui.TootList.GetStatus(ui.TootList.GetIndex())
- if err != nil {
- log.Fatalln(err)
- }
- err = ui.app.API.DeleteStatus(status)
- if err != nil {
- log.Fatalln(err)
- }
+ ui.StatusView.AddFeed(
+ NewTimeline(ui.app, TimelineHome),
+ )
}
func (conf *Config) ClearContent(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) {
diff --git a/util.go b/util.go
index 3160b87..508807a 100644
--- a/util.go
+++ b/util.go
@@ -1,6 +1,7 @@
package main
import (
+ "fmt"
"io"
"io/ioutil"
"log"
@@ -11,6 +12,7 @@ import (
"regexp"
"strings"
+ "github.com/mattn/go-mastodon"
"github.com/microcosm-cc/bluemonday"
"github.com/rivo/tview"
"golang.org/x/net/html"
@@ -54,6 +56,7 @@ func cleanTootHTML(content string) (string, []URL) {
urls := getURLs(stripped)
stripped = bluemonday.NewPolicy().AllowElements("p", "br").Sanitize(content)
stripped = strings.ReplaceAll(stripped, "
", "\n")
+ stripped = strings.ReplaceAll(stripped, "
", "\n")
stripped = strings.ReplaceAll(stripped, "
", "") stripped = strings.ReplaceAll(stripped, "
", "\n\n") stripped = strings.TrimSpace(stripped) @@ -211,3 +214,23 @@ func FindFiles(s string) []string { } return files } + +func ColorKey(style StyleConfig, pre, key, end string) string { + color := fmt.Sprintf("[#%x]", style.TextSpecial2.Hex()) + normal := fmt.Sprintf("[#%x]", style.Text.Hex()) + key = tview.Escape("[" + key + "]") + text := fmt.Sprintf("%s%s%s%s%s", pre, color, key, normal, end) + return text +} + +func FormatUsername(a mastodon.Account) string { + if a.DisplayName != "" { + return fmt.Sprintf("%s (%s)", a.DisplayName, a.Acct) + } + return a.Acct +} + +func SublteText(style StyleConfig, text string) string { + subtle := fmt.Sprintf("[#%x]", style.Subtle.Hex()) + return fmt.Sprintf("%s%s", subtle, text) +}