diff --git a/changelog.yaml b/changelog.yaml index e765495..2de5412 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -1,4 +1,9 @@ +0.24.0: + date: TBA + changes: + - "TUI: Implement deleting own status messages" + 0.23.1: date: 2019-09-04 changes: diff --git a/toot/tui/app.py b/toot/tui/app.py index e934c90..fbd58e7 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -9,6 +9,7 @@ from .compose import StatusComposer from .constants import PALETTE from .entities import Status from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource +from .overlays import StatusDeleteConfirmation from .timeline import Timeline from .utils import show_media @@ -165,8 +166,11 @@ class TUI(urwid.Frame): def _compose(*args): self.show_compose() + def _delete(timeline, status): + if status.is_mine: + self.show_delete_confirmation(status) + def _reply(timeline, status): - logger.info("reply") self.show_compose(status) def _source(timeline, status): @@ -178,14 +182,15 @@ class TUI(urwid.Frame): def _menu(timeline, status): self.show_context_menu(status) - urwid.connect_signal(timeline, "focus", self.refresh_footer) - urwid.connect_signal(timeline, "reblog", self.async_toggle_reblog) - urwid.connect_signal(timeline, "favourite", self.async_toggle_favourite) - urwid.connect_signal(timeline, "source", _source) urwid.connect_signal(timeline, "compose", _compose) - urwid.connect_signal(timeline, "reply", _reply) + urwid.connect_signal(timeline, "delete", _delete) + urwid.connect_signal(timeline, "favourite", self.async_toggle_favourite) + urwid.connect_signal(timeline, "focus", self.refresh_footer) urwid.connect_signal(timeline, "media", _media) urwid.connect_signal(timeline, "menu", _menu) + urwid.connect_signal(timeline, "reblog", self.async_toggle_reblog) + urwid.connect_signal(timeline, "reply", _reply) + urwid.connect_signal(timeline, "source", _source) def build_timeline(self, name, statuses): def _close(*args): @@ -206,6 +211,10 @@ class TUI(urwid.Frame): return timeline + def make_status(self, status_data): + is_mine = self.user.username == status_data["account"]["acct"] + return Status(status_data, is_mine, self.app.instance) + def show_thread(self, status): def _close(*args): """When thread is closed, go back to the main timeline.""" @@ -216,8 +225,8 @@ class TUI(urwid.Frame): # This is pretty fast, so it's probably ok to block while context is # loaded, can be made async later if needed context = api.context(self.app, self.user, status.id) - ancestors = [Status(s, self.app.instance) for s in context["ancestors"]] - descendants = [Status(s, self.app.instance) for s in context["descendants"]] + ancestors = [self.make_status(s) for s in context["ancestors"]] + descendants = [self.make_status(s) for s in context["descendants"]] statuses = ancestors + [status] + descendants focus = len(ancestors) @@ -241,7 +250,7 @@ class TUI(urwid.Frame): finally: self.footer.clear_message() - return [Status(s, self.app.instance) for s in data] + return [self.make_status(s) for s in data] def _done_initial(statuses): """Process initial batch of statuses, construct a Timeline.""" @@ -334,12 +343,28 @@ class TUI(urwid.Frame): # TODO: show context menu pass + def show_delete_confirmation(self, status): + def _delete(widget): + promise = self.async_delete_status(self.timeline, status) + promise.add_done_callback(lambda *args: self.close_overlay()) + + def _close(widget): + self.close_overlay() + + widget = StatusDeleteConfirmation(status) + urwid.connect_signal(widget, "close", _close) + urwid.connect_signal(widget, "delete", _delete) + self.open_overlay(widget, title="Delete status?", options=dict( + align="center", width=("relative", 60), + valign="middle", height=5, + )) + def post_status(self, content, warning, visibility, in_reply_to_id): data = api.post_status(self.app, self.user, content, spoiler_text=warning, visibility=visibility, in_reply_to_id=in_reply_to_id) - status = Status(data, self.app.instance) + status = self.make_status(data) # TODO: instead of this, fetch new items from the timeline? self.timeline.prepend_status(status) @@ -361,7 +386,8 @@ class TUI(urwid.Frame): # Create a new Status with flipped favourited flag new_data = status.data new_data["favourited"] = not status.favourited - timeline.update_status(Status(new_data, status.instance)) + new_status = self.make_status(new_data) + timeline.update_status(new_status) self.run_in_thread( _unfavourite if status.favourited else _favourite, @@ -381,13 +407,23 @@ class TUI(urwid.Frame): # Create a new Status with flipped reblogged flag new_data = status.data new_data["reblogged"] = not status.reblogged - timeline.update_status(Status(new_data, status.instance)) + new_status = self.make_status(new_data) + timeline.update_status(new_status) self.run_in_thread( _unreblog if status.reblogged else _reblog, done_callback=_done ) + def async_delete_status(self, timeline, status): + def _delete(): + api.delete_status(self.app, self.user, status.id) + + def _done(loop): + timeline.remove_status(status) + + return self.run_in_thread(_delete, done_callback=_done) + # --- Overlay handling ----------------------------------------------------- default_overlay_options = dict( diff --git a/toot/tui/entities.py b/toot/tui/entities.py index 1a7d334..324a774 100644 --- a/toot/tui/entities.py +++ b/toot/tui/entities.py @@ -6,23 +6,16 @@ from .utils import parse_datetime Author = namedtuple("Author", ["account", "display_name"]) -def get_author(data, instance): - # Show the author, not the persopn who reblogged - status = data["reblog"] or data - acct = status['account']['acct'] - acct = acct if "@" in acct else "{}@{}".format(acct, instance) - return Author(acct, status['account']['display_name']) - - class Status: """ A wrapper around the Status entity data fetched from Mastodon. https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#status """ - def __init__(self, data, instance): + def __init__(self, data, is_mine, default_instance): self.data = data - self.instance = instance + self.is_mine = is_mine + self.default_instance = default_instance # This can be toggled by the user self.show_sensitive = False @@ -33,14 +26,21 @@ class Status: self.display_name = self.data["account"]["display_name"] self.account = self.get_account() self.created_at = parse_datetime(data["created_at"]) - self.author = get_author(data, instance) + self.author = self.get_author() self.favourited = data.get("favourited", False) self.reblogged = data.get("reblogged", False) self.in_reply_to = data.get("in_reply_to_id") + def get_author(self): + # Show the author, not the persopn who reblogged + data = self.data["reblog"] or self.data + acct = data['account']['acct'] + acct = acct if "@" in acct else "{}@{}".format(acct, self.default_instance) + return Author(acct, data['account']['display_name']) + def get_account(self): acct = self.data['account']['acct'] - return acct if "@" in acct else "{}@{}".format(acct, self.instance) + return acct if "@" in acct else "{}@{}".format(acct, self.default_instance) def __repr__(self): return "".format(self.id) diff --git a/toot/tui/overlays.py b/toot/tui/overlays.py index 625f533..64f7872 100644 --- a/toot/tui/overlays.py +++ b/toot/tui/overlays.py @@ -30,6 +30,23 @@ class ExceptionStackTrace(urwid.ListBox): super().__init__(walker) +class StatusDeleteConfirmation(urwid.ListBox): + signals = ["delete", "close"] + + def __init__(self, status): + yes = SelectableText("Yes, send it to heck") + no = SelectableText("No, I'll spare it for now") + + urwid.connect_signal(yes, "click", lambda *args: self._emit("delete")) + urwid.connect_signal(no, "click", lambda *args: self._emit("close")) + + walker = urwid.SimpleFocusListWalker([ + urwid.AttrWrap(yes, "", "blue_selected"), + urwid.AttrWrap(no, "", "blue_selected"), + ]) + super().__init__(walker) + + class GotoMenu(urwid.ListBox): signals = [ "home_timeline", diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index faa67f2..c086a52 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -17,6 +17,7 @@ class Timeline(urwid.Columns): signals = [ "close", # Close thread "compose", # Compose a new toot + "delete", # Delete own status "favourite", # Favourite status "focus", # Focus changed "media", # Display media attachments @@ -110,6 +111,10 @@ class Timeline(urwid.Columns): self._emit("compose") return + if key in ("d", "D"): + self._emit("delete", status) + return + if key in ("f", "F"): self._emit("favourite", status) return @@ -184,6 +189,14 @@ class Timeline(urwid.Columns): if index == self.status_list.body.focus: self.draw_status_details(status) + def remove_status(self, status): + index = self.get_status_index(status.id) + assert self.statuses[index].id == status.id # Sanity check + + del(self.statuses[index]) + del(self.status_list.body[index]) + self.refresh_status_details() + class StatusDetails(urwid.Pile): def __init__(self, status, in_thread): @@ -247,8 +260,18 @@ class StatusDetails(urwid.Pile): # Push things to bottom yield ("weight", 1, urwid.SolidFill(" ")) - options = "[B]oost [F]avourite [V]iew {}[R]eply So[u]rce [H]elp".format( - "[T]hread " if not self.in_thread else "") + options = [ + "[B]oost", + "[D]elete" if status.is_mine else "", + "[F]avourite", + "[V]iew", + "[T]hread" if not self.in_thread else "", + "[R]eply", + "So[u]rce", + "[H]elp", + ] + options = " ".join(o for o in options if o) + options = highlight_keys(options, "cyan_bold", "cyan") yield ("pack", urwid.Text(options))