diff --git a/context.md b/context.md new file mode 100644 index 00000000..85e01463 --- /dev/null +++ b/context.md @@ -0,0 +1,28 @@ +# Contexto de trabajo + +## Objetivo final +Igualar la experiencia de Bluesky con Mastodon en la interfaz (men?s, di?logos, buffers y accesos), manteniendo las diferencias s?lo cuando el protocolo lo exige. Mastodon es la referencia. + +## Estado actual +Se est? siguiendo `falta.md` por orden. Los puntos 1 a 5 ya est?n marcados como "Hecho". + +## Cambios recientes +- Activado autocompletado en el di?logo "Ver timeline..." y validaci?n de usuario. +- Reposts/Likes ahora abren buffers con paginaci?n bajo "Timelines". +- Restauraci?n de followers/following propios sin duplicar. +- Estructura del ?rbol: se a?adi? "Searches" en Bluesky. +- Men?s: para Bluesky, las opciones no aplicables se ocultan (etiqueta vac?a) usando el sentinel "HIDE" en `handler.menus`. + +## Puntos pendientes (seg?n falta.md) +- 6) Perfil de usuario (igualar estructura si el protocolo permite). +- 7) Di?logo de acciones de usuario (autocompletado/b?squeda avanzada). +- 8) Consistencia de nombres/etiquetas. +- 9) Paginaci?n en listados restantes. +- 10) Accesibilidad/teclado. +- 11) Persistencia total (b?squedas y otros buffers). + +## Notas t?cnicas +- `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas (label vac?o + disabled). +- Buffers de Reposts/Likes usan `PostUserListBuffer` y `get_post_likes/get_post_reposts` con cursor. +- El nodo "Searches" ahora existe en Bluesky y se usa al crear b?squedas. + diff --git a/falta.md b/falta.md new file mode 100644 index 00000000..35b72d51 --- /dev/null +++ b/falta.md @@ -0,0 +1,57 @@ +# Pendientes para igualar la UI de Bluesky con Mastodon + +Objetivo: la experiencia debe ser id?ntica a Mastodon siempre que el protocolo lo permita; si no existe algo en blueski que si en mastodon, debe no diseƱarse. Por ejemplo comunities no tiene mucho sentido. + +## 1) Di?logo "Ver timeline..." (Alt+Win+I) +- Autocompletado de usuarios como en Mastodon (bot?n "Autocomplete users"). +- Selecci?n y listado de m?ltiples usuarios (no solo autor/mentions con facetas). +- Resoluci?n de handles/DIDs en segundo plano con feedback accesible. +Hecho. + +## 2) Listas de Reposts/Likes (desde "Ver post") +- En Mastodon se abren listas tipo buffer; en Bluesky ahora es di?logo con paginaci?n. +- Igualar creando buffers dedicados (UserBuffer/FollowersBuffer) bajo nodo "Timelines" o "Searches". +- Mantener paginaci?n y persistencia coherentes con Mastodon (cursor + get_more_items). +Hecho. + +## 3) Restauraci?n de buffers "Followers/Following" propios +- En ejecuci?n ya reutiliza los buffers principales del usuario. +- Al restaurar tras reinicio, debe saltar a los buffers propios (si ya existen) y no duplicar. +Hecho. + +## 4) Estructura del ?rbol (Treebook) +- Mastodon crea nodos vac?os: "Timelines", "Searches", "Communities". +- En Bluesky solo existe "Timelines". +- Crear nodos equivalentes siempre y cuando aplique por protocolo. +Hecho. + +## 5) Men?s/acciones de ?tem +- Mastodon incluye OCR, filtros, listas, community timelines, etc. +- Bluesky carece de varias acciones. +- Decidir por acci?n: implementar, deshabilitar o mostrar mensaje "No soportado" para igualar UI. +Hecho. + +## 6) Perfil de usuario +- Mastodon muestra campos y acciones adicionales. +- Bluesky tiene datos m?nimos. +- Igualar en la medida de lo posible. Si blueski no da x datos, no se crea nada. + +## 7) Di?logo de acciones de usuario +- Mastodon: autocompletado y b?squeda avanzada. +- Bluesky: di?logo sin autocompletado. +- Igualar con autocompletado y/o b?squeda en segundo plano. + +## 8) Consistencia de nombres y etiquetas +- Algunos textos difieren ("Reposts" vs "Boosts", "Likes" vs "Favorites"). +- Definir equivalencias y usar mismas etiquetas donde aplique. + +## 9) Paginaci?n en listados +- Bluesky: implementada en Reposts/Likes y Followers/Following. +- Faltan otros listados equivalentes (por ejemplo, b?squedas de usuarios si se implementan). + +## 10) Accesibilidad/teclado +- Verificar atajos en todos los nuevos di?logos/buffers. +- Asegurar foco inicial y navegaci?n id?ntica a Mastodon. + +## 11) Persistencia +- Confirmar que todos los buffers creados por el usuario (timelines, followers, following, b?squedas) se guardan/restauran. diff --git a/src/controller/blueski/handler.py b/src/controller/blueski/handler.py index b5196476..f4a8deee 100644 --- a/src/controller/blueski/handler.py +++ b/src/controller/blueski/handler.py @@ -5,6 +5,8 @@ import wx import asyncio import output from mysc.thread_utils import call_threaded +import widgetUtils +from extra.autocompletionUsers import completion from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog from typing import Any import languageHandler # Ensure _() injection @@ -18,9 +20,36 @@ class Handler: def __init__(self): super().__init__() self.menus = dict( - compose="&Post", + # Application menu + updateProfile="HIDE", + menuitem_search=_("&Search"), + lists="HIDE", + manageAliases="HIDE", + # Item menu + compose=_("&Post"), + reply=_("Re&ply"), + share=_("&Boost"), + fav=_("&Add to favorites"), + unfav="HIDE", + view=_("&Show post"), + view_conversation=_("View conversa&tion"), + ocr="HIDE", + delete=_("&Delete"), + # User menu + follow=_("&Actions..."), + timeline=_("&View timeline..."), + dm=_("Direct me&ssage"), + addAlias="HIDE", + addToList="HIDE", + removeFromList="HIDE", + details=_("S&how user profile"), + favs="HIDE", + # Buffer menu + community_timeline="HIDE", + filter="HIDE", + manage_filters="HIDE", ) - self.item_menu = "&Post" + self.item_menu = _("&Post") def create_buffers(self, session, createAccounts=True, controller=None): name = session.get_name() @@ -149,6 +178,17 @@ class Handler: ) timelines_position = controller.view.search("timelines", name) + # Searches container (Bluesky supports search buffers) + pub.sendMessage( + "createBuffer", + buffer_type="EmptyBuffer", + session_type="base", + buffer_title=_("Searches"), + parent_tab=root_position, + start=False, + kwargs=dict(parent=controller.view.nb, name="searches", account=name) + ) + # Saved user timelines try: timelines = session.settings["other_buffers"].get("timelines") @@ -202,6 +242,10 @@ class Handler: handle = g(profile, "handle") or actor except Exception: handle = actor + own_actor = session.db.get("user_id") or session.db.get("user_name") + own_handle = session.db.get("user_name") + if actor == own_actor or (own_handle and actor == own_handle) or (handle and own_handle and handle == own_handle): + continue title = _("Followers for {user}").format(user=handle) pub.sendMessage( "createBuffer", @@ -234,6 +278,10 @@ class Handler: handle = g(profile, "handle") or actor except Exception: handle = actor + own_actor = session.db.get("user_id") or session.db.get("user_name") + own_handle = session.db.get("user_name") + if actor == own_actor or (own_handle and actor == own_handle) or (handle and own_handle and handle == own_handle): + continue title = _("Following for {user}").format(user=handle) pub.sendMessage( "createBuffer", @@ -497,9 +545,17 @@ class Handler: from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog dlg = userTimelineDialog.UserTimeline(users=users, default=default) + try: + widgetUtils.connect_event( + dlg.autocompletion, + widgetUtils.BUTTON_PRESSED, + lambda *args, **kwargs: completion.autocompletionUsers(dlg, buffer.session.session_id).show_menu("free"), + ) + except Exception: + pass try: if hasattr(dlg, "autocompletion"): - dlg.autocompletion.Enable(False) + dlg.autocompletion.Enable(True) except Exception: pass if dlg.ShowModal() != wx.ID_OK: @@ -512,6 +568,13 @@ class Handler: if user.startswith("@"): user = user[1:] + try: + profile = buffer.session.get_profile(user) + if profile is None: + output.speak(_("User not found."), True) + return + except Exception: + pass user_payload = {"handle": user} if action == "posts": result = self.open_user_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload) @@ -642,6 +705,16 @@ class Handler: own_handle = session.db.get("user_name") if actor == own_actor or (own_handle and actor == own_handle) or (handle and own_handle and handle == own_handle): name = "followers" if list_type == "followers" else "following" + try: + stored = session.settings["other_buffers"].get("followers_timelines" if list_type == "followers" else "following_timelines") or [] + if isinstance(stored, str): + stored = [t for t in stored.split(",") if t] + if actor in stored: + stored.remove(actor) + session.settings["other_buffers"]["followers_timelines" if list_type == "followers" else "following_timelines"] = stored + session.settings.write() + except Exception: + pass index = main_controller.view.search(name, account_name) if index is not None: main_controller.view.change_buffer(index) @@ -753,7 +826,7 @@ class Handler: buffer_type="SearchBuffer", session_type="blueski", buffer_title=title, - parent_tab=controller.view.search(account_name, account_name), + parent_tab=controller.view.search("searches", account_name), start=True, kwargs=dict( parent=controller.view.nb, diff --git a/src/controller/blueski/messages.py b/src/controller/blueski/messages.py index d77ee4a0..e7ce1570 100644 --- a/src/controller/blueski/messages.py +++ b/src/controller/blueski/messages.py @@ -274,12 +274,28 @@ class viewPost(base_messages.basicMessage): 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"), - fetch_fn=lambda cursor=None: self.session.get_post_reposts(self.post_uri, limit=50, cursor=cursor), - cursor=res.get("cursor") if isinstance(res, dict) else None) + import application + controller = application.app.controller + account_name = self.session.get_name() + list_name = f"{self.post_uri}-reposts" + existing = controller.search_buffer(list_name, account_name) + if existing: + index = controller.view.search(list_name, account_name) + if index is not None: + controller.view.change_buffer(index) + return + title = _("people who reposted this post") + from pubsub import pub + pub.sendMessage( + "createBuffer", + buffer_type="PostUserListBuffer", + session_type="blueski", + buffer_title=title, + parent_tab=controller.view.search("timelines", account_name), + start=True, + kwargs=dict(parent=controller.view.nb, name=list_name, session=self.session, + post_uri=self.post_uri, api_method="get_post_reposts") + ) except Exception: pass @@ -287,11 +303,27 @@ class viewPost(base_messages.basicMessage): 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"), - fetch_fn=lambda cursor=None: self.session.get_post_likes(self.post_uri, limit=50, cursor=cursor), - cursor=res.get("cursor") if isinstance(res, dict) else None) + import application + controller = application.app.controller + account_name = self.session.get_name() + list_name = f"{self.post_uri}-likes" + existing = controller.search_buffer(list_name, account_name) + if existing: + index = controller.view.search(list_name, account_name) + if index is not None: + controller.view.change_buffer(index) + return + title = _("people who liked this post") + from pubsub import pub + pub.sendMessage( + "createBuffer", + buffer_type="PostUserListBuffer", + session_type="blueski", + buffer_title=title, + parent_tab=controller.view.search("timelines", account_name), + start=True, + kwargs=dict(parent=controller.view.nb, name=list_name, session=self.session, + post_uri=self.post_uri, api_method="get_post_likes") + ) except Exception: pass diff --git a/src/controller/buffers/blueski/__init__.py b/src/controller/buffers/blueski/__init__.py index 9958af77..6cedbfda 100644 --- a/src/controller/buffers/blueski/__init__.py +++ b/src/controller/buffers/blueski/__init__.py @@ -10,5 +10,5 @@ from .timeline import ( UserTimeline, SearchBuffer, ) -from .user import FollowersBuffer, FollowingBuffer, BlocksBuffer +from .user import FollowersBuffer, FollowingBuffer, BlocksBuffer, PostUserListBuffer from .chat import ConversationListBuffer, ChatBuffer as ChatMessageBuffer diff --git a/src/controller/buffers/blueski/user.py b/src/controller/buffers/blueski/user.py index ac1e6ed4..7bf8215c 100644 --- a/src/controller/buffers/blueski/user.py +++ b/src/controller/buffers/blueski/user.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import logging +import output from .base import BaseBuffer from wxUI.buffers.blueski import panels as BlueskiPanels from sessions.blueski import compose @@ -12,6 +13,7 @@ class UserBuffer(BaseBuffer): kwargs["compose_func"] = "compose_user" super(UserBuffer, self).__init__(*args, **kwargs) self.type = "user" + self.next_cursor = None def create_buffer(self, parent, name): self.buffer = BlueskiPanels.UserPanel(parent, name) @@ -36,6 +38,7 @@ class UserBuffer(BaseBuffer): else: res = getattr(self.session, api_method)(limit=count) items = res.get("items", []) + self.next_cursor = res.get("cursor") # Clear existing items for these lists to start fresh? # Or append? Standard lists in TWBlue usually append. @@ -47,6 +50,31 @@ class UserBuffer(BaseBuffer): log.exception(f"Error fetching user list for {self.name}") return 0 + def get_more_items(self): + api_method = self.kwargs.get("api_method") + if not api_method or not self.next_cursor: + return + + count = self.session.settings["general"].get("max_posts_per_call", 50) + actor = ( + self.kwargs.get("actor") + or self.kwargs.get("did") + or self.kwargs.get("handle") + or self.kwargs.get("id") + ) + try: + if api_method in ("get_followers", "get_follows"): + res = getattr(self.session, api_method)(actor=actor, limit=count, cursor=self.next_cursor) + else: + res = getattr(self.session, api_method)(limit=count, cursor=self.next_cursor) + items = res.get("items", []) + self.next_cursor = res.get("cursor") + added = self.process_items(items, play_sound=False) + if added: + output.speak(_(u"%s items retrieved") % (str(added)), True) + except Exception: + log.exception(f"Error fetching more user list items for {self.name}") + class FollowersBuffer(UserBuffer): def __init__(self, *args, **kwargs): kwargs["api_method"] = "get_followers" @@ -109,3 +137,51 @@ class BlocksBuffer(UserBuffer): def __init__(self, *args, **kwargs): kwargs["api_method"] = "get_blocks" super(BlocksBuffer, self).__init__(*args, **kwargs) + + +class PostUserListBuffer(UserBuffer): + def __init__(self, *args, **kwargs): + self.post_uri = kwargs.get("post_uri") + self.api_method = kwargs.get("api_method") + super(PostUserListBuffer, self).__init__(*args, **kwargs) + self.type = "post_user_list" + + def start_stream(self, mandatory=False, play_sound=True): + if not self.api_method or not self.post_uri: + return 0 + count = self.session.settings["general"].get("max_posts_per_call", 50) + try: + res = getattr(self.session, self.api_method)(self.post_uri, limit=count) + items = res.get("items", []) + self.next_cursor = res.get("cursor") + return self.process_items(items, play_sound) + except Exception: + log.exception("Error fetching post user list for %s", self.name) + return 0 + + def get_more_items(self): + if not self.api_method or not self.post_uri or not self.next_cursor: + return + count = self.session.settings["general"].get("max_posts_per_call", 50) + try: + res = getattr(self.session, self.api_method)(self.post_uri, limit=count, cursor=self.next_cursor) + items = res.get("items", []) + self.next_cursor = res.get("cursor") + added = self.process_items(items, play_sound=False) + if added: + output.speak(_(u"%s items retrieved") % (str(added)), True) + except Exception: + log.exception("Error fetching more post user list items for %s", self.name) + + 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 + return True diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 7f71e5a2..f3ab6244 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -401,6 +401,7 @@ class Controller(object): "FollowersBuffer": BlueskiUsers.FollowersBuffer, "FollowingBuffer": BlueskiUsers.FollowingBuffer, "BlocksBuffer": BlueskiUsers.BlocksBuffer, + "PostUserListBuffer": BlueskiUsers.PostUserListBuffer, "ConversationListBuffer": BlueskiChats.ConversationListBuffer, "ChatMessageBuffer": BlueskiChats.ChatBuffer, "chat_messages": BlueskiChats.ChatBuffer, @@ -1075,7 +1076,10 @@ class Controller(object): for m in list(handler.menus.keys()): if hasattr(self.view, m): menu_item = getattr(self.view, m) - if handler.menus[m] == None: + if handler.menus[m] == "HIDE": + menu_item.Enable(False) + menu_item.SetItemLabel("") + elif handler.menus[m] == None: menu_item.Enable(False) else: menu_item.Enable(True)