-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds translate status feature (when connected to a Mastodon 4.x server with translation enabled) #252
Adds translate status feature (when connected to a Mastodon 4.x server with translation enabled) #252
Changes from 2 commits
a146e49
884c409
4d8fd72
39f4670
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -11,7 +11,7 @@ | |||||
from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks, StatusZoom | ||||||
from .overlays import StatusDeleteConfirmation | ||||||
from .timeline import Timeline | ||||||
from .utils import parse_content_links, show_media | ||||||
from .utils import Option, parse_content_links, show_media, versiontuple | ||||||
|
||||||
logger = logging.getLogger(__name__) | ||||||
|
||||||
|
@@ -106,6 +106,7 @@ def __init__(self, app, user): | |||||
self.timeline = None | ||||||
self.overlay = None | ||||||
self.exception = None | ||||||
self.can_translate = Option.UNKNOWN | ||||||
|
||||||
super().__init__(self.body, header=self.header, footer=self.footer) | ||||||
|
||||||
|
@@ -206,6 +207,7 @@ def _zoom(timeline, status_details): | |||||
urwid.connect_signal(timeline, "source", _source) | ||||||
urwid.connect_signal(timeline, "links", _links) | ||||||
urwid.connect_signal(timeline, "zoom", _zoom) | ||||||
urwid.connect_signal(timeline, "translate", self.async_translate) | ||||||
|
||||||
def build_timeline(self, name, statuses, local): | ||||||
def _close(*args): | ||||||
|
@@ -232,7 +234,7 @@ def _toggle_save(timeline, status): | |||||
self.loop.set_alarm_in(5, lambda *args: self.footer.clear_message()) | ||||||
config.save_config(self.config) | ||||||
|
||||||
timeline = Timeline(name, statuses) | ||||||
timeline = Timeline(name, statuses, self.can_translate) | ||||||
|
||||||
self.connect_default_timeline_signals(timeline) | ||||||
urwid.connect_signal(timeline, "next", _next) | ||||||
|
@@ -261,8 +263,8 @@ def _close(*args): | |||||
statuses = ancestors + [status] + descendants | ||||||
focus = len(ancestors) | ||||||
|
||||||
timeline = Timeline("thread", statuses, focus, is_thread=True) | ||||||
|
||||||
timeline = Timeline("thread", statuses, can_translate, focus, | ||||||
is_thread=True) | ||||||
self.connect_default_timeline_signals(timeline) | ||||||
urwid.connect_signal(timeline, "close", _close) | ||||||
|
||||||
|
@@ -303,15 +305,31 @@ def async_load_instance(self): | |||||
Attempt to update max_toot_chars from instance data. | ||||||
Does not work on vanilla Mastodon, works on Pleroma. | ||||||
See: https://github.com/tootsuite/mastodon/issues/4915 | ||||||
|
||||||
Also attempt to update translation flag from instance | ||||||
data. Translation is only present on Mastodon 4+ servers | ||||||
where the administrator has enabled this feature. | ||||||
See: https://github.com/mastodon/mastodon/issues/19328 | ||||||
""" | ||||||
def _load_instance(): | ||||||
return api.get_instance(self.app.instance) | ||||||
|
||||||
def _done(instance): | ||||||
if "max_toot_chars" in instance: | ||||||
self.max_toot_chars = instance["max_toot_chars"] | ||||||
if "translation" in instance: | ||||||
# instance is advertising translation service | ||||||
self.can_translate = Option.YES | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at mastodon/mastodon@8046cf3#diff-fdc9457d4797dfa4b6e351a974127f11b1efc7eaeadb84718976f98af3618026 Just having "translation" in instance info is not enough, we need to check if
Suggested change
(this is presuming we change this from an Option to bool) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||||||
else: | ||||||
if "version" in instance and versiontuple(instance["version"])[0] < 4: | ||||||
# Mastodon versions < 4 do not have translation service | ||||||
self.can_translate = Option.NO | ||||||
|
||||||
# translation service for Mastodon version 4.0.0-4.0.2 that do not advertise | ||||||
# is indeterminate; as of now versions up to 4.0.2 cannot advertise | ||||||
# even if they provide the service, but future versions, perhaps 4.0.3+ | ||||||
# will be able to advertise. | ||||||
|
||||||
return self.run_in_thread(_load_instance, done_callback=_done) | ||||||
ihabunek marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
def refresh_footer(self, timeline): | ||||||
"""Show status details in footer.""" | ||||||
|
@@ -484,6 +502,41 @@ def _done(loop): | |||||
done_callback=_done | ||||||
) | ||||||
|
||||||
def async_translate(self, timeline, status): | ||||||
def _translate(): | ||||||
logger.info("Translating {}".format(status)) | ||||||
self.footer.set_message("Translating status {}".format(status.id)) | ||||||
|
||||||
try: | ||||||
response = api.translate(self.app, self.user, status.id) | ||||||
# we were successful so we know translation service is available. | ||||||
# make our timeline aware of that right away. | ||||||
self.can_translate = Option.YES | ||||||
timeline.update_can_translate(Option.YES) | ||||||
except: | ||||||
response = None | ||||||
finally: | ||||||
self.footer.clear_message() | ||||||
|
||||||
return response | ||||||
|
||||||
def _done(response): | ||||||
if response is not None: | ||||||
# Create a new Status that is translated | ||||||
new_data = status.data | ||||||
new_data["content"] = response["content"] | ||||||
new_data["detected_source_language"] = response["detected_source_language"] | ||||||
new_status = self.make_status(new_data) | ||||||
|
||||||
timeline.update_status(new_status) | ||||||
self.footer.set_message(f"Translated status {status.id} from {response['detected_source_language']}") | ||||||
else: | ||||||
self.footer.set_error_message("Translate server error") | ||||||
|
||||||
self.loop.set_alarm_in(5, lambda *args: self.footer.clear_message()) | ||||||
|
||||||
self.run_in_thread(_translate, done_callback=_done ) | ||||||
|
||||||
def async_delete_status(self, timeline, status): | ||||||
def _delete(): | ||||||
api.delete_status(self.app, self.user, status.id) | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
|
||
from toot.utils import format_content | ||
|
||
from .utils import highlight_hashtags, parse_datetime, highlight_keys | ||
from .utils import Option, highlight_hashtags, parse_datetime, highlight_keys | ||
from .widgets import SelectableText, SelectableColumns | ||
|
||
logger = logging.getLogger("toot") | ||
|
@@ -28,19 +28,21 @@ class Timeline(urwid.Columns): | |
"source", # Show status source | ||
"links", # Show status links | ||
"thread", # Show thread for status | ||
"translate", # Translate status | ||
"save", # Save current timeline | ||
"zoom", # Open status in scrollable popup window | ||
] | ||
|
||
def __init__(self, name, statuses, focus=0, is_thread=False): | ||
def __init__(self, name, statuses, can_translate, focus=0, is_thread=False): | ||
self.name = name | ||
self.is_thread = is_thread | ||
self.statuses = statuses | ||
self.can_translate = can_translate | ||
self.status_list = self.build_status_list(statuses, focus=focus) | ||
try: | ||
self.status_details = StatusDetails(statuses[focus], is_thread) | ||
self.status_details = StatusDetails(statuses[focus], is_thread, can_translate) | ||
except IndexError: | ||
self.status_details = StatusDetails(None, is_thread) | ||
self.status_details = StatusDetails(None, is_thread, can_translate) | ||
|
||
super().__init__([ | ||
("weight", 40, self.status_list), | ||
|
@@ -97,7 +99,7 @@ def refresh_status_details(self): | |
self.draw_status_details(status) | ||
|
||
def draw_status_details(self, status): | ||
self.status_details = StatusDetails(status, self.is_thread) | ||
self.status_details = StatusDetails(status, self.is_thread, self.can_translate) | ||
self.contents[2] = urwid.Padding(self.status_details, left=1), ("weight", 60, False) | ||
|
||
def keypress(self, size, key): | ||
|
@@ -157,7 +159,12 @@ def keypress(self, size, key): | |
self._emit("links", status) | ||
return | ||
|
||
if key in ("t", "T"): | ||
if key in ("n", "N"): | ||
if self.can_translate != Option.NO: | ||
self._emit("translate", status) | ||
return | ||
|
||
if key in ("r", "R"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You changed the key binding for thread here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ouch. Not sure how that happened. Fixed. |
||
self._emit("thread", status) | ||
return | ||
|
||
|
@@ -226,9 +233,11 @@ def remove_status(self, status): | |
del(self.status_list.body[index]) | ||
self.refresh_status_details() | ||
|
||
def update_can_translate(self, can_translate): | ||
self.can_translate = can_translate | ||
|
||
class StatusDetails(urwid.Pile): | ||
def __init__(self, status, in_thread): | ||
def __init__(self, status, in_thread, can_translate=Option.UNKNOWN): | ||
""" | ||
Parameters | ||
---------- | ||
|
@@ -239,6 +248,7 @@ def __init__(self, status, in_thread): | |
Whether the status is rendered from a thread status list. | ||
""" | ||
self.in_thread = in_thread | ||
self.can_translate = can_translate | ||
reblogged_by = status.author if status and status.reblog else None | ||
widget_list = list(self.content_generator(status.original, reblogged_by) | ||
if status else ()) | ||
|
@@ -290,10 +300,14 @@ def content_generator(self, status, reblogged_by): | |
application = application.get("name") | ||
|
||
yield ("pack", urwid.AttrWrap(urwid.Divider("-"), "gray")) | ||
|
||
translated = status.data.get("detected_source_language") | ||
|
||
yield ("pack", urwid.Text([ | ||
("gray", "⤶ {} ".format(status.data["replies_count"])), | ||
("yellow" if status.reblogged else "gray", "♺ {} ".format(status.data["reblogs_count"])), | ||
("yellow" if status.favourited else "gray", "★ {}".format(status.data["favourites_count"])), | ||
("yellow" if translated else "gray", " · Translated from {} ".format(translated) if translated else ""), | ||
("gray", " · {}".format(application) if application else ""), | ||
])) | ||
|
||
|
@@ -310,6 +324,9 @@ def content_generator(self, status, reblogged_by): | |
"[R]eply", | ||
"So[u]rce", | ||
"[Z]oom", | ||
"Tra[n]slate" if self.can_translate == Option.YES \ | ||
else "Tra[n]slate?" if self.can_translate == Option.UNKNOWN \ | ||
else "", | ||
"[H]elp", | ||
] | ||
options = " ".join(o for o in options if o) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think using Option here is very beneficial and it complicates things. I would just set it to
False
here, and set it toTrue
later when we determine if translate is available.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done