mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 09:27:33 +01:00
Chats, plantillas, movidas varias.
This commit is contained in:
@@ -45,7 +45,7 @@ speech_reporting = boolean(default=True)
|
|||||||
|
|
||||||
[templates]
|
[templates]
|
||||||
post = string(default="$display_name, $safe_text $date.")
|
post = string(default="$display_name, $safe_text $date.")
|
||||||
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts.")
|
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
||||||
notification = string(default="$display_name $text, $date")
|
notification = string(default="$display_name $text, $date")
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class Handler:
|
|||||||
templates_cfg = buffer.session.settings.get("templates", {})
|
templates_cfg = buffer.session.settings.get("templates", {})
|
||||||
template_state = {
|
template_state = {
|
||||||
"post": templates_cfg.get("post", "$display_name, $safe_text $date."),
|
"post": templates_cfg.get("post", "$display_name, $safe_text $date."),
|
||||||
"person": templates_cfg.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts."),
|
"person": templates_cfg.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at."),
|
||||||
"notification": templates_cfg.get("notification", "$display_name $text, $date"),
|
"notification": templates_cfg.get("notification", "$display_name $text, $date"),
|
||||||
}
|
}
|
||||||
dlg.set_template_labels(template_state["post"], template_state["person"], template_state["notification"])
|
dlg.set_template_labels(template_state["post"], template_state["person"], template_state["notification"])
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ class BaseBuffer(base.Buffer):
|
|||||||
index = self.buffer.list.get_selected()
|
index = self.buffer.list.get_selected()
|
||||||
if index < 0:
|
if index < 0:
|
||||||
return
|
return
|
||||||
|
# Only update if the list has at least 3 columns (Author, Text, Date)
|
||||||
|
if self.buffer.list.list.GetColumnCount() < 3:
|
||||||
|
return
|
||||||
|
|
||||||
def g(obj, key, default=None):
|
def g(obj, key, default=None):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
@@ -474,28 +477,31 @@ class BaseBuffer(base.Buffer):
|
|||||||
self.send_message()
|
self.send_message()
|
||||||
|
|
||||||
def send_message(self, *args, **kwargs):
|
def send_message(self, *args, **kwargs):
|
||||||
# Global shortcut for DM
|
# Global shortcut for DM - similar to Mastodon's implementation
|
||||||
item = self.get_item()
|
|
||||||
if not item:
|
# Use the robust author extraction method
|
||||||
output.speak(_("No user selected to message."), True)
|
author_details = self.get_selected_item_author_details()
|
||||||
|
if not author_details:
|
||||||
|
output.speak(_("No item selected."), True)
|
||||||
return
|
return
|
||||||
|
|
||||||
author = getattr(item, "author", None) or (item.get("post", {}).get("author") if isinstance(item, dict) else None)
|
did = author_details.get("did")
|
||||||
if not author:
|
handle = author_details.get("handle") or "unknown"
|
||||||
# Try item itself if it's a user object (UserBuffer)
|
|
||||||
author = item
|
|
||||||
|
|
||||||
did = getattr(author, "did", None) or author.get("did")
|
|
||||||
handle = getattr(author, "handle", "unknown") or (author.get("handle") if isinstance(author, dict) else "unknown")
|
|
||||||
|
|
||||||
if not did:
|
if not did:
|
||||||
|
output.speak(_("Could not identify user to message."), True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show simple text entry dialog for sending DM
|
# Use full post dialog like Mastodon
|
||||||
dlg = wx.TextEntryDialog(None, _("Message to {0}:").format(handle), _("Send Message"))
|
title = _("Conversation with {0}").format(handle)
|
||||||
if dlg.ShowModal() == wx.ID_OK:
|
caption = _("Write your message here")
|
||||||
text = dlg.GetValue()
|
initial_text = "@{} ".format(handle)
|
||||||
|
|
||||||
|
post = blueski_messages.post(session=self.session, title=title, caption=caption, text=initial_text)
|
||||||
|
if post.message.ShowModal() == wx.ID_OK:
|
||||||
|
text, files, cw_text, langs = post.get_data()
|
||||||
if text:
|
if text:
|
||||||
|
def do_send():
|
||||||
try:
|
try:
|
||||||
api = self.session._ensure_client()
|
api = self.session._ensure_client()
|
||||||
dm_client = api.with_bsky_chat_proxy()
|
dm_client = api.with_bsky_chat_proxy()
|
||||||
@@ -503,12 +509,14 @@ class BaseBuffer(base.Buffer):
|
|||||||
res = dm_client.chat.bsky.convo.get_convo_for_members({"members": [did]})
|
res = dm_client.chat.bsky.convo.get_convo_for_members({"members": [did]})
|
||||||
convo_id = res.convo.id
|
convo_id = res.convo.id
|
||||||
self.session.send_chat_message(convo_id, text)
|
self.session.send_chat_message(convo_id, text)
|
||||||
self.session.sound.play("dm_sent.ogg")
|
wx.CallAfter(self.session.sound.play, "dm_sent.ogg")
|
||||||
output.speak(_("Message sent."), True)
|
wx.CallAfter(output.speak, _("Message sent."))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Error sending Bluesky DM: %s", e)
|
log.error("Error sending Bluesky DM: %s", e)
|
||||||
output.speak(_("Failed to send message."), True)
|
wx.CallAfter(output.speak, _("Failed to send message."), True)
|
||||||
dlg.Destroy()
|
call_threaded(do_send)
|
||||||
|
if hasattr(post.message, "Destroy"):
|
||||||
|
post.message.Destroy()
|
||||||
|
|
||||||
def view(self, *args, **kwargs):
|
def view(self, *args, **kwargs):
|
||||||
self.view_item()
|
self.view_item()
|
||||||
@@ -800,7 +808,7 @@ class BaseBuffer(base.Buffer):
|
|||||||
post_template = template_settings.get("post", "$display_name, $safe_text $date.")
|
post_template = template_settings.get("post", "$display_name, $safe_text $date.")
|
||||||
return templates.render_notification(item, template, post_template, self.session.settings, relative_times, offset_hours)
|
return templates.render_notification(item, template, post_template, self.session.settings, relative_times, offset_hours)
|
||||||
if self.type in ("user", "post_user_list"):
|
if self.type in ("user", "post_user_list"):
|
||||||
template = template_settings.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts.")
|
template = template_settings.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
||||||
return templates.render_user(item, template, self.session.settings, relative_times, offset_hours)
|
return templates.render_user(item, template, self.session.settings, relative_times, offset_hours)
|
||||||
template = template_settings.get("post", "$display_name, $safe_text $date.")
|
template = template_settings.get("post", "$display_name, $safe_text $date.")
|
||||||
return templates.render_post(item, template, self.session.settings, relative_times, offset_hours)
|
return templates.render_post(item, template, self.session.settings, relative_times, offset_hours)
|
||||||
@@ -888,20 +896,43 @@ class BaseBuffer(base.Buffer):
|
|||||||
return getattr(obj, key, default)
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
author = None
|
author = None
|
||||||
|
|
||||||
|
# Check if item itself is a user object (UserBuffer, FollowersBuffer, etc.)
|
||||||
if g(item, "did") or g(item, "handle"):
|
if g(item, "did") or g(item, "handle"):
|
||||||
author = item
|
author = item
|
||||||
else:
|
else:
|
||||||
author = g(item, "author")
|
# Use the same pattern as compose_post: get actual_post first
|
||||||
|
# This handles FeedViewPost (item.post) and PostView (item itself)
|
||||||
|
actual_post = g(item, "post", item)
|
||||||
|
author = g(actual_post, "author")
|
||||||
|
|
||||||
|
# If still no author, try other nested structures
|
||||||
if not author:
|
if not author:
|
||||||
post = g(item, "post") or g(item, "record")
|
# Try record.author
|
||||||
author = g(post, "author") if post else None
|
record = g(item, "record")
|
||||||
|
if record:
|
||||||
|
author = g(record, "author")
|
||||||
|
|
||||||
|
# Try subject (for notifications)
|
||||||
|
if not author:
|
||||||
|
subject = g(item, "subject")
|
||||||
|
if subject:
|
||||||
|
actual_subject = g(subject, "post", subject)
|
||||||
|
author = g(actual_subject, "author")
|
||||||
|
|
||||||
if not author:
|
if not author:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
did = g(author, "did")
|
||||||
|
handle = g(author, "handle")
|
||||||
|
|
||||||
|
# Only return if we have at least did or handle
|
||||||
|
if not did and not handle:
|
||||||
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"did": g(author, "did"),
|
"did": did,
|
||||||
"handle": g(author, "handle"),
|
"handle": handle,
|
||||||
}
|
}
|
||||||
|
|
||||||
def process_items(self, items, play_sound=True, avoid_autoreading=False):
|
def process_items(self, items, play_sound=True, avoid_autoreading=False):
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import logging
|
import logging
|
||||||
import wx
|
import wx
|
||||||
|
import widgetUtils
|
||||||
import output
|
import output
|
||||||
from .base import BaseBuffer
|
from .base import BaseBuffer
|
||||||
|
from controller.blueski import messages as blueski_messages
|
||||||
from wxUI.buffers.blueski import panels as BlueskiPanels
|
from wxUI.buffers.blueski import panels as BlueskiPanels
|
||||||
from sessions.blueski import compose
|
from sessions.blueski import compose
|
||||||
from mysc.thread_utils import call_threaded
|
from mysc.thread_utils import call_threaded
|
||||||
@@ -10,6 +12,8 @@ from mysc.thread_utils import call_threaded
|
|||||||
log = logging.getLogger("controller.buffers.blueski.chat")
|
log = logging.getLogger("controller.buffers.blueski.chat")
|
||||||
|
|
||||||
class ConversationListBuffer(BaseBuffer):
|
class ConversationListBuffer(BaseBuffer):
|
||||||
|
"""Buffer for listing conversations, similar to Mastodon's ConversationListBuffer."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs["compose_func"] = "compose_convo"
|
kwargs["compose_func"] = "compose_convo"
|
||||||
super(ConversationListBuffer, self).__init__(*args, **kwargs)
|
super(ConversationListBuffer, self).__init__(*args, **kwargs)
|
||||||
@@ -20,6 +24,95 @@ class ConversationListBuffer(BaseBuffer):
|
|||||||
self.buffer = BlueskiPanels.ChatPanel(parent, name)
|
self.buffer = BlueskiPanels.ChatPanel(parent, name)
|
||||||
self.buffer.session = self.session
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def bind_events(self):
|
||||||
|
"""Bind events like Mastodon's ConversationListBuffer."""
|
||||||
|
self.buffer.set_focus_function(self.onFocus)
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
|
||||||
|
# Buttons
|
||||||
|
if hasattr(self.buffer, "post"):
|
||||||
|
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.on_post, self.buffer.post)
|
||||||
|
if hasattr(self.buffer, "reply"):
|
||||||
|
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
|
||||||
|
if hasattr(self.buffer, "new_chat"):
|
||||||
|
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.on_new_chat, self.buffer.new_chat)
|
||||||
|
|
||||||
|
def get_item(self):
|
||||||
|
"""Get the last message from the selected conversation (like Mastodon)."""
|
||||||
|
index = self.buffer.list.get_selected()
|
||||||
|
if index > -1 and self.session.db.get(self.name) is not None and len(self.session.db[self.name]) > index:
|
||||||
|
convo = self.session.db[self.name][index]
|
||||||
|
# Return lastMessage for compatibility with item-based operations
|
||||||
|
last_msg = getattr(convo, "lastMessage", None) or (convo.get("lastMessage") if isinstance(convo, dict) else None)
|
||||||
|
return last_msg
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_conversation(self):
|
||||||
|
"""Get the full conversation object."""
|
||||||
|
index = self.buffer.list.get_selected()
|
||||||
|
if index > -1 and self.session.db.get(self.name) is not None and len(self.session.db[self.name]) > index:
|
||||||
|
return self.session.db[self.name][index]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_convo_id(self, conversation):
|
||||||
|
"""Extract conversation ID from a conversation object, handling different field names."""
|
||||||
|
if not conversation:
|
||||||
|
return None
|
||||||
|
# Try different possible field names for the conversation ID
|
||||||
|
for attr in ("id", "convo_id", "convoId"):
|
||||||
|
val = getattr(conversation, attr, None)
|
||||||
|
if val:
|
||||||
|
return val
|
||||||
|
if isinstance(conversation, dict) and attr in conversation:
|
||||||
|
return conversation[attr]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_new_chat(self, *args, **kwargs):
|
||||||
|
"""Start a new conversation by entering a handle."""
|
||||||
|
dlg = wx.TextEntryDialog(None, _("Enter the handle of the user (e.g., user.bsky.social):"), _("New Chat"))
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
handle = dlg.GetValue().strip()
|
||||||
|
if handle:
|
||||||
|
if handle.startswith("@"):
|
||||||
|
handle = handle[1:]
|
||||||
|
def do_create():
|
||||||
|
try:
|
||||||
|
# Resolve handle to DID
|
||||||
|
profile = self.session.get_profile(handle)
|
||||||
|
if not profile:
|
||||||
|
wx.CallAfter(output.speak, _("User not found."), True)
|
||||||
|
return
|
||||||
|
did = getattr(profile, "did", None) or (profile.get("did") if isinstance(profile, dict) else None)
|
||||||
|
if not did:
|
||||||
|
wx.CallAfter(output.speak, _("Could not get user ID."), True)
|
||||||
|
return
|
||||||
|
# Get or create conversation
|
||||||
|
convo = self.session.get_or_create_convo([did])
|
||||||
|
if not convo:
|
||||||
|
wx.CallAfter(output.speak, _("Could not create conversation."), True)
|
||||||
|
return
|
||||||
|
convo_id = self.get_convo_id(convo)
|
||||||
|
user_handle = getattr(profile, "handle", None) or (profile.get("handle") if isinstance(profile, dict) else None) or handle
|
||||||
|
title = _("Chat: {0}").format(user_handle)
|
||||||
|
# Create the buffer
|
||||||
|
import application
|
||||||
|
wx.CallAfter(
|
||||||
|
application.app.controller.create_buffer,
|
||||||
|
buffer_type="chat_messages",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
kwargs={"session": self.session, "convo_id": convo_id, "name": title},
|
||||||
|
start=True
|
||||||
|
)
|
||||||
|
# Refresh conversation list
|
||||||
|
wx.CallAfter(self.start_stream, True, False)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error creating new conversation")
|
||||||
|
wx.CallAfter(output.speak, _("Error creating conversation."), True)
|
||||||
|
call_threaded(do_create)
|
||||||
|
dlg.Destroy()
|
||||||
|
|
||||||
def start_stream(self, mandatory=False, play_sound=True):
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
count = self.get_max_items()
|
count = self.get_max_items()
|
||||||
try:
|
try:
|
||||||
@@ -28,30 +121,95 @@ class ConversationListBuffer(BaseBuffer):
|
|||||||
self.session.db[self.name] = []
|
self.session.db[self.name] = []
|
||||||
self.buffer.list.clear()
|
self.buffer.list.clear()
|
||||||
return self.process_items(items, play_sound)
|
return self.process_items(items, play_sound)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.error("Error fetching conversations: %s", e)
|
log.exception("Error fetching conversations")
|
||||||
|
output.speak(_("Error loading conversations."), True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def fav(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unfav(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def can_share(self):
|
||||||
|
return False
|
||||||
|
|
||||||
def url(self, *args, **kwargs):
|
def url(self, *args, **kwargs):
|
||||||
# In chat list, Enter (URL) should open the chat conversation buffer
|
"""Enter key opens the chat conversation buffer."""
|
||||||
self.view_chat()
|
self.view_chat()
|
||||||
|
|
||||||
def send_message(self, *args, **kwargs):
|
def send_message(self, *args, **kwargs):
|
||||||
# Global shortcut for DM
|
"""Global DM shortcut - reply to conversation."""
|
||||||
self.view_chat()
|
return self.reply()
|
||||||
|
|
||||||
|
def reply(self, *args, **kwargs):
|
||||||
|
"""Reply to the selected conversation (like Mastodon)."""
|
||||||
|
conversation = self.get_conversation()
|
||||||
|
if not conversation:
|
||||||
|
output.speak(_("No conversation selected."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
convo_id = self.get_convo_id(conversation)
|
||||||
|
if not convo_id:
|
||||||
|
log.error("Could not get conversation ID from conversation object")
|
||||||
|
output.speak(_("Could not identify conversation."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get participants for title
|
||||||
|
members = getattr(conversation, "members", []) or (conversation.get("members", []) if isinstance(conversation, dict) else [])
|
||||||
|
user_did = self.session.db.get("user_id")
|
||||||
|
others = [m for m in members if (getattr(m, "did", None) or (m.get("did") if isinstance(m, dict) else None)) != user_did]
|
||||||
|
if not others:
|
||||||
|
others = members
|
||||||
|
|
||||||
|
if others:
|
||||||
|
first_user = others[0]
|
||||||
|
username = getattr(first_user, "handle", None) or (first_user.get("handle") if isinstance(first_user, dict) else None) or "unknown"
|
||||||
|
else:
|
||||||
|
username = "unknown"
|
||||||
|
|
||||||
|
title = _("Conversation with {0}").format(username)
|
||||||
|
caption = _("Write your message here")
|
||||||
|
initial_text = "@{} ".format(username)
|
||||||
|
|
||||||
|
post = blueski_messages.post(session=self.session, title=title, caption=caption, text=initial_text)
|
||||||
|
if post.message.ShowModal() == wx.ID_OK:
|
||||||
|
text, files, cw_text, langs = post.get_data()
|
||||||
|
if text:
|
||||||
|
def do_send():
|
||||||
|
try:
|
||||||
|
self.session.send_chat_message(convo_id, text)
|
||||||
|
wx.CallAfter(self.session.sound.play, "dm_sent.ogg")
|
||||||
|
wx.CallAfter(output.speak, _("Message sent."))
|
||||||
|
wx.CallAfter(self.start_stream, True, False)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error sending message")
|
||||||
|
wx.CallAfter(output.speak, _("Failed to send message."), True)
|
||||||
|
call_threaded(do_send)
|
||||||
|
if hasattr(post.message, "Destroy"):
|
||||||
|
post.message.Destroy()
|
||||||
|
|
||||||
def view_chat(self):
|
def view_chat(self):
|
||||||
item = self.get_item()
|
"""Open the conversation in a separate buffer."""
|
||||||
if not item: return
|
conversation = self.get_conversation()
|
||||||
|
if not conversation:
|
||||||
|
output.speak(_("No conversation selected."), True)
|
||||||
|
return
|
||||||
|
|
||||||
convo_id = getattr(item, "id", None) or item.get("id")
|
convo_id = self.get_convo_id(conversation)
|
||||||
if not convo_id: return
|
if not convo_id:
|
||||||
|
log.error("Could not get conversation ID from conversation object: %r", conversation)
|
||||||
|
output.speak(_("Could not identify conversation."), True)
|
||||||
|
return
|
||||||
|
|
||||||
# Determine participants names for title
|
# Determine participants names for title
|
||||||
members = getattr(item, "members", []) or item.get("members", [])
|
members = getattr(conversation, "members", []) or (conversation.get("members", []) if isinstance(conversation, dict) else [])
|
||||||
others = [m for m in members if (getattr(m, "did", None) or m.get("did")) != self.session.db["user_id"]]
|
user_did = self.session.db.get("user_id")
|
||||||
if not others: others = members
|
others = [m for m in members if (getattr(m, "did", None) or (m.get("did") if isinstance(m, dict) else None)) != user_did]
|
||||||
names = ", ".join([getattr(m, "handle", "unknown") or m.get("handle") for m in others])
|
if not others:
|
||||||
|
others = members
|
||||||
|
names = ", ".join([getattr(m, "handle", None) or (m.get("handle") if isinstance(m, dict) else None) or "unknown" for m in others])
|
||||||
|
|
||||||
title = _("Chat: {0}").format(names)
|
title = _("Chat: {0}").format(names)
|
||||||
|
|
||||||
@@ -64,7 +222,12 @@ class ConversationListBuffer(BaseBuffer):
|
|||||||
start=True
|
start=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def destroy_status(self):
|
||||||
|
pass
|
||||||
|
|
||||||
class ChatBuffer(BaseBuffer):
|
class ChatBuffer(BaseBuffer):
|
||||||
|
"""Buffer for displaying messages in a conversation, similar to Mastodon's ConversationBuffer."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs["compose_func"] = "compose_chat_message"
|
kwargs["compose_func"] = "compose_chat_message"
|
||||||
super(ChatBuffer, self).__init__(*args, **kwargs)
|
super(ChatBuffer, self).__init__(*args, **kwargs)
|
||||||
@@ -87,15 +250,44 @@ class ChatBuffer(BaseBuffer):
|
|||||||
self.buffer.list.clear()
|
self.buffer.list.clear()
|
||||||
items = list(reversed(items))
|
items = list(reversed(items))
|
||||||
return self.process_items(items, play_sound)
|
return self.process_items(items, play_sound)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.error("Error fetching chat messages: %s", e)
|
log.exception("Error fetching chat messages")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def get_more_items(self):
|
||||||
|
output.speak(_("This action is not supported for this buffer"), True)
|
||||||
|
|
||||||
|
def fav(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unfav(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def can_share(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def destroy_status(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def url(self, *args, **kwargs):
|
||||||
|
"""Enter key opens reply dialog in chat."""
|
||||||
|
self.on_reply(None)
|
||||||
|
|
||||||
def on_reply(self, evt):
|
def on_reply(self, evt):
|
||||||
# Open a text entry chat box
|
"""Open dialog to send a message in this conversation."""
|
||||||
dlg = wx.TextEntryDialog(None, _("Message:"), _("Send Message"), style=wx.TE_MULTILINE | wx.OK | wx.CANCEL)
|
if not self.convo_id:
|
||||||
if dlg.ShowModal() == wx.ID_OK:
|
output.speak(_("Cannot send message: no conversation selected."), True)
|
||||||
text = dlg.GetValue()
|
return
|
||||||
|
|
||||||
|
# Get conversation title from buffer name or use generic
|
||||||
|
title = _("Send Message")
|
||||||
|
if self.name and self.name.startswith(_("Chat: ")):
|
||||||
|
title = self.name
|
||||||
|
caption = _("Write your message here")
|
||||||
|
|
||||||
|
post = blueski_messages.post(session=self.session, title=title, caption=caption, text="")
|
||||||
|
if post.message.ShowModal() == wx.ID_OK:
|
||||||
|
text, files, cw_text, langs = post.get_data()
|
||||||
if text:
|
if text:
|
||||||
def do_send():
|
def do_send():
|
||||||
try:
|
try:
|
||||||
@@ -104,10 +296,30 @@ class ChatBuffer(BaseBuffer):
|
|||||||
wx.CallAfter(output.speak, _("Message sent."))
|
wx.CallAfter(output.speak, _("Message sent."))
|
||||||
wx.CallAfter(self.start_stream, True, False)
|
wx.CallAfter(self.start_stream, True, False)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
log.exception("Error sending chat message")
|
||||||
wx.CallAfter(output.speak, _("Failed to send message."), True)
|
wx.CallAfter(output.speak, _("Failed to send message."), True)
|
||||||
call_threaded(do_send)
|
call_threaded(do_send)
|
||||||
dlg.Destroy()
|
if hasattr(post.message, "Destroy"):
|
||||||
|
post.message.Destroy()
|
||||||
|
|
||||||
|
def reply(self, *args, **kwargs):
|
||||||
|
"""Handle reply action (from menu or keyboard shortcut)."""
|
||||||
|
self.on_reply(None)
|
||||||
|
|
||||||
def send_message(self, *args, **kwargs):
|
def send_message(self, *args, **kwargs):
|
||||||
# Global shortcut for DM
|
"""Global shortcut for DM."""
|
||||||
self.on_reply(None)
|
self.on_reply(None)
|
||||||
|
|
||||||
|
def remove_buffer(self, force=False):
|
||||||
|
"""Allow removing this buffer."""
|
||||||
|
from wxUI import commonMessageDialogs
|
||||||
|
if force == False:
|
||||||
|
dlg = commonMessageDialogs.remove_buffer()
|
||||||
|
else:
|
||||||
|
dlg = widgetUtils.YES
|
||||||
|
if dlg == widgetUtils.YES:
|
||||||
|
if self.name in self.session.db:
|
||||||
|
self.session.db.pop(self.name)
|
||||||
|
return True
|
||||||
|
elif dlg == widgetUtils.NO:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -128,6 +128,67 @@ class NotificationBuffer(BaseBuffer):
|
|||||||
self.buffer = BlueskiPanels.NotificationPanel(parent, name)
|
self.buffer = BlueskiPanels.NotificationPanel(parent, name)
|
||||||
self.buffer.session = self.session
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def _hydrate_notifications(self, notifications):
|
||||||
|
"""Fetch subject post text for like/repost notifications."""
|
||||||
|
if not notifications:
|
||||||
|
return notifications
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
# Collect URIs for likes/reposts that need subject post text
|
||||||
|
uris_to_fetch = []
|
||||||
|
for notif in notifications:
|
||||||
|
reason = g(notif, "reason", "")
|
||||||
|
if reason in ("like", "repost"):
|
||||||
|
reason_subject = g(notif, "reasonSubject") or g(notif, "reason_subject")
|
||||||
|
if reason_subject and isinstance(reason_subject, str):
|
||||||
|
uris_to_fetch.append(reason_subject)
|
||||||
|
|
||||||
|
if not uris_to_fetch:
|
||||||
|
return notifications
|
||||||
|
|
||||||
|
# Fetch posts in batch
|
||||||
|
posts_map = {}
|
||||||
|
try:
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
if api and uris_to_fetch:
|
||||||
|
# getPosts accepts up to 25 URIs at a time
|
||||||
|
for i in range(0, len(uris_to_fetch), 25):
|
||||||
|
batch = uris_to_fetch[i:i+25]
|
||||||
|
res = api.app.bsky.feed.get_posts({"uris": batch})
|
||||||
|
for post in getattr(res, "posts", []):
|
||||||
|
uri = g(post, "uri")
|
||||||
|
if uri:
|
||||||
|
record = g(post, "record", {})
|
||||||
|
text = g(record, "text", "")
|
||||||
|
posts_map[uri] = text
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Error fetching subject posts for notifications: %s", e)
|
||||||
|
|
||||||
|
# Attach subject post text to notifications
|
||||||
|
enriched = []
|
||||||
|
for notif in notifications:
|
||||||
|
reason = g(notif, "reason", "")
|
||||||
|
if reason in ("like", "repost"):
|
||||||
|
reason_subject = g(notif, "reasonSubject") or g(notif, "reason_subject")
|
||||||
|
if reason_subject and reason_subject in posts_map:
|
||||||
|
# Create a modified notification with subject post text
|
||||||
|
if isinstance(notif, dict):
|
||||||
|
notif = dict(notif)
|
||||||
|
notif["_subject_text"] = posts_map[reason_subject]
|
||||||
|
else:
|
||||||
|
# For ATProto model objects, add as attribute
|
||||||
|
try:
|
||||||
|
notif._subject_text = posts_map[reason_subject]
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
enriched.append(notif)
|
||||||
|
|
||||||
|
return enriched
|
||||||
|
|
||||||
def start_stream(self, mandatory=False, play_sound=True):
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
count = self.get_max_items()
|
count = self.get_max_items()
|
||||||
api = self.session._ensure_client()
|
api = self.session._ensure_client()
|
||||||
@@ -139,6 +200,7 @@ class NotificationBuffer(BaseBuffer):
|
|||||||
self.next_cursor = getattr(res, "cursor", None)
|
self.next_cursor = getattr(res, "cursor", None)
|
||||||
if not notifications:
|
if not notifications:
|
||||||
return 0
|
return 0
|
||||||
|
notifications = self._hydrate_notifications(notifications)
|
||||||
return self.process_items(notifications, play_sound)
|
return self.process_items(notifications, play_sound)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Error fetching notifications: %s", e)
|
log.error("Error fetching notifications: %s", e)
|
||||||
@@ -155,6 +217,7 @@ class NotificationBuffer(BaseBuffer):
|
|||||||
res = api.app.bsky.notification.list_notifications({"limit": count, "cursor": self.next_cursor})
|
res = api.app.bsky.notification.list_notifications({"limit": count, "cursor": self.next_cursor})
|
||||||
notifications = list(getattr(res, "notifications", []))
|
notifications = list(getattr(res, "notifications", []))
|
||||||
self.next_cursor = getattr(res, "cursor", None)
|
self.next_cursor = getattr(res, "cursor", None)
|
||||||
|
notifications = self._hydrate_notifications(notifications)
|
||||||
added = self.process_items(notifications, play_sound=False)
|
added = self.process_items(notifications, play_sound=False)
|
||||||
if added:
|
if added:
|
||||||
output.speak(_(u"%s items retrieved") % added, True)
|
output.speak(_(u"%s items retrieved") % added, True)
|
||||||
@@ -162,7 +225,8 @@ class NotificationBuffer(BaseBuffer):
|
|||||||
log.error("Error fetching more notifications: %s", e)
|
log.error("Error fetching more notifications: %s", e)
|
||||||
|
|
||||||
def add_new_item(self, notification):
|
def add_new_item(self, notification):
|
||||||
return self.process_items([notification], play_sound=True)
|
notifications = self._hydrate_notifications([notification])
|
||||||
|
return self.process_items(notifications, play_sound=True)
|
||||||
|
|
||||||
|
|
||||||
class Conversation(BaseBuffer):
|
class Conversation(BaseBuffer):
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ class Controller(object):
|
|||||||
self.started = True
|
self.started = True
|
||||||
if len(self.accounts) > 0:
|
if len(self.accounts) > 0:
|
||||||
b = self.get_first_buffer(self.accounts[0])
|
b = self.get_first_buffer(self.accounts[0])
|
||||||
|
self.menubar_current_handler = b.session.type
|
||||||
self.update_menus(handler=self.get_handler(b.session.type))
|
self.update_menus(handler=self.get_handler(b.session.type))
|
||||||
|
|
||||||
def _start_session_buffers(self, session):
|
def _start_session_buffers(self, session):
|
||||||
@@ -698,8 +699,13 @@ class Controller(object):
|
|||||||
|
|
||||||
def send_dm(self, *args, **kwargs):
|
def send_dm(self, *args, **kwargs):
|
||||||
buffer = self.get_current_buffer()
|
buffer = self.get_current_buffer()
|
||||||
|
if buffer is None:
|
||||||
|
output.speak(_("No buffer selected."), True)
|
||||||
|
return
|
||||||
if hasattr(buffer, "send_message"):
|
if hasattr(buffer, "send_message"):
|
||||||
buffer.send_message()
|
buffer.send_message()
|
||||||
|
else:
|
||||||
|
output.speak(_("Cannot send messages from this buffer."), True)
|
||||||
|
|
||||||
def post_retweet(self, *args, **kwargs):
|
def post_retweet(self, *args, **kwargs):
|
||||||
buffer = self.get_current_buffer()
|
buffer = self.get_current_buffer()
|
||||||
@@ -959,21 +965,81 @@ class Controller(object):
|
|||||||
self.view.check_menuitem("autoread", autoread)
|
self.view.check_menuitem("autoread", autoread)
|
||||||
|
|
||||||
def update_menus(self, handler):
|
def update_menus(self, handler):
|
||||||
|
# Initialize storage for hidden menu items if not present
|
||||||
|
if not hasattr(self, "_hidden_menu_items"):
|
||||||
|
self._hidden_menu_items = {}
|
||||||
|
if not hasattr(self, "_original_menu_labels"):
|
||||||
|
self._original_menu_labels = {}
|
||||||
|
|
||||||
if hasattr(handler, "menus"):
|
if hasattr(handler, "menus"):
|
||||||
for m in list(handler.menus.keys()):
|
for m in list(handler.menus.keys()):
|
||||||
if hasattr(self.view, m):
|
if hasattr(self.view, m):
|
||||||
menu_item = getattr(self.view, m)
|
menu_item = getattr(self.view, m)
|
||||||
|
# Store original label on first encounter
|
||||||
|
if m not in self._original_menu_labels:
|
||||||
|
self._original_menu_labels[m] = menu_item.GetItemLabel()
|
||||||
|
|
||||||
if handler.menus[m] == "HIDE":
|
if handler.menus[m] == "HIDE":
|
||||||
menu_item.Enable(False)
|
# Actually hide the menu item by removing it from parent menu
|
||||||
menu_item.SetItemLabel("")
|
if m not in self._hidden_menu_items:
|
||||||
|
try:
|
||||||
|
parent_menu = menu_item.GetMenu()
|
||||||
|
if parent_menu:
|
||||||
|
# Store menu item info for restoration
|
||||||
|
position = self._find_menu_item_position(parent_menu, menu_item)
|
||||||
|
item_id = menu_item.GetId()
|
||||||
|
# Remove returns the removed item - store that reference
|
||||||
|
removed_item = parent_menu.Remove(item_id)
|
||||||
|
if removed_item:
|
||||||
|
self._hidden_menu_items[m] = {
|
||||||
|
"menu": parent_menu,
|
||||||
|
"item": removed_item,
|
||||||
|
"position": position
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error hiding menu item {m}: {e}")
|
||||||
elif handler.menus[m] == None:
|
elif handler.menus[m] == None:
|
||||||
|
# Restore if hidden, then disable
|
||||||
|
self._restore_menu_item(m)
|
||||||
menu_item.Enable(False)
|
menu_item.Enable(False)
|
||||||
else:
|
else:
|
||||||
|
# Restore if hidden, then enable with new label
|
||||||
|
self._restore_menu_item(m)
|
||||||
menu_item.Enable(True)
|
menu_item.Enable(True)
|
||||||
menu_item.SetItemLabel(handler.menus[m])
|
menu_item.SetItemLabel(handler.menus[m])
|
||||||
if hasattr(handler, "item_menu"):
|
if hasattr(handler, "item_menu"):
|
||||||
self.view.menubar.SetMenuLabel(1, handler.item_menu)
|
self.view.menubar.SetMenuLabel(1, handler.item_menu)
|
||||||
|
|
||||||
|
def _find_menu_item_position(self, menu, item):
|
||||||
|
"""Find the position of a menu item within its parent menu."""
|
||||||
|
for i, menu_item in enumerate(menu.GetMenuItems()):
|
||||||
|
if menu_item.GetId() == item.GetId():
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def _restore_menu_item(self, name):
|
||||||
|
"""Restore a previously hidden menu item."""
|
||||||
|
if not hasattr(self, "_hidden_menu_items"):
|
||||||
|
return
|
||||||
|
if name not in self._hidden_menu_items:
|
||||||
|
return
|
||||||
|
info = self._hidden_menu_items[name]
|
||||||
|
parent_menu = info["menu"]
|
||||||
|
item = info["item"]
|
||||||
|
position = info["position"]
|
||||||
|
try:
|
||||||
|
# Re-insert at original position
|
||||||
|
if position >= 0 and position < parent_menu.GetMenuItemCount():
|
||||||
|
parent_menu.Insert(position, item)
|
||||||
|
else:
|
||||||
|
parent_menu.Append(item)
|
||||||
|
# Restore original label if available
|
||||||
|
if hasattr(self, "_original_menu_labels") and name in self._original_menu_labels:
|
||||||
|
item.SetItemLabel(self._original_menu_labels[name])
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error restoring menu item {name}: {e}")
|
||||||
|
del self._hidden_menu_items[name]
|
||||||
|
|
||||||
def fix_wrong_buffer(self):
|
def fix_wrong_buffer(self):
|
||||||
buf = self.get_best_buffer()
|
buf = self.get_best_buffer()
|
||||||
if buf == None:
|
if buf == None:
|
||||||
|
|||||||
@@ -222,42 +222,65 @@ def compose_notification(notification, db, settings, relative_times, show_screen
|
|||||||
# Notification reason/type
|
# Notification reason/type
|
||||||
reason = g(notification, "reason", "unknown")
|
reason = g(notification, "reason", "unknown")
|
||||||
|
|
||||||
# Get post text if available
|
# Get post text - try multiple locations depending on notification type
|
||||||
record = g(notification, "record", {})
|
record = g(notification, "record", {})
|
||||||
|
post_text = ""
|
||||||
|
|
||||||
|
# For mentions, replies, quotes: text is in the record itself
|
||||||
post_text = g(record, "text", "")
|
post_text = g(record, "text", "")
|
||||||
|
|
||||||
# Format like Mastodon: "{username} has action: {status}"
|
# For likes and reposts: try to get the subject post text
|
||||||
|
if not post_text and reason in ("like", "repost"):
|
||||||
|
# First check for hydrated subject text (added by NotificationBuffer)
|
||||||
|
post_text = g(notification, "_subject_text", "")
|
||||||
|
|
||||||
|
# Check if there's a reasonSubject with embedded post data
|
||||||
|
if not post_text:
|
||||||
|
reason_subject = g(notification, "reasonSubject") or g(notification, "reason_subject")
|
||||||
|
if reason_subject:
|
||||||
|
# Sometimes the subject post is embedded
|
||||||
|
subject_record = g(reason_subject, "record", {})
|
||||||
|
post_text = g(subject_record, "text", "")
|
||||||
|
|
||||||
|
# Check if there's subject post data in other locations
|
||||||
|
if not post_text:
|
||||||
|
subject = g(record, "subject", {})
|
||||||
|
subject_text = g(subject, "text", "")
|
||||||
|
if subject_text:
|
||||||
|
post_text = subject_text
|
||||||
|
|
||||||
|
# Format: action text without username (username is already in column 0)
|
||||||
if reason == "like":
|
if reason == "like":
|
||||||
if post_text:
|
if post_text:
|
||||||
text = _("{username} has added to favorites: {status}").format(username=user_str, status=post_text)
|
text = _("has added to favorites: {status}").format(status=post_text)
|
||||||
else:
|
else:
|
||||||
text = _("{username} has added to favorites").format(username=user_str)
|
text = _("has added to favorites")
|
||||||
elif reason == "repost":
|
elif reason == "repost":
|
||||||
if post_text:
|
if post_text:
|
||||||
text = _("{username} has reposted: {status}").format(username=user_str, status=post_text)
|
text = _("has reposted: {status}").format(status=post_text)
|
||||||
else:
|
else:
|
||||||
text = _("{username} has reposted").format(username=user_str)
|
text = _("has reposted")
|
||||||
elif reason == "follow":
|
elif reason == "follow":
|
||||||
text = _("{username} has followed you.").format(username=user_str)
|
text = _("has followed you.")
|
||||||
elif reason == "mention":
|
elif reason == "mention":
|
||||||
if post_text:
|
if post_text:
|
||||||
text = _("{username} has mentioned you: {status}").format(username=user_str, status=post_text)
|
text = _("has mentioned you: {status}").format(status=post_text)
|
||||||
else:
|
else:
|
||||||
text = _("{username} has mentioned you").format(username=user_str)
|
text = _("has mentioned you")
|
||||||
elif reason == "reply":
|
elif reason == "reply":
|
||||||
if post_text:
|
if post_text:
|
||||||
text = _("{username} has replied: {status}").format(username=user_str, status=post_text)
|
text = _("has replied: {status}").format(status=post_text)
|
||||||
else:
|
else:
|
||||||
text = _("{username} has replied").format(username=user_str)
|
text = _("has replied")
|
||||||
elif reason == "quote":
|
elif reason == "quote":
|
||||||
if post_text:
|
if post_text:
|
||||||
text = _("{username} has quoted your post: {status}").format(username=user_str, status=post_text)
|
text = _("has quoted your post: {status}").format(status=post_text)
|
||||||
else:
|
else:
|
||||||
text = _("{username} has quoted your post").format(username=user_str)
|
text = _("has quoted your post")
|
||||||
elif reason == "starterpack-joined":
|
elif reason == "starterpack-joined":
|
||||||
text = _("{username} has joined your starter pack.").format(username=user_str)
|
text = _("has joined your starter pack.")
|
||||||
else:
|
else:
|
||||||
text = f"{user_str}: {reason}"
|
text = reason
|
||||||
|
|
||||||
# Date
|
# Date
|
||||||
indexed_at = g(notification, "indexedAt", "") or g(notification, "indexed_at", "")
|
indexed_at = g(notification, "indexedAt", "") or g(notification, "indexed_at", "")
|
||||||
@@ -315,10 +338,10 @@ def compose_user(user, db, settings, relative_times, show_screen_names=False, sa
|
|||||||
ts = ""
|
ts = ""
|
||||||
|
|
||||||
# Format like Mastodon: "Name (@handle). X followers, Y following, Z posts. Joined date"
|
# Format like Mastodon: "Name (@handle). X followers, Y following, Z posts. Joined date"
|
||||||
if ts:
|
# Use the exact same translatable string as Mastodon (sessions/mastodon/compose.py)
|
||||||
|
if not ts:
|
||||||
|
ts = _("unknown")
|
||||||
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (display_name, handle, followers, following, posts, ts)]
|
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (display_name, handle, followers, following, posts, ts)]
|
||||||
else:
|
|
||||||
return [_("%s (@%s). %s followers, %s following, %s posts.") % (display_name, handle, followers, following, posts)]
|
|
||||||
|
|
||||||
|
|
||||||
def compose_convo(convo, db, settings, relative_times, show_screen_names=False, safe=True):
|
def compose_convo(convo, db, settings, relative_times, show_screen_names=False, safe=True):
|
||||||
|
|||||||
@@ -528,12 +528,18 @@ class Session(base.baseSession):
|
|||||||
api = self._ensure_client()
|
api = self._ensure_client()
|
||||||
if not actors:
|
if not actors:
|
||||||
return {"items": []}
|
return {"items": []}
|
||||||
|
# API limit is 25 actors per request, batch if needed
|
||||||
|
all_profiles = []
|
||||||
|
batch_size = 25
|
||||||
|
for i in range(0, len(actors), batch_size):
|
||||||
|
batch = actors[i:i + batch_size]
|
||||||
try:
|
try:
|
||||||
res = api.app.bsky.actor.get_profiles({"actors": actors})
|
res = api.app.bsky.actor.get_profiles({"actors": batch})
|
||||||
return {"items": getattr(res, "profiles", []) or []}
|
profiles = getattr(res, "profiles", []) or []
|
||||||
|
all_profiles.extend(profiles)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Error fetching Bluesky profiles batch")
|
log.exception("Error fetching Bluesky profiles batch")
|
||||||
return {"items": []}
|
return {"items": all_profiles}
|
||||||
|
|
||||||
def get_post_likes(self, uri: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
def get_post_likes(self, uri: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
api = self._ensure_client()
|
api = self._ensure_client()
|
||||||
@@ -734,24 +740,57 @@ class Session(base.baseSession):
|
|||||||
api = self._ensure_client()
|
api = self._ensure_client()
|
||||||
# Chat API requires using the chat proxy
|
# Chat API requires using the chat proxy
|
||||||
dm_client = api.with_bsky_chat_proxy()
|
dm_client = api.with_bsky_chat_proxy()
|
||||||
res = dm_client.chat.bsky.convo.list_convos({"limit": limit, "cursor": cursor})
|
dm = dm_client.chat.bsky.convo
|
||||||
return {"items": res.convos, "cursor": res.cursor}
|
params = {"limit": limit}
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
try:
|
||||||
|
res = dm.list_convos(params)
|
||||||
|
return {"items": res.convos, "cursor": getattr(res, "cursor", None)}
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error listing conversations")
|
||||||
|
return {"items": [], "cursor": None}
|
||||||
|
|
||||||
def get_convo_messages(self, convo_id: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
def get_convo_messages(self, convo_id: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
api = self._ensure_client()
|
api = self._ensure_client()
|
||||||
dm_client = api.with_bsky_chat_proxy()
|
dm_client = api.with_bsky_chat_proxy()
|
||||||
res = dm_client.chat.bsky.convo.get_messages({"convoId": convo_id, "limit": limit, "cursor": cursor})
|
dm = dm_client.chat.bsky.convo
|
||||||
return {"items": res.messages, "cursor": res.cursor}
|
params = {"convoId": convo_id, "limit": limit}
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
try:
|
||||||
|
res = dm.get_messages(params)
|
||||||
|
return {"items": res.messages, "cursor": getattr(res, "cursor", None)}
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error getting conversation messages")
|
||||||
|
return {"items": [], "cursor": None}
|
||||||
|
|
||||||
def send_chat_message(self, convo_id: str, text: str) -> Any:
|
def send_chat_message(self, convo_id: str, text: str) -> Any:
|
||||||
api = self._ensure_client()
|
api = self._ensure_client()
|
||||||
dm_client = api.with_bsky_chat_proxy()
|
dm_client = api.with_bsky_chat_proxy()
|
||||||
return dm_client.chat.bsky.convo.send_message({
|
dm = dm_client.chat.bsky.convo
|
||||||
|
try:
|
||||||
|
return dm.send_message({
|
||||||
"convoId": convo_id,
|
"convoId": convo_id,
|
||||||
"message": {
|
"message": {
|
||||||
"text": text
|
"text": text
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error sending chat message")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_or_create_convo(self, members: list[str]) -> dict[str, Any] | None:
|
||||||
|
"""Get or create a conversation with the given members (DIDs)."""
|
||||||
|
api = self._ensure_client()
|
||||||
|
dm_client = api.with_bsky_chat_proxy()
|
||||||
|
dm = dm_client.chat.bsky.convo
|
||||||
|
try:
|
||||||
|
res = dm.get_convo_for_members({"members": members})
|
||||||
|
return res.convo
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error getting/creating conversation")
|
||||||
|
return None
|
||||||
|
|
||||||
# Streaming/Polling methods
|
# Streaming/Polling methods
|
||||||
|
|
||||||
|
|||||||
@@ -227,32 +227,42 @@ def render_notification(notification, template, post_template, settings, relativ
|
|||||||
screen_name = _g(author, "handle", "")
|
screen_name = _g(author, "handle", "")
|
||||||
reason = _g(notification, "reason", "unknown")
|
reason = _g(notification, "reason", "unknown")
|
||||||
record = _g(notification, "record") or {}
|
record = _g(notification, "record") or {}
|
||||||
|
|
||||||
|
# Get post text - try multiple locations depending on notification type
|
||||||
post_text = _g(record, "text", "") or ""
|
post_text = _g(record, "text", "") or ""
|
||||||
|
|
||||||
|
# For likes and reposts: try to get the subject post text
|
||||||
|
if not post_text and reason in ("like", "repost"):
|
||||||
|
# First check for hydrated subject text (added by NotificationBuffer)
|
||||||
|
post_text = _g(notification, "_subject_text", "") or ""
|
||||||
|
|
||||||
|
# Check if there's a reasonSubject with embedded post data
|
||||||
|
if not post_text:
|
||||||
|
reason_subject = _g(notification, "reasonSubject") or _g(notification, "reason_subject")
|
||||||
|
if reason_subject:
|
||||||
|
subject_record = _g(reason_subject, "record", {})
|
||||||
|
post_text = _g(subject_record, "text", "") or ""
|
||||||
|
|
||||||
|
# Check subject in record
|
||||||
|
if not post_text:
|
||||||
|
subject = _g(record, "subject", {})
|
||||||
|
post_text = _g(subject, "text", "") or ""
|
||||||
|
|
||||||
|
# Format: action text without username (username is already in display_name for template)
|
||||||
if reason == "like":
|
if reason == "like":
|
||||||
text = _("{username} has added to favorites: {status}").format(
|
text = _("has added to favorites: {status}").format(status=post_text) if post_text else _("has added to favorites")
|
||||||
username=display_name, status=post_text
|
|
||||||
) if post_text else _("{username} has added to favorites").format(username=display_name)
|
|
||||||
elif reason == "repost":
|
elif reason == "repost":
|
||||||
text = _("{username} has reposted: {status}").format(
|
text = _("has reposted: {status}").format(status=post_text) if post_text else _("has reposted")
|
||||||
username=display_name, status=post_text
|
|
||||||
) if post_text else _("{username} has reposted").format(username=display_name)
|
|
||||||
elif reason == "follow":
|
elif reason == "follow":
|
||||||
text = _("{username} has followed you.").format(username=display_name)
|
text = _("has followed you.")
|
||||||
elif reason == "mention":
|
elif reason == "mention":
|
||||||
text = _("{username} has mentioned you: {status}").format(
|
text = _("has mentioned you: {status}").format(status=post_text) if post_text else _("has mentioned you")
|
||||||
username=display_name, status=post_text
|
|
||||||
) if post_text else _("{username} has mentioned you").format(username=display_name)
|
|
||||||
elif reason == "reply":
|
elif reason == "reply":
|
||||||
text = _("{username} has replied: {status}").format(
|
text = _("has replied: {status}").format(status=post_text) if post_text else _("has replied")
|
||||||
username=display_name, status=post_text
|
|
||||||
) if post_text else _("{username} has replied").format(username=display_name)
|
|
||||||
elif reason == "quote":
|
elif reason == "quote":
|
||||||
text = _("{username} has quoted your post: {status}").format(
|
text = _("has quoted your post: {status}").format(status=post_text) if post_text else _("has quoted your post")
|
||||||
username=display_name, status=post_text
|
|
||||||
) if post_text else _("{username} has quoted your post").format(username=display_name)
|
|
||||||
else:
|
else:
|
||||||
text = "{user}: {reason}".format(user=display_name or screen_name, reason=reason)
|
text = reason
|
||||||
|
|
||||||
indexed_at = _g(notification, "indexedAt") or _g(notification, "indexed_at")
|
indexed_at = _g(notification, "indexedAt") or _g(notification, "indexed_at")
|
||||||
date = process_date(indexed_at, relative_times, offset_hours) if indexed_at else ""
|
date = process_date(indexed_at, relative_times, offset_hours) if indexed_at else ""
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class UserPanel(wx.Panel):
|
|||||||
self.list.list.SetFocus()
|
self.list.list.SetFocus()
|
||||||
|
|
||||||
class ChatPanel(wx.Panel):
|
class ChatPanel(wx.Panel):
|
||||||
|
"""Panel for conversation list, similar to Mastodon's conversationListPanel."""
|
||||||
def __init__(self, parent, name, account="Unknown"):
|
def __init__(self, parent, name, account="Unknown"):
|
||||||
super().__init__(parent, name=name)
|
super().__init__(parent, name=name)
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -113,19 +114,35 @@ class ChatPanel(wx.Panel):
|
|||||||
|
|
||||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
# List: Participants, Last Message, Date
|
# List: User, Text, Date (like Mastodon)
|
||||||
self.list = widgets.list(self, _("Participants"), _("Last Message"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
self.list = widgets.list(self, _("User"), _("Text"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 200)
|
self.list.set_windows_size(0, 200)
|
||||||
self.list.set_windows_size(1, 600)
|
self.list.set_windows_size(1, 600)
|
||||||
self.list.set_windows_size(2, 200)
|
self.list.set_windows_size(2, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
|
# Buttons (like Mastodon: Post, Reply)
|
||||||
|
self.post = wx.Button(self, -1, _("Post"))
|
||||||
|
self.reply = wx.Button(self, -1, _("Reply"))
|
||||||
|
self.new_chat = wx.Button(self, -1, _("New Chat"))
|
||||||
|
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.new_chat, 0, wx.ALL, 5)
|
||||||
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
|
|
||||||
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
|
|
||||||
def set_focus_function(self, func):
|
def set_focus_function(self, func):
|
||||||
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, func)
|
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, func)
|
||||||
|
|
||||||
|
def set_position(self, reversed=False):
|
||||||
|
if reversed == False:
|
||||||
|
self.list.select_item(self.list.get_count()-1)
|
||||||
|
else:
|
||||||
|
self.list.select_item(0)
|
||||||
|
|
||||||
def set_focus_in_list(self):
|
def set_focus_in_list(self):
|
||||||
self.list.list.SetFocus()
|
self.list.list.SetFocus()
|
||||||
|
|
||||||
@@ -136,6 +153,7 @@ class ChatMessagePanel(HomePanel):
|
|||||||
# Adjust buttons for chat
|
# Adjust buttons for chat
|
||||||
self.repost.Hide()
|
self.repost.Hide()
|
||||||
self.like.Hide()
|
self.like.Hide()
|
||||||
|
self.dm.Hide() # Hide Chat button since we're already in a chat
|
||||||
self.reply.SetLabel(_("Send Message"))
|
self.reply.SetLabel(_("Send Message"))
|
||||||
|
|
||||||
# Refresh columns
|
# Refresh columns
|
||||||
|
|||||||
Reference in New Issue
Block a user