mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-07 01:47:32 +01:00
Commit
This commit is contained in:
@@ -24,13 +24,13 @@ class Handler:
|
||||
from pubsub import pub
|
||||
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
|
||||
root_position = controller.view.search(name, name)
|
||||
# Home timeline only for now
|
||||
# Discover/home timeline
|
||||
from pubsub import pub
|
||||
pub.sendMessage(
|
||||
"createBuffer",
|
||||
buffer_type="home_timeline",
|
||||
session_type="atprotosocial",
|
||||
buffer_title=_("Home"),
|
||||
session_type="blueski",
|
||||
buffer_title=_("Discover"),
|
||||
parent_tab=root_position,
|
||||
start=True,
|
||||
kwargs=dict(parent=controller.view.nb, name="home_timeline", session=session)
|
||||
@@ -39,8 +39,8 @@ class Handler:
|
||||
pub.sendMessage(
|
||||
"createBuffer",
|
||||
buffer_type="following_timeline",
|
||||
session_type="atprotosocial",
|
||||
buffer_title=_("Following"),
|
||||
session_type="blueski",
|
||||
buffer_title=_("Following (Chronological)"),
|
||||
parent_tab=root_position,
|
||||
start=False,
|
||||
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session)
|
||||
@@ -71,7 +71,7 @@ class Handler:
|
||||
current_mode = None
|
||||
ask_default = True if current_mode in (None, "ask") else False
|
||||
|
||||
from wxUI.dialogs.atprotosocial.configuration import AccountSettingsDialog
|
||||
from wxUI.dialogs.blueski.configuration import AccountSettingsDialog
|
||||
dlg = AccountSettingsDialog(controller.view, ask_before_boost=ask_default)
|
||||
resp = dlg.ShowModal()
|
||||
if resp == wx.ID_OK:
|
||||
@@ -8,34 +8,34 @@ from typing import Any
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file would typically contain functions to generate complex message bodies or
|
||||
# interactive components for ATProtoSocial, similar to how it might be done for Mastodon.
|
||||
# Since ATProtoSocial's interactive features (beyond basic posts) are still evolving
|
||||
# interactive components for Blueski, similar to how it might be done for Mastodon.
|
||||
# Since Blueski's interactive features (beyond basic posts) are still evolving
|
||||
# or client-dependent (like polls), this might be less complex initially.
|
||||
|
||||
# Example: If ATProtoSocial develops a standard for "cards" or interactive messages,
|
||||
# Example: If Blueski develops a standard for "cards" or interactive messages,
|
||||
# functions to create those would go here. For now, we can imagine placeholders.
|
||||
|
||||
def format_welcome_message(session: Any) -> dict[str, Any]:
|
||||
"""
|
||||
Generates a welcome message for a new ATProtoSocial session.
|
||||
Generates a welcome message for a new Blueski session.
|
||||
This is just a placeholder and example.
|
||||
"""
|
||||
# user_profile = session.util.get_own_profile_info() # Assuming this method exists and is async or cached
|
||||
# handle = user_profile.get("handle", _("your ATProtoSocial account")) if user_profile else _("your ATProtoSocial account")
|
||||
# handle = user_profile.get("handle", _("your Blueski account")) if user_profile else _("your Blueski account")
|
||||
# Expect session to expose username via db/settings
|
||||
handle = (getattr(session, "db", {}).get("user_name")
|
||||
or getattr(getattr(session, "settings", {}), "get", lambda *_: {})("atprotosocial").get("handle")
|
||||
or getattr(getattr(session, "settings", {}), "get", lambda *_: {})("blueski").get("handle")
|
||||
or _("your Bluesky account"))
|
||||
|
||||
|
||||
return {
|
||||
"text": _("Welcome to Approve for ATProtoSocial! Your account {handle} is connected.").format(handle=handle),
|
||||
# "blocks": [ # If ATProtoSocial supports a block kit like Slack or Discord
|
||||
"text": _("Welcome to Approve for Blueski! Your account {handle} is connected.").format(handle=handle),
|
||||
# "blocks": [ # If Blueski supports a block kit like Slack or Discord
|
||||
# {
|
||||
# "type": "section",
|
||||
# "text": {
|
||||
# "type": "mrkdwn", # Or ATProtoSocial's equivalent
|
||||
# "text": _("Welcome to Approve for ATProtoSocial! Your account *{handle}* is connected.").format(handle=handle)
|
||||
# "type": "mrkdwn", # Or Blueski's equivalent
|
||||
# "text": _("Welcome to Approve for Blueski! Your account *{handle}* is connected.").format(handle=handle)
|
||||
# }
|
||||
# },
|
||||
# {
|
||||
@@ -44,7 +44,7 @@ def format_welcome_message(session: Any) -> dict[str, Any]:
|
||||
# {
|
||||
# "type": "button",
|
||||
# "text": {"type": "plain_text", "text": _("Post your first Skeet")},
|
||||
# "action_id": "atprotosocial_compose_new_post" # Example action ID
|
||||
# "action_id": "blueski_compose_new_post" # Example action ID
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
@@ -65,16 +65,16 @@ def format_error_message(error_description: str, details: str | None = None) ->
|
||||
# ]
|
||||
return message
|
||||
|
||||
# More functions could be added here as ATProtoSocial's capabilities become clearer
|
||||
# More functions could be added here as Blueski's capabilities become clearer
|
||||
# or as specific formatting needs for Approve arise. For example:
|
||||
# - Formatting a post for display with all its embeds and cards.
|
||||
# - Generating help messages specific to ATProtoSocial features.
|
||||
# - Generating help messages specific to Blueski features.
|
||||
# - Creating interactive messages for polls (if supported via some convention).
|
||||
|
||||
# Example of adapting a function that might exist in mastodon_messages:
|
||||
# def build_post_summary_message(session: ATProtoSocialSession, post_uri: str, post_content: dict) -> dict[str, Any]:
|
||||
# def build_post_summary_message(session: BlueskiSession, post_uri: str, post_content: dict) -> dict[str, Any]:
|
||||
# """
|
||||
# Builds a summary message for an ATProtoSocial post.
|
||||
# Builds a summary message for an Blueski post.
|
||||
# """
|
||||
# author_handle = post_content.get("author", {}).get("handle", "Unknown user")
|
||||
# text_preview = post_content.get("text", "")[:100] # First 100 chars of text
|
||||
@@ -88,4 +88,4 @@ def format_error_message(error_description: str, details: str | None = None) ->
|
||||
# # Potentially with "blocks" for richer formatting if the platform supports it
|
||||
# }
|
||||
|
||||
logger.info("ATProtoSocial messages module loaded (placeholders).")
|
||||
logger.info("Blueski messages module loaded (placeholders).")
|
||||
@@ -8,29 +8,29 @@ fromapprove.translation import translate as _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.config import ConfigSectionProxy
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file is for defining forms and handling for ATProtoSocial-specific settings
|
||||
# This file is for defining forms and handling for Blueski-specific settings
|
||||
# that might be more complex than simple key-value pairs handled by Session.get_settings_inputs.
|
||||
# For ATProtoSocial, initial settings might be simple (handle, app password),
|
||||
# For Blueski, initial settings might be simple (handle, app password),
|
||||
# but this structure allows for expansion.
|
||||
|
||||
|
||||
class ATProtoSocialSettingsForm(Form):
|
||||
class BlueskiSettingsForm(Form):
|
||||
"""
|
||||
A settings form for ATProtoSocial sessions.
|
||||
A settings form for Blueski sessions.
|
||||
This would mirror the kind of settings found in Session.get_settings_inputs
|
||||
but using the WTForms-like Form structure for more complex validation or layout.
|
||||
"""
|
||||
# Example fields - these should align with what ATProtoSocialSession.get_settings_inputs defines
|
||||
# and what ATProtoSocialSession.get_configurable_values expects for its config.
|
||||
# Example fields - these should align with what BlueskiSession.get_settings_inputs defines
|
||||
# and what BlueskiSession.get_configurable_values expects for its config.
|
||||
|
||||
# instance_url = TextField(
|
||||
# _("Instance URL"),
|
||||
# default="https://bsky.social", # Default PDS for Bluesky
|
||||
# description=_("The base URL of your ATProtoSocial PDS instance (e.g., https://bsky.social)."),
|
||||
# description=_("The base URL of your Blueski PDS instance (e.g., https://bsky.social)."),
|
||||
# validators=[], # Add validators if needed, e.g., URL validator
|
||||
# )
|
||||
handle = TextField(
|
||||
@@ -43,19 +43,19 @@ class ATProtoSocialSettingsForm(Form):
|
||||
description=_("Your Bluesky App Password. Generate this in your Bluesky account settings."),
|
||||
validators=[], # e.g., DataRequired()
|
||||
)
|
||||
# Add more fields as needed for ATProtoSocial configuration.
|
||||
# Add more fields as needed for Blueski configuration.
|
||||
# For example, if there were specific notification settings, content filters, etc.
|
||||
|
||||
submit = SubmitField(_("Save ATProtoSocial Settings"))
|
||||
submit = SubmitField(_("Save Blueski Settings"))
|
||||
|
||||
|
||||
async def get_settings_form(
|
||||
user_id: str,
|
||||
session: ATProtoSocialSession | None = None,
|
||||
config: ConfigSectionProxy | None = None, # User-specific config for ATProtoSocial
|
||||
) -> ATProtoSocialSettingsForm:
|
||||
session: BlueskiSession | None = None,
|
||||
config: ConfigSectionProxy | None = None, # User-specific config for Blueski
|
||||
) -> BlueskiSettingsForm:
|
||||
"""
|
||||
Creates and pre-populates the ATProtoSocial settings form.
|
||||
Creates and pre-populates the Blueski settings form.
|
||||
"""
|
||||
form_data = {}
|
||||
if session: # If a session exists, use its current config
|
||||
@@ -68,31 +68,31 @@ async def get_settings_form(
|
||||
form_data["handle"] = config.handle.get("")
|
||||
form_data["app_password"] = ""
|
||||
|
||||
form = ATProtoSocialSettingsForm(formdata=None, **form_data) # formdata=None for initial display
|
||||
form = BlueskiSettingsForm(formdata=None, **form_data) # formdata=None for initial display
|
||||
return form
|
||||
|
||||
|
||||
async def process_settings_form(
|
||||
form: ATProtoSocialSettingsForm,
|
||||
form: BlueskiSettingsForm,
|
||||
user_id: str,
|
||||
session: ATProtoSocialSession | None = None, # Pass if update should affect live session
|
||||
config: ConfigSectionProxy | None = None, # User-specific config for ATProtoSocial
|
||||
session: BlueskiSession | None = None, # Pass if update should affect live session
|
||||
config: ConfigSectionProxy | None = None, # User-specific config for Blueski
|
||||
) -> bool:
|
||||
"""
|
||||
Processes the submitted ATProtoSocial settings form and updates configuration.
|
||||
Processes the submitted Blueski settings form and updates configuration.
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
if not form.validate(): # Assuming form has a validate method
|
||||
logger.warning(f"ATProtoSocial settings form validation failed for user {user_id}: {form.errors}")
|
||||
logger.warning(f"Blueski settings form validation failed for user {user_id}: {form.errors}")
|
||||
return False
|
||||
|
||||
if not config and session: # Try to get config via session if not directly provided
|
||||
# This depends on how ConfigSectionProxy is obtained.
|
||||
# config = approve.config.config.sessions.atprotosocial[user_id] # Example path
|
||||
# config = approve.config.config.sessions.blueski[user_id] # Example path
|
||||
pass # Needs actual way to get config proxy
|
||||
|
||||
if not config:
|
||||
logger.error(f"Cannot process ATProtoSocial settings for user {user_id}: no config proxy available.")
|
||||
logger.error(f"Cannot process Blueski settings for user {user_id}: no config proxy available.")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -101,11 +101,11 @@ async def process_settings_form(
|
||||
await config.handle.set(form.handle.data)
|
||||
await config.app_password.set(form.app_password.data) # Ensure this is stored securely
|
||||
|
||||
logger.info(f"ATProtoSocial settings updated for user {user_id}.")
|
||||
logger.info(f"Blueski settings updated for user {user_id}.")
|
||||
|
||||
# If there's an active session, it might need to be reconfigured or restarted
|
||||
if session:
|
||||
logger.info(f"Requesting ATProtoSocial session re-initialization for user {user_id} due to settings change.")
|
||||
logger.info(f"Requesting Blueski session re-initialization for user {user_id} due to settings change.")
|
||||
# await session.stop() # Stop it
|
||||
# # Update session instance with new values directly or rely on it re-reading config
|
||||
# session.api_base_url = form.instance_url.data
|
||||
@@ -118,11 +118,11 @@ async def process_settings_form(
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving ATProtoSocial settings for user {user_id}: {e}", exc_info=True)
|
||||
logger.error(f"Error saving Blueski settings for user {user_id}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
# Any additional ATProtoSocial-specific settings views or handlers would go here.
|
||||
# For instance, if ATProtoSocial had features like "Relays" or "Feed Generators"
|
||||
# Any additional Blueski-specific settings views or handlers would go here.
|
||||
# For instance, if Blueski had features like "Relays" or "Feed Generators"
|
||||
# that needed UI configuration within Approve, those forms and handlers could be defined here.
|
||||
|
||||
logger.info("ATProtoSocial settings module loaded (placeholders).")
|
||||
logger.info("Blueski settings module loaded (placeholders).")
|
||||
@@ -7,29 +7,29 @@ from typing import TYPE_CHECKING, Any
|
||||
fromapprove.translation import translate as _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file would handle the logic for a template editor specific to ATProtoSocial.
|
||||
# This file would handle the logic for a template editor specific to Blueski.
|
||||
# A template editor allows users to customize how certain information or messages
|
||||
# from ATProtoSocial are displayed in Approve.
|
||||
# from Blueski are displayed in Approve.
|
||||
|
||||
# For ATProtoSocial, this might be less relevant initially if its content structure
|
||||
# For Blueski, this might be less relevant initially if its content structure
|
||||
# is simpler than Mastodon's, or if user-customizable templates are not a primary feature.
|
||||
# However, having the structure allows for future expansion.
|
||||
|
||||
# Example: Customizing the format of a "new follower" notification, or how a "skeet" is displayed.
|
||||
|
||||
class ATProtoSocialTemplateEditor:
|
||||
def __init__(self, session: ATProtoSocialSession) -> None:
|
||||
class BlueskiTemplateEditor:
|
||||
def __init__(self, session: BlueskiSession) -> None:
|
||||
self.session = session
|
||||
# self.user_id = session.user_id
|
||||
# self.config_prefix = f"sessions.atprotosocial.{self.user_id}.templates." # Example config path
|
||||
# self.config_prefix = f"sessions.blueski.{self.user_id}.templates." # Example config path
|
||||
|
||||
def get_editable_templates(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Returns a list of templates that the user can edit for ATProtoSocial.
|
||||
Returns a list of templates that the user can edit for Blueski.
|
||||
Each entry should describe the template, its purpose, and current value.
|
||||
"""
|
||||
# This would typically fetch template definitions from a default set
|
||||
@@ -40,8 +40,8 @@ class ATProtoSocialTemplateEditor:
|
||||
# {
|
||||
# "id": "new_follower_notification", # Unique ID for this template
|
||||
# "name": _("New Follower Notification Format"),
|
||||
# "description": _("Customize how new follower notifications from ATProtoSocial are displayed."),
|
||||
# "default_template": "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on ATProtoSocial!",
|
||||
# "description": _("Customize how new follower notifications from Blueski are displayed."),
|
||||
# "default_template": "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on Blueski!",
|
||||
# "current_template": self._get_template_content("new_follower_notification"),
|
||||
# "variables": [ # Available variables for this template
|
||||
# {"name": "actor.displayName", "description": _("Display name of the new follower")},
|
||||
@@ -50,10 +50,10 @@ class ATProtoSocialTemplateEditor:
|
||||
# ],
|
||||
# "category": "notifications", # For grouping in UI
|
||||
# },
|
||||
# # Add more editable templates for ATProtoSocial here
|
||||
# # Add more editable templates for Blueski here
|
||||
# ]
|
||||
# return templates
|
||||
return [] # Placeholder - no editable templates defined yet for ATProtoSocial
|
||||
return [] # Placeholder - no editable templates defined yet for Blueski
|
||||
|
||||
def _get_template_content(self, template_id: str) -> str:
|
||||
"""
|
||||
@@ -70,7 +70,7 @@ class ATProtoSocialTemplateEditor:
|
||||
"""
|
||||
# This could be hardcoded or loaded from a defaults file.
|
||||
# if template_id == "new_follower_notification":
|
||||
# return "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on ATProtoSocial!"
|
||||
# return "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on Blueski!"
|
||||
# # ... other default templates
|
||||
return "" # Placeholder
|
||||
|
||||
@@ -81,10 +81,10 @@ class ATProtoSocialTemplateEditor:
|
||||
# config_key = self.config_prefix + template_id
|
||||
# try:
|
||||
# await approve.config.config.set_value(config_key, content) # Example config access
|
||||
# logger.info(f"ATProtoSocial template '{template_id}' saved for user {self.user_id}.")
|
||||
# logger.info(f"Blueski template '{template_id}' saved for user {self.user_id}.")
|
||||
# return True
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error saving ATProtoSocial template '{template_id}' for user {self.user_id}: {e}")
|
||||
# logger.error(f"Error saving Blueski template '{template_id}' for user {self.user_id}: {e}")
|
||||
# return False
|
||||
return False # Placeholder
|
||||
|
||||
@@ -104,9 +104,9 @@ class ATProtoSocialTemplateEditor:
|
||||
# # return preview
|
||||
# return f"Preview for '{template_id}': {content_to_render}" # Basic placeholder
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error generating preview for ATProtoSocial template '{template_id}': {e}")
|
||||
# logger.error(f"Error generating preview for Blueski template '{template_id}': {e}")
|
||||
# return _("Error generating preview.")
|
||||
return _("Template previews not yet implemented for ATProtoSocial.") # Placeholder
|
||||
return _("Template previews not yet implemented for Blueski.") # Placeholder
|
||||
|
||||
def _get_sample_data_for_template(self, template_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -125,29 +125,29 @@ class ATProtoSocialTemplateEditor:
|
||||
|
||||
# Functions to be called by the main controller/handler for template editor actions.
|
||||
|
||||
async def get_editor_config(session: ATProtoSocialSession) -> dict[str, Any]:
|
||||
async def get_editor_config(session: BlueskiSession) -> dict[str, Any]:
|
||||
"""
|
||||
Get the configuration needed to display the template editor for ATProtoSocial.
|
||||
Get the configuration needed to display the template editor for Blueski.
|
||||
"""
|
||||
editor = ATProtoSocialTemplateEditor(session)
|
||||
editor = BlueskiTemplateEditor(session)
|
||||
return {
|
||||
"editable_templates": editor.get_editable_templates(),
|
||||
"help_text": _("Customize ATProtoSocial message formats. Use variables shown for each template."),
|
||||
"help_text": _("Customize Blueski message formats. Use variables shown for each template."),
|
||||
}
|
||||
|
||||
async def save_template(session: ATProtoSocialSession, template_id: str, content: str) -> bool:
|
||||
async def save_template(session: BlueskiSession, template_id: str, content: str) -> bool:
|
||||
"""
|
||||
Save a modified template for ATProtoSocial.
|
||||
Save a modified template for Blueski.
|
||||
"""
|
||||
editor = ATProtoSocialTemplateEditor(session)
|
||||
editor = BlueskiTemplateEditor(session)
|
||||
return await editor.save_template_content(template_id, content)
|
||||
|
||||
async def get_template_preview_html(session: ATProtoSocialSession, template_id: str, content: str) -> str:
|
||||
async def get_template_preview_html(session: BlueskiSession, template_id: str, content: str) -> str:
|
||||
"""
|
||||
Get an HTML preview for a template with given content.
|
||||
"""
|
||||
editor = ATProtoSocialTemplateEditor(session)
|
||||
editor = BlueskiTemplateEditor(session)
|
||||
return editor.get_template_preview(template_id, custom_content=content)
|
||||
|
||||
|
||||
logger.info("ATProtoSocial template editor module loaded (placeholders).")
|
||||
logger.info("Blueski template editor module loaded (placeholders).")
|
||||
@@ -7,32 +7,32 @@ fromapprove.translation import translate as _
|
||||
# fromapprove.controller.mastodon import userActions as mastodon_user_actions # If adapting
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file defines user-specific actions that can be performed on ATProtoSocial entities,
|
||||
# This file defines user-specific actions that can be performed on Blueski entities,
|
||||
# typically represented as buttons or links in the UI, often on user profiles or posts.
|
||||
|
||||
# For ATProtoSocial, actions might include:
|
||||
# - Viewing a user's profile on Bluesky/ATProtoSocial instance.
|
||||
# For Blueski, actions might include:
|
||||
# - Viewing a user's profile on Bluesky/Blueski instance.
|
||||
# - Following/Unfollowing a user.
|
||||
# - Muting/Blocking a user.
|
||||
# - Reporting a user.
|
||||
# - Fetching a user's latest posts.
|
||||
|
||||
# These actions are often presented in a context menu or as direct buttons.
|
||||
# The `get_user_actions` method in the ATProtoSocialSession class would define these.
|
||||
# The `get_user_actions` method in the BlueskiSession class would define these.
|
||||
# This file would contain the implementation or further handling logic if needed,
|
||||
# or if actions are too complex for simple lambda/method calls in the session class.
|
||||
|
||||
# Example structure for defining an action:
|
||||
# (This might be more detailed if actions require forms or multi-step processes)
|
||||
|
||||
# def view_profile_action(session: ATProtoSocialSession, user_id: str) -> dict[str, Any]:
|
||||
# def view_profile_action(session: BlueskiSession, user_id: str) -> dict[str, Any]:
|
||||
# """
|
||||
# Generates data for a "View Profile on ATProtoSocial" action.
|
||||
# user_id here would be the ATProtoSocial DID or handle.
|
||||
# Generates data for a "View Profile on Blueski" action.
|
||||
# user_id here would be the Blueski DID or handle.
|
||||
# """
|
||||
# # profile_url = f"https://bsky.app/profile/{user_id}" # Example, construct from handle or DID
|
||||
# # This might involve resolving DID to handle or vice-versa if only one is known.
|
||||
@@ -40,20 +40,20 @@ logger = logging.getLogger(__name__)
|
||||
# # profile_url = f"https://bsky.app/profile/{handle}"
|
||||
|
||||
# return {
|
||||
# "id": "atprotosocial_view_profile",
|
||||
# "id": "blueski_view_profile",
|
||||
# "label": _("View Profile on Bluesky"),
|
||||
# "icon": "external-link-alt", # FontAwesome icon name
|
||||
# "action_type": "link", # "link", "modal", "api_call"
|
||||
# "url": profile_url, # For "link" type
|
||||
# # "api_endpoint": "/api/atprotosocial/user_action", # For "api_call"
|
||||
# # "api_endpoint": "/api/blueski/user_action", # For "api_call"
|
||||
# # "payload": {"action": "view_profile", "target_user_id": user_id},
|
||||
# "confirmation_required": False,
|
||||
# }
|
||||
|
||||
|
||||
# async def follow_user_action_handler(session: ATProtoSocialSession, target_user_id: str) -> dict[str, Any]:
|
||||
# async def follow_user_action_handler(session: BlueskiSession, target_user_id: str) -> dict[str, Any]:
|
||||
# """
|
||||
# Handles the 'follow_user' action for ATProtoSocial.
|
||||
# Handles the 'follow_user' action for Blueski.
|
||||
# target_user_id should be the DID of the user to follow.
|
||||
# """
|
||||
# # success = await session.util.follow_user(target_user_id)
|
||||
@@ -65,11 +65,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# The list of available actions is typically defined in the Session class,
|
||||
# e.g., ATProtoSocialSession.get_user_actions(). That method would return a list
|
||||
# e.g., BlueskiSession.get_user_actions(). That method would return a list
|
||||
# of dictionaries, and this file might provide handlers for more complex actions
|
||||
# if they aren't simple API calls defined directly in the session's util.
|
||||
|
||||
# For now, this file can be a placeholder if most actions are simple enough
|
||||
# to be handled directly by the session.util methods or basic handler routes.
|
||||
|
||||
logger.info("ATProtoSocial userActions module loaded (placeholders).")
|
||||
logger.info("Blueski userActions module loaded (placeholders).")
|
||||
@@ -7,39 +7,39 @@ fromapprove.translation import translate as _
|
||||
# fromapprove.controller.mastodon import userList as mastodon_user_list # If adapting
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
|
||||
# Define a type for what a user entry in a list might look like for ATProtoSocial
|
||||
ATProtoSocialUserListItem = dict[str, Any] # e.g. {"did": "...", "handle": "...", "displayName": "..."}
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
# Define a type for what a user entry in a list might look like for Blueski
|
||||
BlueskiUserListItem = dict[str, Any] # e.g. {"did": "...", "handle": "...", "displayName": "..."}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file is responsible for fetching and managing lists of users from ATProtoSocial.
|
||||
# This file is responsible for fetching and managing lists of users from Blueski.
|
||||
# Examples include:
|
||||
# - Followers of a user
|
||||
# - Users a user is following
|
||||
# - Users who liked or reposted a post
|
||||
# - Users in a specific list or feed (if ATProtoSocial supports user lists like Twitter/Mastodon)
|
||||
# - Users in a specific list or feed (if Blueski supports user lists like Twitter/Mastodon)
|
||||
# - Search results for users
|
||||
|
||||
# The structure will likely involve:
|
||||
# - A base class or functions for paginating through user lists from the ATProtoSocial API.
|
||||
# - A base class or functions for paginating through user lists from the Blueski API.
|
||||
# - Specific functions for each type of user list.
|
||||
# - Formatting ATProtoSocial user data into a consistent structure for UI display.
|
||||
# - Formatting Blueski user data into a consistent structure for UI display.
|
||||
|
||||
async def fetch_followers(
|
||||
session: ATProtoSocialSession,
|
||||
session: BlueskiSession,
|
||||
user_id: str, # DID of the user whose followers to fetch
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> AsyncGenerator[ATProtoSocialUserListItem, None]:
|
||||
) -> AsyncGenerator[BlueskiUserListItem, None]:
|
||||
"""
|
||||
Asynchronously fetches a list of followers for a given ATProtoSocial user.
|
||||
Asynchronously fetches a list of followers for a given Blueski user.
|
||||
user_id is the DID of the target user.
|
||||
Yields user data dictionaries.
|
||||
"""
|
||||
# client = await session.util._get_client() # Get authenticated client
|
||||
# if not client:
|
||||
# logger.warning(f"ATProtoSocial client not available for fetching followers of {user_id}.")
|
||||
# logger.warning(f"Blueski client not available for fetching followers of {user_id}.")
|
||||
# return
|
||||
|
||||
# current_cursor = cursor
|
||||
@@ -80,7 +80,7 @@ async def fetch_followers(
|
||||
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot fetch followers for {user_id}: ATProtoSocial session not ready.")
|
||||
logger.warning(f"Cannot fetch followers for {user_id}: Blueski session not ready.")
|
||||
# yield {} # Stop iteration if not ready
|
||||
return
|
||||
|
||||
@@ -94,22 +94,22 @@ async def fetch_followers(
|
||||
logger.info(f"No followers data returned for user {user_id}.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fetch_followers for ATProtoSocial user {user_id}: {e}", exc_info=True)
|
||||
logger.error(f"Error in fetch_followers for Blueski user {user_id}: {e}", exc_info=True)
|
||||
# Depending on desired error handling, could raise or yield an error marker
|
||||
|
||||
|
||||
async def fetch_following(
|
||||
session: ATProtoSocialSession,
|
||||
session: BlueskiSession,
|
||||
user_id: str, # DID of the user whose followed accounts to fetch
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> AsyncGenerator[ATProtoSocialUserListItem, None]:
|
||||
) -> AsyncGenerator[BlueskiUserListItem, None]:
|
||||
"""
|
||||
Asynchronously fetches a list of users followed by a given ATProtoSocial user.
|
||||
Asynchronously fetches a list of users followed by a given Blueski user.
|
||||
Yields user data dictionaries.
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot fetch following for {user_id}: ATProtoSocial session not ready.")
|
||||
logger.warning(f"Cannot fetch following for {user_id}: Blueski session not ready.")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -122,21 +122,21 @@ async def fetch_following(
|
||||
logger.info(f"No following data returned for user {user_id}.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fetch_following for ATProtoSocial user {user_id}: {e}", exc_info=True)
|
||||
logger.error(f"Error in fetch_following for Blueski user {user_id}: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def search_users(
|
||||
session: ATProtoSocialSession,
|
||||
session: BlueskiSession,
|
||||
query: str,
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> AsyncGenerator[ATProtoSocialUserListItem, None]:
|
||||
) -> AsyncGenerator[BlueskiUserListItem, None]:
|
||||
"""
|
||||
Searches for users on ATProtoSocial based on a query string.
|
||||
Searches for users on Blueski based on a query string.
|
||||
Yields user data dictionaries.
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot search users for '{query}': ATProtoSocial session not ready.")
|
||||
logger.warning(f"Cannot search users for '{query}': Blueski session not ready.")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -149,25 +149,25 @@ async def search_users(
|
||||
logger.info(f"No users found for search term '{query}'.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in search_users for ATProtoSocial query '{query}': {e}", exc_info=True)
|
||||
logger.error(f"Error in search_users for Blueski query '{query}': {e}", exc_info=True)
|
||||
|
||||
# This function is designed to be called by an API endpoint that returns JSON
|
||||
async def get_user_list_paginated(
|
||||
session: ATProtoSocialSession,
|
||||
session: BlueskiSession,
|
||||
list_type: str, # "followers", "following", "search"
|
||||
identifier: str, # User DID for followers/following, or search query for search
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> tuple[list[ATProtoSocialUserListItem], str | None]:
|
||||
) -> tuple[list[BlueskiUserListItem], str | None]:
|
||||
"""
|
||||
Fetches a paginated list of users (followers, following, or search results)
|
||||
and returns the list and the next cursor.
|
||||
"""
|
||||
users_list: list[ATProtoSocialUserListItem] = []
|
||||
users_list: list[BlueskiUserListItem] = []
|
||||
next_cursor: str | None = None
|
||||
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot fetch user list '{list_type}': ATProtoSocial session not ready.")
|
||||
logger.warning(f"Cannot fetch user list '{list_type}': Blueski session not ready.")
|
||||
return [], None
|
||||
|
||||
try:
|
||||
@@ -192,13 +192,13 @@ async def get_user_list_paginated(
|
||||
return users_list, next_cursor
|
||||
|
||||
|
||||
async def get_user_profile_details(session: ATProtoSocialSession, user_ident: str) -> ATProtoSocialUserListItem | None:
|
||||
async def get_user_profile_details(session: BlueskiSession, user_ident: str) -> BlueskiUserListItem | None:
|
||||
"""
|
||||
Fetches detailed profile information for a user by DID or handle.
|
||||
Returns a dictionary of formatted profile data, or None if not found/error.
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot get profile for {user_ident}: ATProtoSocial session not ready.")
|
||||
logger.warning(f"Cannot get profile for {user_ident}: Blueski session not ready.")
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -222,4 +222,4 @@ async def get_user_profile_details(session: ATProtoSocialSession, user_ident: st
|
||||
# The UI part of Approve that displays user lists would call these functions.
|
||||
# Each function needs to handle pagination as provided by the ATProto API (usually cursor-based).
|
||||
|
||||
logger.info("ATProtoSocial userList module loaded (placeholders).")
|
||||
logger.info("Blueski userList module loaded (placeholders).")
|
||||
@@ -280,6 +280,12 @@ class BaseBuffer(base.Buffer):
|
||||
return
|
||||
menu = menus.base()
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
|
||||
# Enable/disable edit based on whether the post belongs to the user
|
||||
item = self.get_item()
|
||||
if item and item.account.id == self.session.db["user_id"] and item.reblog == None:
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
|
||||
else:
|
||||
menu.edit.Enable(False)
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
||||
if self.can_share() == True:
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
||||
@@ -501,6 +507,49 @@ class BaseBuffer(base.Buffer):
|
||||
log.exception("")
|
||||
self.session.db[self.name] = items
|
||||
|
||||
def edit_status(self, event=None, item=None, *args, **kwargs):
|
||||
if item == None:
|
||||
item = self.get_item()
|
||||
# Check if the post belongs to the current user
|
||||
if item.account.id != self.session.db["user_id"] or item.reblog != None:
|
||||
output.speak(_("You can only edit your own posts."))
|
||||
return
|
||||
# Check if post has a poll with votes - warn user before proceeding
|
||||
if hasattr(item, 'poll') and item.poll is not None:
|
||||
votes_count = item.poll.votes_count if hasattr(item.poll, 'votes_count') else 0
|
||||
if votes_count > 0:
|
||||
# Show confirmation dialog
|
||||
warning_title = _("Warning: Poll with votes")
|
||||
warning_message = _("This post contains a poll with {votes} votes.\n\n"
|
||||
"According to Mastodon's API, editing this post will reset ALL votes to zero, "
|
||||
"even if you don't modify the poll itself.\n\n"
|
||||
"Do you want to continue editing?").format(votes=votes_count)
|
||||
dialog = wx.MessageDialog(self.buffer, warning_message, warning_title,
|
||||
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING)
|
||||
result = dialog.ShowModal()
|
||||
dialog.Destroy()
|
||||
if result != wx.ID_YES:
|
||||
output.speak(_("Edit cancelled"))
|
||||
return
|
||||
# Log item info for debugging
|
||||
log.debug("Editing status: id={}, has_media_attachments={}, media_count={}".format(
|
||||
item.id,
|
||||
hasattr(item, 'media_attachments'),
|
||||
len(item.media_attachments) if hasattr(item, 'media_attachments') else 0
|
||||
))
|
||||
# Create edit dialog with existing post data
|
||||
title = _("Edit post")
|
||||
caption = _("Edit your post here")
|
||||
post = messages.editPost(session=self.session, item=item, title=title, caption=caption)
|
||||
response = post.message.ShowModal()
|
||||
if response == wx.ID_OK:
|
||||
post_data = post.get_data()
|
||||
# Call edit_post method in session
|
||||
# Note: visibility and language cannot be changed when editing per Mastodon API
|
||||
call_threaded(self.session.edit_post, post_id=post.post_id, posts=post_data)
|
||||
if hasattr(post.message, "destroy"):
|
||||
post.message.destroy()
|
||||
|
||||
def user_details(self):
|
||||
item = self.get_item()
|
||||
pass
|
||||
|
||||
@@ -161,6 +161,13 @@ class NotificationsBuffer(BaseBuffer):
|
||||
menu = menus.notification(notification.type)
|
||||
if self.is_post():
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
|
||||
# Enable/disable edit based on whether the post belongs to the user
|
||||
if hasattr(menu, 'edit'):
|
||||
status = self.get_post()
|
||||
if status and status.account.id == self.session.db["user_id"] and status.reblog == None:
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
|
||||
else:
|
||||
menu.edit.Enable(False)
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
||||
if self.can_share() == True:
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
||||
|
||||
@@ -25,7 +25,7 @@ from mysc import localization
|
||||
from mysc.thread_utils import call_threaded
|
||||
from mysc.repeating_timer import RepeatingTimer
|
||||
from controller.mastodon import handler as MastodonHandler
|
||||
from controller.atprotosocial import handler as ATProtoSocialHandler # Added import
|
||||
from controller.blueski import handler as BlueskiHandler # Added import
|
||||
from . import settings, userAlias
|
||||
|
||||
log = logging.getLogger("mainController")
|
||||
@@ -99,8 +99,8 @@ class Controller(object):
|
||||
try:
|
||||
if type == "mastodon":
|
||||
return MastodonHandler.Handler()
|
||||
if type == "atprotosocial":
|
||||
return ATProtoSocialHandler.Handler()
|
||||
if type == "blueski":
|
||||
return BlueskiHandler.Handler()
|
||||
except Exception:
|
||||
log.exception("Error creating handler for type %s", type)
|
||||
return None
|
||||
@@ -207,8 +207,8 @@ class Controller(object):
|
||||
if handler is None:
|
||||
if type == "mastodon":
|
||||
handler = MastodonHandler.Handler()
|
||||
elif type == "atprotosocial":
|
||||
handler = ATProtoSocialHandler.Handler()
|
||||
elif type == "blueski":
|
||||
handler = BlueskiHandler.Handler()
|
||||
self.handlers[type] = handler
|
||||
return handler
|
||||
|
||||
@@ -250,9 +250,9 @@ class Controller(object):
|
||||
for i in sessions.sessions:
|
||||
log.debug("Working on session %s" % (i,))
|
||||
if sessions.sessions[i].is_logged == False:
|
||||
# Try auto-login for ATProtoSocial sessions if credentials exist
|
||||
# Try auto-login for Blueski sessions if credentials exist
|
||||
try:
|
||||
if getattr(sessions.sessions[i], "type", None) == "atprotosocial":
|
||||
if getattr(sessions.sessions[i], "type", None) == "blueski":
|
||||
sessions.sessions[i].login()
|
||||
except Exception:
|
||||
log.exception("Auto-login attempt failed for session %s", i)
|
||||
@@ -260,7 +260,7 @@ class Controller(object):
|
||||
self.create_ignored_session_buffer(sessions.sessions[i])
|
||||
continue
|
||||
# Supported session types
|
||||
valid_session_types = ["mastodon", "atprotosocial"]
|
||||
valid_session_types = ["mastodon", "blueski"]
|
||||
if sessions.sessions[i].type in valid_session_types:
|
||||
try:
|
||||
handler = self.get_handler(type=sessions.sessions[i].type)
|
||||
@@ -323,15 +323,15 @@ class Controller(object):
|
||||
log.debug("Creating buffer of type {0} with parent_tab of {2} arguments {1}".format(buffer_type, kwargs, parent_tab))
|
||||
if kwargs.get("parent") == None:
|
||||
kwargs["parent"] = self.view.nb
|
||||
if not hasattr(buffers, session_type) and session_type != "atprotosocial": # Allow atprotosocial to be handled separately
|
||||
if not hasattr(buffers, session_type) and session_type != "blueski": # Allow blueski to be handled separately
|
||||
raise AttributeError("Session type %s does not exist yet." % (session_type))
|
||||
|
||||
try:
|
||||
buffer_panel_class = None
|
||||
if session_type == "atprotosocial":
|
||||
from wxUI.buffers.atprotosocial import panels as ATProtoSocialPanels # Import new panels
|
||||
if session_type == "blueski":
|
||||
from wxUI.buffers.blueski import panels as BlueskiPanels # Import new panels
|
||||
if buffer_type == "home_timeline":
|
||||
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialHomeTimelinePanel
|
||||
buffer_panel_class = BlueskiPanels.BlueskiHomeTimelinePanel
|
||||
# kwargs for HomeTimelinePanel: parent, name, session
|
||||
# 'name' is buffer_title, 'parent' is self.view.nb
|
||||
# 'session' needs to be fetched based on user_id in kwargs
|
||||
@@ -343,38 +343,38 @@ class Controller(object):
|
||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
||||
|
||||
elif buffer_type == "user_timeline":
|
||||
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialUserTimelinePanel
|
||||
buffer_panel_class = BlueskiPanels.BlueskiUserTimelinePanel
|
||||
# kwargs for UserTimelinePanel: parent, name, session, target_user_did, target_user_handle
|
||||
if "user_id" in kwargs and "session" not in kwargs:
|
||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
||||
kwargs.pop("user_id", None)
|
||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
||||
# target_user_did and target_user_handle must be in kwargs from atprotosocial.Handler
|
||||
# target_user_did and target_user_handle must be in kwargs from blueski.Handler
|
||||
|
||||
elif buffer_type == "notifications":
|
||||
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialNotificationPanel
|
||||
buffer_panel_class = BlueskiPanels.BlueskiNotificationPanel
|
||||
if "user_id" in kwargs and "session" not in kwargs:
|
||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
||||
kwargs.pop("user_id", None)
|
||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
||||
# target_user_did and target_user_handle must be in kwargs from atprotosocial.Handler
|
||||
# target_user_did and target_user_handle must be in kwargs from blueski.Handler
|
||||
|
||||
elif buffer_type == "notifications":
|
||||
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialNotificationPanel
|
||||
buffer_panel_class = BlueskiPanels.BlueskiNotificationPanel
|
||||
if "user_id" in kwargs and "session" not in kwargs:
|
||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
||||
kwargs.pop("user_id", None)
|
||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
||||
elif buffer_type == "user_list_followers" or buffer_type == "user_list_following":
|
||||
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialUserListPanel
|
||||
buffer_panel_class = BlueskiPanels.BlueskiUserListPanel
|
||||
elif buffer_type == "following_timeline":
|
||||
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialFollowingTimelinePanel
|
||||
buffer_panel_class = BlueskiPanels.BlueskiFollowingTimelinePanel
|
||||
# Clean stray keys that this panel doesn't accept
|
||||
kwargs.pop("user_id", None)
|
||||
kwargs.pop("list_type", None)
|
||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
||||
else:
|
||||
log.warning(f"Unsupported ATProtoSocial buffer type: {buffer_type}. Falling back to generic.")
|
||||
log.warning(f"Unsupported Blueski buffer type: {buffer_type}. Falling back to generic.")
|
||||
# Fallback to trying to find it in generic buffers or error
|
||||
available_buffers = getattr(buffers, "base", None) # Or some generic panel module
|
||||
if available_buffers and hasattr(available_buffers, buffer_type):
|
||||
@@ -382,7 +382,7 @@ class Controller(object):
|
||||
elif available_buffers and hasattr(available_buffers, "TimelinePanel"): # Example generic
|
||||
buffer_panel_class = getattr(available_buffers, "TimelinePanel")
|
||||
else:
|
||||
raise AttributeError(f"ATProtoSocial buffer type {buffer_type} not found in atprotosocial.panels or base panels.")
|
||||
raise AttributeError(f"Blueski buffer type {buffer_type} not found in blueski.panels or base panels.")
|
||||
else: # Existing logic for other session types
|
||||
available_buffers = getattr(buffers, session_type)
|
||||
if not hasattr(available_buffers, buffer_type):
|
||||
@@ -549,6 +549,15 @@ class Controller(object):
|
||||
buffer = self.search_buffer(buffer.name, buffer.account)
|
||||
buffer.destroy_status()
|
||||
|
||||
def edit_post(self, *args, **kwargs):
|
||||
""" Edits a post in the current buffer.
|
||||
Users can only edit their own posts."""
|
||||
buffer = self.view.get_current_buffer()
|
||||
if hasattr(buffer, "account"):
|
||||
buffer = self.search_buffer(buffer.name, buffer.account)
|
||||
if hasattr(buffer, "edit_status"):
|
||||
buffer.edit_status()
|
||||
|
||||
def exit(self, *args, **kwargs):
|
||||
if config.app["app-settings"]["ask_at_exit"] == True:
|
||||
answer = commonMessageDialogs.exit_dialog(self.view)
|
||||
@@ -598,7 +607,7 @@ class Controller(object):
|
||||
|
||||
session = buffer.session
|
||||
# Compose for Bluesky (ATProto): dialog with attachments/CW/language
|
||||
if getattr(session, "type", "") == "atprotosocial":
|
||||
if getattr(session, "type", "") == "blueski":
|
||||
# In invisible interface, prefer a quick, minimal compose to avoid complex UI
|
||||
if self.showing == False:
|
||||
# Parent=None so it shows even if main window is hidden
|
||||
@@ -620,7 +629,7 @@ class Controller(object):
|
||||
else:
|
||||
dlg.Destroy()
|
||||
return
|
||||
from wxUI.dialogs.atprotosocial.postDialogs import Post as ATPostDialog
|
||||
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
|
||||
dlg = ATPostDialog()
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
text, files, cw_text, langs = dlg.get_payload()
|
||||
@@ -712,7 +721,7 @@ class Controller(object):
|
||||
return
|
||||
|
||||
session = buffer.session
|
||||
if getattr(session, "type", "") == "atprotosocial":
|
||||
if getattr(session, "type", "") == "blueski":
|
||||
if self.showing == False:
|
||||
dlg = wx.TextEntryDialog(None, _("Write your reply:"), _("Reply"))
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
@@ -732,7 +741,7 @@ class Controller(object):
|
||||
else:
|
||||
dlg.Destroy()
|
||||
return
|
||||
from wxUI.dialogs.atprotosocial.postDialogs import Post as ATPostDialog
|
||||
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
|
||||
dlg = ATPostDialog(caption=_("Reply"))
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
text, files, cw_text, langs = dlg.get_payload()
|
||||
@@ -774,7 +783,7 @@ class Controller(object):
|
||||
session = getattr(buffer, "session", None)
|
||||
if not session:
|
||||
return
|
||||
if getattr(session, "type", "") == "atprotosocial":
|
||||
if getattr(session, "type", "") == "blueski":
|
||||
item_uri = None
|
||||
if hasattr(buffer, "get_selected_item_id"):
|
||||
item_uri = buffer.get_selected_item_id()
|
||||
@@ -819,7 +828,7 @@ class Controller(object):
|
||||
dlg.Destroy()
|
||||
return
|
||||
|
||||
from wxUI.dialogs.atprotosocial.postDialogs import Post as ATPostDialog
|
||||
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
|
||||
dlg = ATPostDialog(caption=_("Quote post"))
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
text, files, cw_text, langs = dlg.get_payload()
|
||||
@@ -865,7 +874,7 @@ class Controller(object):
|
||||
buffer = self.get_current_buffer()
|
||||
if hasattr(buffer, "add_to_favorites"): # Generic buffer method
|
||||
return buffer.add_to_favorites()
|
||||
elif buffer.session and buffer.session.KIND == "atprotosocial":
|
||||
elif buffer.session and buffer.session.KIND == "blueski":
|
||||
item_uri = buffer.get_selected_item_id()
|
||||
if not item_uri:
|
||||
output.speak(_("No item selected to like."), True)
|
||||
@@ -894,7 +903,7 @@ class Controller(object):
|
||||
buffer = self.get_current_buffer()
|
||||
if hasattr(buffer, "remove_from_favorites"): # Generic buffer method
|
||||
return buffer.remove_from_favorites()
|
||||
elif buffer.session and buffer.session.KIND == "atprotosocial":
|
||||
elif buffer.session and buffer.session.KIND == "blueski":
|
||||
item_uri = buffer.get_selected_item_id()
|
||||
if not item_uri:
|
||||
output.speak(_("No item selected to unlike."), True)
|
||||
@@ -1423,9 +1432,9 @@ class Controller(object):
|
||||
def update_buffers(self):
|
||||
for i in self.buffers[:]:
|
||||
if i.session != None and i.session.is_logged == True:
|
||||
# For ATProtoSocial, initial load is in session.start() or manual.
|
||||
# For Blueski, initial load is in session.start() or manual.
|
||||
# Periodic updates would need a separate timer or manual refresh via update_buffer.
|
||||
if i.session.KIND != "atprotosocial":
|
||||
if i.session.KIND != "blueski":
|
||||
try:
|
||||
i.start_stream(mandatory=True) # This is likely for streaming connections or timed polling within buffer
|
||||
except Exception as err:
|
||||
@@ -1444,7 +1453,7 @@ class Controller(object):
|
||||
async def do_update():
|
||||
new_ids = []
|
||||
try:
|
||||
if session.KIND == "atprotosocial":
|
||||
if session.KIND == "blueski":
|
||||
if bf.name == f"{session.label} Home": # Assuming buffer name indicates type
|
||||
# Its panel's load_initial_posts calls session.fetch_home_timeline
|
||||
if hasattr(bf, "load_initial_posts"): # Generic for timeline panels
|
||||
@@ -1462,7 +1471,7 @@ class Controller(object):
|
||||
await bf.load_initial_users(limit=config.app["app-settings"].get("items_per_request", 30))
|
||||
new_ids = [u.get("did") for u in getattr(bf, "user_list_data", []) if isinstance(u,dict)]
|
||||
else:
|
||||
if hasattr(bf, "start_stream"): # Fallback for non-ATProtoSocial panels or unhandled types
|
||||
if hasattr(bf, "start_stream"): # Fallback for non-Blueski panels or unhandled types
|
||||
count = bf.start_stream(mandatory=True, avoid_autoreading=True)
|
||||
if count is not None: new_ids = [str(x) for x in range(count)] # Dummy IDs for count
|
||||
else:
|
||||
@@ -1506,14 +1515,14 @@ class Controller(object):
|
||||
# e.g., bf.pagination_cursor or bf.older_items_cursor
|
||||
# This cursor should be set by the result of previous fetch_..._timeline(new_only=False) calls.
|
||||
|
||||
# For ATProtoSocial, session methods like fetch_home_timeline store their own cursor (e.g., session.home_timeline_cursor)
|
||||
# For Blueski, session methods like fetch_home_timeline store their own cursor (e.g., session.home_timeline_cursor)
|
||||
# The panel (bf) itself should manage its own cursor for "load more"
|
||||
|
||||
current_cursor = None
|
||||
can_load_more_natively = False
|
||||
|
||||
if session.KIND == "atprotosocial":
|
||||
if hasattr(bf, "load_more_posts"): # For ATProtoSocialUserTimelinePanel & ATProtoSocialHomeTimelinePanel
|
||||
if session.KIND == "blueski":
|
||||
if hasattr(bf, "load_more_posts"): # For BlueskiUserTimelinePanel & BlueskiHomeTimelinePanel
|
||||
can_load_more_natively = True
|
||||
if hasattr(bf, "load_more_posts"):
|
||||
can_load_more_natively = True
|
||||
@@ -1530,7 +1539,7 @@ class Controller(object):
|
||||
else:
|
||||
output.speak(_(u"This buffer does not support loading more items in this way."), True)
|
||||
return
|
||||
else: # For other non-ATProtoSocial session types
|
||||
else: # For other non-Blueski session types
|
||||
if hasattr(bf, "get_more_items"):
|
||||
return bf.get_more_items()
|
||||
else:
|
||||
@@ -1541,7 +1550,7 @@ class Controller(object):
|
||||
|
||||
async def do_load_more():
|
||||
try:
|
||||
if session.KIND == "atprotosocial":
|
||||
if session.KIND == "blueski":
|
||||
if hasattr(bf, "load_more_posts"):
|
||||
await bf.load_more_posts(limit=config.app["app-settings"].get("items_per_request", 20))
|
||||
elif hasattr(bf, "load_more_users"):
|
||||
@@ -1664,7 +1673,7 @@ class Controller(object):
|
||||
if handler and hasattr(handler, 'user_details'):
|
||||
# The handler's user_details method is responsible for extracting context
|
||||
# (e.g., selected user) from the buffer and displaying the profile.
|
||||
# For ATProtoSocial, handler.user_details calls the ShowUserProfileDialog.
|
||||
# For Blueski, handler.user_details calls the ShowUserProfileDialog.
|
||||
# It's an async method, so needs to be called appropriately.
|
||||
async def _show_details():
|
||||
await handler.user_details(buffer)
|
||||
|
||||
@@ -48,7 +48,7 @@ class Handler(object):
|
||||
addAlias=_("Add a&lias"),
|
||||
addToList=None,
|
||||
removeFromList=None,
|
||||
details=_("Show user profile"),
|
||||
details=_("S&how user profile"),
|
||||
favs=None,
|
||||
# In buffer Menu.
|
||||
community_timeline =_("Create c&ommunity timeline"),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import os
|
||||
import re
|
||||
import wx
|
||||
import logging
|
||||
import widgetUtils
|
||||
import config
|
||||
import output
|
||||
@@ -14,6 +15,8 @@ from wxUI.dialogs.mastodon import postDialogs
|
||||
from extra.autocompletionUsers import completion
|
||||
from . import userList
|
||||
|
||||
log = logging.getLogger("controller.mastodon.messages")
|
||||
|
||||
def character_count(post_text, post_cw, character_limit=500):
|
||||
# We will use text for counting character limit only.
|
||||
full_text = post_text+post_cw
|
||||
@@ -262,6 +265,108 @@ class post(messages.basicMessage):
|
||||
visibility_setting = visibility_settings.index(setting)
|
||||
self.message.visibility.SetSelection(setting)
|
||||
|
||||
class editPost(post):
|
||||
def __init__(self, session, item, title, caption, *args, **kwargs):
|
||||
""" Initialize edit dialog with existing post data.
|
||||
|
||||
Note: Per Mastodon API, visibility and language cannot be changed when editing.
|
||||
These fields will be displayed but disabled in the UI.
|
||||
"""
|
||||
# Extract text from post
|
||||
if item.reblog != None:
|
||||
item = item.reblog
|
||||
text = item.content
|
||||
# Remove HTML tags from content
|
||||
import re
|
||||
text = re.sub('<[^<]+?>', '', text)
|
||||
# Initialize parent class
|
||||
super(editPost, self).__init__(session, title, caption, text=text, *args, **kwargs)
|
||||
# Store the post ID for editing
|
||||
self.post_id = item.id
|
||||
# Set visibility (read-only, cannot be changed)
|
||||
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
|
||||
self.message.visibility.SetSelection(visibility_settings.get(item.visibility, 0))
|
||||
self.message.visibility.Enable(False) # Disable as it cannot be edited
|
||||
# Set language (read-only, cannot be changed)
|
||||
if item.language:
|
||||
self.set_language(item.language)
|
||||
self.message.language.Enable(False) # Disable as it cannot be edited
|
||||
# Set sensitive content and spoiler
|
||||
if item.sensitive:
|
||||
self.message.sensitive.SetValue(True)
|
||||
if item.spoiler_text:
|
||||
self.message.spoiler.ChangeValue(item.spoiler_text)
|
||||
self.message.on_sensitivity_changed()
|
||||
# Load existing poll (if any)
|
||||
# Note: You cannot have both media and a poll, so check poll first
|
||||
if hasattr(item, 'poll') and item.poll is not None:
|
||||
log.debug("Loading existing poll for post {}".format(self.post_id))
|
||||
poll = item.poll
|
||||
# Extract poll options (just the text, not the votes)
|
||||
poll_options = [option.title for option in poll.options]
|
||||
# Calculate expires_in based on current time and expires_at
|
||||
# For editing, we need to provide a new expiration time
|
||||
# Since we can't get the original expires_in, use a default or let user configure
|
||||
# For now, use 1 day (86400 seconds) as default
|
||||
expires_in = 86400
|
||||
if hasattr(poll, 'expires_at') and poll.expires_at and not poll.expired:
|
||||
# Calculate remaining time if poll hasn't expired
|
||||
from dateutil import parser as date_parser
|
||||
import datetime
|
||||
try:
|
||||
expires_at = poll.expires_at
|
||||
if isinstance(expires_at, str):
|
||||
expires_at = date_parser.parse(expires_at)
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
remaining = (expires_at - now).total_seconds()
|
||||
if remaining > 0:
|
||||
expires_in = int(remaining)
|
||||
except Exception as e:
|
||||
log.warning("Could not calculate poll expiration: {}".format(e))
|
||||
|
||||
poll_info = {
|
||||
"type": "poll",
|
||||
"file": "",
|
||||
"description": _("Poll with {} options").format(len(poll_options)),
|
||||
"options": poll_options,
|
||||
"expires_in": expires_in,
|
||||
"multiple": poll.multiple if hasattr(poll, 'multiple') else False,
|
||||
"hide_totals": poll.voters_count == 0 if hasattr(poll, 'voters_count') else False
|
||||
}
|
||||
self.attachments.append(poll_info)
|
||||
self.message.add_item(item=[poll_info["file"], poll_info["type"], poll_info["description"]])
|
||||
log.debug("Loaded poll with {} options. WARNING: Editing will reset all votes!".format(len(poll_options)))
|
||||
# Load existing media attachments (only if no poll)
|
||||
elif hasattr(item, 'media_attachments'):
|
||||
log.debug("Loading existing media attachments for post {}".format(self.post_id))
|
||||
log.debug("Item has media_attachments attribute, count: {}".format(len(item.media_attachments)))
|
||||
if len(item.media_attachments) > 0:
|
||||
for media in item.media_attachments:
|
||||
log.debug("Processing media: id={}, type={}, url={}".format(media.id, media.type, media.url))
|
||||
media_info = {
|
||||
"id": media.id, # Keep the existing media ID
|
||||
"type": media.type,
|
||||
"file": media.url, # URL of existing media
|
||||
"description": media.description or ""
|
||||
}
|
||||
# Include focus point if available
|
||||
if hasattr(media, 'meta') and media.meta and 'focus' in media.meta:
|
||||
focus = media.meta['focus']
|
||||
media_info["focus"] = (focus.get('x'), focus.get('y'))
|
||||
log.debug("Added focus point: {}".format(media_info["focus"]))
|
||||
self.attachments.append(media_info)
|
||||
# Display in the attachment list
|
||||
display_name = media.url.split('/')[-1]
|
||||
log.debug("Adding item to UI: name={}, type={}, desc={}".format(display_name, media.type, media.description or ""))
|
||||
self.message.add_item(item=[display_name, media.type, media.description or ""])
|
||||
log.debug("Total attachments loaded: {}".format(len(self.attachments)))
|
||||
else:
|
||||
log.debug("media_attachments list is empty")
|
||||
else:
|
||||
log.debug("Item has no poll or media attachments")
|
||||
# Update text processor to reflect the loaded content
|
||||
self.text_processor()
|
||||
|
||||
class viewPost(post):
|
||||
def __init__(self, session, post, offset_hours=0, date="", item_url=""):
|
||||
self.session = session
|
||||
|
||||
Reference in New Issue
Block a user