From: Simon Tatham Date: Sat, 13 Jan 2024 10:57:21 +0000 (+0000) Subject: Ability to set most of your own options. X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=3938494e5d17c46f14cb360859b9c6f780a0a6c7;p=mastodonochrome.git Ability to set most of your own options. For the moment, I've left out the account bio and the extra info fields, because those both require UI extensions we don't have yet. Added to TODO. --- diff --git a/TODO.md b/TODO.md index a1b45e4..bb2ceea 100644 --- a/TODO.md +++ b/TODO.md @@ -187,23 +187,26 @@ the second L keypress goes to the actual log files. ## More options in the Examine → Options menus -### Your own options: ESC Y O +### Your own options: \[ESC\]\[Y\]\[O\] -If the user is you, then this menu sets client configuration. For -example, the default language for your posts (if you want to override -the one we get from the system locale). Maybe also UI options in -future if we add any. +Still to do, and harder UI-wise than the rest of the user options: -It should also let you [set server-side -options](https://docs.joinmastodon.org/methods/accounts/#update_credentials) -about your account, such as your display name, flags like `bot`, and -info fields. +* setting the account's bio: involves spawning a full-screen editor + activity +* setting the account's "fields" (key-value pairs): involves managing + a variable-length set of pairs, and either dynamically choosing menu + keystrokes for them all, or having a new kind of activity consisting + of a sequence of single-line editors that you can cursor up and down + to select one to edit. + +Maybe also client-side UI options in future, if we add any. ### Options for other users -I think we've got all the sensible server-side options about other -users (except _maybe_ getting a notification when they post, mentioned -separately below). +I think we've got all the usual server-side options about other users. +Getting a notification when they post is mentioned separately below. +Perhaps the only missing one is forcibly stopping a user from +following _you_. But I wonder if it might be worth having client-side options for users. For example, perhaps the client could usefully keep a list of @@ -227,11 +230,10 @@ in some useful part of the API? ### Locked accounts You can lock your account so that people can only follow you if you -give permission. We should support that: +give permission. We already support actually _doing_ this -- it's one +of the options in the \[ESC\]\[Y\]\[O\] options menu. But there's more +to do to make sure this works well: -* support [marking your own account as - locked](https://docs.joinmastodon.org/methods/accounts/#update_credentials) - via the ESC Y O options menu * put follow requests into some kind of notifications feed that causes you to hear about them * allow you to [get a list of your current pending @@ -303,6 +305,18 @@ announcements from the instance. We should probably pay attention to that. On client startup, check it for unread ones, and display those in the style of real Monochrome's MOTD. +# Future directions + +## UI configuration options + +Some of the keypaths to features of this client are very strange if +you're _not_ already used to the Mono UI. Perhaps there should be an +alternative set of keypaths that make more sense. This may need to be +a configurable option (or several). + +Not everyone will want their notifications feed broken up the way we +do it here. That should be an option as well, in some fashion. + ## Archive support The Mastodon web UI lets you download an archive of all your own posts diff --git a/src/client.rs b/src/client.rs index ee4e61b..4568a9a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -121,6 +121,7 @@ pub enum ClientError { InternalError(String), // message UrlParseError(String, String), // url, message UrlError(String, String), // url, message + NoAccountSource, } impl super::TopLevelErrorCandidate for ClientError {} @@ -152,6 +153,8 @@ impl std::fmt::Display for ClientError { write!(f, "Parse failure {} (retrieving URL: {})", msg, url), ClientError::UrlError(ref url, ref msg) => write!(f, "{} (retrieving URL: {})", msg, url), + ClientError::NoAccountSource => + write!(f, "server did not send 'source' details for our account"), } } } diff --git a/src/options.rs b/src/options.rs index bf37ad0..08d187f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,20 +1,41 @@ -use super::client::{Client, ClientError, Boosts, Followness, AccountFlag}; +use super::client::{ + Client, ClientError, Boosts, Followness, AccountFlag, AccountDetails +}; use super::coloured_string::ColouredString; use super::tui::{ ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, }; use super::text::*; +use super::types::Visibility; use super::editor::{EditableMenuLine, EditableMenuLineData}; struct YourOptionsMenu { title: FileHeader, - coming_soon: CentredInfoLine, normal_status: FileStatusLineFinal, edit_status: FileStatusLineFinal, + el_display_name: EditableMenuLine, // N + cl_default_vis: CyclingMenuLine, + cl_default_sensitive: CyclingMenuLine, + el_default_language: EditableMenuLine>, + + cl_locked: CyclingMenuLine, + cl_bot: CyclingMenuLine, + cl_discoverable: CyclingMenuLine, + cl_hide_collections: CyclingMenuLine, + cl_indexable: CyclingMenuLine, + + // fields (harder because potentially open-ended number of them) + // note (bio) (harder because flip to an editor) } impl YourOptionsMenu { - fn new() -> Self { + fn new(client: &mut Client) -> Result { + let ac = client.account_by_id(&client.our_account_id())?; + let source = match ac.source { + Some(source) => source, + None => return Err(ClientError::NoAccountSource), + }; + let title = FileHeader::new(ColouredString::general( "Your user options [ESC][Y][O]", "HHHHHHHHHHHHHHHHHHHKKKHHKHHKH")); @@ -24,29 +45,121 @@ impl YourOptionsMenu { let edit_status = FileStatusLine::new() .message("Edit line and press Return").finalise(); - let coming_soon = CentredInfoLine::new( - ColouredString::uniform("Not yet implemented", '!')); + let el_display_name = EditableMenuLine::new( + Pr('N'), ColouredString::plain("Display name: "), + ac.display_name.clone()); + let cl_default_vis = CyclingMenuLine::new( + Pr('V'), ColouredString::plain("Default post visibility: "), + &Visibility::long_descriptions(), source.privacy); + let el_default_language = EditableMenuLine::new( + Pr('L'), ColouredString::plain("Default language: "), + source.language.clone()); + let cl_default_sensitive = CyclingMenuLine::new( + Pr('S'), + ColouredString::plain("Posts marked sensitive by default: "), + &[(false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r'))], source.sensitive); + let cl_locked = CyclingMenuLine::new( + Ctrl('K'), + ColouredString::plain("Locked (you must approve followers): "), + &[(false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r'))], ac.locked); + let cl_hide_collections = CyclingMenuLine::new( + Ctrl('F'), + ColouredString::plain("Hide your lists of followers and followed users: "), + &[(false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r'))], + source.hide_collections == Some(true)); + let cl_discoverable = CyclingMenuLine::new( + Ctrl('D'), + ColouredString::plain("Discoverable (listed in profile directory): "), + &[(false, ColouredString::uniform("no", 'r')), + (true, ColouredString::uniform("yes", 'f'))], + source.discoverable == Some(true)); + let cl_indexable = CyclingMenuLine::new( + Ctrl('X'), + ColouredString::plain("Indexable (people can search for your posts): "), + &[(false, ColouredString::uniform("no", 'r')), + (true, ColouredString::uniform("yes", 'f'))], + source.indexable == Some(true)); + let cl_bot = CyclingMenuLine::new( + Ctrl('B'), + ColouredString::plain("Bot (account identifies as automated): "), + &[(false, ColouredString::uniform("no", 'f')), + (true, ColouredString::uniform("yes", 'H'))], ac.bot); let mut menu = YourOptionsMenu { title, - coming_soon, normal_status, edit_status, + el_display_name, + cl_default_vis, + cl_default_sensitive, + el_default_language, + cl_locked, + cl_bot, + cl_discoverable, + cl_hide_collections, + cl_indexable, }; menu.fix_widths(); - menu + Ok(menu) } fn fix_widths(&mut self) -> (usize, usize) { - let /* mut */ lmaxwid = 0; - let /* mut */ rmaxwid = 0; - // self.cl_WHATEVER.check_widths(&mut lmaxwid, &mut rmaxwid); + let mut lmaxwid = 0; + let mut rmaxwid = 0; + self.el_display_name.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_default_vis.check_widths(&mut lmaxwid, &mut rmaxwid); + self.el_default_language.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_default_sensitive.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_locked.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_bot.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_discoverable.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_hide_collections.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_indexable.check_widths(&mut lmaxwid, &mut rmaxwid); + + self.el_display_name.reset_widths(); + self.cl_default_vis.reset_widths(); + self.el_default_language.reset_widths(); + self.cl_default_sensitive.reset_widths(); + self.cl_locked.reset_widths(); + self.cl_bot.reset_widths(); + self.cl_discoverable.reset_widths(); + self.cl_hide_collections.reset_widths(); + self.cl_indexable.reset_widths(); + + self.el_display_name.ensure_widths(lmaxwid, rmaxwid); + self.cl_default_vis.ensure_widths(lmaxwid, rmaxwid); + self.el_default_language.ensure_widths(lmaxwid, rmaxwid); + self.cl_default_sensitive.ensure_widths(lmaxwid, rmaxwid); + self.cl_locked.ensure_widths(lmaxwid, rmaxwid); + self.cl_bot.ensure_widths(lmaxwid, rmaxwid); + self.cl_discoverable.ensure_widths(lmaxwid, rmaxwid); + self.cl_hide_collections.ensure_widths(lmaxwid, rmaxwid); + self.cl_indexable.ensure_widths(lmaxwid, rmaxwid); + + (lmaxwid, rmaxwid) + } - // self.cl_WHATEVER.reset_widths(); - // self.cl_WHATEVER.ensure_widths(lmaxwid, rmaxwid); + fn submit(&self, client: &mut Client) -> LogicalAction { + let details = AccountDetails { + display_name: self.el_display_name.get_data().clone(), + default_visibility: self.cl_default_vis.get_value(), + default_sensitive: self.cl_default_sensitive.get_value(), + default_language: self.el_default_language.get_data().clone(), + locked: self.cl_locked.get_value(), + bot: self.cl_bot.get_value(), + discoverable: self.cl_discoverable.get_value(), + hide_collections: self.cl_hide_collections.get_value(), + indexable: self.cl_indexable.get_value(), + }; - (lmaxwid, rmaxwid) + match client.set_account_details(&client.our_account_id(), details) { + Ok(..) => LogicalAction::Pop, + Err(..) => LogicalAction::Beep, // FIXME: report the error! + } } } @@ -54,17 +167,30 @@ impl ActivityState for YourOptionsMenu { fn draw(&self, w: usize, h: usize) -> (Vec, CursorPosition) { let mut lines = Vec::new(); - let /* mut */ cursorpos = CursorPosition::End; + let mut cursorpos = CursorPosition::End; lines.extend_from_slice(&self.title.render(w)); lines.extend_from_slice(&BlankLine::render_static()); - lines.extend_from_slice(&self.coming_soon.render(w)); - // FIXME menu items + lines.push(self.el_display_name.render( + w, &mut cursorpos, lines.len())); + lines.extend_from_slice(&BlankLine::render_static()); + lines.extend_from_slice(&self.cl_default_vis.render(w)); + lines.push(self.el_default_language.render( + w, &mut cursorpos, lines.len())); + lines.extend_from_slice(&self.cl_default_sensitive.render(w)); + lines.extend_from_slice(&BlankLine::render_static()); + lines.extend_from_slice(&self.cl_locked.render(w)); + lines.extend_from_slice(&self.cl_hide_collections.render(w)); + lines.extend_from_slice(&self.cl_discoverable.render(w)); + lines.extend_from_slice(&self.cl_indexable.render(w)); + lines.extend_from_slice(&BlankLine::render_static()); + lines.extend_from_slice(&self.cl_bot.render(w)); while lines.len() + 1 < h { lines.extend_from_slice(&BlankLine::render_static()); } - if false /* self.el_WHATEVER.is_editing() */ { + if self.el_display_name.is_editing() || + self.el_default_language.is_editing(){ lines.extend_from_slice(&self.edit_status.render(w)); } else { lines.extend_from_slice(&self.normal_status.render(w)); @@ -73,26 +199,36 @@ impl ActivityState for YourOptionsMenu { (lines, cursorpos) } - fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) -> + fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> LogicalAction { // Let editable menu lines have first crack at the keypress - if false /* self.el_WHATEVER.handle_keypress(key) */ + if self.el_display_name.handle_keypress(key) || + self.el_default_language.handle_keypress(key) { self.fix_widths(); return LogicalAction::Nothing; } match key { + Space => self.submit(client), Pr('q') | Pr('Q') => LogicalAction::Pop, + Pr('n') | Pr('N') => self.el_display_name.start_editing(), + Pr('v') | Pr('V') => self.cl_default_vis.cycle(), + Pr('l') | Pr('L') => self.el_default_language.start_editing(), + Pr('s') | Pr('S') => self.cl_default_sensitive.cycle(), + Ctrl('K') => self.cl_locked.cycle(), + Ctrl('F') => self.cl_hide_collections.cycle(), + Ctrl('D') => self.cl_discoverable.cycle(), + Ctrl('X') => self.cl_indexable.cycle(), + Ctrl('B') => self.cl_bot.cycle(), _ => LogicalAction::Nothing, } } - fn resize(&mut self, _w: usize, _h: usize) { - /* - self.el_WHATEVER.resize(w); - */ + fn resize(&mut self, w: usize, _h: usize) { + self.el_display_name.resize(w); + self.el_default_language.resize(w); } } @@ -323,7 +459,7 @@ pub fn user_options_menu(client: &mut Client, id: &str) -> Result, ClientError> { if id == client.our_account_id() { - Ok(Box::new(YourOptionsMenu::new())) + Ok(Box::new(YourOptionsMenu::new(client)?)) } else { Ok(Box::new(OtherUserOptionsMenu::new(client, id)?)) } diff --git a/src/posting.rs b/src/posting.rs index ccb26cb..5915e55 100644 --- a/src/posting.rs +++ b/src/posting.rs @@ -117,17 +117,8 @@ impl PostMenu { let ml_edit = MenuKeypressLine::new( Pr('A'), ColouredString::plain("Re-edit post")); let cl_vis = CyclingMenuLine::new( - Pr('V'), ColouredString::plain("Visibility: "), &[ - (Visibility::Public, ColouredString::uniform("public", 'f')), - (Visibility::Unlisted, ColouredString::plain( - "unlisted (anyone can see it, but feeds omit it)")), - (Visibility::Private, ColouredString::general( - "private (followees and @mentioned users can see it)", - "rrrrrrr ")), - (Visibility::Direct, ColouredString::general( - "direct (only @mentioned users can see it)", - "rrrrrr ")), - ], post.m.visibility); + Pr('V'), ColouredString::plain("Visibility: "), + &Visibility::long_descriptions(), post.m.visibility); let cl_sensitive = CyclingMenuLine::new( Pr('S'), ColouredString::plain("Mark post as sensitive: "), &[(false, ColouredString::plain("no")), diff --git a/src/types.rs b/src/types.rs index 05dcce7..b1c6da1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; use std::boxed::Box; use std::option::Option; +use super::coloured_string::ColouredString; + #[derive(Deserialize, Debug, Clone)] pub struct AccountField { pub name: String, @@ -159,6 +161,22 @@ pub enum Visibility { #[serde(rename = "direct")] Direct, } +impl Visibility { + pub fn long_descriptions() -> [(Visibility, ColouredString); 4] { + [ + (Visibility::Public, ColouredString::uniform("public", 'f')), + (Visibility::Unlisted, ColouredString::plain( + "unlisted (visible but not shown in feeds)")), + (Visibility::Private, ColouredString::general( + "private (to followees and @mentioned users)", + "rrrrrrr ")), + (Visibility::Direct, ColouredString::general( + "direct (only to @mentioned users)", + "rrrrrr ")), + ] + } +} + #[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum MediaType { #[serde(rename = "unknown")] Unknown,