extend_future: Option<HashMap<String, String>>,
}
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Followness {
+ NotFollowing,
+ Following {
+ boosts: Boosts,
+ languages: Vec<String>,
+ }
+}
+
+impl Followness {
+ pub fn from_rel(rel: &Relationship) -> Followness {
+ if !rel.following {
+ Followness::NotFollowing
+ } else {
+ let boosts = if rel.showing_reblogs {
+ Boosts::Show
+ } else {
+ Boosts::Hide
+ };
+ let languages = rel.languages.clone().unwrap_or(Vec::new());
+ Followness::Following {
+ boosts,
+ languages,
+ }
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum AccountFlag { Block, Mute }
+
pub struct Client {
auth: AuthConfig,
client: reqwest::blocking::Client,
self.cache_poll(&poll);
Ok(())
}
+
+ pub fn set_following(&mut self, id: &str, follow: Followness)
+ -> Result<(), ClientError>
+ {
+ let req = match follow {
+ Followness::NotFollowing =>
+ Req::post(&format!("v1/accounts/{id}/unfollow")),
+ Followness::Following { boosts, languages } => {
+ let mut req = Req::post(&format!("v1/accounts/{id}/follow"))
+ .param("reblogs", boosts == Boosts::Show);
+ for language in languages {
+ req = req.param("languages[]", &language);
+ }
+ req
+ }
+ };
+ let (url, rsp) = self.api_request(req)?;
+ let rspstatus = rsp.status();
+ if !rspstatus.is_success() {
+ Err(ClientError::UrlError(url, rspstatus.to_string()))
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn set_account_flag(&mut self, id: &str, flag: AccountFlag,
+ enable: bool) -> Result<(), ClientError>
+ {
+ let req = match (flag, enable) {
+ (AccountFlag::Block, true) =>
+ Req::post(&format!("v1/accounts/{id}/block")),
+ (AccountFlag::Block, false) =>
+ Req::post(&format!("v1/accounts/{id}/unblock")),
+ (AccountFlag::Mute, true) =>
+ Req::post(&format!("v1/accounts/{id}/mute")),
+ (AccountFlag::Mute, false) =>
+ Req::post(&format!("v1/accounts/{id}/unmute")),
+ };
+ let (url, rsp) = self.api_request(req)?;
+ let rspstatus = rsp.status();
+ if !rspstatus.is_success() {
+ Err(ClientError::UrlError(url, rspstatus.to_string()))
+ } else {
+ Ok(())
+ }
+ }
}
--- /dev/null
+use super::client::{Client, ClientError, Boosts, Followness, AccountFlag};
+use super::coloured_string::ColouredString;
+use super::tui::{
+ ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*,
+};
+use super::text::*;
+use super::editor::{EditableMenuLine, EditableMenuLineData};
+
+struct YourOptionsMenu {
+ title: FileHeader,
+ coming_soon: CentredInfoLine,
+ normal_status: FileStatusLineFinal,
+ edit_status: FileStatusLineFinal,
+}
+
+impl YourOptionsMenu {
+ fn new() -> Self {
+ let title = FileHeader::new(ColouredString::general(
+ "Your user options [ESC][Y][O]",
+ "HHHHHHHHHHHHHHHHHHHKKKHHKHHKH"));
+
+ let normal_status = FileStatusLine::new()
+ .add(Return, "Back", 10).finalise();
+ let edit_status = FileStatusLine::new()
+ .message("Edit line and press Return").finalise();
+
+ let coming_soon = CentredInfoLine::new(
+ ColouredString::uniform("Not yet implemented", '!'));
+
+ let mut menu = YourOptionsMenu {
+ title,
+ coming_soon,
+ normal_status,
+ edit_status,
+ };
+ menu.fix_widths();
+ 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);
+
+ // self.cl_WHATEVER.reset_widths();
+
+ // self.cl_WHATEVER.ensure_widths(lmaxwid, rmaxwid);
+
+ (lmaxwid, rmaxwid)
+ }
+}
+
+impl ActivityState for YourOptionsMenu {
+ fn draw(&self, w: usize, h: usize)
+ -> (Vec<ColouredString>, CursorPosition) {
+ let mut lines = Vec::new();
+ 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
+
+ while lines.len() + 1 < h {
+ lines.extend_from_slice(&BlankLine::render_static());
+ }
+
+ if false /* self.el_WHATEVER.is_editing() */ {
+ lines.extend_from_slice(&self.edit_status.render(w));
+ } else {
+ lines.extend_from_slice(&self.normal_status.render(w));
+ }
+
+ (lines, cursorpos)
+ }
+
+ 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) */
+ {
+ self.fix_widths();
+ return LogicalAction::Nothing;
+ }
+
+ match key {
+ Pr('q') | Pr('Q') => LogicalAction::Pop,
+ _ => LogicalAction::Nothing,
+ }
+ }
+
+ fn resize(&mut self, _w: usize, _h: usize) {
+ /*
+ self.el_WHATEVER.resize(w);
+ */
+ }
+}
+
+struct LanguageVector(Vec<String>);
+impl EditableMenuLineData for LanguageVector {
+ fn display(&self) -> ColouredString {
+ if self.0.is_empty() {
+ ColouredString::uniform("any", '0')
+ } else {
+ // Make the comma separators appear in the 'boring' colour
+ let mut s = ColouredString::plain("");
+ let mut sep = ColouredString::plain("");
+ for lang in &self.0 {
+ s = s + &sep + ColouredString::plain(lang);
+ sep = ColouredString::uniform(",", '0');
+ }
+ s
+ }
+ }
+
+ fn to_text(&self) -> String { self.0.as_slice().join(",") }
+
+ fn from_text(text: &str) -> Self {
+ Self(text.split(|c| c == ' ' || c == ',')
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_owned()).collect())
+ }
+}
+
+struct OtherUserOptionsMenu {
+ title: FileHeader,
+ normal_status: FileStatusLineFinal,
+ edit_status: FileStatusLineFinal,
+ cl_follow: CyclingMenuLine<bool>,
+ cl_boosts: CyclingMenuLine<Boosts>,
+ el_languages: EditableMenuLine<LanguageVector>,
+ cl_block: CyclingMenuLine<bool>,
+ cl_mute: CyclingMenuLine<bool>,
+ id: String,
+ prev_follow: Followness,
+ prev_block: bool,
+ prev_mute: bool,
+}
+
+impl OtherUserOptionsMenu {
+ fn new(client: &mut Client, id: &str) -> Result<Self, ClientError> {
+ let ac = client.account_by_id(id)?;
+ let name = client.fq(&ac.acct);
+ let rel = client.account_relationship_by_id(id)?;
+
+ let title = FileHeader::new(ColouredString::uniform(
+ &format!("Your options for user {name}"), 'H'));
+
+ let normal_status = FileStatusLine::new()
+ .add(Return, "Back", 10).finalise();
+ let edit_status = FileStatusLine::new()
+ .message("Edit line and press Return").finalise();
+ let cl_follow = CyclingMenuLine::new(
+ Pr('F'), ColouredString::plain("Follow this user: "),
+ &[(false, ColouredString::plain("no")),
+ (true, ColouredString::uniform("yes", 'f'))],
+ rel.following);
+ let boosts = if rel.following {
+ if rel.showing_reblogs { Boosts::Show } else { Boosts::Hide }
+ } else {
+ // Default, if we start off not following the user
+ Boosts::Show
+ };
+ let cl_boosts = CyclingMenuLine::new(
+ Pr('B'), ColouredString::plain(" Include their boosts: "),
+ &[(Boosts::Hide, ColouredString::plain("no")),
+ (Boosts::Show, ColouredString::uniform("yes", 'f'))], boosts);
+ let el_languages = EditableMenuLine::new(
+ Pr('L'), ColouredString::plain(" Include languages: "),
+ LanguageVector(rel.languages.clone()
+ .unwrap_or_else(|| Vec::new())));
+
+ let cl_block = CyclingMenuLine::new(
+ Ctrl('B'), ColouredString::plain("Block this user: "),
+ &[(false, ColouredString::plain("no")),
+ (true, ColouredString::uniform("yes", 'r'))],
+ rel.blocking);
+ // Can't use the obvious ^M because it's also Return, of course!
+ let cl_mute = CyclingMenuLine::new(
+ Ctrl('U'), ColouredString::plain("Mute this user: "),
+ &[(false, ColouredString::plain("no")),
+ (true, ColouredString::uniform("yes", 'r'))],
+ rel.muting);
+
+ let prev_follow = Followness::from_rel(&rel);
+
+ let mut menu = OtherUserOptionsMenu {
+ title,
+ normal_status,
+ edit_status,
+ cl_follow,
+ cl_boosts,
+ el_languages,
+ cl_block,
+ cl_mute,
+ id: id.to_owned(),
+ prev_follow,
+ prev_block: rel.blocking,
+ prev_mute: rel.muting,
+ };
+ menu.fix_widths();
+ Ok(menu)
+ }
+
+ fn fix_widths(&mut self) -> (usize, usize) {
+ let mut lmaxwid = 0;
+ let mut rmaxwid = 0;
+ self.cl_follow.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.cl_boosts.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.el_languages.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.cl_block.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.cl_mute.check_widths(&mut lmaxwid, &mut rmaxwid);
+
+ self.cl_follow.reset_widths();
+ self.cl_boosts.reset_widths();
+ self.el_languages.reset_widths();
+ self.cl_block.reset_widths();
+ self.cl_mute.reset_widths();
+
+ self.cl_follow.ensure_widths(lmaxwid, rmaxwid);
+ self.cl_boosts.ensure_widths(lmaxwid, rmaxwid);
+ self.el_languages.ensure_widths(lmaxwid, rmaxwid);
+ self.cl_block.ensure_widths(lmaxwid, rmaxwid);
+ self.cl_mute.ensure_widths(lmaxwid, rmaxwid);
+
+ (lmaxwid, rmaxwid)
+ }
+
+ fn submit(&self, client: &mut Client) -> LogicalAction {
+ let new_follow = if self.cl_follow.get_value() {
+ Followness::Following {
+ boosts: self.cl_boosts.get_value(),
+ languages: self.el_languages.get_data().0.clone(),
+ }
+ } else {
+ Followness::NotFollowing
+ };
+
+ if new_follow != self.prev_follow {
+ if client.set_following(&self.id, new_follow).is_err() {
+ return LogicalAction::Beep; // FIXME: report the error!
+ }
+ }
+
+ let new_block = self.cl_block.get_value();
+ if new_block != self.prev_block {
+ if client.set_account_flag(&self.id, AccountFlag::Block,
+ new_block).is_err()
+ {
+ return LogicalAction::Beep; // FIXME: report the error!
+ }
+ }
+
+ let new_mute = self.cl_mute.get_value();
+ if new_mute != self.prev_mute {
+ if client.set_account_flag(&self.id, AccountFlag::Mute,
+ new_mute).is_err()
+ {
+ return LogicalAction::Beep; // FIXME: report the error!
+ }
+ }
+
+ LogicalAction::Pop
+ }
+}
+
+impl ActivityState for OtherUserOptionsMenu {
+ fn draw(&self, w: usize, h: usize)
+ -> (Vec<ColouredString>, CursorPosition) {
+ let mut lines = Vec::new();
+ 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.cl_follow.render(w));
+ lines.extend_from_slice(&self.cl_boosts.render(w));
+ lines.push(self.el_languages.render(
+ w, &mut cursorpos, lines.len()));
+ lines.extend_from_slice(&BlankLine::render_static());
+ lines.extend_from_slice(&self.cl_block.render(w));
+ lines.extend_from_slice(&self.cl_mute.render(w));
+
+ while lines.len() + 1 < h {
+ lines.extend_from_slice(&BlankLine::render_static());
+ }
+
+ if self.el_languages.is_editing() {
+ lines.extend_from_slice(&self.edit_status.render(w));
+ } else {
+ lines.extend_from_slice(&self.normal_status.render(w));
+ }
+
+ (lines, cursorpos)
+ }
+
+ fn handle_keypress(&mut self, key: OurKey, client: &mut Client) ->
+ LogicalAction
+ {
+ // Let editable menu lines have first crack at the keypress
+ if self.el_languages.handle_keypress(key)
+ {
+ self.fix_widths();
+ return LogicalAction::Nothing;
+ }
+
+ match key {
+ Space => self.submit(client),
+ Pr('q') | Pr('Q') => LogicalAction::Pop,
+ Pr('f') | Pr('F') => self.cl_follow.cycle(),
+ Pr('b') | Pr('B') => self.cl_boosts.cycle(),
+ Pr('l') | Pr('L') => self.el_languages.start_editing(),
+ Ctrl('B') => self.cl_block.cycle(),
+ Ctrl('U') => self.cl_mute.cycle(),
+ _ => LogicalAction::Nothing,
+ }
+ }
+
+ fn resize(&mut self, w: usize, _h: usize) {
+ self.el_languages.resize(w);
+ }
+}
+
+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()))
+ } else {
+ Ok(Box::new(OtherUserOptionsMenu::new(client, id)?))
+ }
+}