2025-05-26 14:11:01 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
2026-01-11 20:13:56 +01:00
|
|
|
import wx
|
2026-02-01 13:01:32 +01:00
|
|
|
import asyncio
|
2026-01-11 20:13:56 +01:00
|
|
|
import output
|
2026-02-01 13:01:32 +01:00
|
|
|
from mysc.thread_utils import call_threaded
|
2026-02-01 13:57:01 +01:00
|
|
|
import widgetUtils
|
|
|
|
|
from extra.autocompletionUsers import completion
|
2026-01-11 20:13:56 +01:00
|
|
|
from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog
|
2025-08-30 22:48:00 +02:00
|
|
|
from typing import Any
|
|
|
|
|
import languageHandler # Ensure _() injection
|
2025-05-26 14:11:01 +00:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
class Handler:
|
|
|
|
|
"""Handler for Bluesky integration: creates minimal buffers."""
|
2025-05-26 14:11:01 +00:00
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.menus = dict(
|
2026-02-01 13:57:01 +01:00
|
|
|
# Application menu
|
|
|
|
|
updateProfile="HIDE",
|
|
|
|
|
menuitem_search=_("&Search"),
|
|
|
|
|
lists="HIDE",
|
|
|
|
|
manageAliases="HIDE",
|
|
|
|
|
# Item menu
|
|
|
|
|
compose=_("&Post"),
|
|
|
|
|
reply=_("Re&ply"),
|
|
|
|
|
share=_("&Boost"),
|
|
|
|
|
fav=_("&Add to favorites"),
|
|
|
|
|
unfav="HIDE",
|
|
|
|
|
view=_("&Show post"),
|
|
|
|
|
view_conversation=_("View conversa&tion"),
|
2026-02-01 19:49:49 +01:00
|
|
|
ocr=_("&OCR"),
|
2026-02-01 13:57:01 +01:00
|
|
|
delete=_("&Delete"),
|
|
|
|
|
# User menu
|
|
|
|
|
follow=_("&Actions..."),
|
|
|
|
|
timeline=_("&View timeline..."),
|
|
|
|
|
dm=_("Direct me&ssage"),
|
|
|
|
|
addAlias="HIDE",
|
|
|
|
|
addToList="HIDE",
|
|
|
|
|
removeFromList="HIDE",
|
|
|
|
|
details=_("S&how user profile"),
|
|
|
|
|
favs="HIDE",
|
|
|
|
|
# Buffer menu
|
|
|
|
|
community_timeline="HIDE",
|
|
|
|
|
filter="HIDE",
|
|
|
|
|
manage_filters="HIDE",
|
2025-08-30 22:48:00 +02:00
|
|
|
)
|
2026-02-01 13:57:01 +01:00
|
|
|
self.item_menu = _("&Post")
|
2025-05-26 14:11:01 +00:00
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
def create_buffers(self, session, createAccounts=True, controller=None):
|
|
|
|
|
name = session.get_name()
|
|
|
|
|
if createAccounts:
|
|
|
|
|
from pubsub import pub
|
2026-01-11 20:13:56 +01:00
|
|
|
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=session.logged)
|
|
|
|
|
|
|
|
|
|
if not session.logged:
|
|
|
|
|
logger.debug(f"Session {session.session_id} is not logged in, skipping timeline buffer creation.")
|
|
|
|
|
return
|
|
|
|
|
if name not in controller.accounts:
|
|
|
|
|
controller.accounts.append(name)
|
|
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
root_position = controller.view.search(name, name)
|
|
|
|
|
from pubsub import pub
|
2026-02-01 10:42:05 +01:00
|
|
|
# Home (Following-only timeline - reverse-chronological)
|
2025-08-30 22:48:00 +02:00
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="following_timeline",
|
2026-01-10 19:46:53 +01:00
|
|
|
session_type="blueski",
|
2026-02-01 10:42:05 +01:00
|
|
|
buffer_title=_("Home"),
|
2025-08-30 22:48:00 +02:00
|
|
|
parent_tab=root_position,
|
2026-02-01 19:15:31 +01:00
|
|
|
start=True,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session, sound="tweet_received.ogg")
|
2025-05-26 14:11:01 +00:00
|
|
|
)
|
2026-02-01 19:15:31 +01:00
|
|
|
# Discover timeline
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="home_timeline",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Discover"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="home_timeline", session=session, sound="tweet_received.ogg")
|
2026-02-01 19:15:31 +01:00
|
|
|
)
|
2026-02-01 10:42:05 +01:00
|
|
|
# Mentions (replies, mentions, quotes)
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="MentionsBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Mentions"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="mentions", session=session, sound="mention_received.ogg")
|
2026-02-01 10:42:05 +01:00
|
|
|
)
|
2026-02-18 08:46:17 +01:00
|
|
|
# Chats
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="ConversationListBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Chats"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
|
|
|
|
kwargs=dict(parent=controller.view.nb, name="direct_messages", session=session, sound="dm_received.ogg")
|
|
|
|
|
)
|
2026-01-11 20:13:56 +01:00
|
|
|
# Notifications
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="notifications",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Notifications"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="notifications", session=session, sound="new_event.ogg")
|
2026-01-11 20:13:56 +01:00
|
|
|
)
|
2026-02-01 10:42:05 +01:00
|
|
|
# Sent posts
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="SentBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Sent"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="sent", session=session, sound="tweet_received.ogg")
|
2026-02-01 10:42:05 +01:00
|
|
|
)
|
2026-01-11 20:13:56 +01:00
|
|
|
# Likes
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="likes",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Likes"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="likes", session=session, sound="favourite.ogg")
|
2026-01-11 20:13:56 +01:00
|
|
|
)
|
|
|
|
|
# Followers
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="FollowersBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Followers"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="followers", session=session, sound="update_followers.ogg")
|
2026-01-11 20:13:56 +01:00
|
|
|
)
|
2026-02-01 10:42:05 +01:00
|
|
|
# Followings (Users you follow)
|
2026-01-11 20:13:56 +01:00
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="FollowingBuffer",
|
|
|
|
|
session_type="blueski",
|
2026-02-01 19:15:31 +01:00
|
|
|
buffer_title=_("Following"),
|
2026-01-11 20:13:56 +01:00
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name="following", session=session, sound="update_followers.ogg")
|
2026-01-11 20:13:56 +01:00
|
|
|
)
|
|
|
|
|
# Blocks
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="BlocksBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=_("Blocked Users"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
|
|
|
|
kwargs=dict(parent=controller.view.nb, name="blocked", session=session)
|
|
|
|
|
)
|
2025-05-26 14:11:01 +00:00
|
|
|
|
2026-02-01 13:07:16 +01:00
|
|
|
# Timelines container
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="EmptyBuffer",
|
|
|
|
|
session_type="base",
|
|
|
|
|
buffer_title=_("Timelines"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
|
|
|
|
kwargs=dict(parent=controller.view.nb, name="timelines", account=name)
|
|
|
|
|
)
|
|
|
|
|
timelines_position = controller.view.search("timelines", name)
|
|
|
|
|
|
2026-02-01 13:57:01 +01:00
|
|
|
# Searches container (Bluesky supports search buffers)
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="EmptyBuffer",
|
|
|
|
|
session_type="base",
|
|
|
|
|
buffer_title=_("Searches"),
|
|
|
|
|
parent_tab=root_position,
|
|
|
|
|
start=False,
|
|
|
|
|
kwargs=dict(parent=controller.view.nb, name="searches", account=name)
|
|
|
|
|
)
|
2026-02-01 14:48:00 +01:00
|
|
|
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,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name=buffer_name, session=session, query=query, sound="search_updated.ogg")
|
2026-02-01 14:48:00 +01:00
|
|
|
)
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to restore Bluesky search buffers: %s", e)
|
2026-02-01 13:57:01 +01:00
|
|
|
|
2026-02-01 13:01:32 +01:00
|
|
|
# Saved user timelines
|
|
|
|
|
try:
|
|
|
|
|
timelines = session.settings["other_buffers"].get("timelines")
|
|
|
|
|
if timelines is None:
|
|
|
|
|
timelines = []
|
|
|
|
|
if isinstance(timelines, str):
|
|
|
|
|
timelines = [t for t in timelines.split(",") if t]
|
|
|
|
|
for actor in timelines:
|
|
|
|
|
handle = actor
|
2026-02-01 13:13:00 +01:00
|
|
|
try:
|
|
|
|
|
if isinstance(actor, str) and actor.startswith("did:"):
|
|
|
|
|
profile = session.get_profile(actor)
|
|
|
|
|
if profile:
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
handle = g(profile, "handle") or actor
|
|
|
|
|
except Exception:
|
|
|
|
|
handle = actor
|
2026-02-01 13:01:32 +01:00
|
|
|
title = _("Timeline for {user}").format(user=handle)
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="UserTimeline",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
2026-02-01 13:07:16 +01:00
|
|
|
parent_tab=timelines_position,
|
2026-02-01 13:01:32 +01:00
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name=f"{handle}-timeline", session=session, actor=actor, handle=handle, sound="tweet_timeline.ogg")
|
2026-02-01 13:01:32 +01:00
|
|
|
)
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to restore Bluesky timeline buffers: %s", e)
|
2026-02-01 13:01:32 +01:00
|
|
|
|
2026-02-01 13:08:35 +01:00
|
|
|
# Saved followers/following timelines
|
|
|
|
|
try:
|
|
|
|
|
followers = session.settings["other_buffers"].get("followers_timelines")
|
|
|
|
|
if followers is None:
|
|
|
|
|
followers = []
|
|
|
|
|
if isinstance(followers, str):
|
|
|
|
|
followers = [t for t in followers.split(",") if t]
|
|
|
|
|
for actor in followers:
|
|
|
|
|
handle = actor
|
2026-02-01 13:13:00 +01:00
|
|
|
try:
|
|
|
|
|
if isinstance(actor, str) and actor.startswith("did:"):
|
|
|
|
|
profile = session.get_profile(actor)
|
|
|
|
|
if profile:
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
handle = g(profile, "handle") or actor
|
|
|
|
|
except Exception:
|
|
|
|
|
handle = actor
|
2026-02-01 13:57:01 +01:00
|
|
|
own_actor = session.db.get("user_id") or session.db.get("user_name")
|
|
|
|
|
own_handle = session.db.get("user_name")
|
|
|
|
|
if actor == own_actor or (own_handle and actor == own_handle) or (handle and own_handle and handle == own_handle):
|
|
|
|
|
continue
|
2026-02-01 13:08:35 +01:00
|
|
|
title = _("Followers for {user}").format(user=handle)
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="FollowersBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
|
|
|
|
parent_tab=timelines_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name=f"{handle}-followers", session=session, actor=actor, handle=handle, sound="new_event.ogg")
|
2026-02-01 13:08:35 +01:00
|
|
|
)
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to restore Bluesky followers buffers: %s", e)
|
2026-02-01 13:08:35 +01:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
following = session.settings["other_buffers"].get("following_timelines")
|
|
|
|
|
if following is None:
|
|
|
|
|
following = []
|
|
|
|
|
if isinstance(following, str):
|
|
|
|
|
following = [t for t in following.split(",") if t]
|
|
|
|
|
for actor in following:
|
|
|
|
|
handle = actor
|
2026-02-01 13:13:00 +01:00
|
|
|
try:
|
|
|
|
|
if isinstance(actor, str) and actor.startswith("did:"):
|
|
|
|
|
profile = session.get_profile(actor)
|
|
|
|
|
if profile:
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
handle = g(profile, "handle") or actor
|
|
|
|
|
except Exception:
|
|
|
|
|
handle = actor
|
2026-02-01 13:57:01 +01:00
|
|
|
own_actor = session.db.get("user_id") or session.db.get("user_name")
|
|
|
|
|
own_handle = session.db.get("user_name")
|
|
|
|
|
if actor == own_actor or (own_handle and actor == own_handle) or (handle and own_handle and handle == own_handle):
|
|
|
|
|
continue
|
2026-02-01 13:08:35 +01:00
|
|
|
title = _("Following for {user}").format(user=handle)
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="FollowingBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
|
|
|
|
parent_tab=timelines_position,
|
|
|
|
|
start=False,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name=f"{handle}-following", session=session, actor=actor, handle=handle, sound="new_event.ogg")
|
2026-02-01 13:08:35 +01:00
|
|
|
)
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to restore Bluesky following buffers: %s", e)
|
2026-02-01 13:08:35 +01:00
|
|
|
|
2026-02-01 10:42:05 +01:00
|
|
|
# Start the background poller for real-time-like updates
|
|
|
|
|
try:
|
|
|
|
|
session.start_streaming()
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to start Bluesky streaming for session %s: %s", name, e)
|
2026-02-01 10:42:05 +01:00
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
def start_buffer(self, controller, buffer):
|
|
|
|
|
"""Start a newly created Bluesky buffer."""
|
2025-05-26 14:11:01 +00:00
|
|
|
try:
|
2025-08-30 22:48:00 +02:00
|
|
|
if hasattr(buffer, "start_stream"):
|
|
|
|
|
buffer.start_stream(mandatory=True, play_sound=False)
|
|
|
|
|
# Enable periodic auto-refresh to simulate real-time updates
|
|
|
|
|
if hasattr(buffer, "enable_auto_refresh"):
|
|
|
|
|
buffer.enable_auto_refresh()
|
|
|
|
|
finally:
|
|
|
|
|
# Ensure we won't try to start it again
|
|
|
|
|
try:
|
|
|
|
|
buffer.needs_init = False
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-05-26 14:11:01 +00:00
|
|
|
|
2025-11-07 09:24:02 +01:00
|
|
|
def account_settings(self, buffer, controller):
|
|
|
|
|
"""Open a minimal account settings dialog for Bluesky."""
|
|
|
|
|
try:
|
|
|
|
|
current_mode = None
|
|
|
|
|
try:
|
|
|
|
|
current_mode = buffer.session.settings["general"].get("boost_mode")
|
|
|
|
|
except Exception:
|
|
|
|
|
current_mode = None
|
|
|
|
|
ask_default = True if current_mode in (None, "ask") else False
|
|
|
|
|
|
2026-01-10 19:46:53 +01:00
|
|
|
from wxUI.dialogs.blueski.configuration import AccountSettingsDialog
|
2026-02-01 21:10:46 +01:00
|
|
|
from .templateEditor import EditTemplate
|
2025-11-07 09:24:02 +01:00
|
|
|
dlg = AccountSettingsDialog(controller.view, ask_before_boost=ask_default)
|
2026-02-01 21:10:46 +01:00
|
|
|
try:
|
|
|
|
|
if buffer.session.settings.get("templates") is None:
|
|
|
|
|
buffer.session.settings["templates"] = {}
|
|
|
|
|
templates_cfg = buffer.session.settings.get("templates", {})
|
|
|
|
|
template_state = {
|
2026-02-15 23:50:00 +00:00
|
|
|
"post": templates_cfg.get("post", "$display_name, $reply_to$safe_text $date."),
|
2026-02-03 13:28:12 +01:00
|
|
|
"person": templates_cfg.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at."),
|
2026-02-01 21:10:46 +01:00
|
|
|
"notification": templates_cfg.get("notification", "$display_name $text, $date"),
|
|
|
|
|
}
|
|
|
|
|
dlg.set_template_labels(template_state["post"], template_state["person"], template_state["notification"])
|
|
|
|
|
|
|
|
|
|
def edit_post_template(*args, **kwargs):
|
|
|
|
|
control = EditTemplate(template=template_state["post"], type="post")
|
|
|
|
|
result = control.run_dialog()
|
|
|
|
|
if result:
|
|
|
|
|
buffer.session.settings["templates"]["post"] = result
|
|
|
|
|
buffer.session.settings.write()
|
|
|
|
|
template_state["post"] = result
|
|
|
|
|
dlg.set_template_labels(template_state["post"], template_state["person"], template_state["notification"])
|
|
|
|
|
|
|
|
|
|
def edit_person_template(*args, **kwargs):
|
|
|
|
|
control = EditTemplate(template=template_state["person"], type="person")
|
|
|
|
|
result = control.run_dialog()
|
|
|
|
|
if result:
|
|
|
|
|
buffer.session.settings["templates"]["person"] = result
|
|
|
|
|
buffer.session.settings.write()
|
|
|
|
|
template_state["person"] = result
|
|
|
|
|
dlg.set_template_labels(template_state["post"], template_state["person"], template_state["notification"])
|
|
|
|
|
|
|
|
|
|
def edit_notification_template(*args, **kwargs):
|
|
|
|
|
control = EditTemplate(template=template_state["notification"], type="notification")
|
|
|
|
|
result = control.run_dialog()
|
|
|
|
|
if result:
|
|
|
|
|
buffer.session.settings["templates"]["notification"] = result
|
|
|
|
|
buffer.session.settings.write()
|
|
|
|
|
template_state["notification"] = result
|
|
|
|
|
dlg.set_template_labels(template_state["post"], template_state["person"], template_state["notification"])
|
|
|
|
|
|
|
|
|
|
widgetUtils.connect_event(dlg.template_post, widgetUtils.BUTTON_PRESSED, edit_post_template)
|
|
|
|
|
widgetUtils.connect_event(dlg.template_person, widgetUtils.BUTTON_PRESSED, edit_person_template)
|
|
|
|
|
widgetUtils.connect_event(dlg.template_notification, widgetUtils.BUTTON_PRESSED, edit_notification_template)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to init Bluesky templates editor: %s", e)
|
2025-11-07 09:24:02 +01:00
|
|
|
resp = dlg.ShowModal()
|
|
|
|
|
if resp == wx.ID_OK:
|
|
|
|
|
vals = dlg.get_values()
|
|
|
|
|
boost_mode = "ask" if vals.get("ask_before_boost") else "direct"
|
|
|
|
|
try:
|
|
|
|
|
buffer.session.settings["general"]["boost_mode"] = boost_mode
|
|
|
|
|
buffer.session.settings.write()
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to persist Bluesky boost_mode setting: %s", e)
|
2025-11-07 09:24:02 +01:00
|
|
|
dlg.Destroy()
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error opening Bluesky account settings dialog: %s", e)
|
2025-11-07 09:24:02 +01:00
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
def user_details(self, buffer):
|
|
|
|
|
"""Show user profile dialog for the selected user/post."""
|
|
|
|
|
session = getattr(buffer, "session", None)
|
|
|
|
|
if not session:
|
|
|
|
|
output.speak(_("No active session to view user details."), True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
item = buffer.get_item() if hasattr(buffer, "get_item") else None
|
|
|
|
|
if not item:
|
|
|
|
|
output.speak(_("No user selected or identified to view details."), True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
|
|
|
|
|
user_ident = None
|
|
|
|
|
|
|
|
|
|
# If we're in a user list, the item itself is the user profile dict/model.
|
|
|
|
|
if g(item, "did") or g(item, "handle"):
|
|
|
|
|
user_ident = g(item, "did") or g(item, "handle")
|
|
|
|
|
else:
|
|
|
|
|
author = g(item, "author")
|
|
|
|
|
if not author:
|
|
|
|
|
post = g(item, "post") or g(item, "record")
|
|
|
|
|
author = g(post, "author") if post else None
|
|
|
|
|
if author:
|
|
|
|
|
user_ident = g(author, "did") or g(author, "handle")
|
|
|
|
|
|
|
|
|
|
if not user_ident:
|
|
|
|
|
output.speak(_("No user selected or identified to view details."), True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
parent = getattr(buffer, "buffer", None) or wx.GetApp().GetTopWindow()
|
|
|
|
|
dialog = ShowUserProfileDialog(parent, session, user_ident)
|
|
|
|
|
dialog.ShowModal()
|
|
|
|
|
dialog.Destroy()
|
|
|
|
|
|
2025-05-26 14:11:01 +00:00
|
|
|
async def handle_action(self, action_name: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
2025-08-30 22:48:00 +02:00
|
|
|
logger.debug("handle_action stub: %s %s %s", action_name, user_id, payload)
|
|
|
|
|
return None
|
2025-05-26 14:11:01 +00:00
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
async def handle_message_command(self, command: str, user_id: str, message_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
|
|
logger.debug("handle_message_command stub: %s %s %s %s", command, user_id, message_id, payload)
|
|
|
|
|
return None
|
2025-05-26 14:11:01 +00:00
|
|
|
|
2025-08-30 22:48:00 +02:00
|
|
|
async def handle_user_command(self, command: str, user_id: str, target_user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
|
|
logger.debug("handle_user_command stub: %s %s %s %s", command, user_id, target_user_id, payload)
|
|
|
|
|
return None
|
2026-01-11 20:13:56 +01:00
|
|
|
|
|
|
|
|
def add_to_favourites(self, buffer):
|
|
|
|
|
"""Standard action for Alt+Win+F"""
|
|
|
|
|
if hasattr(buffer, "add_to_favorites"):
|
|
|
|
|
buffer.add_to_favorites()
|
|
|
|
|
elif hasattr(buffer, "on_like"):
|
|
|
|
|
# Fallback
|
|
|
|
|
buffer.on_like(None)
|
|
|
|
|
|
|
|
|
|
def remove_from_favourites(self, buffer):
|
|
|
|
|
"""Standard action for Alt+Shift+Win+F"""
|
|
|
|
|
if hasattr(buffer, "remove_from_favorites"):
|
|
|
|
|
buffer.remove_from_favorites()
|
|
|
|
|
elif hasattr(buffer, "on_like"):
|
|
|
|
|
buffer.on_like(None)
|
|
|
|
|
|
|
|
|
|
def follow(self, buffer):
|
2026-02-02 09:24:23 +01:00
|
|
|
"""Standard action for Ctrl+Win+S - Opens user actions dialog"""
|
|
|
|
|
if not hasattr(buffer, "get_item"):
|
|
|
|
|
return
|
2026-01-11 20:13:56 +01:00
|
|
|
session = getattr(buffer, "session", None)
|
|
|
|
|
if not session:
|
|
|
|
|
output.speak(_("No active session."), True)
|
|
|
|
|
return
|
|
|
|
|
|
2026-02-02 09:24:23 +01:00
|
|
|
item = buffer.get_item()
|
|
|
|
|
if not item:
|
|
|
|
|
return
|
|
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
|
2026-02-02 09:24:23 +01:00
|
|
|
users = []
|
|
|
|
|
buffer_type = getattr(buffer, "type", "")
|
|
|
|
|
|
|
|
|
|
if buffer_type in ("user", "post_user_list"):
|
|
|
|
|
# User buffer - item is a user object
|
|
|
|
|
handle = g(item, "handle")
|
|
|
|
|
if handle:
|
|
|
|
|
users = [handle]
|
|
|
|
|
elif buffer_type == "notifications":
|
|
|
|
|
# Notification buffer
|
|
|
|
|
author = g(item, "author")
|
|
|
|
|
if author:
|
|
|
|
|
handle = g(author, "handle")
|
|
|
|
|
if handle:
|
|
|
|
|
users.append(handle)
|
|
|
|
|
# Also check for post author in the notification subject
|
|
|
|
|
record = g(item, "record")
|
|
|
|
|
if record:
|
|
|
|
|
subject = g(record, "subject")
|
|
|
|
|
if subject:
|
|
|
|
|
subject_author = g(subject, "author")
|
|
|
|
|
if subject_author:
|
|
|
|
|
subject_handle = g(subject_author, "handle")
|
|
|
|
|
if subject_handle and subject_handle not in users:
|
|
|
|
|
users.append(subject_handle)
|
|
|
|
|
else:
|
|
|
|
|
# Post buffer - extract author and mentioned users
|
|
|
|
|
# Get the actual post (could be nested in "post" key)
|
|
|
|
|
actual_post = g(item, "post", item)
|
|
|
|
|
record = g(actual_post, "record") or {}
|
|
|
|
|
|
|
|
|
|
# Extract mentions from facets
|
|
|
|
|
facets = g(record, "facets") or []
|
|
|
|
|
for facet in facets:
|
|
|
|
|
features = g(facet, "features") or []
|
|
|
|
|
for feature in features:
|
|
|
|
|
ftype = g(feature, "$type") or g(feature, "py_type") or ""
|
|
|
|
|
if "mention" in ftype.lower():
|
|
|
|
|
mention_did = g(feature, "did")
|
|
|
|
|
# We'd need to resolve DID to handle, but for simplicity just skip
|
|
|
|
|
# The main author will be added below
|
|
|
|
|
|
|
|
|
|
# Get the post author
|
|
|
|
|
author = g(actual_post, "author") or g(item, "author")
|
|
|
|
|
if author:
|
|
|
|
|
handle = g(author, "handle")
|
|
|
|
|
if handle and handle not in users:
|
|
|
|
|
users.insert(0, handle)
|
|
|
|
|
|
|
|
|
|
# Ensure we have at least the author if no users found
|
|
|
|
|
if not users:
|
|
|
|
|
author = g(item, "author") or g(g(item, "post"), "author")
|
|
|
|
|
if author:
|
|
|
|
|
handle = g(author, "handle")
|
|
|
|
|
if handle:
|
|
|
|
|
users = [handle]
|
2026-01-11 20:13:56 +01:00
|
|
|
|
|
|
|
|
from controller.blueski import userActions as user_actions_controller
|
|
|
|
|
user_actions_controller.userActions(session, users)
|
|
|
|
|
|
|
|
|
|
def open_conversation(self, controller, buffer):
|
|
|
|
|
"""Standard action for Control+Win+C"""
|
2026-02-17 22:59:59 +01:00
|
|
|
# If this is a chat conversation list, open the selected chat
|
|
|
|
|
if buffer.type == "chat" and hasattr(buffer, "view_chat"):
|
|
|
|
|
buffer.view_chat()
|
|
|
|
|
return
|
2026-01-11 20:13:56 +01:00
|
|
|
item = buffer.get_item()
|
|
|
|
|
if not item:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
uri = None
|
|
|
|
|
if hasattr(buffer, "get_selected_item_id"):
|
|
|
|
|
uri = buffer.get_selected_item_id()
|
|
|
|
|
if not uri:
|
|
|
|
|
uri = getattr(item, "uri", None) or (item.get("post", {}).get("uri") if isinstance(item, dict) else None)
|
|
|
|
|
if not uri: return
|
|
|
|
|
|
|
|
|
|
# Buffer Title
|
2026-02-01 12:39:50 +01:00
|
|
|
handle = None
|
|
|
|
|
display_name = None
|
|
|
|
|
if hasattr(buffer, "get_selected_item_author_details"):
|
|
|
|
|
details = buffer.get_selected_item_author_details()
|
|
|
|
|
if details:
|
|
|
|
|
handle = details.get("handle")
|
|
|
|
|
if not handle:
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
author = g(item, "author") or g(g(item, "post"), "author")
|
|
|
|
|
if author:
|
|
|
|
|
handle = g(author, "handle")
|
|
|
|
|
display_name = g(author, "displayName") or g(author, "display_name")
|
|
|
|
|
label = handle or display_name or _("Unknown")
|
|
|
|
|
title = _("Conversation with {0}").format(label)
|
2026-01-11 20:13:56 +01:00
|
|
|
|
|
|
|
|
from pubsub import pub
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="conversation",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
|
|
|
|
parent_tab=controller.view.search(buffer.session.get_name(), buffer.session.get_name()) if hasattr(buffer.session, "get_name") else None,
|
|
|
|
|
start=True,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=controller.view.nb, name=title, session=buffer.session, uri=uri, sound="search_updated.ogg")
|
2026-01-11 20:13:56 +01:00
|
|
|
)
|
|
|
|
|
|
2026-02-01 13:01:32 +01:00
|
|
|
def open_timeline(self, controller, buffer, default="posts"):
|
|
|
|
|
if not hasattr(buffer, "get_item"):
|
|
|
|
|
return
|
|
|
|
|
item = buffer.get_item()
|
|
|
|
|
if not item:
|
|
|
|
|
output.speak(_("No user selected."), True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
|
2026-02-01 13:13:00 +01:00
|
|
|
users = []
|
2026-02-01 13:01:32 +01:00
|
|
|
handle = None
|
|
|
|
|
if hasattr(buffer, "get_selected_item_author_details"):
|
|
|
|
|
details = buffer.get_selected_item_author_details()
|
|
|
|
|
if details:
|
|
|
|
|
handle = details.get("handle") or details.get("did")
|
|
|
|
|
if not handle:
|
|
|
|
|
if g(item, "handle") or g(item, "did"):
|
|
|
|
|
handle = g(item, "handle") or g(item, "did")
|
|
|
|
|
else:
|
|
|
|
|
author = g(item, "author") or g(g(item, "post"), "author")
|
|
|
|
|
if author:
|
|
|
|
|
handle = g(author, "handle") or g(author, "did")
|
|
|
|
|
|
|
|
|
|
if not handle:
|
|
|
|
|
output.speak(_("No user selected."), True)
|
|
|
|
|
return
|
2026-02-01 13:13:00 +01:00
|
|
|
users.append(handle)
|
|
|
|
|
|
|
|
|
|
# Add mentioned users if available (facets)
|
|
|
|
|
record = g(g(item, "post"), "record") or g(item, "record")
|
|
|
|
|
facets = g(record, "facets", []) if record else []
|
|
|
|
|
handle_cache = {}
|
|
|
|
|
|
|
|
|
|
def resolve_handle(did):
|
|
|
|
|
if did in handle_cache:
|
|
|
|
|
return handle_cache[did]
|
|
|
|
|
try:
|
|
|
|
|
profile = buffer.session.get_profile(did)
|
|
|
|
|
if profile:
|
|
|
|
|
h = g(profile, "handle")
|
|
|
|
|
if h:
|
|
|
|
|
handle_cache[did] = h
|
|
|
|
|
return h
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
self_did = buffer.session.db.get("user_id")
|
|
|
|
|
for facet in facets or []:
|
|
|
|
|
features = g(facet, "features", []) or []
|
|
|
|
|
for feat in features:
|
|
|
|
|
ftype = g(feat, "$type") or g(feat, "py_type") or ""
|
|
|
|
|
if "facet#mention" in ftype:
|
|
|
|
|
did = g(feat, "did")
|
|
|
|
|
if not did or did == self_did:
|
|
|
|
|
continue
|
|
|
|
|
h = resolve_handle(did)
|
|
|
|
|
if h and h not in users:
|
|
|
|
|
users.append(h)
|
2026-02-01 13:01:32 +01:00
|
|
|
|
|
|
|
|
from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog
|
2026-02-01 13:13:00 +01:00
|
|
|
dlg = userTimelineDialog.UserTimeline(users=users, default=default)
|
2026-02-01 13:57:01 +01:00
|
|
|
try:
|
|
|
|
|
widgetUtils.connect_event(
|
|
|
|
|
dlg.autocompletion,
|
|
|
|
|
widgetUtils.BUTTON_PRESSED,
|
|
|
|
|
lambda *args, **kwargs: completion.autocompletionUsers(dlg, buffer.session.session_id).show_menu("free"),
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-02-01 13:01:32 +01:00
|
|
|
try:
|
|
|
|
|
if hasattr(dlg, "autocompletion"):
|
2026-02-01 13:57:01 +01:00
|
|
|
dlg.autocompletion.Enable(True)
|
2026-02-01 13:01:32 +01:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
if dlg.ShowModal() != wx.ID_OK:
|
|
|
|
|
dlg.Destroy()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
action = dlg.get_action()
|
|
|
|
|
user = dlg.get_user().strip() or handle
|
|
|
|
|
dlg.Destroy()
|
|
|
|
|
|
|
|
|
|
if user.startswith("@"):
|
|
|
|
|
user = user[1:]
|
2026-02-01 13:57:01 +01:00
|
|
|
try:
|
|
|
|
|
profile = buffer.session.get_profile(user)
|
|
|
|
|
if profile is None:
|
|
|
|
|
output.speak(_("User not found."), True)
|
|
|
|
|
return
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-02-01 13:01:32 +01:00
|
|
|
user_payload = {"handle": user}
|
|
|
|
|
if action == "posts":
|
|
|
|
|
result = self.open_user_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload)
|
|
|
|
|
elif action == "followers":
|
|
|
|
|
result = self.open_followers_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload)
|
|
|
|
|
elif action == "following":
|
|
|
|
|
result = self.open_following_timeline(main_controller=controller, session=buffer.session, user_payload=user_payload)
|
|
|
|
|
else:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if asyncio.iscoroutine(result):
|
|
|
|
|
call_threaded(asyncio.run, result)
|
|
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
def open_followers_timeline(self, main_controller, session, user_payload=None):
|
|
|
|
|
actor, handle = self._resolve_actor(session, user_payload)
|
|
|
|
|
if not actor:
|
|
|
|
|
output.speak(_("No user selected."), True)
|
|
|
|
|
return
|
|
|
|
|
self._open_user_list(main_controller, session, actor, handle, list_type="followers")
|
|
|
|
|
|
|
|
|
|
def open_following_timeline(self, main_controller, session, user_payload=None):
|
|
|
|
|
actor, handle = self._resolve_actor(session, user_payload)
|
|
|
|
|
if not actor:
|
|
|
|
|
output.speak(_("No user selected."), True)
|
|
|
|
|
return
|
|
|
|
|
self._open_user_list(main_controller, session, actor, handle, list_type="following")
|
|
|
|
|
|
2026-02-01 13:01:32 +01:00
|
|
|
def open_user_timeline(self, main_controller, session, user_payload=None):
|
2026-02-01 12:49:33 +01:00
|
|
|
"""Open posts timeline for a user (Alt+Win+I)."""
|
|
|
|
|
actor, handle = self._resolve_actor(session, user_payload)
|
|
|
|
|
if not actor:
|
|
|
|
|
output.speak(_("No user selected."), True)
|
|
|
|
|
return
|
|
|
|
|
|
2026-02-01 13:13:00 +01:00
|
|
|
actor, handle = self._resolve_actor(session, {"did": actor, "handle": handle})
|
|
|
|
|
if not handle:
|
|
|
|
|
handle = actor
|
2026-02-01 13:01:32 +01:00
|
|
|
|
2026-02-01 12:49:33 +01:00
|
|
|
account_name = session.get_name()
|
|
|
|
|
list_name = f"{handle}-timeline"
|
|
|
|
|
if main_controller.search_buffer(list_name, account_name):
|
|
|
|
|
index = main_controller.view.search(list_name, account_name)
|
|
|
|
|
if index is not None:
|
|
|
|
|
main_controller.view.change_buffer(index)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
title = _("Timeline for {user}").format(user=handle)
|
|
|
|
|
from pubsub import pub
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="UserTimeline",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
2026-02-01 13:07:16 +01:00
|
|
|
parent_tab=main_controller.view.search("timelines", account_name),
|
2026-02-01 12:49:33 +01:00
|
|
|
start=True,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=main_controller.view.nb, name=list_name, session=session, actor=actor, handle=handle, sound="tweet_timeline.ogg")
|
2026-02-01 12:49:33 +01:00
|
|
|
)
|
2026-02-01 13:01:32 +01:00
|
|
|
try:
|
|
|
|
|
timelines = session.settings["other_buffers"].get("timelines")
|
|
|
|
|
if timelines is None:
|
|
|
|
|
timelines = []
|
|
|
|
|
if isinstance(timelines, str):
|
|
|
|
|
timelines = [t for t in timelines.split(",") if t]
|
2026-02-01 13:13:00 +01:00
|
|
|
key = actor or handle
|
2026-02-01 13:07:16 +01:00
|
|
|
if key in timelines:
|
|
|
|
|
from wxUI import commonMessageDialogs
|
|
|
|
|
commonMessageDialogs.timeline_exist()
|
|
|
|
|
return
|
|
|
|
|
if key:
|
2026-02-01 13:01:32 +01:00
|
|
|
timelines.append(key)
|
|
|
|
|
session.settings["other_buffers"]["timelines"] = timelines
|
|
|
|
|
session.settings.write()
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to persist Bluesky timeline buffer: %s", e)
|
2026-02-01 12:49:33 +01:00
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
def _resolve_actor(self, session, user_payload):
|
|
|
|
|
def g(obj, key, default=None):
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
return obj.get(key, default)
|
|
|
|
|
return getattr(obj, key, default)
|
|
|
|
|
|
|
|
|
|
actor = None
|
|
|
|
|
handle = None
|
|
|
|
|
if user_payload:
|
|
|
|
|
actor = g(user_payload, "did") or g(user_payload, "handle")
|
|
|
|
|
handle = g(user_payload, "handle") or g(user_payload, "did")
|
2026-02-01 13:01:32 +01:00
|
|
|
if isinstance(actor, str):
|
|
|
|
|
actor = actor.strip()
|
|
|
|
|
if actor.startswith("@"):
|
|
|
|
|
actor = actor[1:]
|
|
|
|
|
if isinstance(handle, str):
|
|
|
|
|
handle = handle.strip()
|
|
|
|
|
if handle.startswith("@"):
|
|
|
|
|
handle = handle[1:]
|
2026-02-01 13:13:00 +01:00
|
|
|
# Resolve handle -> DID when possible, and keep handle for titles
|
|
|
|
|
try:
|
|
|
|
|
if isinstance(actor, str) and not actor.startswith("did:"):
|
|
|
|
|
profile = session.get_profile(actor)
|
|
|
|
|
if profile:
|
|
|
|
|
did = g(profile, "did")
|
|
|
|
|
if did:
|
|
|
|
|
actor = did
|
|
|
|
|
if not handle:
|
|
|
|
|
handle = g(profile, "handle")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
if not actor:
|
|
|
|
|
actor = session.db.get("user_id") or session.db.get("user_name")
|
|
|
|
|
handle = session.db.get("user_name") or actor
|
2026-02-01 13:13:00 +01:00
|
|
|
|
|
|
|
|
if not handle and isinstance(actor, str):
|
|
|
|
|
try:
|
|
|
|
|
if actor.startswith("did:"):
|
|
|
|
|
profile = session.get_profile(actor)
|
|
|
|
|
if profile:
|
|
|
|
|
handle = g(profile, "handle")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
return actor, handle
|
|
|
|
|
|
|
|
|
|
def _open_user_list(self, main_controller, session, actor, handle, list_type):
|
|
|
|
|
account_name = session.get_name()
|
2026-02-01 13:13:00 +01:00
|
|
|
if not handle:
|
|
|
|
|
handle = actor
|
|
|
|
|
own_actor = session.db.get("user_id") or session.db.get("user_name")
|
|
|
|
|
own_handle = session.db.get("user_name")
|
|
|
|
|
if actor == own_actor or (own_handle and actor == own_handle) or (handle and own_handle and handle == own_handle):
|
|
|
|
|
name = "followers" if list_type == "followers" else "following"
|
2026-02-01 13:57:01 +01:00
|
|
|
try:
|
|
|
|
|
stored = session.settings["other_buffers"].get("followers_timelines" if list_type == "followers" else "following_timelines") or []
|
|
|
|
|
if isinstance(stored, str):
|
|
|
|
|
stored = [t for t in stored.split(",") if t]
|
|
|
|
|
if actor in stored:
|
|
|
|
|
stored.remove(actor)
|
|
|
|
|
session.settings["other_buffers"]["followers_timelines" if list_type == "followers" else "following_timelines"] = stored
|
|
|
|
|
session.settings.write()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-02-01 13:13:00 +01:00
|
|
|
index = main_controller.view.search(name, account_name)
|
|
|
|
|
if index is not None:
|
|
|
|
|
main_controller.view.change_buffer(index)
|
|
|
|
|
return
|
2026-01-11 20:13:56 +01:00
|
|
|
list_name = f"{handle}-{list_type}"
|
|
|
|
|
if main_controller.search_buffer(list_name, account_name):
|
|
|
|
|
index = main_controller.view.search(list_name, account_name)
|
|
|
|
|
if index is not None:
|
|
|
|
|
main_controller.view.change_buffer(index)
|
|
|
|
|
return
|
|
|
|
|
|
2026-02-01 13:08:35 +01:00
|
|
|
settings_key = "followers_timelines" if list_type == "followers" else "following_timelines"
|
|
|
|
|
try:
|
|
|
|
|
stored = session.settings["other_buffers"].get(settings_key)
|
|
|
|
|
if stored is None:
|
|
|
|
|
stored = []
|
|
|
|
|
if isinstance(stored, str):
|
|
|
|
|
stored = [t for t in stored.split(",") if t]
|
2026-02-01 13:13:00 +01:00
|
|
|
key = actor or handle
|
2026-02-01 13:08:35 +01:00
|
|
|
if key in stored:
|
|
|
|
|
from wxUI import commonMessageDialogs
|
|
|
|
|
commonMessageDialogs.timeline_exist()
|
|
|
|
|
return
|
|
|
|
|
except Exception:
|
|
|
|
|
stored = None
|
|
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
title = _("Followers for {user}").format(user=handle) if list_type == "followers" else _("Following for {user}").format(user=handle)
|
|
|
|
|
from pubsub import pub
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="FollowersBuffer" if list_type == "followers" else "FollowingBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
2026-02-01 13:08:35 +01:00
|
|
|
parent_tab=main_controller.view.search("timelines", account_name),
|
2026-01-11 20:13:56 +01:00
|
|
|
start=True,
|
2026-02-02 14:32:18 +01:00
|
|
|
kwargs=dict(parent=main_controller.view.nb, name=list_name, session=session, actor=actor, handle=handle, sound="new_event.ogg")
|
2026-01-11 20:13:56 +01:00
|
|
|
)
|
2026-02-01 13:08:35 +01:00
|
|
|
try:
|
|
|
|
|
if stored is None:
|
|
|
|
|
stored = session.settings["other_buffers"].get(settings_key) or []
|
|
|
|
|
if isinstance(stored, str):
|
|
|
|
|
stored = [t for t in stored.split(",") if t]
|
2026-02-01 13:13:00 +01:00
|
|
|
key = actor or handle
|
2026-02-01 13:08:35 +01:00
|
|
|
if key:
|
|
|
|
|
stored.append(key)
|
|
|
|
|
session.settings["other_buffers"][settings_key] = stored
|
|
|
|
|
session.settings.write()
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to persist Bluesky %s buffer: %s", list_type, e)
|
2026-01-11 20:13:56 +01:00
|
|
|
|
|
|
|
|
def delete(self, buffer, controller):
|
|
|
|
|
"""Standard action for delete key / menu item"""
|
|
|
|
|
item = buffer.get_item()
|
|
|
|
|
if not item: return
|
2026-02-01 10:42:05 +01:00
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
uri = getattr(item, "uri", None) or (item.get("post", {}).get("uri") if isinstance(item, dict) else None)
|
|
|
|
|
if not uri: return
|
2026-02-01 10:42:05 +01:00
|
|
|
|
2026-01-11 20:13:56 +01:00
|
|
|
import wx
|
|
|
|
|
if wx.MessageBox(_("Are you sure you want to delete this post?"), _("Delete post"), wx.YES_NO | wx.ICON_QUESTION) == wx.YES:
|
|
|
|
|
if buffer.session.delete_post(uri):
|
|
|
|
|
import output
|
|
|
|
|
output.speak(_("Post deleted."))
|
|
|
|
|
# Refresh buffer
|
|
|
|
|
if hasattr(buffer, "start_stream"):
|
|
|
|
|
buffer.start_stream(mandatory=True, play_sound=False)
|
|
|
|
|
else:
|
|
|
|
|
import output
|
|
|
|
|
output.speak(_("Failed to delete post."))
|
2026-02-01 10:42:05 +01:00
|
|
|
|
|
|
|
|
def search(self, controller, session):
|
|
|
|
|
"""Open search dialog and create search buffer for results."""
|
|
|
|
|
dlg = wx.TextEntryDialog(
|
|
|
|
|
controller.view,
|
|
|
|
|
_("Enter search term:"),
|
|
|
|
|
_("Search Bluesky")
|
|
|
|
|
)
|
|
|
|
|
if dlg.ShowModal() != wx.ID_OK:
|
|
|
|
|
dlg.Destroy()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
query = dlg.GetValue().strip()
|
|
|
|
|
dlg.Destroy()
|
|
|
|
|
|
|
|
|
|
if not query:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Create unique buffer name for this search
|
|
|
|
|
buffer_name = f"search_{query[:20]}"
|
|
|
|
|
account_name = session.get_name()
|
|
|
|
|
|
|
|
|
|
# Check if buffer already exists
|
|
|
|
|
existing = controller.search_buffer(buffer_name, account_name)
|
|
|
|
|
if existing:
|
|
|
|
|
# Navigate to existing buffer
|
|
|
|
|
index = controller.view.search(buffer_name, account_name)
|
|
|
|
|
if index is not None:
|
|
|
|
|
controller.view.change_buffer(index)
|
|
|
|
|
# Refresh search
|
|
|
|
|
existing.search_query = query
|
|
|
|
|
existing.start_stream(mandatory=True, play_sound=False)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Create new search buffer
|
|
|
|
|
title = _("Search: {query}").format(query=query)
|
|
|
|
|
from pubsub import pub
|
|
|
|
|
pub.sendMessage(
|
|
|
|
|
"createBuffer",
|
|
|
|
|
buffer_type="SearchBuffer",
|
|
|
|
|
session_type="blueski",
|
|
|
|
|
buffer_title=title,
|
2026-02-01 13:57:01 +01:00
|
|
|
parent_tab=controller.view.search("searches", account_name),
|
2026-02-01 10:42:05 +01:00
|
|
|
start=True,
|
|
|
|
|
kwargs=dict(
|
|
|
|
|
parent=controller.view.nb,
|
|
|
|
|
name=buffer_name,
|
|
|
|
|
session=session,
|
2026-02-02 14:32:18 +01:00
|
|
|
query=query,
|
|
|
|
|
sound="search_updated.ogg"
|
2026-02-01 10:42:05 +01:00
|
|
|
)
|
|
|
|
|
)
|
2026-02-01 14:48:00 +01:00
|
|
|
|
|
|
|
|
# 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()
|
2026-02-01 19:15:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to save search to settings: %s", e)
|