From a72505e63bcc83388b786079e0000f1e9337fe17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Pav=C3=B3n=20Abi=C3=A1n?= Date: Mon, 2 Feb 2026 09:24:23 +0100 Subject: [PATCH] =?UTF-8?q?Arreglados=20un=20mont=C3=B3n=20de=20bugs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/blueski/handler.py | 77 +++++++-- src/controller/buffers/blueski/base.py | 206 +++++++++++++++--------- src/controller/mainController.py | 2 +- src/keystrokeEditor/actions/__init__.py | 3 +- src/keystrokeEditor/actions/blueski.py | 52 ++++++ src/wxUI/dialogs/blueski/postDialogs.py | 42 +++++ 6 files changed, 288 insertions(+), 94 deletions(-) create mode 100644 src/keystrokeEditor/actions/blueski.py diff --git a/src/controller/blueski/handler.py b/src/controller/blueski/handler.py index fd41b344..ccd9fcc6 100644 --- a/src/controller/blueski/handler.py +++ b/src/controller/blueski/handler.py @@ -475,31 +475,80 @@ class Handler: buffer.on_like(None) def follow(self, buffer): - """Standard action for Ctrl+Win+S""" + """Standard action for Ctrl+Win+S - Opens user actions dialog""" + if not hasattr(buffer, "get_item"): + return session = getattr(buffer, "session", None) if not session: output.speak(_("No active session."), True) return + item = buffer.get_item() + if not item: + return + def g(obj, key, default=None): if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) - user_ident = None - item = buffer.get_item() if hasattr(buffer, "get_item") else None - if item: - if g(item, "handle") or g(item, "did"): - user_ident = g(item, "handle") or g(item, "did") - else: - author = g(item, "author") - if not author: - post = g(item, "post") or g(item, "record") - author = g(post, "author") if post else None - if author: - user_ident = g(author, "handle") or g(author, "did") + users = [] + buffer_type = getattr(buffer, "type", "") + + if buffer_type in ("user", "post_user_list"): + # User buffer - item is a user object + handle = g(item, "handle") + if handle: + users = [handle] + elif buffer_type == "notifications": + # Notification buffer + author = g(item, "author") + if author: + handle = g(author, "handle") + if handle: + users.append(handle) + # Also check for post author in the notification subject + record = g(item, "record") + if record: + subject = g(record, "subject") + if subject: + subject_author = g(subject, "author") + if subject_author: + subject_handle = g(subject_author, "handle") + if subject_handle and subject_handle not in users: + users.append(subject_handle) + else: + # Post buffer - extract author and mentioned users + # Get the actual post (could be nested in "post" key) + actual_post = g(item, "post", item) + record = g(actual_post, "record") or {} + + # Extract mentions from facets + facets = g(record, "facets") or [] + for facet in facets: + features = g(facet, "features") or [] + for feature in features: + ftype = g(feature, "$type") or g(feature, "py_type") or "" + if "mention" in ftype.lower(): + mention_did = g(feature, "did") + # We'd need to resolve DID to handle, but for simplicity just skip + # The main author will be added below + + # Get the post author + author = g(actual_post, "author") or g(item, "author") + if author: + handle = g(author, "handle") + if handle and handle not in users: + users.insert(0, handle) + + # Ensure we have at least the author if no users found + if not users: + author = g(item, "author") or g(g(item, "post"), "author") + if author: + handle = g(author, "handle") + if handle: + users = [handle] - users = [user_ident] if user_ident else [] from controller.blueski import userActions as user_actions_controller user_actions_controller.userActions(session, users) diff --git a/src/controller/buffers/blueski/base.py b/src/controller/buffers/blueski/base.py index 8ffa0303..812ae95c 100644 --- a/src/controller/buffers/blueski/base.py +++ b/src/controller/buffers/blueski/base.py @@ -167,9 +167,13 @@ class BaseBuffer(base.Buffer): menu = menus.baseMenu() widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply) widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.repost) + if hasattr(menu, "quote"): + widgetUtils.connect_event(menu, widgetUtils.MENU, self.quote, menuitem=menu.quote) widgetUtils.connect_event(menu, widgetUtils.MENU, self.add_to_favorites, menuitem=menu.like) widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions) widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl) + if hasattr(menu, "openInBrowser"): + widgetUtils.connect_event(menu, widgetUtils.MENU, self.open_in_browser, menuitem=menu.openInBrowser) widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view) widgetUtils.connect_event(menu, widgetUtils.MENU, self.copy, menuitem=menu.copy) widgetUtils.connect_event(menu, widgetUtils.MENU, self.destroy_status, menuitem=menu.remove) @@ -296,20 +300,88 @@ class BaseBuffer(base.Buffer): call_threaded(do_send) def on_repost(self, evt): - self.share_item(confirm=True) + self.share_item() - def share_item(self, confirm=False, *args, **kwargs): - item = self.get_item() - if not item: return - uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None) - - if confirm: - if wx.MessageBox(_("Repost this?"), _("Confirm"), wx.YES_NO | wx.ICON_QUESTION) != wx.YES: - return + def share_item(self, event=None, item=None, *args, **kwargs): + if item is None: + item = self.get_item() + if not item: + return - self.session.repost(uri) - self.session.sound.play("retweet_send.ogg") - output.speak(_("Reposted.")) + def g(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + # Get the URI for reposting + uri = g(item, "uri") or g(g(item, "post"), "uri") + cid = g(item, "cid") or g(g(item, "post"), "cid") + if not uri: + output.speak(_("Could not find post to repost."), True) + return + + # Check boost_mode setting + boost_mode = self.session.settings["general"].get("boost_mode", "ask") + if boost_mode == "ask": + from wxUI.dialogs.blueski.postDialogs import repost_question + answer = repost_question() + if answer == 1: + self._direct_repost(uri) + elif answer == 2: + self.quote(item=item) + else: + self._direct_repost(uri) + + def _direct_repost(self, uri): + try: + self.session.repost(uri) + self.session.sound.play("retweet_send.ogg") + output.speak(_("Reposted.")) + except Exception as e: + log.error("Error reposting: %s", e) + output.speak(_("Failed to repost."), True) + + def quote(self, event=None, item=None, *args, **kwargs): + if item is None: + item = self.get_item() + if not item: + return + + def g(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + uri = g(item, "uri") or g(g(item, "post"), "uri") + if not uri: + output.speak(_("Could not find post to quote."), True) + return + + title = _("Quote post") + caption = _("Write your comment here") + dlg = blueski_messages.post(session=self.session, title=title, caption=caption) + if dlg.message.ShowModal() == wx.ID_OK: + text, files, cw, langs = dlg.get_data() + if text or files: + def do_quote(): + try: + result = self.session.send_message( + message=text, + files=files, + cw_text=cw, + langs=langs, + quote_uri=uri, + ) + if result: + wx.CallAfter(self.session.sound.play, "retweet_send.ogg") + wx.CallAfter(output.speak, _("Quote posted.")) + else: + wx.CallAfter(output.speak, _("Failed to post quote."), True) + except Exception as e: + log.error("Error posting quote: %s", e) + wx.CallAfter(output.speak, _("Failed to post quote."), True) + call_threaded(do_quote) + dlg.message.Destroy() def on_like(self, evt): self.toggle_favorite(confirm=False) @@ -419,28 +491,24 @@ class BaseBuffer(base.Buffer): if not did: return - if self.showing == False: - dlg = wx.TextEntryDialog(None, _("Message to {0}:").format(handle), _("Send Message")) - if dlg.ShowModal() == wx.ID_OK: - text = dlg.GetValue() - if text: - try: - api = self.session._ensure_client() - dm_client = api.with_bsky_chat_proxy() - # Get or create conversation - res = dm_client.chat.bsky.convo.get_convo_for_members({"members": [did]}) - convo_id = res.convo.id - self.session.send_chat_message(convo_id, text) - self.session.sound.play("dm_sent.ogg") - output.speak(_("Message sent."), True) - except Exception as e: - log.error("Error sending Bluesky DM: %s", e) - output.speak(_("Failed to send message."), True) - dlg.Destroy() - return - - # If showing, we'll just open the chat buffer for now as it's more structured - self.view_chat_with_user(did, handle) + # Show simple text entry dialog for sending DM + dlg = wx.TextEntryDialog(None, _("Message to {0}:").format(handle), _("Send Message")) + if dlg.ShowModal() == wx.ID_OK: + text = dlg.GetValue() + if text: + try: + api = self.session._ensure_client() + dm_client = api.with_bsky_chat_proxy() + # Get or create conversation + res = dm_client.chat.bsky.convo.get_convo_for_members({"members": [did]}) + convo_id = res.convo.id + self.session.send_chat_message(convo_id, text) + self.session.sound.play("dm_sent.ogg") + output.speak(_("Message sent."), True) + except Exception as e: + log.error("Error sending Bluesky DM: %s", e) + output.speak(_("Failed to send message."), True) + dlg.Destroy() def view(self, *args, **kwargs): self.view_item() @@ -461,46 +529,31 @@ class BaseBuffer(base.Buffer): def url_(self, *args, **kwargs): self.url() - - def url(self, *args, **kwargs): - item = self.get_item() - if not item: return + def url(self, announce=True, item=None, *args, **kwargs): + """Open URLs found in the post content.""" + if item is None: + item = self.get_item() + if not item: + return import webbrowser - - def g(obj, key, default=None): - if isinstance(obj, dict): - return obj.get(key, default) - return getattr(obj, key, default) + from wxUI.dialogs import urlList - uri = g(item, "uri") - author = g(item, "author") or g(g(item, "post"), "author") - handle = g(author, "handle") - - if uri and handle: - # URI format: at://did:plc:xxx/app.bsky.feed.post/rkey - if "app.bsky.feed.post" in uri: - rkey = uri.split("/")[-1] - url = f"https://bsky.app/profile/{handle}/post/{rkey}" - webbrowser.open(url) - return - elif "app.bsky.feed.like" in uri: - # It's a like notification, try to get the subject - subject = g(item, "subject") - subject_uri = g(subject, "uri") if subject else None - if subject_uri: - rkey = subject_uri.split("/")[-1] - # We might not have the handle of the post author here easily if it's not in the notification - # But let's try... - # Actually, notification items usually have enough info or we can't deep direct link easily without fetching. - # For now, let's just open the profile of the liker - pass - - # Fallback to profile - if handle: - url = f"https://bsky.app/profile/{handle}" - webbrowser.open(url) - return + urls = utils.find_urls(item) + url = "" + if len(urls) == 1: + url = urls[0] + elif len(urls) > 1: + urls_list = urlList.urlList() + urls_list.populate_list(urls) + if urls_list.get_response() == widgetUtils.OK: + url = urls_list.get_string() + if hasattr(urls_list, "destroy"): + urls_list.destroy() + if url != '': + if announce: + output.speak(_(u"Opening URL..."), True) + webbrowser.open_new_tab(url) def user_actions(self, *args, **kwargs): pub.sendMessage("execute-action", action="follow") @@ -617,13 +670,10 @@ class BaseBuffer(base.Buffer): def reply(self, *args, **kwargs): self.on_reply(None) - + def post_status(self, *args, **kwargs): self.on_post(None) - - def share_item(self, *args, **kwargs): - self.on_repost(None) - + def destroy_status(self, *args, **kwargs): # Delete post item = self.get_item() @@ -1007,14 +1057,14 @@ class BaseBuffer(base.Buffer): if "app.bsky.feed.post" in uri: rkey = uri.split("/")[-1] url = f"https://bsky.app/profile/{handle}/post/{rkey}" - output.speak(_("Opening in browser...")) + output.speak(_("Opening item in web browser...")) webbrowser.open(url) return # Fallback to profile if handle: url = f"https://bsky.app/profile/{handle}" - output.speak(_("Opening profile in browser...")) + output.speak(_("Opening item in web browser...")) webbrowser.open(url) def save_positions(self): diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 5c2a91a4..fadde33e 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -1426,7 +1426,7 @@ class Controller(object): current_cursor = None can_load_more_natively = False - if session.KIND == "blueski": + if getattr(session, "KIND", None) == "blueski": if hasattr(bf, "load_more_posts"): # For BlueskiUserTimelinePanel & BlueskiHomeTimelinePanel can_load_more_natively = True if hasattr(bf, "load_more_posts"): diff --git a/src/keystrokeEditor/actions/__init__.py b/src/keystrokeEditor/actions/__init__.py index f18d4a6f..5d46d1a2 100644 --- a/src/keystrokeEditor/actions/__init__.py +++ b/src/keystrokeEditor/actions/__init__.py @@ -1 +1,2 @@ -from . import mastodon \ No newline at end of file +from . import mastodon +from . import blueski \ No newline at end of file diff --git a/src/keystrokeEditor/actions/blueski.py b/src/keystrokeEditor/actions/blueski.py new file mode 100644 index 00000000..ed7a0097 --- /dev/null +++ b/src/keystrokeEditor/actions/blueski.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +actions = { + "up": _(u"Go up in the current buffer"), + "down": _(u"Go down in the current buffer"), + "left": _(u"Go to the previous buffer"), + "right": _(u"Go to the next buffer"), + "next_account": _(u"Focus the next session"), + "previous_account": _(u"Focus the previous session"), + "show_hide": _(u"Show or hide the GUI"), + "post_tweet": _("Make a new post"), + "post_reply": _(u"Reply"), + "post_retweet": _(u"Repost"), + "send_dm": _(u"Send direct message"), + "add_to_favourites": _("Add post to likes"), + "remove_from_favourites": _(u"Remove post from likes"), + "toggle_like": _("Add/remove post from likes"), + "follow": _(u"Open the user actions dialogue"), + "user_details": _(u"See user details"), + "view_item": _(u"Show post"), + "exit": _(u"Quit"), + "open_timeline": _(u"Open user timeline"), + "remove_buffer": _(u"Destroy buffer"), + "url": _(u"Open URL"), + "open_in_browser": _(u"View in browser"), + "volume_up": _(u"Increase volume by 5%"), + "volume_down": _(u"Decrease volume by 5%"), + "go_home": _(u"Jump to the first element of a buffer"), + "go_end": _(u"Jump to the last element of the current buffer"), + "go_page_up": _(u"Jump 20 elements up in the current buffer"), + "go_page_down": _(u"Jump 20 elements down in the current buffer"), + "delete": _("Delete post"), + "clear_buffer": _(u"Empty the current buffer"), + "repeat_item": _(u"Repeat last item"), + "copy_to_clipboard": _(u"Copy to clipboard"), + "toggle_buffer_mute": _(u"Mute/unmute the active buffer"), + "toggle_session_mute": _(u"Mute/unmute the current session"), + "toggle_autoread": _(u"Toggle the automatic reading of incoming posts in the active buffer"), + "search": _(u"Search"), + "find": _(u"Find a string in the currently focused buffer"), + "edit_keystrokes": _(u"Show the keystroke editor"), + "get_more_items": _(u"Load previous items"), + "open_conversation": _(u"View conversation"), + "check_for_updates": _(u"Check and download updates"), + "configuration": _(u"Opens the global settings dialogue"), + "accountConfiguration": _(u"Opens the account settings dialogue"), + "audio": _(u"Try to play a media file"), + "update_buffer": _(u"Updates the buffer and retrieves possible lost items there."), + "ocr_image": _(u"Extracts the text from a picture and displays the result in a dialog."), + "seekLeft": _(u"Seek media backward"), + "seekRight": _(u"Seek media forward"), + "manage_accounts": _(u"Manage accounts"), +} diff --git a/src/wxUI/dialogs/blueski/postDialogs.py b/src/wxUI/dialogs/blueski/postDialogs.py index 98ea00be..6346a1a0 100644 --- a/src/wxUI/dialogs/blueski/postDialogs.py +++ b/src/wxUI/dialogs/blueski/postDialogs.py @@ -228,3 +228,45 @@ class viewText(wx.Dialog): panel.SetSizer(mainBox) self.SetClientSize(mainBox.CalcMin()) + +class RepostDialog(wx.Dialog): + def __init__(self): + super(RepostDialog, self).__init__(None, title=_("Repost")) + p = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + lbl = wx.StaticText(p, wx.ID_ANY, _("What would you like to do with this post?")) + sizer.Add(lbl, 0, wx.ALL, 10) + + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.btn_repost = wx.Button(p, wx.ID_ANY, _("Repost")) + self.btn_quote = wx.Button(p, wx.ID_ANY, _("Quote")) + self.btn_cancel = wx.Button(p, wx.ID_CANCEL, _("Cancel")) + + btn_sizer.Add(self.btn_repost, 0, wx.ALL, 5) + btn_sizer.Add(self.btn_quote, 0, wx.ALL, 5) + btn_sizer.Add(self.btn_cancel, 0, wx.ALL, 5) + + sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER) + p.SetSizer(sizer) + sizer.Fit(self) + + self.btn_repost.Bind(wx.EVT_BUTTON, self.on_repost) + self.btn_quote.Bind(wx.EVT_BUTTON, self.on_quote) + self.result = 0 + + def on_repost(self, event): + self.result = 1 + self.EndModal(wx.ID_OK) + + def on_quote(self, event): + self.result = 2 + self.EndModal(wx.ID_OK) + + +def repost_question(): + dlg = RepostDialog() + dlg.ShowModal() + result = dlg.result + dlg.Destroy() + return result +