Mastodonochrome's TODO list =========================== # Missing features ## Line recall Single-line editors should retain history, and support [UP] and [DOWN] to page through it. In particular, that means if you try to examine a user and mistype their name, you can recall the failed attempt and edit it, rather than having to try again from scratch. This will need some persistent inter-activity state stored in the `TuiLogicalState`, either in a `RefCell` or passed explicitly to activity constructors and updated on completion of the activity. Also some UI thought about what history should be shared with what. The most obvious policy is a separate history per bottom-line prompt activity, but I haven't thought carefully about whether that's actually the best. ## Error handling improvements Error Log entries could usefully include various extra detail: * `ClientError` should distinguish HTTP methods, so that the inappropriate text 'fetching URL' isn't used in the case where you were trying to _do_ something, like post a toot. * probably the method of `Client` that generated an error ought to annotate it with a description of the client operation being performed, so that it's not just 'while fetching this URL' but 'while trying to retrieve a status / account / etc', and maybe even which one. * the caller of that method in turn might also want to add further context, like 'I was trying to fetch this status because this other one was replying to it'. We don't have good handling for I/O errors while saving the user's LDB. That's not a _client_ error, but we could make an extra enum branch in `ClientError` anyway, so that the TuiLogicalState could put it in the Error Log to notify the user of a problem. Then perhaps we could set a flag to suppress further LDB-saving errors (don't want to keep whinging every 2 minutes or on every file change!), and clear the flag again if a later LDB save is successful (e.g. the user has freed up some disk space). ## Reading ### Selecting users @mentioned in posts When a user is @mentioned, the usual rendering in the HTML from the server is to show just the local part of their name, and nowhere in the HTML is a full machine-readable version of the fully qualified `username@domain` account name. So you can't easily get the whole username from the HTML, and therefore, the names of mentioned users are not eligible for selection if you press \[E\] to examine one of the user names visible on the screen. Instead you have to press \[I\] to see the entire post info, and from there, look at the separate list of full usernames of @mentioned users, which is generated not from the HTML but from the separate array provided by the server in the `mentions` field of the JSON `Status` object. But I now realise that each entry in the `mentions` array includes a URL to the user's profile page – the one for humans, not the API. And that's the same URL used as the hyperlink target when the mention is shown in the HTML. So we _could_ match up the HTML text of a mention to the fully qualified username, by interpreting `mentions` as a map from URL → account name. ### Better UI for unfolding sensitive posts If you make a top-level post which merits a content warning, and it spawns a discussion thread, then replies in the thread are likely to be posted with the same content warning. But the whole thread is presumably on a topic that you personally are prepared to talk about, or you wouldn't have started it. So it would be useful to have a way to unfold the whole thread at once. Especially if you're actually _viewing_ the whole thread at once, but you might very well also want replies to be automatically unfolded as they trickle in. But this is difficult! There's no entry in the [`Status`](https://docs.joinmastodon.org/entities/Status/) record that acts as a thread identifier, so you can't tell reliably whether two posts are part of the same thread, except by requesting the context of both of them and seeing if they start at the same initial post. So perhaps we need some well-defined but less ambitious feature that has something of the same effect. Things that _would_ be reasonably easy, and _maybe_ the combination of them would give me what I want: * Add an 'unfold whole thread' option to the selection UI state you get into by pressing \[U\] in a feed. This would request the whole _current_ thread of the post, and unfold everything in it in one go, but would not by itself cause future updates in the thread to be auto-unfolded * When _you_ post something marked as sensitive, start it off unfolded, because you surely aren't going to be startled by your own words. * When retrieving a reply to an unfolded post, automatically unfold that too. Then any thread affected by either of the previous two points will continue updating in an unfolded state. But that's not a complete design. Does 'unfold whole thread' apply to just the ancestor/descendant context, or the whole thing? (Two keystrokes, maybe?) And surely 'auto-unfold replies to this unfolded post' ought not to apply to _everything_, because it certainly works better for some kinds of folded thing than others. (For example, the "question as CW, punchline as post" style of Mastodon joke-posting would play very badly with this, by auto-spoiling every new joke posted in a thread.) And the more different options you apply, which affect state that's complicated and multifaceted and (worst of all) not shown explicitly in the UI, the more confusing the whole thing is. So, more design work needed. (But perhaps 'unfold whole thread as it currently is' is the easy part; I think just having a keystroke for that would at least save the user doing the same job via post-by-post repetitive faffing, would have no knock-on consequences of accidentally showing/hiding future stuff, and would fit neatly into the UI because it would just be an alternative keystroke to \[SPACE\] after selecting a post to unfold. Maybe the answer is to do that, and then see whether anything more is needed at all.) ## Posting There are several features of posting that aren't supported yet. I think the right approach is to change the editor subsystem's data model so that it's richer than just a `String`. I've changed my mind about this at least twice already, but I _currently_ think the right data model is something like a top-level `Vec` of structures representing an individual post (allowing a multi-post thread), each of which stores a `String` of the text plus extra data about media and polls on that post. This also makes it easy to restart counting the character limit per post in the thread, so each is highlighted appropriately, and to find the max character count across all posts so that it can be applied to the content warning in the post-compose menu. ### Posting a whole thread I want an editor keystroke that introduces a toot boundary marker. Then you could post a multi-toot thread by editing it all in one go, with a clear idea of where the boundaries would end up; each one would be highlighted if it was going to end up too long. At posting time, each one would be automatically marked as a reply to the previous one, and of course the starting toot would still be a reply to whatever the whole thread was responding to (if anything). For some purposes it might also be useful to change the content warning settings for individual posts in the thread. It's easy to think of examples, like 'I had this problem \[explanation\] and then I ended up doing \[solution hidden so you can think about it yourself first\]'. But I think that should be an optional alternative to the existing setup: it certainly _is_ useful to be able to post a whole thread under the same CW settings without having to set them individually per post. So I think if this happens at all it should be more like a CW _override_ per post, with the post-composer CW being applied to any post that didn't override it. ### Adding media Definitely need some way to add media attachments to a toot. This will need some kind of file selector UI. Not sure whether that would be better as a straight BottomLineEditor for a filename with added tab completion, or a full-on full-screen filesystem browser. Perhaps just the former until anyone has effort for the latter. The [`Instance`](https://docs.joinmastodon.org/entities/Instance/) record from the server lists its supported MIME types for media attachments. I think the [`magic`](https://docs.rs/magic/latest/magic/) Rust crate will identify the MIME type of a candidate file, and we could use that to check whether the file is acceptable. The media record in the editor will also need to include a space for alt text, which you should be able to edit as part of the main editor buffer. Inserting a media attachment should hint strongly that you should fill that in, e.g. by putting the cursor in the alt-text slot and also showing a red error marker for 'no alt text here yet'. One hard part here is that Mastodon also likes you to nominate a particular area of the image as the most interesting part, so that if a client is showing a limited-size preview or something, it can zoom in on that. That would be difficult to do interactively in a non-graphical client! Perhaps we just have to apologetically not have that feature. ### Posting polls You should be able to post polls. ### Mentions arriving while you're editing If you got a beep for an incoming mention while you were writing a post of your own, then in real Mono, finishing up your post causes you to be belatedly thrown into your mentions. Perhaps we should replicate that. ### Longer-term draft saving If you've got a post half written and you remove the composition activity completely from the activity stack, say by \[ESC\]\[G\], it would be nice to save the draft _somewhere_ to come back to. Real Mono puts it in a weird limbo, so that it's never quite obvious whether you currently have one, or how to recall it if you do (several keystrokes _might_ and it's not reliably the same one every time). So I think we can do better, probably via a visible 'Drafts' menu or some such. (This will also require dynamic menu keypress assignment.) This is separate from the question of temporarily leaving a composition activity that's still _on_ the activity stack, via [ESC]. ## Editor improvements ### Block cut and paste There should be _some_ method of cut/copy/pasting a block of text, beyond the existing ^K cut-to-end-of-line. Real Monochrome's UI for this is moderately horrible, and in particular, works at the granularity of lines rather than characters, which is not a good fit for this client's auto-wrapping reimagining of its editor. So I don't know whether to stick with the most Mono-like thing I can, or redesign more significantly. ### Importing a file Real Mono, for those privileged users with a shell login, has a keystroke (F8) to read a disk file and insert it into the editor buffer. That might well be useful here too. ### Mentioning users If you want to mention a user who isn't already participating in the thread, you have to type out their user id by hand in the editor, and hope you didn't misremember it or make a typo. My current workaround for this is to \[ESC\]\[E\] the user id in mid-compose, check their user info page looks like the one I'm expecting, and then return from the examine page and enter the same user id into the composer buffer. But even that is awkward, because I have to use my terminal program's copy and paste to make sure the version I enter into the buffer didn't introduce a typo _after_ I did the check. I'm not sure what a good UI for this would be. At the very least it would be nice to avoid needing to depend on copy-paste outside the application. Maybe you should enter the account name into the compose buffer _first_, and then you'd just need a hot key to examine the user whose @mention the cursor is sitting on? That would be easy, but still requires you to remember the entire name in the first place, which might still be one hurdle too many. (Perhaps an even smaller intervention than that would be to share the paste buffer between the composer and the \[ESC\]\[E\] prompt, which _literally_ does nothing but eliminate the dependency on external copy-paste. But that would still need an awkward non-obvious sequence of keystrokes and I think I prefer the hot key approach.) Or maybe an interactive UI for entering the name in the first place, supporting tab-completion or interactive selection from accounts you already know about in some way (at the very least, users you follow), and a way to check whether the name you've entered looks plausible, and then insert it into the buffer once you're done? More design thinking needed. ## Examining users ### Show URLs and mentions in full Account bios can include hyperlinks and @mentions. We currently show them the same way we show ordinary posts: with the link text rather than link target, and the short form of the account name, both because that's what appears if you render the server-provided HTML in the obvious way. But in an ordinary post, you can view the full details using \[I\]. When you examine a user, there's no such affordance. So if an account bio says (for example) 'see `@this@other.account` for related stuff', you currently have to step outside this client to follow the link. Should be easy enough to fix, by adding extra info to the Examine User page, matching what's on the Post Info page. ## Better handling of updates ### Deleted posts outside the home feed We handle deleting from the home feed, but don't detect it in circumstances where we _don't_ get a stream update about it, for example when it's in a thread view, or one of the timelines that we don't currently fetch streams for. ### Cache invalidation We keep a cache of various kinds of record we've received from the server – `Status`, `Account`, etc. Things stay in our cache indefinitely, and we don't proactively try to retrieve updated versions of them, though we do update a cache entry any time the server happens to send us a new version of the same record anyway (e.g. if you request the thread of a post, you get a fresh copy of that post). This doesn't work very well. Statuses are often changed behind our back, for many legit reasons. They can be edited by the poster; their status can be changed by _us_ (e.g. you favourite a post using an unrelated client logged in to the same Mastodon account); details like the number of favouriters / boosters / voters can update constantly. Probably what we ought to do is to regard _everything_ cached from the server as having a limited lifetime, and after that lifetime, fetch it again if needed. This includes all the things that `Client` keeps in big `HashMap`s; it _also_ includes the lists of ids we've fetched from feeds, because those too can change for various reasons: posters can delete statuses they've posted, users who boosted a post can changed their mind and unboost it again, and also your home timeline can change retrospectively when you follow or unfollow a user. Caching individual objects is the easy part of this. It's easy to write a `CacheMap` type that works like a `HashMap`, but every entry also has a timestamp, and there's an expire method that deletes stuff older than a threshold. Caching feeds is harder, because the unit of one that you fetch isn't a discrete block: you fetch a range at a time and it might partially overlap existing ranges. So now there's a whole load of awkward problems to solve. You want some kind of data structure holding a list of ids, marking arbitrary ranges of it as having a given size. It should support finding out that a region of the list is expired, in which case users should avoid actually asking for the ids in that range, but we still have to remember what they _were_, so we can use them to estimate file percentages. Then, even scrolling up and down a feed becomes an action that can cause network access, if you try to scroll into an expired section of the feed. And if the part of the timeline actually in view on the screen expires from cache, we should re-fetch it from the server proactively, to pick up updates like polls and fave status. Another consideration is avoiding accidentally hammering the server. Fetching a section of a timeline is a single HTTP request returning a big list of `Status`, which is fine. But if you accidentally expired the individual `Status`es from the internal cache _before_ expiring the section of timeline referring to them, then 32 calls to `Client::status_by_id` would each generate a separate HTTP request for a single status. This is all very complicated and needs serious thought. ### Streaming improvements Currently we have exactly one streaming subthread, handling your home timeline and notifications. But it's also possible to stream other things: the public timelines (local and global), and hashtags. So perhaps while you're viewing one of those timelines we should start a stream thread to keep it up to date? This would also require having a way to shut the stream down again. Public timelines move fast, and you wouldn't want that stream to keep running forever just because you glanced into the timeline once. Communicating back to the stream thread to interrupt it in mid-read is probably too painful, but streaming connections generally deliver a periodic heartbeat, so perhaps an easier approach is to make a 'please shut down now' atomic flag of some kind, stick it in an `Arc` so it can be shared with the main thread, and have the stream thread check it whenever it happens to wake up anyway. Other things can be updated but don't have a streaming API. For example, if you're viewing a particular user's posts, it would be nice to append to _that_ feed too if they happen to post something while you're watching. So another thing we could do is to run a regular timer tick which wakes us up and makes us poll whatever feed we're currently watching. If we're going to have that poll, it should probably be unconditional, _even_ if there's also a stream running. Then it acts as a fallback if the stream gets hung up somehow. ## Menu marker for new things In real Monochrome, files that have new things to read are marked in the menu by a red +. Perhaps at least the home timeline could usefully be marked that way. ## Logs menu improvements There should be a summary page showing general health info; in particular, whether streaming subthread(s) are currently working. Currently, the \[ESC\]\[L\] page is labelled 'client logs' and \[ESC\]\[L\]\[L\] is 'server logs', which is why I've put the HTTP log on the former. But then, in real Mono, the first page is a general status display and log files are all on the second. Perhaps that's more sensible than the client/server distinction. ## More options in the Examine → Options menus ### Your own options: \[ESC\]\[Y\]\[O\] Still to do, and harder UI-wise than the rest of the user options: * 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. We'll also want a way to set client-side UI options, maybe in this menu or maybe elsewhere: * length of time to retain Error Log and HTTP Log entries (or maybe other ways of deciding how to prune them) ### Options for other users We support blocking and muting users, but only as a binary status. The API also allows you to mute a user for a specified length of time, and we don't provide access to that version of the request. We don't support forcibly removing a user from your list of followers. Getting a notification when a user posts is mentioned separately below. 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 usernames we know about and occasionally read but don't Properly Follow, so as to provide their home feeds in a convenient submenu. ## Outlying protocol features ### Following hashtags There's an API request to [follow an entire hashtag](https://docs.joinmastodon.org/methods/tags/#follow), so that posts with that tag appear on your home timeline, even if they're posted by people you've never heard of. (However, this doesn't seem to come with any of the other follow options – for example, you can't follow a tag but restrict to the subset of its posts in languages you speak.) ### Scheduled posts You can ask the server to schedule a post to go out later. This could be slotted in as another option in the post-composer menu. ### Being notified of users' posts There's an option in the account relationship API to ask for a user's posts to show up in your notifications feed. Perhaps we should support setting that flag on a user, and receiving the resulting notifications in some useful part of the API? ### Being notified of follow requests At the moment, if your account is locked and someone requests to follow you, you find out via an indication on the Main Menu. But maybe some locked-account users would prefer an immediate notification, via a feed you get thrown into like \[ESC\]\[R\]? Without talking to some, I don't know whether that would be useful, so I haven't implemented it yet. ### General search The [search API query](https://docs.joinmastodon.org/methods/search/#v2) lets you send an arbitrary search string to the server, and request accounts and/or hashtags and/or statuses that match it. Also you can restrict the search domain to only people you follow, or to only one account (presumably so you can look up 'that toot I know Chris made last year'). ### Rate limiting HTTP responses from the Mastodon server generally seem to carry a set of headers along the lines of X-Ratelimit-Limit: 300 X-Ratelimit-Remaining: 300 X-Ratelimit-Reset: 2024-01-04T19:50:00.318188Z If we actually paid attention to those, we could check them in the centralised `execute_and_log_request` function, and if the 'remaining' looked dangerously low, set a flag that would make us wait a bit before submitting the request. Then we should be proof against getting blocked by the server for accidental misbehaviour. ### Server information The [Instance record](https://docs.joinmastodon.org/entities/Instance/) from the server includes information we could usefully show in a page somewhere. Like the `description` field, and the list of rules. There's also an [announcements API query](https://docs.joinmastodon.org/methods/announcements/) for 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. And we can also spot new announcements in mid-run, because they appear as events on the home timeline stream. # 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 from the user settings menu, in the ActivityPub file format. This appears to be a zip file containing a handful of JSON files and all your media (including avatar image and also anything you attached to your posts). It would be nice to be able to use this client to browse that archive, using the same UI as for reading the live server (restricted as appropriate where data isn't available). Perhaps even use it _in conjunction_ with the live server, so that when your posts are in reply to other posts, you can retrieve the replied-to posts from the live server if you need to remember what they were about. # Internal code cleanups ## Inter-activity communications The systems for passing data between related activities are horrible. When the user presses `[/]` to search within a `File`, spawning a `GetSearchExpression` overlay activity, it communicates its results back by calling the `got_search_expression` method in the `ActivityState` trait. And the way that the post editor and the posting menu pass a `Post` back and forth between themselves is equally horrible, involving a special argument to several methods of `TuiLogicalState` that's only used in those particular cases. Both of those are horrible in the same way: there's a thing general to all activity types which is only used by one particular pair of them. And they're not even the same as each other! I'm not yet sure what ought to replace them, though. An obvious starting point is to share data (search expression or `Post`) between the related activities via an `Rc>`. But you still have to get clones of that `Rc` to the places it's needed in the first place, and you can't put an `Rc` in the `Activity` value that spawns the second activity from the first one, because that's used as a map key so it has to be `Hash` and `Eq`. So _something_ less nice is needed, and I'm not sure what's least nasty. ## Reorganise the posting activities Now there are three pairs of activities involving passing a `Post` back and forth. Perhaps they should be just one pair of activities, with a sub-struct describing what kind of post?