Hilos funcionan.

This commit is contained in:
Jesús Pavón Abián
2026-02-01 13:01:32 +01:00
parent 6a5e4407ac
commit 9bb1522eca
3 changed files with 175 additions and 15 deletions

View File

@@ -2,7 +2,9 @@ from __future__ import annotations
import logging import logging
import wx import wx
import asyncio
import output import output
from mysc.thread_utils import call_threaded
from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog
from typing import Any from typing import Any
import languageHandler # Ensure _() injection import languageHandler # Ensure _() injection
@@ -135,6 +137,28 @@ class Handler:
kwargs=dict(parent=controller.view.nb, name="direct_messages", session=session) 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 # Start the background poller for real-time-like updates
try: try:
session.start_streaming() session.start_streaming()
@@ -319,6 +343,66 @@ class Handler:
kwargs=dict(parent=controller.view.nb, name=title, session=buffer.session, uri=uri) 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): def open_followers_timeline(self, main_controller, session, user_payload=None):
actor, handle = self._resolve_actor(session, user_payload) actor, handle = self._resolve_actor(session, user_payload)
if not actor: if not actor:
@@ -333,13 +417,28 @@ class Handler:
return return
self._open_user_list(main_controller, session, actor, handle, list_type="following") 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).""" """Open posts timeline for a user (Alt+Win+I)."""
actor, handle = self._resolve_actor(session, user_payload) actor, handle = self._resolve_actor(session, user_payload)
if not actor: if not actor:
output.speak(_("No user selected."), True) output.speak(_("No user selected."), True)
return 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() account_name = session.get_name()
list_name = f"{handle}-timeline" list_name = f"{handle}-timeline"
if main_controller.search_buffer(list_name, account_name): if main_controller.search_buffer(list_name, account_name):
@@ -357,8 +456,21 @@ class Handler:
buffer_title=title, buffer_title=title,
parent_tab=main_controller.view.search(account_name, account_name), parent_tab=main_controller.view.search(account_name, account_name),
start=True, 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 _resolve_actor(self, session, user_payload):
def g(obj, key, default=None): def g(obj, key, default=None):
@@ -371,6 +483,14 @@ class Handler:
if user_payload: if user_payload:
actor = g(user_payload, "did") or g(user_payload, "handle") actor = g(user_payload, "did") or g(user_payload, "handle")
handle = g(user_payload, "handle") or g(user_payload, "did") 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: if not actor:
actor = session.db.get("user_id") or session.db.get("user_name") actor = session.db.get("user_id") or session.db.get("user_name")
handle = session.db.get("user_name") or actor handle = session.db.get("user_name") or actor

View File

@@ -159,22 +159,28 @@ class Conversation(BaseBuffer):
res = api.app.bsky.feed.get_post_thread(params) res = api.app.bsky.feed.get_post_thread(params)
except Exception: except Exception:
res = api.app.bsky.feed.get_post_thread({"uri": self.root_uri}) 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): def g(obj, key, default=None):
if isinstance(obj, dict): if isinstance(obj, dict):
return obj.get(key, default) return obj.get(key, default)
return getattr(obj, key, default) return getattr(obj, key, default)
# Find the root of the thread tree thread = getattr(res, "thread", None) or (res.get("thread") if isinstance(res, dict) else None)
curr = thread if not thread:
while g(curr, "parent"): return 0
curr = g(curr, "parent")
final_items = [] 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): def traverse(node):
if not node: if not node:
return return
@@ -185,7 +191,7 @@ class Conversation(BaseBuffer):
for r in replies: for r in replies:
traverse(r) traverse(r)
traverse(curr) traverse(thread)
# Clear existing items to avoid duplication when refreshing a thread view (which changes structure little) # Clear existing items to avoid duplication when refreshing a thread view (which changes structure little)
self.session.db[self.name] = [] self.session.db[self.name] = []
@@ -324,6 +330,7 @@ class UserTimeline(BaseBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.actor = kwargs.get("actor") self.actor = kwargs.get("actor")
self.handle = kwargs.get("handle")
super(UserTimeline, self).__init__(*args, **kwargs) super(UserTimeline, self).__init__(*args, **kwargs)
self.type = "user_timeline" self.type = "user_timeline"
@@ -341,13 +348,19 @@ class UserTimeline(BaseBuffer):
except Exception: except Exception:
pass pass
actor = self.actor
if isinstance(actor, str):
actor = actor.strip()
if actor.startswith("@"):
actor = actor[1:]
api = self.session._ensure_client() api = self.session._ensure_client()
if not api: if not api:
return 0 return 0
try: try:
res = api.app.bsky.feed.get_author_feed({ res = api.app.bsky.feed.get_author_feed({
"actor": self.actor, "actor": actor,
"limit": count, "limit": count,
}) })
items = getattr(res, "feed", []) or [] items = getattr(res, "feed", []) or []
@@ -357,6 +370,34 @@ class UserTimeline(BaseBuffer):
return self.process_items(list(items), play_sound) 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): class SearchBuffer(BaseBuffer):
"""Buffer for search results (posts).""" """Buffer for search results (posts)."""

View File

@@ -1739,10 +1739,9 @@ class Controller(object):
if author_details: if author_details:
user_payload = author_details user_payload = author_details
async def _open_timeline(): result = handler.open_user_timeline(main_controller=self, session=session_to_use, user_payload=user_payload)
# Pass self (mainController) to the handler method so it can call self.add_buffer if asyncio.iscoroutine(result):
await handler.open_user_timeline(main_controller=self, session=session_to_use, user_payload=user_payload) call_threaded(asyncio.run, result)
wx.CallAfter(asyncio.create_task, _open_timeline())
elif hasattr(handler, 'openPostTimeline'): # Fallback for older handler structure elif hasattr(handler, 'openPostTimeline'): # Fallback for older handler structure
# This path might not correctly pass main_controller if the old handler expects it differently # This path might not correctly pass main_controller if the old handler expects it differently