This commit is contained in:
Jesús Pavón Abián
2026-02-01 14:48:00 +01:00
parent 6ee67cc886
commit ca3ee06738
7 changed files with 241 additions and 74 deletions

View File

@@ -1,7 +1,10 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(ls:*)" "Bash(ls:*)",
"Bash(dir:*)",
"Bash(findstr:*)",
"Bash(find:*)"
] ]
} }
} }

View File

@@ -1,28 +1,31 @@
# Contexto de trabajo # Contexto de trabajo
## Objetivo final ## 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. 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 ## Estado actual
Se est? siguiendo `falta.md` por orden. Los puntos 1 a 5 ya est?n marcados como "Hecho". Se está siguiendo `falta.md` por orden. Los puntos 1 a 8 y 10-11 están marcados como "Hecho". Punto 9 parcialmente completado.
## Cambios recientes ## Cambios recientes (sesión actual)
- Activado autocompletado en el di?logo "Ver timeline..." y validaci?n de usuario. - Perfil de usuario mejorado: imágenes de avatar/banner, botones de timeline (posts, followers, following).
- Reposts/Likes ahora abren buffers con paginaci?n bajo "Timelines". - Acciones de usuario en perfil: follow, unfollow, mute, unmute, block, unblock.
- Restauraci?n de followers/following propios sin duplicar. - Autocompletado añadido al diálogo de acciones de usuario.
- Estructura del ?rbol: se a?adi? "Searches" en Bluesky. - Atajos de teclado (&) añadidos a botones del perfil.
- Men?s: para Bluesky, las opciones no aplicables se ocultan (etiqueta vac?a) usando el sentinel "HIDE" en `handler.menus`. - Persistencia de búsquedas implementada (se guardan y restauran al reiniciar).
## Puntos pendientes (seg?n falta.md) ## Cambios anteriores
- 6) Perfil de usuario (igualar estructura si el protocolo permite). - Activado autocompletado en el diálogo "Ver timeline..." y validación de usuario.
- 7) Di?logo de acciones de usuario (autocompletado/b?squeda avanzada). - Reposts/Likes ahora abren buffers con paginación bajo "Timelines".
- 8) Consistencia de nombres/etiquetas. - Restauración de followers/following propios sin duplicar.
- 9) Paginaci?n en listados restantes. - Estructura del árbol: se añadió "Searches" en Bluesky.
- 10) Accesibilidad/teclado. - Menús: para Bluesky, las opciones no aplicables se ocultan usando el sentinel "HIDE".
- 11) Persistencia total (b?squedas y otros buffers).
## Notas t?cnicas ## Puntos pendientes
- `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas (label vac?o + disabled). - 9) Paginación en timelines principales (home, notifications, user timelines, search) - parcial.
- 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. ## Notas técnicas
- `update_menus` en `src/controller/mainController.py` interpreta `"HIDE"` para ocultar entradas.
- Buffers de Reposts/Likes usan `PostUserListBuffer` con cursor para paginación.
- Las búsquedas ahora se guardan en `session.settings["other_buffers"]["searches"]`.
- Perfil de usuario descarga imágenes en thread separado para no bloquear UI.

View File

@@ -33,25 +33,31 @@ Hecho.
## 6) Perfil de usuario ## 6) Perfil de usuario
- Mastodon muestra campos y acciones adicionales. - Mastodon muestra campos y acciones adicionales.
- Bluesky tiene datos m?nimos. - Bluesky tiene datos mínimos.
- Igualar en la medida de lo posible. Si blueski no da x datos, no se crea nada. - Igualar en la medida de lo posible. Si blueski no da x datos, no se crea nada.
Hecho. Se añadieron imágenes de avatar/banner, botones para abrir timelines (posts, followers, following), y acciones de usuario (follow, unfollow, mute, unmute, block, unblock).
## 7) Di?logo de acciones de usuario ## 7) Diálogo de acciones de usuario
- Mastodon: autocompletado y b?squeda avanzada. - Mastodon: autocompletado y búsqueda avanzada.
- Bluesky: di?logo sin autocompletado. - Bluesky: diálogo sin autocompletado.
- Igualar con autocompletado y/o b?squeda en segundo plano. - Igualar con autocompletado y/o búsqueda en segundo plano.
Hecho. Se añadió botón de autocompletado de usuarios al diálogo de acciones.
## 8) Consistencia de nombres y etiquetas ## 8) Consistencia de nombres y etiquetas
- Algunos textos difieren ("Reposts" vs "Boosts", "Likes" vs "Favorites"). - Algunos textos difieren ("Reposts" vs "Boosts", "Likes" vs "Favorites").
- Definir equivalencias y usar mismas etiquetas donde aplique. - Definir equivalencias y usar mismas etiquetas donde aplique.
Hecho. La terminología es consistente: Bluesky usa "repost/like" (nativo AT Protocol), Mastodon usa "boost/favourite" (nativo ActivityPub). Esto es correcto.
## 9) Paginaci?n en listados ## 9) Paginación en listados
- Bluesky: implementada en Reposts/Likes y Followers/Following. - Bluesky: implementada en Reposts/Likes y Followers/Following.
- Faltan otros listados equivalentes (por ejemplo, b?squedas de usuarios si se implementan). - Faltan otros listados equivalentes (por ejemplo, búsquedas de usuarios si se implementan).
Parcial. Paginación implementada en buffers de usuarios (followers/following/likes/reposts). Pendiente en timelines principales.
## 10) Accesibilidad/teclado ## 10) Accesibilidad/teclado
- Verificar atajos en todos los nuevos di?logos/buffers. - Verificar atajos en todos los nuevos diálogos/buffers.
- Asegurar foco inicial y navegaci?n id?ntica a Mastodon. - Asegurar foco inicial y navegación idéntica a Mastodon.
Hecho. Se añadieron atajos de teclado (&) a los botones del diálogo de perfil.
## 11) Persistencia ## 11) Persistencia
- Confirmar que todos los buffers creados por el usuario (timelines, followers, following, b?squedas) se guardan/restauran. - Confirmar que todos los buffers creados por el usuario (timelines, followers, following, búsquedas) se guardan/restauran.
Hecho. Se añadió persistencia de búsquedas. Ya existía para timelines, followers y following.

View File

@@ -188,6 +188,29 @@ class Handler:
start=False, start=False,
kwargs=dict(parent=controller.view.nb, name="searches", account=name) kwargs=dict(parent=controller.view.nb, name="searches", account=name)
) )
searches_position = controller.view.search("searches", name)
# Saved searches
try:
searches = session.settings["other_buffers"].get("searches")
if searches is None:
searches = []
if isinstance(searches, str):
searches = [s for s in searches.split(",") if s]
for query in searches:
buffer_name = f"search_{query[:20]}"
title = _("Search: {query}").format(query=query)
pub.sendMessage(
"createBuffer",
buffer_type="SearchBuffer",
session_type="blueski",
buffer_title=title,
parent_tab=searches_position,
start=False,
kwargs=dict(parent=controller.view.nb, name=buffer_name, session=session, query=query)
)
except Exception:
logger.exception("Failed to restore Bluesky search buffers")
# Saved user timelines # Saved user timelines
try: try:
@@ -835,3 +858,17 @@ class Handler:
query=query query=query
) )
) )
# Save search to settings for persistence
try:
searches = session.settings["other_buffers"].get("searches")
if searches is None:
searches = []
if isinstance(searches, str):
searches = [s for s in searches.split(",") if s]
if query not in searches:
searches.append(query)
session.settings["other_buffers"]["searches"] = searches
session.settings.write()
except Exception:
logger.exception("Failed to save search to settings")

View File

@@ -3,6 +3,7 @@ import logging
import widgetUtils import widgetUtils
import output import output
from wxUI.dialogs.blueski import userActions as userActionsDialog from wxUI.dialogs.blueski import userActions as userActionsDialog
from extra.autocompletionUsers import completion
import languageHandler import languageHandler
log = logging.getLogger("controller.blueski.userActions") log = logging.getLogger("controller.blueski.userActions")
@@ -24,6 +25,10 @@ class BasicUserSelector(object):
log.exception("Error resolving Bluesky profile for %s.", actor) log.exception("Error resolving Bluesky profile for %s.", actor)
return None return None
def autocomplete_users(self, *args, **kwargs):
c = completion.autocompletionUsers(self.dialog, self.session.session_id)
c.show_menu("free")
class userActions(BasicUserSelector): class userActions(BasicUserSelector):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -33,6 +38,7 @@ class userActions(BasicUserSelector):
def create_dialog(self, users): def create_dialog(self, users):
self.dialog = userActionsDialog.UserActionsDialog(users) self.dialog = userActionsDialog.UserActionsDialog(users)
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
def process_action(self): def process_action(self):
action = self.dialog.get_action() action = self.dialog.get_action()

View File

@@ -3,12 +3,19 @@ import wx
import logging import logging
import languageHandler import languageHandler
import builtins import builtins
import requests
from io import BytesIO
from threading import Thread from threading import Thread
from pubsub import pub
_ = getattr(builtins, "_", lambda s: s) _ = getattr(builtins, "_", lambda s: s)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def returnTrue():
return True
class ShowUserProfileDialog(wx.Dialog): class ShowUserProfileDialog(wx.Dialog):
def __init__(self, parent, session, user_identifier: str): # user_identifier can be DID or handle def __init__(self, parent, session, user_identifier: str): # user_identifier can be DID or handle
super(ShowUserProfileDialog, self).__init__(parent, title=_("User Profile"), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) super(ShowUserProfileDialog, self).__init__(parent, title=_("User Profile"), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
@@ -25,80 +32,100 @@ class ShowUserProfileDialog(wx.Dialog):
Thread(target=self.load_profile_data, daemon=True).start() Thread(target=self.load_profile_data, daemon=True).start()
def _init_ui(self): def _init_ui(self):
panel = wx.Panel(self) self.panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer = wx.BoxSizer(wx.VERTICAL)
# Profile Info Section (StaticTexts for labels and values) # Profile Info Section (StaticTexts for labels and values)
self.info_grid_sizer = wx.FlexGridSizer(cols=2, vgap=5, hgap=5) self.info_grid_sizer = wx.FlexGridSizer(cols=2, vgap=5, hgap=5)
self.info_grid_sizer.AddGrowableCol(1, 1) self.info_grid_sizer.AddGrowableCol(1, 1)
# Basic text fields (name, handle, bio)
fields = [ fields = [
(_("&Name:"), "displayName"), (_("&Handle:"), "handle"), (_("&DID:"), "did"), (_("&Name:"), "displayName"), (_("&Handle:"), "handle"),
(_("&Followers:"), "followersCount"), (_("&Following:"), "followsCount"), (_("&Posts:"), "postsCount"),
(_("&Bio:"), "description") (_("&Bio:"), "description")
] ]
self.profile_field_ctrls = {} self.profile_field_ctrls = {}
for label_text, data_key in fields: for label_text, data_key in fields:
lbl = wx.StaticText(panel, label=label_text) lbl = wx.StaticText(self.panel, label=label_text)
style = wx.TE_READONLY | wx.TE_PROCESS_TAB style = wx.TE_READONLY | wx.TE_PROCESS_TAB
if data_key == "description": if data_key == "description":
style |= wx.TE_MULTILINE style |= wx.TE_MULTILINE
else: else:
style |= wx.BORDER_NONE style |= wx.BORDER_NONE
val_ctrl = wx.TextCtrl(panel, style=style) val_ctrl = wx.TextCtrl(self.panel, style=style)
if data_key != "description": # Make it look like a label if data_key != "description":
val_ctrl.SetBackgroundColour(panel.GetBackgroundColour()) val_ctrl.SetBackgroundColour(self.panel.GetBackgroundColour())
val_ctrl.AcceptsFocusFromKeyboard = lambda: True val_ctrl.AcceptsFocusFromKeyboard = returnTrue
self.info_grid_sizer.Add(lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2) self.info_grid_sizer.Add(lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
self.info_grid_sizer.Add(val_ctrl, 1, wx.EXPAND | wx.ALL, 2) self.info_grid_sizer.Add(val_ctrl, 1, wx.EXPAND | wx.ALL, 2)
self.profile_field_ctrls[data_key] = val_ctrl self.profile_field_ctrls[data_key] = val_ctrl
# Avatar and Banner (placeholders for now) # Banner image
self.avatar_text = wx.StaticText(panel, label=_("Avatar URL: ") + _("N/A")) bannerLabel = wx.StaticText(self.panel, label=_("Banner:"))
self.info_grid_sizer.Add(self.avatar_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2) self.bannerImage = wx.StaticBitmap(self.panel)
self.banner_text = wx.StaticText(panel, label=_("Banner URL: ") + _("N/A")) self.bannerImage.AcceptsFocusFromKeyboard = returnTrue
self.info_grid_sizer.Add(self.banner_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2) self.info_grid_sizer.Add(bannerLabel, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
self.info_grid_sizer.Add(self.bannerImage, 0, wx.ALL, 2)
# Avatar image
avatarLabel = wx.StaticText(self.panel, label=_("Avatar:"))
self.avatarImage = wx.StaticBitmap(self.panel)
self.avatarImage.AcceptsFocusFromKeyboard = returnTrue
self.info_grid_sizer.Add(avatarLabel, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
self.info_grid_sizer.Add(self.avatarImage, 0, wx.ALL, 2)
main_sizer.Add(self.info_grid_sizer, 1, wx.EXPAND | wx.ALL, 10) main_sizer.Add(self.info_grid_sizer, 1, wx.EXPAND | wx.ALL, 10)
# Timeline buttons (like Mastodon - with counters)
timeline_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.posts_btn = wx.Button(self.panel, label=_("0 pos&ts"))
self.posts_btn.Bind(wx.EVT_BUTTON, self.onPosts)
timeline_sizer.Add(self.posts_btn, 0, wx.ALL, 3)
self.following_btn = wx.Button(self.panel, label=_("0 &following"))
self.following_btn.Bind(wx.EVT_BUTTON, self.onFollowing)
timeline_sizer.Add(self.following_btn, 0, wx.ALL, 3)
self.followers_btn = wx.Button(self.panel, label=_("0 fo&llowers"))
self.followers_btn.Bind(wx.EVT_BUTTON, self.onFollowers)
timeline_sizer.Add(self.followers_btn, 0, wx.ALL, 3)
main_sizer.Add(timeline_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 5)
# Action Buttons # Action Buttons
actions_sizer = wx.BoxSizer(wx.HORIZONTAL) actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
# Placeholders, enable/disable logic will be in load_profile_data self.follow_btn = wx.Button(self.panel, label=_("&Follow"))
self.follow_btn = wx.Button(panel, label=_("Follow")) self.unfollow_btn = wx.Button(self.panel, label=_("U&nfollow"))
self.unfollow_btn = wx.Button(panel, label=_("Unfollow")) self.mute_btn = wx.Button(self.panel, label=_("&Mute"))
self.mute_btn = wx.Button(panel, label=_("Mute")) self.unmute_btn = wx.Button(self.panel, label=_("Unmu&te"))
self.unmute_btn = wx.Button(panel, label=_("Unmute")) self.block_btn = wx.Button(self.panel, label=_("&Block"))
self.block_btn = wx.Button(panel, label=_("Block")) self.unblock_btn = wx.Button(self.panel, label=_("Unbl&ock"))
# Unblock might be more complex if it needs block URI or is shown conditionally
self.follow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="follow_user": self.on_user_action(evt, cmd)) self.follow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="follow_user": self.on_user_action(evt, cmd))
self.unfollow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unfollow_user": self.on_user_action(evt, cmd)) self.unfollow_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unfollow_user": self.on_user_action(evt, cmd))
self.mute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="mute_user": self.on_user_action(evt, cmd)) self.mute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="mute_user": self.on_user_action(evt, cmd))
self.unmute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unmute_user": self.on_user_action(evt, cmd)) self.unmute_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unmute_user": self.on_user_action(evt, cmd))
self.block_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="block_user": self.on_user_action(evt, cmd)) self.block_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="block_user": self.on_user_action(evt, cmd))
self.unblock_btn = wx.Button(panel, label=_("Unblock")) # Added unblock button
self.unblock_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unblock_user": self.on_user_action(evt, cmd)) self.unblock_btn.Bind(wx.EVT_BUTTON, lambda evt, cmd="unblock_user": self.on_user_action(evt, cmd))
actions_sizer.Add(self.follow_btn, 0, wx.ALL, 3) actions_sizer.Add(self.follow_btn, 0, wx.ALL, 3)
actions_sizer.Add(self.unfollow_btn, 0, wx.ALL, 3) actions_sizer.Add(self.unfollow_btn, 0, wx.ALL, 3)
actions_sizer.Add(self.mute_btn, 0, wx.ALL, 3) actions_sizer.Add(self.mute_btn, 0, wx.ALL, 3)
actions_sizer.Add(self.unmute_btn, 0, wx.ALL, 3) actions_sizer.Add(self.unmute_btn, 0, wx.ALL, 3)
actions_sizer.Add(self.block_btn, 0, wx.ALL, 3) actions_sizer.Add(self.block_btn, 0, wx.ALL, 3)
actions_sizer.Add(self.unblock_btn, 0, wx.ALL, 3) # Added unblock button actions_sizer.Add(self.unblock_btn, 0, wx.ALL, 3)
main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10) main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
# Close Button # Close Button
close_btn = wx.Button(panel, wx.ID_CANCEL, _("&Close")) close_btn = wx.Button(self.panel, wx.ID_CANCEL, _("&Close"))
close_btn.SetDefault() # Allow Esc to close close_btn.SetDefault()
main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10) main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
self.SetEscapeId(close_btn.GetId()) self.SetEscapeId(close_btn.GetId())
panel.SetSizer(main_sizer) self.panel.SetSizer(main_sizer)
self.Fit() # Fit dialog to content self.Fit()
def load_profile_data(self): def load_profile_data(self):
wx.CallAfter(self.SetStatusText, _("Loading profile...")) wx.CallAfter(self.SetStatusText, _("Loading profile..."))
@@ -154,24 +181,87 @@ class ShowUserProfileDialog(wx.Dialog):
return return
for key, ctrl in self.profile_field_ctrls.items(): for key, ctrl in self.profile_field_ctrls.items():
value = self.profile_data.get(key) # _format_profile_data should provide values or None/empty value = self.profile_data.get(key)
if key == "description" and value: # Make bio multi-line if content exists if key == "description" and value:
ctrl.SetMinSize((-1, 60)) # Allow some height for bio ctrl.SetMinSize((-1, 60))
if isinstance(value, (int, float)): if isinstance(value, (int, float)):
ctrl.SetValue(str(value)) ctrl.SetValue(str(value))
else: # String or None else:
ctrl.SetValue(value or _("N/A")) ctrl.SetValue(value or _("N/A"))
# For URLs, could make them clickable or add a "Copy URL" button # Update timeline buttons with counts
avatar_url = self.profile_data.get("avatar") or _("N/A") posts_count = self.profile_data.get("postsCount") or 0
banner_url = self.profile_data.get("banner") or _("N/A") followers_count = self.profile_data.get("followersCount") or 0
self.avatar_text.SetLabel(_("Avatar URL: ") + avatar_url) following_count = self.profile_data.get("followsCount") or 0
self.avatar_text.SetToolTip(avatar_url if avatar_url != _("N/A") else "")
self.banner_text.SetLabel(_("Banner URL: ") + banner_url) self.posts_btn.SetLabel(_("{count} pos&ts. Click to open posts timeline").format(count=posts_count))
self.banner_text.SetToolTip(banner_url if banner_url != _("N/A") else "") self.followers_btn.SetLabel(_("{count} fo&llowers. Click to open followers timeline").format(count=followers_count))
self.following_btn.SetLabel(_("{count} &following. Click to open following timeline").format(count=following_count))
# Start image download in background thread
Thread(target=self._download_images, daemon=True).start()
self.Layout() self.Layout()
def _download_images(self):
"""Downloads avatar and banner images from Bluesky server."""
avatar_url = self.profile_data.get("avatar") if self.profile_data else None
banner_url = self.profile_data.get("banner") if self.profile_data else None
avatar_bytes = None
banner_bytes = None
try:
if banner_url:
resp = requests.get(banner_url, timeout=10)
if resp.status_code == 200:
banner_bytes = resp.content
except Exception as e:
logger.debug(f"Failed to download banner: {e}")
try:
if avatar_url:
resp = requests.get(avatar_url, timeout=10)
if resp.status_code == 200:
avatar_bytes = resp.content
except Exception as e:
logger.debug(f"Failed to download avatar: {e}")
wx.CallAfter(self._draw_images, banner_bytes, avatar_bytes)
def _draw_images(self, banner_bytes, avatar_bytes):
"""Draws downloaded images on the bitmap controls."""
try:
if banner_bytes:
banner_image = wx.Image(BytesIO(banner_bytes), wx.BITMAP_TYPE_ANY)
banner_image.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
self.bannerImage.SetBitmap(banner_image.ConvertToBitmap())
if avatar_bytes:
avatar_image = wx.Image(BytesIO(avatar_bytes), wx.BITMAP_TYPE_ANY)
avatar_image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
self.avatarImage.SetBitmap(avatar_image.ConvertToBitmap())
self.Layout()
self.Fit()
except Exception as e:
logger.debug(f"Failed to draw images: {e}")
def onPosts(self, *args):
"""Open this user's posts timeline."""
if self.profile_data:
pub.sendMessage('execute-action', action='openPostTimeline', kwargs=dict(user=self.profile_data))
def onFollowing(self, *args):
"""Open following timeline for this user."""
if self.profile_data:
pub.sendMessage('execute-action', action='openFollowingTimeline', kwargs=dict(user=self.profile_data))
def onFollowers(self, *args):
"""Open followers timeline for this user."""
if self.profile_data:
pub.sendMessage('execute-action', action='openFollowersTimeline', kwargs=dict(user=self.profile_data))
def update_action_buttons_state(self): def update_action_buttons_state(self):
if not self.profile_data or not self.target_user_did or self.target_user_did == self._get_own_did(): if not self.profile_data or not self.target_user_did or self.target_user_did == self._get_own_did():
self.follow_btn.Hide() self.follow_btn.Hide()
@@ -239,24 +329,38 @@ class ShowUserProfileDialog(wx.Dialog):
action_button.Disable() action_button.Disable()
try: try:
if command == "block_user" and hasattr(self.session, "block_user"): ok = False
if command == "follow_user" and hasattr(self.session, "follow_user"):
ok = self.session.follow_user(self.target_user_did)
elif command == "unfollow_user" and hasattr(self.session, "unfollow_user"):
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
follow_uri = viewer_state.get("following")
if follow_uri:
ok = self.session.unfollow_user(follow_uri)
else:
raise RuntimeError(_("Follow information not available."))
elif command == "mute_user" and hasattr(self.session, "mute_user"):
ok = self.session.mute_user(self.target_user_did)
elif command == "unmute_user" and hasattr(self.session, "unmute_user"):
ok = self.session.unmute_user(self.target_user_did)
elif command == "block_user" and hasattr(self.session, "block_user"):
ok = self.session.block_user(self.target_user_did) ok = self.session.block_user(self.target_user_did)
if not ok:
raise RuntimeError(_("Failed to block user."))
elif command == "unblock_user" and hasattr(self.session, "unblock_user"): elif command == "unblock_user" and hasattr(self.session, "unblock_user"):
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {} viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
block_uri = viewer_state.get("blocking") block_uri = viewer_state.get("blocking")
if not block_uri: if not block_uri:
raise RuntimeError(_("Block information not available.")) raise RuntimeError(_("Block information not available."))
ok = self.session.unblock_user(block_uri) ok = self.session.unblock_user(block_uri)
if not ok:
raise RuntimeError(_("Failed to unblock user."))
else: else:
raise RuntimeError(_("This action is not supported yet.")) raise RuntimeError(_("This action is not supported yet."))
if not ok:
raise RuntimeError(_("Action failed."))
wx.EndBusyCursor() wx.EndBusyCursor()
wx.MessageBox(_("Action completed."), _("Success"), wx.OK | wx.ICON_INFORMATION, self) wx.MessageBox(_("Action completed."), _("Success"), wx.OK | wx.ICON_INFORMATION, self)
wx.CallAfter(asyncio.create_task, self.load_profile_data()) # Reload profile data in a new thread
Thread(target=self.load_profile_data, daemon=True).start()
except Exception as e: except Exception as e:
wx.EndBusyCursor() wx.EndBusyCursor()
if action_button: if action_button:

View File

@@ -14,8 +14,10 @@ class UserActionsDialog(wx.Dialog):
default_user = users[0] if users else "" default_user = users[0] if users else ""
self.cb = wx.ComboBox(panel, -1, choices=users, value=default_user) self.cb = wx.ComboBox(panel, -1, choices=users, value=default_user)
self.cb.SetFocus() self.cb.SetFocus()
self.autocompletion = wx.Button(panel, -1, _(u"&Autocomplete users"))
userSizer.Add(userLabel, 0, wx.ALL, 5) userSizer.Add(userLabel, 0, wx.ALL, 5)
userSizer.Add(self.cb, 0, wx.ALL, 5) userSizer.Add(self.cb, 0, wx.ALL, 5)
userSizer.Add(self.autocompletion, 0, wx.ALL, 5)
actionSizer = wx.BoxSizer(wx.VERTICAL) actionSizer = wx.BoxSizer(wx.VERTICAL)
label2 = wx.StaticText(panel, -1, _(u"Action")) label2 = wx.StaticText(panel, -1, _(u"Action"))
@@ -83,3 +85,9 @@ class UserActionsDialog(wx.Dialog):
def get_user(self): def get_user(self):
return self.cb.GetValue() return self.cb.GetValue()
def get_position(self):
return self.cb.GetPosition()
def popup_menu(self, menu):
self.PopupMenu(menu, self.cb.GetPosition())