2025-05-30 16:16:21 +00:00
# -*- coding: utf-8 -*-
import wx
import asyncio
import logging
from pubsub import pub
from approve . translation import translate as _
from approve . notifications import NotificationError
2026-01-10 19:46:53 +01:00
# Assuming controller.blueski.userList.get_user_profile_details and session.util._format_profile_data exist
2025-05-30 16:16:21 +00:00
# For direct call to util:
2026-01-10 19:46:53 +01:00
# from sessions.blueski import utils as BlueskiUtils
2025-05-30 16:16:21 +00:00
logger = logging . getLogger ( __name__ )
class ShowUserProfileDialog ( wx . Dialog ) :
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 )
self . session = session
self . user_identifier = user_identifier
self . profile_data = None # Will store the formatted profile dict
self . target_user_did = None # Will store the resolved DID of the profile being viewed
self . _init_ui ( )
self . SetMinSize ( ( 400 , 300 ) )
self . CentreOnParent ( )
wx . CallAfter ( asyncio . create_task , self . load_profile_data ( ) )
def _init_ui ( self ) :
panel = wx . Panel ( self )
main_sizer = wx . BoxSizer ( wx . VERTICAL )
# Profile Info Section (StaticTexts for labels and values)
self . info_grid_sizer = wx . FlexGridSizer ( cols = 2 , vgap = 5 , hgap = 5 )
self . info_grid_sizer . AddGrowableCol ( 1 , 1 )
fields = [
( _ ( " Display Name: " ) , " displayName " ) , ( _ ( " Handle: " ) , " handle " ) , ( _ ( " DID: " ) , " did " ) ,
( _ ( " Followers: " ) , " followersCount " ) , ( _ ( " Following: " ) , " followsCount " ) , ( _ ( " Posts: " ) , " postsCount " ) ,
( _ ( " Bio: " ) , " description " )
]
self . profile_field_ctrls = { }
for label_text , data_key in fields :
lbl = wx . StaticText ( panel , label = label_text )
val_ctrl = wx . TextCtrl ( panel , style = wx . TE_READONLY | wx . TE_MULTILINE if data_key == " description " else wx . TE_READONLY | wx . BORDER_NONE )
if data_key != " description " : # Make it look like a label
val_ctrl . SetBackgroundColour ( panel . GetBackgroundColour ( ) )
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 . profile_field_ctrls [ data_key ] = val_ctrl
# Avatar and Banner (placeholders for now)
self . avatar_text = wx . StaticText ( panel , label = _ ( " Avatar URL: " ) + _ ( " N/A " ) )
self . info_grid_sizer . Add ( self . avatar_text , 0 , wx . ALIGN_RIGHT | wx . ALIGN_TOP | wx . ALL , 2 )
self . banner_text = wx . StaticText ( panel , label = _ ( " Banner URL: " ) + _ ( " N/A " ) )
self . info_grid_sizer . Add ( self . banner_text , 0 , wx . ALIGN_RIGHT | wx . ALIGN_TOP | wx . ALL , 2 )
main_sizer . Add ( self . info_grid_sizer , 1 , wx . EXPAND | wx . ALL , 10 )
# Action Buttons
actions_sizer = wx . BoxSizer ( wx . HORIZONTAL )
# Placeholders, enable/disable logic will be in load_profile_data
self . follow_btn = wx . Button ( panel , label = _ ( " Follow " ) )
self . unfollow_btn = wx . Button ( panel , label = _ ( " Unfollow " ) )
self . mute_btn = wx . Button ( panel , label = _ ( " Mute " ) )
self . unmute_btn = wx . Button ( panel , label = _ ( " Unmute " ) )
self . block_btn = wx . Button ( panel , label = _ ( " Block " ) )
# 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 . 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 . 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 . 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 ) )
actions_sizer . Add ( self . follow_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 . unmute_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
main_sizer . Add ( actions_sizer , 0 , wx . ALIGN_CENTER | wx . TOP | wx . BOTTOM , 10 )
# Close Button
close_btn = wx . Button ( panel , wx . ID_CANCEL , _ ( " Close " ) )
close_btn . SetDefault ( ) # Allow Esc to close
main_sizer . Add ( close_btn , 0 , wx . ALIGN_RIGHT | wx . ALL , 10 )
panel . SetSizer ( main_sizer )
self . Fit ( ) # Fit dialog to content
async def load_profile_data ( self ) :
self . SetStatusText ( _ ( " Loading profile... " ) )
for ctrl in self . profile_field_ctrls . values ( ) :
ctrl . SetValue ( _ ( " Loading... " ) )
# Initially hide all action buttons until state is known
self . follow_btn . Hide ( )
self . unfollow_btn . Hide ( )
self . mute_btn . Hide ( )
self . unmute_btn . Hide ( )
self . block_btn . Hide ( )
self . unblock_btn . Hide ( )
try :
raw_profile = await self . session . util . get_user_profile ( self . user_identifier )
if raw_profile :
self . profile_data = self . session . util . _format_profile_data ( raw_profile ) # This should return a dict
self . target_user_did = self . profile_data . get ( " did " ) # Store the canonical DID
self . user_identifier = self . target_user_did # Update identifier to resolved DID for consistency
self . update_ui_fields ( )
self . update_action_buttons_state ( )
self . SetTitle ( _ ( " Profile: {handle} " ) . format ( handle = self . profile_data . get ( " handle " , " " ) ) )
self . SetStatusText ( _ ( " Profile loaded. " ) )
else :
for ctrl in self . profile_field_ctrls . values ( ) :
ctrl . SetValue ( _ ( " Not found. " ) )
self . SetStatusText ( _ ( " Profile not found for ' {ident} ' . " ) . format ( ident = self . user_identifier ) )
wx . MessageBox ( _ ( " User profile for ' {ident} ' not found. " ) . format ( ident = self . user_identifier ) , _ ( " Error " ) , wx . OK | wx . ICON_ERROR , self )
except Exception as e :
logger . error ( f " Error loading profile for { self . user_identifier } : { e } " , exc_info = True )
for ctrl in self . profile_field_ctrls . values ( ) :
ctrl . SetValue ( _ ( " Error loading. " ) )
self . SetStatusText ( _ ( " Error loading profile. " ) )
wx . MessageBox ( _ ( " Error loading profile: {error} " ) . format ( error = str ( e ) ) , _ ( " Error " ) , wx . OK | wx . ICON_ERROR , self )
finally :
self . Layout ( ) # Refresh layout after hiding/showing buttons
def update_ui_fields ( self ) :
if not self . profile_data :
return
for key , ctrl in self . profile_field_ctrls . items ( ) :
value = self . profile_data . get ( key ) # _format_profile_data should provide values or None/empty
if key == " description " and value : # Make bio multi-line if content exists
ctrl . SetMinSize ( ( - 1 , 60 ) ) # Allow some height for bio
if isinstance ( value , ( int , float ) ) :
ctrl . SetValue ( str ( value ) )
else : # String or None
ctrl . SetValue ( value or _ ( " N/A " ) )
# For URLs, could make them clickable or add a "Copy URL" button
avatar_url = self . profile_data . get ( " avatar " ) or _ ( " N/A " )
banner_url = self . profile_data . get ( " banner " ) or _ ( " N/A " )
self . avatar_text . SetLabel ( _ ( " Avatar URL: " ) + avatar_url )
self . avatar_text . SetToolTip ( avatar_url if avatar_url != _ ( " N/A " ) else " " )
self . banner_text . SetLabel ( _ ( " Banner URL: " ) + banner_url )
self . banner_text . SetToolTip ( banner_url if banner_url != _ ( " N/A " ) else " " )
self . Layout ( )
def update_action_buttons_state ( self ) :
if not self . profile_data or not self . target_user_did or self . target_user_did == self . session . util . get_own_did ( ) :
self . follow_btn . Hide ( )
self . unfollow_btn . Hide ( )
self . mute_btn . Hide ( )
self . unmute_btn . Hide ( )
self . block_btn . Hide ( )
self . unblock_btn . Hide ( )
self . Layout ( )
return
viewer_state = self . profile_data . get ( " viewer " , { } )
is_following = bool ( viewer_state . get ( " following " ) )
is_muted = bool ( viewer_state . get ( " muted " ) )
# 'blocking' in viewer state is the URI of *our* block record, if we are blocking them.
is_blocking_them = bool ( viewer_state . get ( " blocking " ) )
# 'blockedBy' means *they* are blocking us. If true, most actions might fail or be hidden.
is_blocked_by_them = bool ( viewer_state . get ( " blockedBy " ) )
if is_blocked_by_them : # If they block us, we can't do much.
self . follow_btn . Hide ( )
self . unfollow_btn . Hide ( )
self . mute_btn . Hide ( )
self . unmute_btn . Hide ( )
# We can still block them, or unblock them if we previously did.
self . block_btn . Show ( not is_blocking_them )
self . unblock_btn . Show ( is_blocking_them )
self . Layout ( )
return
self . follow_btn . Show ( not is_following and not is_blocking_them )
self . unfollow_btn . Show ( is_following and not is_blocking_them )
self . mute_btn . Show ( not is_muted and not is_blocking_them )
self . unmute_btn . Show ( is_muted and not is_blocking_them )
self . block_btn . Show ( not is_blocking_them ) # Show block if we are not currently blocking them (even if they block us)
self . unblock_btn . Show ( is_blocking_them ) # Show unblock if we are currently blocking them
self . Layout ( ) # Refresh sizer to show/hide buttons correctly
def on_user_action ( self , event , command : str ) :
if not self . target_user_did : # Should be set by load_profile_data
wx . MessageBox ( _ ( " User identifier (DID) not available for this action. " ) , _ ( " Error " ) , wx . OK | wx . ICON_ERROR )
return
# Confirmation for sensitive actions
confirmation_map = {
" unfollow_user " : _ ( " Are you sure you want to unfollow @ {handle} ? " ) . format ( handle = self . profile_data . get ( " handle " , " this user " ) ) ,
" block_user " : _ ( " Are you sure you want to block @ {handle} ? This will prevent them from interacting with you and hide their content. " ) . format ( handle = self . profile_data . get ( " handle " , " this user " ) ) ,
# Unblock usually doesn't need confirmation, but can be added if desired.
}
if command in confirmation_map :
dlg = wx . MessageDialog ( self , confirmation_map [ command ] , _ ( " Confirm Action " ) , wx . YES_NO | wx . ICON_QUESTION )
if dlg . ShowModal ( ) != wx . ID_YES :
dlg . Destroy ( )
return
dlg . Destroy ( )
async def do_action ( ) :
wx . BeginBusyCursor ( )
self . SetStatusText ( _ ( " Performing action: {action} ... " ) . format ( action = command ) )
action_button = event . GetEventObject ( )
if action_button : action_button . Disable ( ) # Disable the clicked button
try :
# Ensure controller_handler is available on the session
if not hasattr ( self . session , ' controller_handler ' ) or not self . session . controller_handler :
app = wx . GetApp ( )
if hasattr ( app , ' mainController ' ) :
self . session . controller_handler = app . mainController . get_handler ( self . session . KIND )
if not self . session . controller_handler : # Still not found
raise RuntimeError ( " Controller handler not found for session. " )
result = await self . session . controller_handler . handle_user_command (
command = command ,
user_id = self . session . uid ,
target_user_id = self . target_user_did ,
payload = { }
)
wx . EndBusyCursor ( )
# Use CallAfter for UI updates from async task
wx . CallAfter ( wx . MessageBox , result . get ( " message " , _ ( " Action completed. " ) ) ,
_ ( " Success " ) if result . get ( " status " ) == " success " else _ ( " Error " ) ,
wx . OK | ( wx . ICON_INFORMATION if result . get ( " status " ) == " success " else wx . ICON_ERROR ) ,
self )
if result . get ( " status " ) == " success " :
# Re-fetch profile data to update UI (especially button states)
wx . CallAfter ( asyncio . create_task , self . load_profile_data ( ) )
else : # Re-enable button if action failed
if action_button : wx . CallAfter ( action_button . Enable , True )
self . SetStatusText ( _ ( " Action failed. " ) )
except NotificationError as e :
wx . EndBusyCursor ( )
if action_button : wx . CallAfter ( action_button . Enable , True )
self . SetStatusText ( _ ( " Action failed. " ) )
wx . CallAfter ( wx . MessageBox , str ( e ) , _ ( " Action Error " ) , wx . OK | wx . ICON_ERROR , self )
except Exception as e :
wx . EndBusyCursor ( )
if action_button : wx . CallAfter ( action_button . Enable , True )
self . SetStatusText ( _ ( " Action failed. " ) )
logger . error ( f " Error performing user action ' { command } ' on { self . target_user_did } : { e } " , exc_info = True )
wx . CallAfter ( wx . MessageBox , _ ( " An unexpected error occurred: {error} " ) . format ( error = str ( e ) ) , _ ( " Error " ) , wx . OK | wx . ICON_ERROR , self )
asyncio . create_task ( do_action ( ) ) # No wx.CallAfter needed for starting the task itself
def SetStatusText ( self , text ) : # Simple status text for dialog title
self . SetTitle ( f " { _ ( ' User Profile ' ) } - { text } " )
` ` ` python
2026-01-10 19:46:53 +01:00
# Example of how this dialog might be called from blueski.Handler.user_details:
2025-05-30 16:16:21 +00:00
# (This is conceptual, actual integration in handler.py will use the dialog)
#
# async def user_details(self, buffer_panel_or_user_ident):
# session = self._get_session(self.current_user_id_from_context) # Get current session
# user_identifier_to_show = None
# if isinstance(buffer_panel_or_user_ident, str): # It's a DID or handle
# user_identifier_to_show = buffer_panel_or_user_ident
# elif hasattr(buffer_panel_or_user_ident, 'get_selected_item_author_details'): # It's a panel
# author_details = buffer_panel_or_user_ident.get_selected_item_author_details()
# if author_details:
# user_identifier_to_show = author_details.get("did") or author_details.get("handle")
#
# if not user_identifier_to_show:
# # Optionally prompt for user_identifier if not found
# output.speak(_("No user selected or identified to view details."), True)
# return
#
# dialog = ShowUserProfileDialog(self.main_controller.view, session, user_identifier_to_show)
# dialog.ShowModal()
# dialog.Destroy()
` ` `