From 9bb1522ecab3b11b440c33124a0828c1da52601b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Pav=C3=B3n=20Abi=C3=A1n?= Date: Sun, 1 Feb 2026 13:01:32 +0100 Subject: [PATCH] Hilos funcionan. --- src/controller/blueski/handler.py | 124 ++++++++++++++++++++- src/controller/buffers/blueski/timeline.py | 59 ++++++++-- src/controller/mainController.py | 7 +- 3 files changed, 175 insertions(+), 15 deletions(-) diff --git a/src/controller/blueski/handler.py b/src/controller/blueski/handler.py index 2712913c..219bc767 100644 --- a/src/controller/blueski/handler.py +++ b/src/controller/blueski/handler.py @@ -2,7 +2,9 @@ from __future__ import annotations import logging import wx +import asyncio import output +from mysc.thread_utils import call_threaded from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog from typing import Any import languageHandler # Ensure _() injection @@ -135,6 +137,28 @@ class Handler: kwargs=dict(parent=controller.view.nb, name="direct_messages", session=session) ) + # Saved user timelines + try: + timelines = session.settings["other_buffers"].get("timelines") + if timelines is None: + timelines = [] + if isinstance(timelines, str): + timelines = [t for t in timelines.split(",") if t] + for actor in timelines: + handle = actor + title = _("Timeline for {user}").format(user=handle) + pub.sendMessage( + "createBuffer", + buffer_type="UserTimeline", + session_type="blueski", + buffer_title=title, + parent_tab=root_position, + start=False, + kwargs=dict(parent=controller.view.nb, name=f"{handle}-timeline", session=session, actor=actor, handle=handle) + ) + except Exception: + logger.exception("Failed to restore Bluesky timeline buffers") + # Start the background poller for real-time-like updates try: session.start_streaming() @@ -319,6 +343,66 @@ class Handler: kwargs=dict(parent=controller.view.nb, name=title, session=buffer.session, uri=uri) ) + def open_timeline(self, controller, buffer, default="posts"): + if not hasattr(buffer, "get_item"): + return + item = buffer.get_item() + if not item: + output.speak(_("No user selected."), True) + return + + def g(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + handle = None + if hasattr(buffer, "get_selected_item_author_details"): + details = buffer.get_selected_item_author_details() + if details: + handle = details.get("handle") or details.get("did") + if not handle: + if g(item, "handle") or g(item, "did"): + handle = g(item, "handle") or g(item, "did") + else: + author = g(item, "author") or g(g(item, "post"), "author") + if author: + handle = g(author, "handle") or g(author, "did") + + if not handle: + output.speak(_("No user selected."), True) + return + + from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog + dlg = userTimelineDialog.UserTimeline(users=[handle], default=default) + try: + if hasattr(dlg, "autocompletion"): + dlg.autocompletion.Enable(False) + except Exception: + pass + if dlg.ShowModal() != wx.ID_OK: + dlg.Destroy() + return + + action = dlg.get_action() + user = dlg.get_user().strip() or handle + dlg.Destroy() + + if user.startswith("@"): + user = user[1:] + user_payload = {"handle": user} + if action == "posts": + result = self.open_user_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload) + elif action == "followers": + result = self.open_followers_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload) + elif action == "following": + result = self.open_following_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload) + else: + return + + if asyncio.iscoroutine(result): + call_threaded(asyncio.run, result) + def open_followers_timeline(self, main_controller, session, user_payload=None): actor, handle = self._resolve_actor(session, user_payload) if not actor: @@ -333,13 +417,28 @@ class Handler: return self._open_user_list(main_controller, session, actor, handle, list_type="following") - async def open_user_timeline(self, main_controller, session, user_payload=None): + def open_user_timeline(self, main_controller, session, user_payload=None): """Open posts timeline for a user (Alt+Win+I).""" actor, handle = self._resolve_actor(session, user_payload) if not actor: output.speak(_("No user selected."), True) return + # If we only have a handle, try to resolve DID for reliability + try: + if isinstance(actor, str) and not actor.startswith("did:"): + profile = session.get_profile(actor) + if profile: + def g(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + did = g(profile, "did") + if did: + actor = did + except Exception: + pass + account_name = session.get_name() list_name = f"{handle}-timeline" if main_controller.search_buffer(list_name, account_name): @@ -357,8 +456,21 @@ class Handler: buffer_title=title, parent_tab=main_controller.view.search(account_name, account_name), start=True, - kwargs=dict(parent=main_controller.view.nb, name=list_name, session=session, actor=actor) + kwargs=dict(parent=main_controller.view.nb, name=list_name, session=session, actor=actor, handle=handle) ) + try: + timelines = session.settings["other_buffers"].get("timelines") + if timelines is None: + timelines = [] + if isinstance(timelines, str): + timelines = [t for t in timelines.split(",") if t] + key = handle or actor + if key and key not in timelines: + timelines.append(key) + session.settings["other_buffers"]["timelines"] = timelines + session.settings.write() + except Exception: + logger.exception("Failed to persist Bluesky timeline buffer") def _resolve_actor(self, session, user_payload): def g(obj, key, default=None): @@ -371,6 +483,14 @@ class Handler: if user_payload: actor = g(user_payload, "did") or g(user_payload, "handle") handle = g(user_payload, "handle") or g(user_payload, "did") + if isinstance(actor, str): + actor = actor.strip() + if actor.startswith("@"): + actor = actor[1:] + if isinstance(handle, str): + handle = handle.strip() + if handle.startswith("@"): + handle = handle[1:] if not actor: actor = session.db.get("user_id") or session.db.get("user_name") handle = session.db.get("user_name") or actor diff --git a/src/controller/buffers/blueski/timeline.py b/src/controller/buffers/blueski/timeline.py index ab3d99a9..63c8fa51 100644 --- a/src/controller/buffers/blueski/timeline.py +++ b/src/controller/buffers/blueski/timeline.py @@ -159,22 +159,28 @@ class Conversation(BaseBuffer): res = api.app.bsky.feed.get_post_thread(params) except Exception: res = api.app.bsky.feed.get_post_thread({"uri": self.root_uri}) - thread = getattr(res, "thread", None) - if not thread: - return 0 def g(obj, key, default=None): if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) - # Find the root of the thread tree - curr = thread - while g(curr, "parent"): - curr = g(curr, "parent") + thread = getattr(res, "thread", None) or (res.get("thread") if isinstance(res, dict) else None) + if not thread: + return 0 final_items = [] + # Add parent chain (oldest to newest) if available + ancestors = [] + parent = g(thread, "parent") + while parent: + ppost = g(parent, "post") + if ppost: + ancestors.insert(0, ppost) + parent = g(parent, "parent") + final_items.extend(ancestors) + def traverse(node): if not node: return @@ -185,7 +191,7 @@ class Conversation(BaseBuffer): for r in replies: traverse(r) - traverse(curr) + traverse(thread) # Clear existing items to avoid duplication when refreshing a thread view (which changes structure little) self.session.db[self.name] = [] @@ -324,6 +330,7 @@ class UserTimeline(BaseBuffer): def __init__(self, *args, **kwargs): self.actor = kwargs.get("actor") + self.handle = kwargs.get("handle") super(UserTimeline, self).__init__(*args, **kwargs) self.type = "user_timeline" @@ -341,13 +348,19 @@ class UserTimeline(BaseBuffer): except Exception: pass + actor = self.actor + if isinstance(actor, str): + actor = actor.strip() + if actor.startswith("@"): + actor = actor[1:] + api = self.session._ensure_client() if not api: return 0 try: res = api.app.bsky.feed.get_author_feed({ - "actor": self.actor, + "actor": actor, "limit": count, }) items = getattr(res, "feed", []) or [] @@ -357,6 +370,34 @@ class UserTimeline(BaseBuffer): return self.process_items(list(items), play_sound) + def remove_buffer(self, force=False): + if not force: + from wxUI import commonMessageDialogs + import widgetUtils + dlg = commonMessageDialogs.remove_buffer() + if dlg != widgetUtils.YES: + return False + try: + self.session.db.pop(self.name, None) + except Exception: + pass + try: + timelines = self.session.settings["other_buffers"].get("timelines") + if timelines is None: + timelines = [] + if isinstance(timelines, str): + timelines = [t for t in timelines.split(",") if t] + actor = self.actor or "" + handle = self.handle or "" + for key in (actor, handle): + if key in timelines: + timelines.remove(key) + self.session.settings["other_buffers"]["timelines"] = timelines + self.session.settings.write() + except Exception: + log.exception("Error updating Bluesky timelines settings") + return True + class SearchBuffer(BaseBuffer): """Buffer for search results (posts).""" diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 4cb554e0..7f71e5a2 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -1739,10 +1739,9 @@ class Controller(object): if author_details: user_payload = author_details - async def _open_timeline(): - # Pass self (mainController) to the handler method so it can call self.add_buffer - await handler.open_user_timeline(main_controller=self, session=session_to_use, user_payload=user_payload) - wx.CallAfter(asyncio.create_task, _open_timeline()) + result = handler.open_user_timeline(main_controller=self, session=session_to_use, user_payload=user_payload) + if asyncio.iscoroutine(result): + call_threaded(asyncio.run, result) elif hasattr(handler, 'openPostTimeline'): # Fallback for older handler structure # This path might not correctly pass main_controller if the old handler expects it differently