From ed7c99cd70ee9015c0ef190f4b53beae666d3438 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Thu, 24 Nov 2022 20:52:31 +0100 Subject: [PATCH] edit toots (#188) --- README.md | 1 + config.example.ini | 10 ++- config/config.go | 6 ++ config/default_config.go | 10 ++- main.go | 2 +- ui/cmdbar.go | 5 ++ ui/commands.go | 18 +++++- ui/composeview.go | 133 ++++++++++++++++++++++++++++++++------- ui/input.go | 10 ++- ui/item_status.go | 3 + ui/pollview.go | 18 ++++++ ui/view.go | 8 ++- 12 files changed, 187 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 070cab8..f7fc8bb 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:bookmarks` lists all your bookmarks * `:clear-notifications` clear all notifications * `:compose` compose a new toot +* `:edit` edit one of your toots * `:favorited` lists toots you've favorited * `:favorites` lists users that favorited the toot * `:follow-tag` followed by the hashtag to follow e.g. `:follow-tag tut` diff --git a/config.example.ini b/config.example.ini index 981de00..946f019 100644 --- a/config.example.ini +++ b/config.example.ini @@ -150,9 +150,9 @@ leader-timeout=1000 # comma. # # Available commands: home, direct, local, federated, clear-notifications, -# compose, history, blocking, bookmarks, saved, favorited, boosts, favorites, -# following, followers, muting, newer, preferences, profile, notifications, -# lists, tag, window, list-placement, list-split, proportions +# compose, edit, history, blocking, bookmarks, saved, favorited, boosts, +# favorites, following, followers, muting, newer, preferences, profile, +# notifications, lists, tag, window, list-placement, list-split, proportions # # The shortcuts are up to you, but keep them quite short and make sure they # don't collide. If you have one shortcut that is "f" and an other one that is @@ -554,6 +554,10 @@ status-avatar="[A]vatar",'a','A' # default="[B]oost","Un[B]oost",'b','B' status-boost="[B]oost","Un[B]oost",'b','B' +# Edit a toot +# default="[E]dit",'e','E' +status-edit="[E]dit",'e','E' + # Delete a toot # default="[D]elete",'d','D' status-delete="[D]elete",'d','D' diff --git a/config/config.go b/config/config.go index 431a148..eb213ff 100644 --- a/config/config.go +++ b/config/config.go @@ -57,6 +57,7 @@ const ( LeaderFederated LeaderClearNotifications LeaderCompose + LeaderEdit LeaderBlocking LeaderBookmarks LeaderSaved @@ -358,6 +359,7 @@ type Input struct { StatusAvatar Key StatusBoost Key StatusDelete Key + StatusEdit Key StatusFavorite Key StatusMedia Key StatusLinks Key @@ -860,6 +862,8 @@ func parseGeneral(cfg *ini.File) General { la.Command = LeaderClearNotifications case "compose": la.Command = LeaderCompose + case "edit": + la.Command = LeaderEdit case "blocking": la.Command = LeaderBlocking case "bookmarks": @@ -1246,6 +1250,7 @@ func parseInput(cfg *ini.File) Input { StatusAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false), StatusBoost: inputStrOrErr([]string{"\"[B]oost\"", "\"Un[B]oost\"", "'b'", "'B'"}, true), StatusDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), + StatusEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false), StatusFavorite: inputStrOrErr([]string{"\"[F]avorite\"", "\"Un[F]avorite\"", "'f'", "'F'"}, true), StatusMedia: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false), StatusLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), @@ -1323,6 +1328,7 @@ func parseInput(cfg *ini.File) Input { ic.StatusAvatar = inputOrErr(cfg, "status-avatar", false, ic.StatusAvatar) ic.StatusBoost = inputOrErr(cfg, "status-boost", true, ic.StatusBoost) ic.StatusDelete = inputOrErr(cfg, "status-delete", false, ic.StatusDelete) + ic.StatusEdit = inputOrErr(cfg, "status-edit", false, ic.StatusEdit) ic.StatusFavorite = inputOrErr(cfg, "status-favorite", true, ic.StatusFavorite) ic.StatusMedia = inputOrErr(cfg, "status-media", false, ic.StatusMedia) ic.StatusLinks = inputOrErr(cfg, "status-links", false, ic.StatusLinks) diff --git a/config/default_config.go b/config/default_config.go index 5dec689..0196e00 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -152,9 +152,9 @@ leader-timeout=1000 # comma. # # Available commands: home, direct, local, federated, clear-notifications, -# compose, history, blocking, bookmarks, saved, favorited, boosts, favorites, -# following, followers, muting, newer, preferences, profile, notifications, -# lists, tag, window, list-placement, list-split, proportions +# compose, edit, history, blocking, bookmarks, saved, favorited, boosts, +# favorites, following, followers, muting, newer, preferences, profile, +# notifications, lists, tag, window, list-placement, list-split, proportions # # The shortcuts are up to you, but keep them quite short and make sure they # don't collide. If you have one shortcut that is "f" and an other one that is @@ -556,6 +556,10 @@ status-avatar="[A]vatar",'a','A' # default="[B]oost","Un[B]oost",'b','B' status-boost="[B]oost","Un[B]oost",'b','B' +# Edit a toot +# default="[E]dit",'e','E' +status-edit="[E]dit",'e','E' + # Delete a toot # default="[D]elete",'d','D' status-delete="[D]elete",'d','D' diff --git a/main.go b/main.go index 72d38c9..19f13c1 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.20" +const version = "1.0.21" func main() { util.SetTerminalTitle("tut") diff --git a/ui/cmdbar.go b/ui/cmdbar.go index d452ea4..214253f 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -67,6 +67,11 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { c.tutView.ComposeCommand() c.ClearInput() c.View.Autocomplete() + case ":edit": + c.ClearInput() + c.View.Autocomplete() + c.Back() + c.tutView.EditCommand() case ":blocking": c.tutView.BlockingCommand() c.Back() diff --git a/ui/commands.go b/ui/commands.go index 295f0a9..116ade0 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -12,7 +12,23 @@ import ( ) func (tv *TutView) ComposeCommand() { - tv.InitPost(nil) + tv.InitPost(nil, nil) +} + +func (tv *TutView) EditCommand() { + item, itemErr := tv.GetCurrentItem() + if itemErr != nil { + return + } + if item.Type() != api.StatusType { + return + } + s := item.Raw().(*mastodon.Status) + s = util.StatusOrReblog(s) + if tv.tut.Client.Me.ID != s.Account.ID { + return + } + tv.InitPost(nil, s) } func (tv *TutView) BlockingCommand() { diff --git a/ui/composeview.go b/ui/composeview.go index 885e409..41b1278 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -9,6 +9,7 @@ import ( "time" "github.com/RasmusLindroth/go-mastodon" + "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" "github.com/gdamore/tcell/v2" @@ -17,8 +18,10 @@ import ( ) type msgToot struct { + ID mastodon.ID Text string - Status *mastodon.Status + Reply *mastodon.Status + Edit *mastodon.Status MediaIDs []mastodon.ID Sensitive bool SpoilerText string @@ -123,7 +126,7 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) { items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeMediaFocus, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposePoll, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeLanguage, true)) - if cv.msg.Status != nil { + if cv.msg.Reply != nil { items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeIncludeQuote, true)) } case ComposeMedia: @@ -142,7 +145,7 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) { } } -func (cv *ComposeView) SetStatus(status *mastodon.Status) { +func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) error { cv.tutView.PollView.Reset() cv.media.Reset() msg := &msgToot{} @@ -155,30 +158,60 @@ func (cv *ComposeView) SetStatus(status *mastodon.Status) { if me.Source != nil && me.Source.Language != nil { lang = *me.Source.Language } - if status != nil { - if status.Reblog != nil { - status = status.Reblog + if reply != nil { + if reply.Reblog != nil { + reply = reply.Reblog } - msg.Status = status - if status.Sensitive { + msg.Reply = reply + if reply.Sensitive { msg.Sensitive = true - msg.SpoilerText = status.SpoilerText + msg.SpoilerText = reply.SpoilerText } - if visibilities[status.Visibility] > visibilities[visibility] { - visibility = status.Visibility + if visibilities[reply.Visibility] > visibilities[visibility] { + visibility = reply.Visibility } } msg.Visibility = visibility msg.Language = lang cv.msg = msg cv.msg.Text = cv.getAccs() - if cv.tutView.tut.Config.General.QuoteReply { + + if edit != nil { + source, err := cv.tutView.tut.Client.Client.GetStatusSource(context.Background(), edit.ID) + if err != nil { + cv.tutView.ShowError( + fmt.Sprintf("Couldn't get status. Error: %v\n", err), + ) + return err + } + msg := &msgToot{} + msg.Edit = edit + msg.ID = source.ID + msg.Text = source.Text + msg.SpoilerText = source.SpoilerText + for _, mid := range edit.MediaAttachments { + msg.MediaIDs = append(msg.MediaIDs, mid.ID) + } + msg.Sensitive = edit.Sensitive + msg.Visibility = edit.Visibility + msg.Language = edit.Language + if edit.Poll != nil { + cv.tutView.PollView.AddPoll(edit.Poll) + } + if len(edit.MediaAttachments) > 0 { + cv.media.AddFromEdit(edit) + } + + cv.msg = msg + } + + if cv.tutView.tut.Config.General.QuoteReply && edit == nil { cv.IncludeQuote() } cv.visibility.SetLabel("Visibility: ") index := 0 for i, v := range visibilitiesStr { - if msg.Visibility == v { + if cv.msg.Visibility == v { index = i break } @@ -190,7 +223,7 @@ func (cv *ComposeView) SetStatus(status *mastodon.Status) { cv.lang.SetLabel("Lang: ") langStrs := []string{} for i, l := range util.Languages { - if msg.Language == l.Code { + if cv.msg.Language == l.Code { index = i } langStrs = append(langStrs, fmt.Sprintf("%s (%s)", l.Local, l.English)) @@ -200,13 +233,14 @@ func (cv *ComposeView) SetStatus(status *mastodon.Status) { cv.UpdateContent() cv.SetControls(ComposeNormal) + return nil } func (cv *ComposeView) getAccs() string { - if cv.msg.Status == nil { + if cv.msg.Reply == nil { return "" } - s := cv.msg.Status + s := cv.msg.Reply var users []string if s.Account.Acct != cv.tutView.tut.Client.Me.Acct { users = append(users, "@"+s.Account.Acct) @@ -259,12 +293,12 @@ func (cv *ComposeView) UpdateContent() { var outputHead string var output string - if cv.msg.Status != nil { + if cv.msg.Reply != nil { var acct string - if cv.msg.Status.Account.DisplayName != "" { - acct = fmt.Sprintf("%s (%s)\n", cv.msg.Status.Account.DisplayName, cv.msg.Status.Account.Acct) + if cv.msg.Reply.Account.DisplayName != "" { + acct = fmt.Sprintf("%s (%s)\n", cv.msg.Reply.Account.DisplayName, cv.msg.Reply.Account.Acct) } else { - acct = fmt.Sprintf("%s\n", cv.msg.Status.Account.Acct) + acct = fmt.Sprintf("%s\n", cv.msg.Reply.Account.Acct) } outputHead += subtleColor + "Replying to " + tview.Escape(acct) + "\n" + normal } @@ -291,7 +325,7 @@ func (cv *ComposeView) IncludeQuote() { return } t := cv.msg.Text - s := cv.msg.Status + s := cv.msg.Reply if s == nil { return } @@ -383,8 +417,11 @@ func (cv *ComposeView) Post() { send := mastodon.Toot{ Status: strings.TrimSpace(toot.Text), } - if toot.Status != nil { - send.InReplyToID = toot.Status.ID + if toot.Reply != nil { + send.InReplyToID = toot.Reply.ID + } + if toot.Edit != nil && toot.Edit.InReplyToID != nil { + send.InReplyToID = toot.Edit.InReplyToID.(mastodon.ID) } if toot.Sensitive { send.Sensitive = true @@ -394,6 +431,10 @@ func (cv *ComposeView) Post() { if cv.HasMedia() { attachments := cv.media.Files for _, ap := range attachments { + if ap.Remote { + send.MediaIDs = append(send.MediaIDs, ap.ID) + continue + } f, err := os.Open(ap.Path) if err != nil { cv.tutView.ShowError( @@ -426,7 +467,25 @@ func (cv *ComposeView) Post() { send.Visibility = cv.msg.Visibility send.Language = cv.msg.Language - _, err := cv.tutView.tut.Client.Client.PostStatus(context.Background(), &send) + var err error + var newPost *mastodon.Status + if toot.Edit != nil { + newPost, err = cv.tutView.tut.Client.Client.UpdateStatus(context.Background(), &send, toot.Edit.ID) + if err == nil { + item, itemErr := cv.tutView.GetCurrentItem() + if itemErr != nil { + return + } + if item.Type() != api.StatusType { + return + } + s := item.Raw().(*mastodon.Status) + *s = *newPost + cv.tutView.RedrawContent() + } + } else { + _, err = cv.tutView.tut.Client.Client.PostStatus(context.Background(), &send) + } if err != nil { cv.tutView.ShowError( fmt.Sprintf("Couldn't post toot. Error: %v\n", err), @@ -466,6 +525,26 @@ func NewMediaList(tv *TutView) *MediaList { type UploadFile struct { Path string Description string + Remote bool + ID mastodon.ID +} + +func (m *MediaList) AddFromEdit(edit *mastodon.Status) { + m.Files = nil + m.list.Clear() + for i, ma := range edit.MediaAttachments { + m.Files = append(m.Files, UploadFile{ + Description: ma.Description, + Remote: true, + ID: ma.ID, + }) + m.list.AddItem(fmt.Sprintf("From edit: %d", i+1), "", 0, nil) + } + index := m.list.GetItemCount() + if index > 0 { + m.list.SetCurrentItem(index - 1) + } + m.Draw() } func (m *MediaList) Reset() { @@ -546,6 +625,12 @@ func (m *MediaList) EditDesc() { return } file := m.Files[index] + if file.Remote { + m.tutView.ShowError( + "Can't edit desc of a file that's already uploaded", + ) + return + } desc, err := OpenEditor(m.tutView, file.Description) if err != nil { m.tutView.ShowError( diff --git a/ui/input.go b/ui/input.go index 036b4cd..82655b6 100644 --- a/ui/input.go +++ b/ui/input.go @@ -119,6 +119,8 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey { tv.ClearNotificationsCommand() case config.LeaderCompose: tv.ComposeCommand() + case config.LeaderEdit: + tv.EditCommand() case config.LeaderBlocking: tv.BlockingCommand() case config.LeaderBookmarks, config.LeaderSaved: @@ -295,7 +297,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { return event } if tv.tut.Config.Input.MainCompose.Match(event.Key(), event.Rune()) { - tv.InitPost(nil) + tv.InitPost(nil, nil) return nil } switch item.Type() { @@ -405,6 +407,10 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas }) return nil } + if tv.tut.Config.Input.StatusEdit.Match(event.Key(), event.Rune()) { + tv.EditCommand() + return nil + } if tv.tut.Config.Input.StatusFavorite.Match(event.Key(), event.Rune()) { txt := "favorite" if favorited { @@ -443,7 +449,7 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas return nil } if tv.tut.Config.Input.StatusReply.Match(event.Key(), event.Rune()) { - tv.InitPost(status) + tv.InitPost(status, nil) return nil } if tv.tut.Config.Input.StatusBookmark.Match(event.Key(), event.Rune()) { diff --git a/ui/item_status.go b/ui/item_status.go index 1766aa5..da53af8 100644 --- a/ui/item_status.go +++ b/ui/item_status.go @@ -214,6 +214,9 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview info = append(info, NewControl(tv.tut.Config, tv.tut.Config.Input.StatusLinks, true)) } info = append(info, NewControl(tv.tut.Config, tv.tut.Config.Input.StatusAvatar, true)) + if status.Account.ID == tv.tut.Client.Me.ID && !isHistory { + info = append(info, NewControl(tv.tut.Config, tv.tut.Config.Input.StatusEdit, true)) + } if status.Account.ID == tv.tut.Client.Me.ID && !isHistory { info = append(info, NewControl(tv.tut.Config, tv.tut.Config.Input.StatusDelete, true)) } diff --git a/ui/pollview.go b/ui/pollview.go index 03cfe25..4ad3294 100644 --- a/ui/pollview.go +++ b/ui/pollview.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "time" "github.com/RasmusLindroth/go-mastodon" "github.com/gdamore/tcell/v2" @@ -101,6 +102,23 @@ func (p *PollView) Reset() { p.redrawInfo() } +func (p *PollView) AddPoll(np *mastodon.Poll) { + p.poll = &mastodon.TootPoll{ + Options: []string{}, + ExpiresInSeconds: durationsTime[durations[4]], + Multiple: false, + HideTotals: false, + } + for _, opt := range np.Options { + p.poll.Options = append(p.poll.Options, opt.Title) + p.list.AddItem(opt.Title, "", 0, nil) + } + p.poll.Multiple = np.Multiple + diff := time.Until(np.ExpiresAt) + p.poll.ExpiresInSeconds = int64(diff.Seconds()) + p.redrawInfo() +} + func (p *PollView) HasPoll() bool { return p.list.GetItemCount() > 1 } diff --git a/ui/view.go b/ui/view.go index 79e09b4..84ff3eb 100644 --- a/ui/view.go +++ b/ui/view.go @@ -165,9 +165,11 @@ func (tv *TutView) PrevFocus() { tv.PrevPageFocus = MainFocus } -func (tv *TutView) InitPost(status *mastodon.Status) { - tv.ComposeView.SetStatus(status) - tv.SetPage(ComposeFocus) +func (tv *TutView) InitPost(status *mastodon.Status, original *mastodon.Status) { + err := tv.ComposeView.SetStatus(status, original) + if err == nil { + tv.SetPage(ComposeFocus) + } } func (tv *TutView) ShowError(s string) {