diff --git a/README.md b/README.md index d4a3202..d6be3b9 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:h` `:help` view help * `:lists` show a list of your lists * `:muting` lists users that you have muted +* `:preferences` update your profile and some other settings * `:profile` go to your profile * `:requests` see following requests * `:saved` alias for bookmarks diff --git a/api/user.go b/api/user.go index 14a0b2e..a8b594b 100644 --- a/api/user.go +++ b/api/user.go @@ -79,3 +79,11 @@ func (ac *AccountClient) FollowRequestAccept(u *mastodon.Account) error { func (ac *AccountClient) FollowRequestDeny(u *mastodon.Account) error { return ac.Client.FollowRequestReject(context.Background(), u.ID) } + +func (ac *AccountClient) SavePreferences(p *mastodon.Profile) error { + acc, err := ac.Client.AccountUpdate(context.Background(), p) + if err == nil { + ac.Me = acc + } + return err +} diff --git a/config.example.ini b/config.example.ini index 6908137..ea630fb 100644 --- a/config.example.ini +++ b/config.example.ini @@ -137,7 +137,7 @@ leader-timeout=1000 # # Available commands: home, direct, local, federated, compose, blocking, # bookmarks, saved, favorited, boosts, favorites, following, followers, muting, -# profile, notifications, lists, tag, window +# preferences, profile, notifications, lists, tag, window # # 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 @@ -639,3 +639,35 @@ poll-multi-toggle="Toggle [M]ultiple",'m','M' # Change the expiration of poll # default="E[X]pires",'x','X' poll-expiration="E[X]pires",'x','X' + +# Change display name +# default="[N]ame",'n','N' +preference-name="[N]ame",'n','N' + +# Change default visibility of toots +# default="[V]isibility",'v','V' +preference-visibility="[V]isibility",'v','V' + +# Change bio in profile +# default="[B]io",'b','B' +preference-bio="[B]io",'b','B' + +# Save your preferences +# default="[S]ave",'s','S' +preference-save="[S]ave",'s','S' + +# Edit profile fields +# default="[F]ields",'f','F' +preference-fields="[F]ields",'f','F' + +# Add new field +# default="[A]dd",'a','A' +preference-fields-add="[A]dd",'a','A' + +# Edit current field +# default="[E]dit",'e','E' +preference-fields-edit="[E]dit",'e','E' + +# Delete current field +# default="[D]elete",'d','D' +preference-fields-delete="[D]elete",'d','D' diff --git a/config/config.go b/config/config.go index feaf378..bb80b69 100644 --- a/config/config.go +++ b/config/config.go @@ -65,6 +65,7 @@ const ( LeaderFollowing LeaderFollowers LeaderMuting + LeaderPreferences LeaderProfile LeaderNotifications LeaderLists @@ -375,6 +376,15 @@ type Input struct { PollDelete Key PollMultiToggle Key PollExpiration Key + + PreferenceName Key + PreferenceVisibility Key + PreferenceBio Key + PreferenceSave Key + PreferenceFields Key + PreferenceFieldsAdd Key + PreferenceFieldsEdit Key + PreferenceFieldsDelete Key } func parseColor(input string, def string, xrdb map[string]string) tcell.Color { @@ -660,6 +670,8 @@ func parseGeneral(cfg *ini.File) General { la.Command = LeaderFollowers case "muting": la.Command = LeaderMuting + case "preferences": + la.Command = LeaderPreferences case "profile": la.Command = LeaderProfile case "notifications": @@ -1050,6 +1062,15 @@ func parseInput(cfg *ini.File) Input { PollDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), PollMultiToggle: inputStrOrErr([]string{"\"Toggle [M]ultiple\"", "'m'", "'M'"}, false), PollExpiration: inputStrOrErr([]string{"\"E[X]pires\"", "'x'", "'X'"}, false), + + PreferenceName: inputStrOrErr([]string{"\"[N]ame\"", "'n'", "'N'"}, false), + PreferenceBio: inputStrOrErr([]string{"\"[B]io\"", "'b'", "'B'"}, false), + PreferenceVisibility: inputStrOrErr([]string{"\"[V]isibility\"", "'v'", "'V'"}, false), + PreferenceSave: inputStrOrErr([]string{"\"[S]ave\"", "'s'", "'S'"}, false), + PreferenceFields: inputStrOrErr([]string{"\"[F]ields\"", "'f'", "'F'"}, false), + PreferenceFieldsAdd: inputStrOrErr([]string{"\"[A]dd\"", "'a'", "'A'"}, false), + PreferenceFieldsEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false), + PreferenceFieldsDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), } ic.GlobalDown = inputOrErr(cfg, "global-down", false, ic.GlobalDown) ic.GlobalUp = inputOrErr(cfg, "global-up", false, ic.GlobalUp) @@ -1114,6 +1135,15 @@ func parseInput(cfg *ini.File) Input { ic.PollDelete = inputOrErr(cfg, "poll-delete", false, ic.PollDelete) ic.PollMultiToggle = inputOrErr(cfg, "poll-multi-toggle", false, ic.PollMultiToggle) ic.PollExpiration = inputOrErr(cfg, "poll-expiration", false, ic.PollExpiration) + + ic.PreferenceName = inputOrErr(cfg, "preference-name", false, ic.PreferenceName) + ic.PreferenceVisibility = inputOrErr(cfg, "preference-visibility", false, ic.PreferenceVisibility) + ic.PreferenceBio = inputOrErr(cfg, "preference-bio", false, ic.PreferenceBio) + ic.PreferenceSave = inputOrErr(cfg, "preference-save", false, ic.PreferenceSave) + ic.PreferenceFields = inputOrErr(cfg, "preference-fields", false, ic.PreferenceFields) + ic.PreferenceFieldsAdd = inputOrErr(cfg, "preference-fields-add", false, ic.PreferenceFieldsAdd) + ic.PreferenceFieldsEdit = inputOrErr(cfg, "preference-fields-edit", false, ic.PreferenceFieldsEdit) + ic.PreferenceFieldsDelete = inputOrErr(cfg, "preference-fields-delete", false, ic.PreferenceFieldsDelete) return ic } diff --git a/config/default_config.go b/config/default_config.go index 57d2507..41dc0b7 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -139,7 +139,7 @@ leader-timeout=1000 # # Available commands: home, direct, local, federated, compose, blocking, # bookmarks, saved, favorited, boosts, favorites, following, followers, muting, -# profile, notifications, lists, tag, window +# preferences, profile, notifications, lists, tag, window # # 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 @@ -641,4 +641,36 @@ poll-multi-toggle="Toggle [M]ultiple",'m','M' # Change the expiration of poll # default="E[X]pires",'x','X' poll-expiration="E[X]pires",'x','X' + +# Change display name +# default="[N]ame",'n','N' +preference-name="[N]ame",'n','N' + +# Change default visibility of toots +# default="[V]isibility",'v','V' +preference-visibility="[V]isibility",'v','V' + +# Change bio in profile +# default="[B]io",'b','B' +preference-bio="[B]io",'b','B' + +# Save your preferences +# default="[S]ave",'s','S' +preference-save="[S]ave",'s','S' + +# Edit profile fields +# default="[F]ields",'f','F' +preference-fields="[F]ields",'f','F' + +# Add new field +# default="[A]dd",'a','A' +preference-fields-add="[A]dd",'a','A' + +# Edit current field +# default="[E]dit",'e','E' +preference-fields-edit="[E]dit",'e','E' + +# Delete current field +# default="[D]elete",'d','D' +preference-fields-delete="[D]elete",'d','D' ` diff --git a/go.mod b/go.mod index 8e1286d..9b4d6af 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/RasmusLindroth/tut go 1.17 require ( - github.com/RasmusLindroth/go-mastodon v0.0.6 + github.com/RasmusLindroth/go-mastodon v0.0.7 github.com/atotto/clipboard v0.1.4 github.com/gdamore/tcell/v2 v2.5.1 github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a diff --git a/go.sum b/go.sum index 83ec885..7d8b335 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/RasmusLindroth/go-mastodon v0.0.6 h1:dhVXungiJeRKIE2ZRUJrPibBJ7YkeM4eVyeg3+q6Juk= -github.com/RasmusLindroth/go-mastodon v0.0.6/go.mod h1:4L0oyiNwq1tUoiByczzhSikxR9RiANzELtZgexxKpPM= +github.com/RasmusLindroth/go-mastodon v0.0.7 h1:iGgkkvDrPHTiAyACUehLH5zragSHCUSbhcYdQEBIn48= +github.com/RasmusLindroth/go-mastodon v0.0.7/go.mod h1:4L0oyiNwq1tUoiByczzhSikxR9RiANzELtZgexxKpPM= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= diff --git a/main.go b/main.go index 3759eae..cfbfd65 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.8" +const version = "1.0.9" func main() { util.MakeDirs() diff --git a/ui/cmdbar.go b/ui/cmdbar.go index ccb6545..7f1ae29 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -100,6 +100,10 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { case ":profile": c.tutView.ProfileCommand() c.Back() + case ":preferences": + c.tutView.PreferencesCommand() + c.ClearInput() + c.View.Autocomplete() case ":timeline", ":tl": if len(parts) < 2 { break @@ -166,7 +170,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,:requests,:saved,:tag,:timeline,:tl,:user,:window,:quit,:q", ",") + words := strings.Split(":blocking,:boosts,:bookmarks,:compose,:favorites,:favorited,:followers,:following,:help,:h,:lists,:muting,:preferences,:profile,:requests,:saved,:tag,:timeline,:tl,:user,:window,:quit,:q", ",") if curr == "" { return entries } diff --git a/ui/commands.go b/ui/commands.go index df7a3b0..230ef89 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -163,3 +163,7 @@ func (tv *TutView) ProfileCommand() { NewUserFeed(tv, item), ) } + +func (tv *TutView) PreferencesCommand() { + tv.SetPage(PreferenceFocus) +} diff --git a/ui/composeview.go b/ui/composeview.go index b19921c..4a3d19e 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -40,7 +40,18 @@ type ComposeView struct { msg *msgToot } -var visibilities = []string{mastodon.VisibilityPublic, mastodon.VisibilityUnlisted, mastodon.VisibilityFollowersOnly, mastodon.VisibilityDirectMessage} +var visibilities = map[string]int{ + mastodon.VisibilityPublic: 0, + mastodon.VisibilityUnlisted: 1, + mastodon.VisibilityFollowersOnly: 2, + mastodon.VisibilityDirectMessage: 3, +} +var visibilitiesStr = []string{ + mastodon.VisibilityPublic, + mastodon.VisibilityUnlisted, + mastodon.VisibilityFollowersOnly, + mastodon.VisibilityDirectMessage, +} func NewComposeView(tv *TutView) *ComposeView { cv := &ComposeView{ @@ -122,6 +133,11 @@ func (cv *ComposeView) SetStatus(status *mastodon.Status) { cv.tutView.PollView.Reset() cv.media.Reset() msg := &msgToot{} + me := cv.tutView.tut.Client.Me + visibility := mastodon.VisibilityPublic + if me.Source != nil && me.Source.Privacy != nil { + visibility = *me.Source.Privacy + } if status != nil { if status.Reblog != nil { status = status.Reblog @@ -131,8 +147,11 @@ func (cv *ComposeView) SetStatus(status *mastodon.Status) { msg.Sensitive = true msg.SpoilerText = status.SpoilerText } - msg.Visibility = status.Visibility + if visibilities[status.Visibility] > visibilities[visibility] { + visibility = status.Visibility + } } + msg.Visibility = visibility cv.msg = msg cv.msg.Text = cv.getAccs() if cv.tutView.tut.Config.General.QuoteReply { @@ -140,13 +159,13 @@ func (cv *ComposeView) SetStatus(status *mastodon.Status) { } cv.visibility.SetLabel("Visibility: ") index := 0 - for i, v := range visibilities { + for i, v := range visibilitiesStr { if msg.Visibility == v { index = i break } } - cv.visibility.SetOptions(visibilities, cv.visibilitySelected) + cv.visibility.SetOptions(visibilitiesStr, cv.visibilitySelected) cv.visibility.SetCurrentOption(index) cv.visibility.SetInputCapture(cv.visibilityInput) cv.UpdateContent() diff --git a/ui/input.go b/ui/input.go index b75ef8a..d736fa7 100644 --- a/ui/input.go +++ b/ui/input.go @@ -50,6 +50,8 @@ func (tv *TutView) Input(event *tcell.EventKey) *tcell.EventKey { return tv.InputVote(event) case HelpFocus: return tv.InputHelp(event) + case PreferenceFocus: + return tv.InputPreference(event) default: return event } @@ -122,6 +124,8 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey { tv.FollowersCommand() case config.LeaderMuting: tv.MutingCommand() + case config.LeaderPreferences: + tv.PreferencesCommand() case config.LeaderProfile: tv.ProfileCommand() case config.LeaderNotifications: @@ -258,7 +262,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { } switch item.Type() { case api.StatusType: - return tv.InputStatus(event, item, item.Raw().(*mastodon.Status)) + return tv.InputStatus(event, item, item.Raw().(*mastodon.Status), nil) case api.UserType, api.ProfileType: if ft == feed.FollowRequests { return tv.InputUser(event, item.Raw().(*api.User), true) @@ -271,15 +275,17 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { case "follow": return tv.InputUser(event, nd.User.Raw().(*api.User), false) case "favourite": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) + user := nd.User.Raw().(*api.User) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data) case "reblog": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) + user := nd.User.Raw().(*api.User) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data) case "mention": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) case "status": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) case "poll": - return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status)) + return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil) case "follow_request": return tv.InputUser(event, nd.User.Raw().(*api.User), true) } @@ -290,7 +296,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { return event } -func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mastodon.Status) *tcell.EventKey { +func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mastodon.Status, nAcc *mastodon.Account) *tcell.EventKey { sr := util.StatusOrReblog(status) hasMedia := len(sr.MediaAttachments) > 0 @@ -303,7 +309,11 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas bookmarked := sr.Bookmarked if tv.tut.Config.Input.StatusAvatar.Match(event.Key(), event.Rune()) { - openAvatar(tv, sr.Account) + if nAcc != nil { + openAvatar(tv, *nAcc) + } else { + openAvatar(tv, sr.Account) + } return nil } if tv.tut.Config.Input.StatusBoost.Match(event.Key(), event.Rune()) { @@ -413,7 +423,11 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas return nil } if tv.tut.Config.Input.StatusUser.Match(event.Key(), event.Rune()) { - user, err := tv.tut.Client.GetUserByID(status.Account.ID) + id := status.Account.ID + if nAcc != nil { + id = nAcc.ID + } + user, err := tv.tut.Client.GetUserByID(id) if err != nil { return nil } @@ -774,6 +788,69 @@ func (tv *TutView) InputVote(event *tcell.EventKey) *tcell.EventKey { return event } +func (tv *TutView) InputPreference(event *tcell.EventKey) *tcell.EventKey { + if tv.PreferenceView.HasFieldFocus() { + return tv.InputPreferenceFields(event) + } + if tv.tut.Config.Input.PreferenceFields.Match(event.Key(), event.Rune()) { + tv.PreferenceView.FieldFocus() + return nil + } + if tv.tut.Config.Input.PreferenceName.Match(event.Key(), event.Rune()) { + tv.PreferenceView.EditDisplayname() + return nil + } + if tv.tut.Config.Input.PreferenceVisibility.Match(event.Key(), event.Rune()) { + tv.PreferenceView.FocusVisibility() + return nil + } + if tv.tut.Config.Input.PreferenceBio.Match(event.Key(), event.Rune()) { + tv.PreferenceView.EditBio() + return nil + } + if tv.tut.Config.Input.PreferenceSave.Match(event.Key(), event.Rune()) { + tv.PreferenceView.Save() + return nil + } + if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) || + tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) { + tv.ModalView.Run( + "Do you want exit the preference view?", func() { + tv.FocusMainNoHistory() + }) + return nil + } + return event +} +func (tv *TutView) InputPreferenceFields(event *tcell.EventKey) *tcell.EventKey { + if tv.tut.Config.Input.GlobalUp.Match(event.Key(), event.Rune()) { + tv.PreferenceView.PrevField() + return nil + } + if tv.tut.Config.Input.GlobalDown.Match(event.Key(), event.Rune()) { + tv.PreferenceView.NextField() + return nil + } + if tv.tut.Config.Input.PreferenceFieldsAdd.Match(event.Key(), event.Rune()) { + tv.PreferenceView.AddField() + return nil + } + if tv.tut.Config.Input.PreferenceFieldsEdit.Match(event.Key(), event.Rune()) { + tv.PreferenceView.EditField() + return nil + } + if tv.tut.Config.Input.PreferenceFieldsDelete.Match(event.Key(), event.Rune()) { + tv.PreferenceView.DeleteField() + return nil + } + if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) || + tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) { + tv.PreferenceView.MainFocus() + return nil + } + return event +} + func (tv *TutView) InputCmdView(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyEnter: diff --git a/ui/open.go b/ui/open.go index bcf965a..7c25e1c 100644 --- a/ui/open.go +++ b/ui/open.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "strings" + "unicode/utf8" ) func openURL(tv *TutView, url string) { @@ -51,6 +52,30 @@ func openCustom(tv *TutView, program string, args []string, terminal bool, url s } } +func OpenEditorLengthLimit(tv *TutView, s string, limit int) (string, bool, error) { + text, err := OpenEditor(tv, s) + if err != nil { + return text, false, err + } + s = strings.TrimSpace(text) + if len(s) == 0 { + return "", false, nil + } + if utf8.RuneCountInString(s) > limit { + ns := "" + i := 0 + for _, r := range s { + if i >= limit { + break + } + ns += string(r) + i++ + } + s = ns + } + return s, true, nil +} + func OpenEditor(tv *TutView, content string) (string, error) { editor, exists := os.LookupEnv("EDITOR") if !exists || editor == "" { diff --git a/ui/pollview.go b/ui/pollview.go index fad7f10..3d9ab78 100644 --- a/ui/pollview.go +++ b/ui/pollview.go @@ -3,7 +3,6 @@ package ui import ( "fmt" "strings" - "unicode/utf8" "github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/tut/config" @@ -130,39 +129,18 @@ func (p *PollView) Next() { } } -func checkOption(s string) (string, bool) { - s = strings.TrimSpace(s) - if len(s) == 0 { - return "", false - } - if utf8.RuneCountInString(s) > 25 { - ns := "" - i := 0 - for _, r := range s { - if i >= 25 { - break - } - ns += string(r) - i++ - } - s = ns - } - return s, true -} - func (p *PollView) Add() { if p.list.GetItemCount() > 3 { p.tutView.ShowError("You can only have a maximum of 4 options.") return } - text, err := OpenEditor(p.tutView, "") + text, valid, err := OpenEditorLengthLimit(p.tutView, "", 25) if err != nil { p.tutView.ShowError( fmt.Sprintf("Couldn't open editor. Error: %v", err), ) return } - text, valid := checkOption(text) if !valid { return } @@ -177,14 +155,13 @@ func (p *PollView) Edit() { return } text, _ := p.list.GetItemText(p.list.GetCurrentItem()) - text, err := OpenEditor(p.tutView, text) + text, valid, err := OpenEditorLengthLimit(p.tutView, text, 25) if err != nil { p.tutView.ShowError( fmt.Sprintf("Couldn't open editor. Error: %v", err), ) return } - text, valid := checkOption(text) if !valid { return } diff --git a/ui/preferenceview.go b/ui/preferenceview.go new file mode 100644 index 0000000..76e4ee8 --- /dev/null +++ b/ui/preferenceview.go @@ -0,0 +1,356 @@ +package ui + +import ( + "fmt" + "strings" + + "github.com/RasmusLindroth/go-mastodon" + "github.com/RasmusLindroth/tut/config" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +var visibilitiesPrefStr = []string{ + mastodon.VisibilityPublic, + mastodon.VisibilityUnlisted, + mastodon.VisibilityFollowersOnly, +} + +type preferences struct { + displayname string + bio string + fields []mastodon.Field + visibility string +} + +type PreferenceView struct { + tutView *TutView + shared *Shared + View *tview.Flex + displayName *tview.TextView + bio *tview.TextView + fields *tview.List + visibility *tview.DropDown + controls *tview.TextView + preferences *preferences + fieldFocus bool +} + +func NewPreferenceView(tv *TutView) *PreferenceView { + p := &PreferenceView{ + tutView: tv, + shared: tv.Shared, + displayName: NewTextView(tv.tut.Config), + bio: NewTextView(tv.tut.Config), + fields: NewList(tv.tut.Config), + visibility: NewDropDown(tv.tut.Config), + controls: NewTextView(tv.tut.Config), + preferences: &preferences{}, + } + p.View = preferenceViewUI(p) + p.MainFocus() + p.Update() + + return p +} + +func preferenceViewUI(p *PreferenceView) *tview.Flex { + p.visibility.SetLabel("Default toot visibility: ") + p.visibility.SetOptions(visibilitiesPrefStr, p.visibilitySelected) + + return tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(p.shared.Top.View, 1, 0, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexColumn). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(p.displayName, 1, 0, false). + AddItem(p.visibility, 2, 0, false). + AddItem(p.fields, 0, 1, false), 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(p.bio, 0, 1, false), 0, 1, false), 0, 1, false). + AddItem(p.controls, 1, 0, false). + AddItem(p.shared.Bottom.View, 2, 0, false) +} + +func (p *PreferenceView) Update() { + pf := &preferences{} + me := p.tutView.tut.Client.Me + pf.displayname = me.DisplayName + if me.Source != nil { + if me.Source.Note != nil { + pf.bio = *me.Source.Note + } + if me.Source.Fields != nil { + for _, f := range *me.Source.Fields { + pf.fields = append(pf.fields, mastodon.Field{ + Name: f.Name, + Value: f.Value, + }) + } + } + if me.Source.Privacy != nil { + pf.visibility = *me.Source.Privacy + } + } + p.preferences = pf + p.update() +} + +func (p *PreferenceView) update() { + pf := p.preferences + p.displayName.SetText(fmt.Sprintf("Display name: %s", tview.Escape(pf.displayname))) + p.bio.SetText(fmt.Sprintf("Bio:\n%s", tview.Escape(pf.bio))) + + p.fields.Clear() + for _, f := range pf.fields { + p.fields.AddItem(fmt.Sprintf("%s: %s\n", tview.Escape(f.Name), tview.Escape(f.Value)), "", 0, nil) + } + + index := 0 + for i, v := range visibilitiesPrefStr { + if pf.visibility == v { + index = i + break + } + } + p.visibility.SetCurrentOption(index) +} + +func (p *PreferenceView) HasFieldFocus() bool { + return p.fieldFocus +} + +func (p *PreferenceView) FieldFocus() { + p.fieldFocus = true + + var items []string + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceFieldsAdd, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceFieldsEdit, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceFieldsDelete, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.GlobalBack, true)) + p.controls.SetText(strings.Join(items, " ")) + + cnf := p.tutView.tut.Config + p.fields.SetSelectedBackgroundColor(cnf.Style.ListSelectedBackground) + p.fields.SetSelectedTextColor(cnf.Style.ListSelectedText) +} + +func (p *PreferenceView) MainFocus() { + p.fieldFocus = false + + var items []string + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceName, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceVisibility, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceBio, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceFields, true)) + items = append(items, config.ColorFromKey(p.tutView.tut.Config, p.tutView.tut.Config.Input.PreferenceSave, true)) + p.controls.SetText(strings.Join(items, " ")) + + cnf := p.tutView.tut.Config + p.fields.SetSelectedBackgroundColor(cnf.Style.Background) + p.fields.SetSelectedTextColor(cnf.Style.Text) +} + +func (p *PreferenceView) PrevField() { + index := p.fields.GetCurrentItem() + if index-1 >= 0 { + p.fields.SetCurrentItem(index - 1) + } +} + +func (p *PreferenceView) NextField() { + index := p.fields.GetCurrentItem() + if index+1 < p.fields.GetItemCount() { + p.fields.SetCurrentItem(index + 1) + } +} + +func (p *PreferenceView) AddField() { + if p.fields.GetItemCount() > 3 { + p.tutView.ShowError("You can have a maximum of four fields.") + return + } + name, valid, err := OpenEditorLengthLimit(p.tutView, "name", 255) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't add name. Error: %v\n", err), + ) + return + } + if !valid { + p.tutView.ShowError("Name can't be empty.") + return + } + value, valid, err := OpenEditorLengthLimit(p.tutView, "value", 255) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't add value. Error: %v\n", err), + ) + return + } + if !valid { + p.tutView.ShowError("Value can't be empty.") + return + } + field := mastodon.Field{ + Name: name, + Value: value, + } + p.preferences.fields = append(p.preferences.fields, field) + p.update() + p.fields.SetCurrentItem(p.fields.GetItemCount() - 1) +} + +func (p *PreferenceView) EditField() { + if p.fields.GetItemCount() == 0 { + return + } + index := p.fields.GetCurrentItem() + if index < 0 || index >= len(p.preferences.fields) { + return + } + curr := p.preferences.fields[index] + name, valid, err := OpenEditorLengthLimit(p.tutView, curr.Name, 255) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't edit name. Error: %v\n", err), + ) + return + } + if !valid { + p.tutView.ShowError("Name can't be empty.") + return + } + value, valid, err := OpenEditorLengthLimit(p.tutView, curr.Value, 255) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't edit value. Error: %v\n", err), + ) + return + } + if !valid { + p.tutView.ShowError("Value can't be empty.") + return + } + field := mastodon.Field{ + Name: name, + Value: value, + } + p.preferences.fields[index] = field + p.update() +} + +func (p *PreferenceView) DeleteField() { + if p.fields.GetItemCount() == 0 { + return + } + index := p.fields.GetCurrentItem() + if index < 0 || index >= len(p.preferences.fields) { + return + } + p.fields.RemoveItem(index) + p.preferences.fields = append(p.preferences.fields[:index], p.preferences.fields[index+1:]...) + p.update() +} + +func (p *PreferenceView) EditBio() { + bio := p.preferences.bio + text, _, err := OpenEditorLengthLimit(p.tutView, bio, 500) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't edit bio. Error: %v\n", err), + ) + return + } + p.preferences.bio = text + p.update() +} + +func (p *PreferenceView) EditDisplayname() { + dn := p.preferences.displayname + text, _, err := OpenEditorLengthLimit(p.tutView, dn, 30) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't edit display name. Error: %v\n", err), + ) + return + } + p.preferences.displayname = text + p.update() +} + +func (p *PreferenceView) visibilityInput(event *tcell.EventKey) *tcell.EventKey { + if p.tutView.tut.Config.Input.GlobalDown.Match(event.Key(), event.Rune()) { + return tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone) + } + if p.tutView.tut.Config.Input.GlobalUp.Match(event.Key(), event.Rune()) { + return tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone) + } + if p.tutView.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) || + p.tutView.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) { + p.exitVisibility() + return nil + } + return event +} + +func (p *PreferenceView) exitVisibility() { + p.tutView.tut.App.SetInputCapture(p.tutView.Input) + p.tutView.tut.App.SetFocus(p.tutView.View) +} + +func (p *PreferenceView) visibilitySelected(s string, index int) { + _, p.preferences.visibility = p.visibility.GetCurrentOption() + p.exitVisibility() +} + +func (p *PreferenceView) FocusVisibility() { + p.tutView.tut.App.SetInputCapture(p.visibilityInput) + p.tutView.tut.App.SetFocus(p.visibility) + ev := tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone) + p.tutView.tut.App.QueueEvent(ev) +} + +func (p *PreferenceView) Save() { + og := &preferences{} + me := p.tutView.tut.Client.Me + og.displayname = me.DisplayName + if me.Source != nil { + if me.Source.Note != nil { + og.bio = *me.Source.Note + } + if me.Source.Fields != nil { + for _, f := range *me.Source.Fields { + og.fields = append(og.fields, mastodon.Field{ + Name: f.Name, + Value: f.Value, + }) + } + } + if me.Source.Privacy != nil { + og.visibility = *me.Source.Privacy + } + } + + profile := mastodon.Profile{ + Source: &mastodon.AccountSource{}, + } + if og.displayname != p.preferences.displayname { + profile.DisplayName = &p.preferences.displayname + } + if og.bio != p.preferences.bio { + profile.Note = &p.preferences.bio + } + if og.visibility != p.preferences.visibility { + profile.Source.Privacy = &p.preferences.visibility + } + profile.Fields = &p.preferences.fields + + err := p.tutView.tut.Client.SavePreferences(&profile) + if err != nil { + p.tutView.ShowError( + fmt.Sprintf("Couldn't update preferences. Error: %v\n", err), + ) + return + } + p.tutView.SetPage(MainFocus) +} diff --git a/ui/statusbar.go b/ui/statusbar.go index d083c6a..d266380 100644 --- a/ui/statusbar.go +++ b/ui/statusbar.go @@ -31,6 +31,7 @@ const ( UserMode VoteMode PollMode + PreferenceMode ) func (sb *StatusBar) SetMode(m ViewMode) { @@ -61,5 +62,7 @@ func (sb *StatusBar) SetMode(m ViewMode) { sb.View.SetText("-- SELECT USER --") case PollMode: sb.View.SetText("-- CREATE POLL --") + case PreferenceMode: + sb.View.SetText("-- PREFERENCES --") } } diff --git a/ui/tutview.go b/ui/tutview.go index 92c5d2c..cd9ac38 100644 --- a/ui/tutview.go +++ b/ui/tutview.go @@ -45,14 +45,15 @@ type TutView struct { Shared *Shared View *tview.Pages - LoginView *LoginView - MainView *MainView - LinkView *LinkView - ComposeView *ComposeView - VoteView *VoteView - PollView *PollView - HelpView *HelpView - ModalView *ModalView + LoginView *LoginView + MainView *MainView + LinkView *LinkView + ComposeView *ComposeView + VoteView *VoteView + PollView *PollView + PreferenceView *PreferenceView + HelpView *HelpView + ModalView *ModalView FileList []string } @@ -163,6 +164,7 @@ func (tv *TutView) loggedIn(acc auth.Account) { tv.ComposeView = NewComposeView(tv) tv.VoteView = NewVoteView(tv) tv.PollView = NewPollView(tv) + tv.PreferenceView = NewPreferenceView(tv) tv.HelpView = NewHelpView(tv) tv.ModalView = NewModalView(tv) @@ -172,6 +174,7 @@ func (tv *TutView) loggedIn(acc auth.Account) { tv.View.AddPage("vote", tv.VoteView.View, true, false) tv.View.AddPage("help", tv.HelpView.View, true, false) tv.View.AddPage("poll", tv.PollView.View, true, false) + tv.View.AddPage("preference", tv.PreferenceView.View, true, false) tv.View.AddPage("modal", tv.ModalView.View, true, false) tv.SetPage(MainFocus) } diff --git a/ui/view.go b/ui/view.go index 622ae33..d4e6ebf 100644 --- a/ui/view.go +++ b/ui/view.go @@ -20,6 +20,7 @@ const ( VoteFocus HelpFocus PollFocus + PreferenceFocus ) func (tv *TutView) GetCurrentFeed() *Feed { @@ -143,7 +144,13 @@ func (tv *TutView) SetPage(f PageFocusAt) { tv.tut.App.SetFocus(tv.View) tv.Shared.Bottom.StatusBar.SetMode(PollMode) tv.Shared.Top.SetText("create a poll") - + case PreferenceFocus: + tv.PageFocus = PreferenceFocus + tv.PreferenceView.Update() + tv.View.SwitchToPage("preference") + tv.tut.App.SetFocus(tv.View) + tv.Shared.Bottom.StatusBar.SetMode(PreferenceMode) + tv.Shared.Top.SetText("preferences") } tv.ShouldSync() }