diff --git a/src/controller/blueski/messages.py b/src/controller/blueski/messages.py index 1bf06f4a..114de73f 100644 --- a/src/controller/blueski/messages.py +++ b/src/controller/blueski/messages.py @@ -232,6 +232,7 @@ class viewPost(base_messages.basicMessage): if not data: output.speak(_("No post available to view."), True) return + self.post_uri = _g(_g(item, "post", item), "uri") or _g(item, "uri") title = _("Post from {}").format(data["author"]) self.message = postDialogs.viewPost( text=data["text"], @@ -251,6 +252,14 @@ class viewPost(base_messages.basicMessage): self.message.enable_button("share") self.item_url = data["item_url"] widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share) + if self.post_uri: + try: + self.message.reposts_button.Enable(True) + self.message.likes_button.Enable(True) + widgetUtils.connect_event(self.message.reposts_button, widgetUtils.BUTTON_PRESSED, self.on_reposts) + widgetUtils.connect_event(self.message.likes_button, widgetUtils.BUTTON_PRESSED, self.on_likes) + except Exception: + pass self.message.ShowModal() def text_processor(self): @@ -260,3 +269,25 @@ class viewPost(base_messages.basicMessage): if hasattr(self, "item_url"): output.copy(self.item_url) output.speak(_("Link copied to clipboard.")) + + def on_reposts(self, *args, **kwargs): + if not self.post_uri: + return + try: + res = self.session.get_post_reposts(self.post_uri, limit=50) + users = res.get("items", []) + from controller.blueski.userList import BlueskyUserList + BlueskyUserList(session=self.session, users=users, title=_("people who reposted this post")) + except Exception: + pass + + def on_likes(self, *args, **kwargs): + if not self.post_uri: + return + try: + res = self.session.get_post_likes(self.post_uri, limit=50) + users = res.get("items", []) + from controller.blueski.userList import BlueskyUserList + BlueskyUserList(session=self.session, users=users, title=_("people who liked this post")) + except Exception: + pass diff --git a/src/controller/blueski/userList.py b/src/controller/blueski/userList.py index 2320aa24..3d9336e1 100644 --- a/src/controller/blueski/userList.py +++ b/src/controller/blueski/userList.py @@ -3,6 +3,10 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, AsyncGenerator +from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog +from controller.userList import UserListController +from controller.blueski import userActions as user_actions_controller + fromapprove.translation import translate as _ # fromapprove.controller.mastodon import userList as mastodon_user_list # If adapting @@ -223,3 +227,41 @@ async def get_user_profile_details(session: BlueskiSession, user_ident: str) -> # Each function needs to handle pagination as provided by the ATProto API (usually cursor-based). logger.info("Blueski userList module loaded (placeholders).") + + +class BlueskyUserList(UserListController): + def process_users(self, users): + def g(obj, key, default=None): + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + processed = [] + for item in users or []: + actor = g(item, "actor") or g(item, "user") or item + did = g(actor, "did") + handle = g(actor, "handle") + display_name = g(actor, "displayName") or g(actor, "display_name") or handle or "Unknown" + label = f"{display_name} (@{handle})" if handle and display_name != handle else (f"@{handle}" if handle else display_name) + processed.append(dict(did=did, handle=handle, display_name=label)) + return processed + + def on_actions(self, *args, **kwargs): + idx = self.dialog.user_list.GetSelection() + if idx < 0 or idx >= len(self.users): + return + handle = self.users[idx].get("handle") + if not handle: + return + user_actions_controller.userActions(self.session, [handle]) + + def on_details(self, *args, **kwargs): + idx = self.dialog.user_list.GetSelection() + if idx < 0 or idx >= len(self.users): + return + user_ident = self.users[idx].get("did") or self.users[idx].get("handle") + if not user_ident: + return + dlg = ShowUserProfileDialog(self.dialog, self.session, user_ident) + dlg.ShowModal() + dlg.Destroy() diff --git a/src/sessions/blueski/session.py b/src/sessions/blueski/session.py index 0db7dbc5..2387b235 100644 --- a/src/sessions/blueski/session.py +++ b/src/sessions/blueski/session.py @@ -485,6 +485,35 @@ class Session(base.baseSession): log.exception("Error fetching Bluesky profile for %s", actor) return None + def get_post_likes(self, uri: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]: + api = self._ensure_client() + try: + params = {"uri": uri, "limit": limit} + if cursor: + params["cursor"] = cursor + res = api.app.bsky.feed.get_likes(params) + return {"items": getattr(res, "likes", []) or [], "cursor": getattr(res, "cursor", None)} + except Exception: + log.exception("Error fetching Bluesky likes for %s", uri) + return {"items": [], "cursor": None} + + def get_post_reposts(self, uri: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]: + api = self._ensure_client() + try: + params = {"uri": uri, "limit": limit} + if cursor: + params["cursor"] = cursor + # SDK uses get_reposted_by (camel or snake) + feed = api.app.bsky.feed + if hasattr(feed, "get_reposted_by"): + res = feed.get_reposted_by(params) + else: + res = feed.get_repostedBy(params) + return {"items": getattr(res, "reposted_by", None) or getattr(res, "repostedBy", None) or getattr(res, "reposted_by", []) or [], "cursor": getattr(res, "cursor", None)} + except Exception: + log.exception("Error fetching Bluesky reposts for %s", uri) + return {"items": [], "cursor": None} + def follow_user(self, did: str) -> bool: api = self._ensure_client() try: diff --git a/src/wxUI/dialogs/userList.py b/src/wxUI/dialogs/userList.py index ff037068..143f8298 100644 --- a/src/wxUI/dialogs/userList.py +++ b/src/wxUI/dialogs/userList.py @@ -26,8 +26,15 @@ class UserListDialog(wx.Dialog): buttons_sizer.Add(self.actions_button, 0, wx.RIGHT, 10) self.details_button = wx.Button(panel, wx.ID_ANY, _("&View profile")) buttons_sizer.Add(self.details_button, 0, wx.RIGHT, 10) + self.load_more_button = wx.Button(panel, wx.ID_ANY, _("&Load more")) + self.load_more_button.Hide() + buttons_sizer.Add(self.load_more_button, 0, wx.RIGHT, 10) close_button = wx.Button(panel, wx.ID_CANCEL, "&Close") buttons_sizer.Add(close_button, 0) main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15) panel.SetSizer(main_sizer) # self.SetSizerAndFit(main_sizer) + + def add_users(self, users): + for user in users: + self.user_list.Append(user)