Skip to content

Commit

Permalink
tui: allow editing toots
Browse files Browse the repository at this point in the history
Add new [E]dit command to the timeline: opens an existing toot to allow
editing it.  Since this is more or less the same operation as posting a
new toot, extend the StatusComposer view to support this rather than
implementing a new view.

Add a new api method, fetch_status_source(), to implement the
/api/v1/statuses/{id}/source endpoint used to fetch the original post
text.
  • Loading branch information
llfw committed Jan 1, 2024
1 parent 301c8d2 commit d999639
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 13 deletions.
55 changes: 55 additions & 0 deletions toot/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,52 @@ def post_status(
return http.post(app, user, '/api/v1/statuses', json=data, headers=headers)


def edit_status(
app,
user,
id,
status,
visibility='public',
media_ids=None,
sensitive=False,
spoiler_text=None,
in_reply_to_id=None,
language=None,
content_type=None,
poll_options=None,
poll_expires_in=None,
poll_multiple=None,
poll_hide_totals=None,
) -> Response:
"""
Edit an existing status
https://docs.joinmastodon.org/methods/statuses/#edit
"""

# Strip keys for which value is None
# Sending null values doesn't bother Mastodon, but it breaks Pleroma
data = drop_empty_values({
'status': status,
'media_ids': media_ids,
'visibility': visibility,
'sensitive': sensitive,
'in_reply_to_id': in_reply_to_id,
'language': language,
'content_type': content_type,
'spoiler_text': spoiler_text,
})

if poll_options:
data["poll"] = {
"options": poll_options,
"expires_in": poll_expires_in,
"multiple": poll_multiple,
"hide_totals": poll_hide_totals,
}

return http.put(app, user, f"/api/v1/statuses/{id}", json=data)


def fetch_status(app, user, id):
"""
Fetch a single status
Expand All @@ -238,6 +284,15 @@ def fetch_status(app, user, id):
return http.get(app, user, f"/api/v1/statuses/{id}")


def fetch_status_source(app, user, id):
"""
Fetch the source (original text) for a single status.
This only works on local toots.
https://docs.joinmastodon.org/methods/statuses/#source
"""
return http.get(app, user, f"/api/v1/statuses/{id}/source")


def scheduled_statuses(app, user):
"""
List scheduled statuses
Expand Down
16 changes: 16 additions & 0 deletions toot/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ def post(app, user, path, headers=None, files=None, data=None, json=None, allow_
return anon_post(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)


def anon_put(url, headers=None, files=None, data=None, json=None, allow_redirects=True):
request = Request(method="PUT", url=url, headers=headers, files=files, data=data, json=json)
response = send_request(request, allow_redirects)

return process_response(response)


def put(app, user, path, headers=None, files=None, data=None, json=None, allow_redirects=True):
url = app.base_url + path

headers = headers or {}
headers["Authorization"] = f"Bearer {user.access_token}"

return anon_put(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)


def patch(app, user, path, headers=None, files=None, data=None, json=None):
url = app.base_url + path

Expand Down
42 changes: 42 additions & 0 deletions toot/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .poll import Poll
from .timeline import Timeline
from .utils import get_max_toot_chars, parse_content_links, copy_to_clipboard
from .widgets import ModalBox

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -429,6 +430,32 @@ def _post(timeline, *args):
urwid.connect_signal(composer, "post", _post)
self.open_overlay(composer, title="Compose status")

def async_edit(self, status):
def _fetch_source():
return api.fetch_status_source(self.app, self.user, status.id).json()

def _done(source):
self.close_overlay()
self.show_edit(status, source)

please_wait = ModalBox("Loading status...")
self.open_overlay(please_wait)

self.run_in_thread(_fetch_source, done_callback=_done)

def show_edit(self, status, source):
def _close(*args):
self.close_overlay()

def _edit(timeline, *args):
self.edit_status(status.data["id"], *args)

composer = StatusComposer(self.max_toot_chars, self.user.username,
visibility=None, edit=status, source=source)
urwid.connect_signal(composer, "close", _close)
urwid.connect_signal(composer, "post", _edit)
self.open_overlay(composer, title="Edit status")

def show_goto_menu(self):
user_timelines = self.config.get("timelines", {})
user_lists = api.get_lists(self.app, self.user) or []
Expand Down Expand Up @@ -576,6 +603,21 @@ def post_status(self, content, warning, visibility, in_reply_to_id):
self.footer.set_message("Status posted {} \\o/".format(status.id))
self.close_overlay()

def edit_status(self, id, content, warning, visibility, in_reply_to_id):
data = api.edit_status(
self.app,
self.user,
id,
content,
spoiler_text=warning,
visibility=visibility,
).json()

status = self.make_status(data)

self.footer.set_message("Status edited {} \\o/".format(status.id))
self.close_overlay()

def show_account(self, account_id):
account = api.whois(self.app, self.user, account_id)
relationship = api.get_relationship(self.app, self.user, account_id)
Expand Down
48 changes: 35 additions & 13 deletions toot/tui/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,57 @@

class StatusComposer(urwid.Frame):
"""
UI for compose and posting a status message.
UI for composing or editing a status message.
To edit a status, provide the original status in 'edit', and optionally
provide the status source (from the /status/:id/source API endpoint) in
'source'; this should have at least a 'text' member, and optionally
'spoiler_text'. If source is not provided, the formatted HTML will be
presented to the user for editing.
"""
signals = ["close", "post"]

def __init__(self, max_chars, username, visibility, in_reply_to=None):
def __init__(self, max_chars, username, visibility, in_reply_to=None,
edit=None, source=None):
self.in_reply_to = in_reply_to
self.max_chars = max_chars
self.username = username

text = self.get_initial_text(in_reply_to)
self.content_edit = EditBox(
edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True)
urwid.connect_signal(self.content_edit.edit, "change", self.text_changed)

self.char_count = urwid.Text(["0/{}".format(max_chars)])
self.edit = edit

self.cw_edit = None
self.cw_add_button = Button("Add content warning",
on_press=self.add_content_warning)
self.cw_remove_button = Button("Remove content warning",
on_press=self.remove_content_warning)

self.visibility = (
in_reply_to.visibility if in_reply_to else visibility
)
if edit:
if source is None:
text = edit.data["content"]
else:
text = source.get("text", edit.data["content"])

if 'spoiler_text' in source:
self.cw_edit = EditBox(multiline=True, allow_tab=True,
edit_text=source['spoiler_text'])

self.visibility = edit.data["visibility"]

else: # not edit
text = self.get_initial_text(in_reply_to)
self.visibility = (
in_reply_to.visibility if in_reply_to else visibility
)

self.content_edit = EditBox(
edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True)
urwid.connect_signal(self.content_edit.edit, "change", self.text_changed)

self.char_count = urwid.Text(["0/{}".format(max_chars)])

self.visibility_button = Button("Visibility: {}".format(self.visibility),
on_press=self.choose_visibility)

self.post_button = Button("Post", on_press=self.post)
self.post_button = Button("Edit" if edit else "Post", on_press=self.post)
self.cancel_button = Button("Cancel", on_press=self.close)

contents = list(self.generate_list_items())
Expand Down
6 changes: 6 additions & 0 deletions toot/tui/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def get_option_text(self, status: Optional[Status]) -> Optional[urwid.Text]:
"[A]ccount" if not status.is_mine else "",
"[B]oost",
"[D]elete" if status.is_mine else "",
"[E]dit" if status.is_mine else "",
"B[o]okmark",
"[F]avourite",
"[V]iew",
Expand Down Expand Up @@ -189,6 +190,11 @@ def keypress(self, size, key):
self.tui.show_delete_confirmation(status)
return

if key in ("e", "E"):
if status.is_mine:
self.tui.async_edit(status)
return

if key in ("f", "F"):
self.tui.async_toggle_favourite(self, status)
return
Expand Down
7 changes: 7 additions & 0 deletions toot/tui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,10 @@ def __init__(self, *args, **kwargs):
button = urwid.RadioButton(*args, **kwargs)
padding = urwid.Padding(button, width=len(args[1]) + 4)
return super().__init__(padding, "button", "button_focused")

class ModalBox(urwid.Frame):
def __init__(self, message):
text = urwid.Text(message)
filler = urwid.Filler(text, valign='top', top=1, bottom=1)
padding = urwid.Padding(filler, left=1, right=1)
return super().__init__(padding)

0 comments on commit d999639

Please sign in to comment.