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 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,

View File

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

View File

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

View File

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

View File

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