chiark / gitweb /
Ability to set most of your own options.
authorSimon Tatham <anakin@pobox.com>
Sat, 13 Jan 2024 10:57:21 +0000 (10:57 +0000)
committerSimon Tatham <anakin@pobox.com>
Sat, 13 Jan 2024 13:57:35 +0000 (13:57 +0000)
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.

TODO.md
src/client.rs
src/options.rs
src/posting.rs
src/types.rs

diff --git a/TODO.md b/TODO.md
index a1b45e4501ba90a3c821f56d3e6ab0ab33b5ba87..bb2ceea369166cbf659ee83632e3b5a11690c8b9 100644 (file)
--- 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
index ee4e61b4238dba331a4e23bc6b2bae20aa3808e5..4568a9adec7d231a88c6fb49461be156b25d3f27 100644 (file)
@@ -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"),
         }
     }
 }
index bf37ad012bb7d5dcd230730b8bbfd5b74a4ccca8..08d187f67741bab9fd0c6d1312d71768b73d8b47 100644 (file)
@@ -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<String>, // N
+    cl_default_vis: CyclingMenuLine<Visibility>,
+    cl_default_sensitive: CyclingMenuLine<bool>,
+    el_default_language: EditableMenuLine<Option<String>>,
+
+    cl_locked: CyclingMenuLine<bool>,
+    cl_bot: CyclingMenuLine<bool>,
+    cl_discoverable: CyclingMenuLine<bool>,
+    cl_hide_collections: CyclingMenuLine<bool>,
+    cl_indexable: CyclingMenuLine<bool>,
+
+    // 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<Self, ClientError> {
+        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<ColouredString>, 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<Box<dyn ActivityState>, 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)?))
     }
index ccb26cbf23843edc3a2caafa3a38db75921a3f6e..5915e55eaf87848538547f53cedc265274a4d43f 100644 (file)
@@ -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")),
index 05dcce786760dcee4dfd521354cb3992f0d5c704..b1c6da169d27ac3fb64ec76683cb737fe72ecc0c 100644 (file)
@@ -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,