Commit de no perder cambios

This commit is contained in:
Jesús Pavón Abián
2026-02-01 13:57:01 +01:00
parent f45af14229
commit 6ee67cc886
7 changed files with 288 additions and 18 deletions

28
context.md Normal file
View File

@@ -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.

57
falta.md Normal file
View File

@@ -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.

View File

@@ -5,6 +5,8 @@ import wx
import asyncio import asyncio
import output import output
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
import widgetUtils
from extra.autocompletionUsers import completion
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
@@ -18,9 +20,36 @@ class Handler:
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.menus = dict( 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): def create_buffers(self, session, createAccounts=True, controller=None):
name = session.get_name() name = session.get_name()
@@ -149,6 +178,17 @@ class Handler:
) )
timelines_position = controller.view.search("timelines", name) 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 # Saved user timelines
try: try:
timelines = session.settings["other_buffers"].get("timelines") timelines = session.settings["other_buffers"].get("timelines")
@@ -202,6 +242,10 @@ class Handler:
handle = g(profile, "handle") or actor handle = g(profile, "handle") or actor
except Exception: except Exception:
handle = actor 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) title = _("Followers for {user}").format(user=handle)
pub.sendMessage( pub.sendMessage(
"createBuffer", "createBuffer",
@@ -234,6 +278,10 @@ class Handler:
handle = g(profile, "handle") or actor handle = g(profile, "handle") or actor
except Exception: except Exception:
handle = actor 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) title = _("Following for {user}").format(user=handle)
pub.sendMessage( pub.sendMessage(
"createBuffer", "createBuffer",
@@ -497,9 +545,17 @@ class Handler:
from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog
dlg = userTimelineDialog.UserTimeline(users=users, default=default) 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: try:
if hasattr(dlg, "autocompletion"): if hasattr(dlg, "autocompletion"):
dlg.autocompletion.Enable(False) dlg.autocompletion.Enable(True)
except Exception: except Exception:
pass pass
if dlg.ShowModal() != wx.ID_OK: if dlg.ShowModal() != wx.ID_OK:
@@ -512,6 +568,13 @@ class Handler:
if user.startswith("@"): if user.startswith("@"):
user = user[1:] 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} user_payload = {"handle": user}
if action == "posts": if action == "posts":
result = self.open_user_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload) 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") 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): 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" 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) index = main_controller.view.search(name, account_name)
if index is not None: if index is not None:
main_controller.view.change_buffer(index) main_controller.view.change_buffer(index)
@@ -753,7 +826,7 @@ class Handler:
buffer_type="SearchBuffer", buffer_type="SearchBuffer",
session_type="blueski", session_type="blueski",
buffer_title=title, buffer_title=title,
parent_tab=controller.view.search(account_name, account_name), parent_tab=controller.view.search("searches", account_name),
start=True, start=True,
kwargs=dict( kwargs=dict(
parent=controller.view.nb, parent=controller.view.nb,

View File

@@ -274,12 +274,28 @@ class viewPost(base_messages.basicMessage):
if not self.post_uri: if not self.post_uri:
return return
try: try:
res = self.session.get_post_reposts(self.post_uri, limit=50) import application
users = res.get("items", []) controller = application.app.controller
from controller.blueski.userList import BlueskyUserList account_name = self.session.get_name()
BlueskyUserList(session=self.session, users=users, title=_("people who reposted this post"), list_name = f"{self.post_uri}-reposts"
fetch_fn=lambda cursor=None: self.session.get_post_reposts(self.post_uri, limit=50, cursor=cursor), existing = controller.search_buffer(list_name, account_name)
cursor=res.get("cursor") if isinstance(res, dict) else None) 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: except Exception:
pass pass
@@ -287,11 +303,27 @@ class viewPost(base_messages.basicMessage):
if not self.post_uri: if not self.post_uri:
return return
try: try:
res = self.session.get_post_likes(self.post_uri, limit=50) import application
users = res.get("items", []) controller = application.app.controller
from controller.blueski.userList import BlueskyUserList account_name = self.session.get_name()
BlueskyUserList(session=self.session, users=users, title=_("people who liked this post"), list_name = f"{self.post_uri}-likes"
fetch_fn=lambda cursor=None: self.session.get_post_likes(self.post_uri, limit=50, cursor=cursor), existing = controller.search_buffer(list_name, account_name)
cursor=res.get("cursor") if isinstance(res, dict) else None) 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: except Exception:
pass pass

View File

@@ -10,5 +10,5 @@ from .timeline import (
UserTimeline, UserTimeline,
SearchBuffer, SearchBuffer,
) )
from .user import FollowersBuffer, FollowingBuffer, BlocksBuffer from .user import FollowersBuffer, FollowingBuffer, BlocksBuffer, PostUserListBuffer
from .chat import ConversationListBuffer, ChatBuffer as ChatMessageBuffer from .chat import ConversationListBuffer, ChatBuffer as ChatMessageBuffer

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import output
from .base import BaseBuffer from .base import BaseBuffer
from wxUI.buffers.blueski import panels as BlueskiPanels from wxUI.buffers.blueski import panels as BlueskiPanels
from sessions.blueski import compose from sessions.blueski import compose
@@ -12,6 +13,7 @@ class UserBuffer(BaseBuffer):
kwargs["compose_func"] = "compose_user" kwargs["compose_func"] = "compose_user"
super(UserBuffer, self).__init__(*args, **kwargs) super(UserBuffer, self).__init__(*args, **kwargs)
self.type = "user" self.type = "user"
self.next_cursor = None
def create_buffer(self, parent, name): def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.UserPanel(parent, name) self.buffer = BlueskiPanels.UserPanel(parent, name)
@@ -36,6 +38,7 @@ class UserBuffer(BaseBuffer):
else: else:
res = getattr(self.session, api_method)(limit=count) res = getattr(self.session, api_method)(limit=count)
items = res.get("items", []) items = res.get("items", [])
self.next_cursor = res.get("cursor")
# Clear existing items for these lists to start fresh? # Clear existing items for these lists to start fresh?
# Or append? Standard lists in TWBlue usually append. # 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}") log.exception(f"Error fetching user list for {self.name}")
return 0 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): class FollowersBuffer(UserBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["api_method"] = "get_followers" kwargs["api_method"] = "get_followers"
@@ -109,3 +137,51 @@ class BlocksBuffer(UserBuffer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["api_method"] = "get_blocks" kwargs["api_method"] = "get_blocks"
super(BlocksBuffer, self).__init__(*args, **kwargs) 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

View File

@@ -401,6 +401,7 @@ class Controller(object):
"FollowersBuffer": BlueskiUsers.FollowersBuffer, "FollowersBuffer": BlueskiUsers.FollowersBuffer,
"FollowingBuffer": BlueskiUsers.FollowingBuffer, "FollowingBuffer": BlueskiUsers.FollowingBuffer,
"BlocksBuffer": BlueskiUsers.BlocksBuffer, "BlocksBuffer": BlueskiUsers.BlocksBuffer,
"PostUserListBuffer": BlueskiUsers.PostUserListBuffer,
"ConversationListBuffer": BlueskiChats.ConversationListBuffer, "ConversationListBuffer": BlueskiChats.ConversationListBuffer,
"ChatMessageBuffer": BlueskiChats.ChatBuffer, "ChatMessageBuffer": BlueskiChats.ChatBuffer,
"chat_messages": BlueskiChats.ChatBuffer, "chat_messages": BlueskiChats.ChatBuffer,
@@ -1075,7 +1076,10 @@ class Controller(object):
for m in list(handler.menus.keys()): for m in list(handler.menus.keys()):
if hasattr(self.view, m): if hasattr(self.view, m):
menu_item = getattr(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) menu_item.Enable(False)
else: else:
menu_item.Enable(True) menu_item.Enable(True)