mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 01:17:32 +01:00
Commit
This commit is contained in:
@@ -56,3 +56,4 @@ winpaths==0.2
|
|||||||
wxPython==4.2.3
|
wxPython==4.2.3
|
||||||
youtube-dl==2021.12.17
|
youtube-dl==2021.12.17
|
||||||
zipp==3.21.0
|
zipp==3.21.0
|
||||||
|
atproto>=0.0.45
|
||||||
|
|||||||
@@ -61,6 +61,31 @@ class Handler:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
from wxUI.dialogs.atprotosocial.configuration import AccountSettingsDialog
|
||||||
|
dlg = AccountSettingsDialog(controller.view, ask_before_boost=ask_default)
|
||||||
|
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()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to persist Bluesky boost_mode setting")
|
||||||
|
dlg.Destroy()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error opening Bluesky account settings dialog")
|
||||||
|
|
||||||
async def handle_action(self, action_name: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
async def handle_action(self, action_name: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
logger.debug("handle_action stub: %s %s %s", action_name, user_id, payload)
|
logger.debug("handle_action stub: %s %s %s", action_name, user_id, payload)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -788,14 +788,33 @@ class Controller(object):
|
|||||||
text = dlg.GetValue().strip()
|
text = dlg.GetValue().strip()
|
||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
try:
|
try:
|
||||||
uri = session.send_message(text, quote_uri=item_uri)
|
if text:
|
||||||
if uri:
|
uri = session.send_message(text, quote_uri=item_uri)
|
||||||
output.speak(_("Quote posted."), True)
|
if uri:
|
||||||
|
output.speak(_("Quote posted."), True)
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to send quote."), True)
|
||||||
else:
|
else:
|
||||||
output.speak(_("Failed to send quote."), True)
|
# Confirm repost (share) depending on preference (boost_mode)
|
||||||
|
ask = True
|
||||||
|
try:
|
||||||
|
ask = session.settings["general"].get("boost_mode", "ask") == "ask"
|
||||||
|
except Exception:
|
||||||
|
ask = True
|
||||||
|
if ask:
|
||||||
|
confirm = wx.MessageDialog(None, _("Would you like to share this post?"), _("Boost"), wx.YES_NO|wx.ICON_QUESTION)
|
||||||
|
if confirm.ShowModal() != wx.ID_YES:
|
||||||
|
confirm.Destroy()
|
||||||
|
return
|
||||||
|
confirm.Destroy()
|
||||||
|
r_uri = session.repost(item_uri)
|
||||||
|
if r_uri:
|
||||||
|
output.speak(_("Post shared."), True)
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to share post."), True)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Error sending Bluesky quote (invisible)")
|
log.exception("Error sharing/quoting Bluesky post (invisible)")
|
||||||
output.speak(_("An error occurred while posting the quote."), True)
|
output.speak(_("An error occurred while sharing the post."), True)
|
||||||
else:
|
else:
|
||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
return
|
return
|
||||||
@@ -806,19 +825,38 @@ class Controller(object):
|
|||||||
text, files, cw_text, langs = dlg.get_payload()
|
text, files, cw_text, langs = dlg.get_payload()
|
||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
try:
|
try:
|
||||||
uri = session.send_message(text, files=files, cw_text=cw_text, is_sensitive=bool(cw_text), languages=langs, quote_uri=item_uri)
|
if text or files or cw_text:
|
||||||
if uri:
|
uri = session.send_message(text, files=files, cw_text=cw_text, is_sensitive=bool(cw_text), languages=langs, quote_uri=item_uri)
|
||||||
output.speak(_("Quote posted."), True)
|
if uri:
|
||||||
try:
|
output.speak(_("Quote posted."), True)
|
||||||
if hasattr(buffer, "start_stream"):
|
try:
|
||||||
buffer.start_stream(mandatory=False, play_sound=False)
|
if hasattr(buffer, "start_stream"):
|
||||||
except Exception:
|
buffer.start_stream(mandatory=False, play_sound=False)
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to send quote."), True)
|
||||||
else:
|
else:
|
||||||
output.speak(_("Failed to send quote."), True)
|
# Confirm repost without comment depending on preference
|
||||||
|
ask = True
|
||||||
|
try:
|
||||||
|
ask = session.settings["general"].get("boost_mode", "ask") == "ask"
|
||||||
|
except Exception:
|
||||||
|
ask = True
|
||||||
|
if ask:
|
||||||
|
confirm = wx.MessageDialog(self.view, _("Would you like to share this post?"), _("Boost"), wx.YES_NO|wx.ICON_QUESTION)
|
||||||
|
if confirm.ShowModal() != wx.ID_YES:
|
||||||
|
confirm.Destroy()
|
||||||
|
return
|
||||||
|
confirm.Destroy()
|
||||||
|
r_uri = session.repost(item_uri)
|
||||||
|
if r_uri:
|
||||||
|
output.speak(_("Post shared."), True)
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to share post."), True)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Error sending Bluesky quote (dialog)")
|
log.exception("Error sharing/quoting Bluesky post (dialog)")
|
||||||
output.speak(_("An error occurred while posting the quote."), True)
|
output.speak(_("An error occurred while sharing the post."), True)
|
||||||
else:
|
else:
|
||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ class Session(base.baseSession):
|
|||||||
# Ensure db exists (can be set to None on logout paths)
|
# Ensure db exists (can be set to None on logout paths)
|
||||||
if not isinstance(self.db, dict):
|
if not isinstance(self.db, dict):
|
||||||
self.db = {}
|
self.db = {}
|
||||||
|
# Ensure general settings have a default for boost confirmations like Mastodon
|
||||||
|
try:
|
||||||
|
if "general" in self.settings and self.settings["general"].get("boost_mode") is None:
|
||||||
|
self.settings["general"]["boost_mode"] = "ask"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
api = self._ensure_client()
|
api = self._ensure_client()
|
||||||
# Prefer resuming session if we have one
|
# Prefer resuming session if we have one
|
||||||
if session_string:
|
if session_string:
|
||||||
@@ -256,12 +262,61 @@ class Session(base.baseSession):
|
|||||||
"images": embed_images,
|
"images": embed_images,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reply-to handling (sets parent/root strong refs)
|
# Helper: normalize various incoming identifiers to an at:// URI
|
||||||
|
def _normalize_to_uri(identifier: str) -> str | None:
|
||||||
|
try:
|
||||||
|
if not isinstance(identifier, str):
|
||||||
|
return None
|
||||||
|
if identifier.startswith("at://"):
|
||||||
|
return identifier
|
||||||
|
if "bsky.app/profile/" in identifier and "/post/" in identifier:
|
||||||
|
# Accept full web URL and try to resolve via get_post_thread below
|
||||||
|
return identifier
|
||||||
|
# Accept bare rkey case by constructing a guess using own handle
|
||||||
|
handle = self.db.get("user_name") or self.settings["atprotosocial"].get("handle")
|
||||||
|
did = self.db.get("user_id") or self.settings["atprotosocial"].get("did")
|
||||||
|
if handle and did and len(identifier) in (13, 14, 15):
|
||||||
|
# rkey length is typically ~13 chars base32
|
||||||
|
return f"at://{did}/app.bsky.feed.post/{identifier}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Reply-to handling (sets correct root/parent strong refs)
|
||||||
if reply_to:
|
if reply_to:
|
||||||
parent_ref = _get_strong_ref(reply_to)
|
# Resolve to proper at:// uri when possible
|
||||||
|
reply_uri = _normalize_to_uri(reply_to) or reply_to
|
||||||
|
parent_ref = _get_strong_ref(reply_uri)
|
||||||
|
root_ref = parent_ref
|
||||||
|
# Try to fetch thread to find actual root for deep replies
|
||||||
|
try:
|
||||||
|
# atproto SDK usually exposes get_post_thread
|
||||||
|
thread_res = None
|
||||||
|
try:
|
||||||
|
thread_res = api.app.bsky.feed.get_post_thread({"uri": reply_uri})
|
||||||
|
except Exception:
|
||||||
|
# Try typed model call variant if available
|
||||||
|
from atproto import models as at_models # type: ignore
|
||||||
|
params = at_models.AppBskyFeedGetPostThread.Params(uri=reply_uri)
|
||||||
|
thread_res = api.app.bsky.feed.get_post_thread(params)
|
||||||
|
thread = getattr(thread_res, "thread", None)
|
||||||
|
# Walk to the root if present
|
||||||
|
node = thread
|
||||||
|
while node and getattr(node, "parent", None):
|
||||||
|
node = getattr(node, "parent")
|
||||||
|
root_uri = getattr(node, "post", None)
|
||||||
|
if root_uri:
|
||||||
|
root_uri = getattr(root_uri, "uri", None)
|
||||||
|
if root_uri and isinstance(root_uri, str):
|
||||||
|
maybe_root = _get_strong_ref(root_uri)
|
||||||
|
if maybe_root:
|
||||||
|
root_ref = maybe_root
|
||||||
|
except Exception:
|
||||||
|
# If anything fails, keep parent as root for a simple two-level reply
|
||||||
|
pass
|
||||||
if parent_ref:
|
if parent_ref:
|
||||||
record["reply"] = {
|
record["reply"] = {
|
||||||
"root": parent_ref,
|
"root": root_ref or parent_ref,
|
||||||
"parent": parent_ref,
|
"parent": parent_ref,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,3 +346,49 @@ class Session(base.baseSession):
|
|||||||
log.exception("Error sending Bluesky post")
|
log.exception("Error sending Bluesky post")
|
||||||
output.speak(_("An error occurred while posting to Bluesky."), True)
|
output.speak(_("An error occurred while posting to Bluesky."), True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def repost(self, post_uri: str, post_cid: str | None = None) -> str | None:
|
||||||
|
"""Create a simple repost of a given post. Returns URI of the repost record or None."""
|
||||||
|
if not self.logged:
|
||||||
|
raise Exceptions.NotLoggedSessionError("You are not logged in yet.")
|
||||||
|
try:
|
||||||
|
api = self._ensure_client()
|
||||||
|
|
||||||
|
def _get_strong_ref(uri: str):
|
||||||
|
try:
|
||||||
|
posts_res = api.app.bsky.feed.get_posts({"uris": [uri]})
|
||||||
|
posts = getattr(posts_res, "posts", None) or []
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
posts_res = api.app.bsky.feed.get_posts(uris=[uri])
|
||||||
|
posts = getattr(posts_res, "posts", None) or []
|
||||||
|
except Exception:
|
||||||
|
posts = []
|
||||||
|
if posts:
|
||||||
|
post0 = posts[0]
|
||||||
|
s_uri = getattr(post0, "uri", uri)
|
||||||
|
s_cid = getattr(post0, "cid", None) or (post0.get("cid") if isinstance(post0, dict) else None)
|
||||||
|
if s_cid:
|
||||||
|
return {"uri": s_uri, "cid": s_cid}
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not post_cid:
|
||||||
|
strong = _get_strong_ref(post_uri)
|
||||||
|
if not strong:
|
||||||
|
return None
|
||||||
|
post_uri = strong["uri"]
|
||||||
|
post_cid = strong["cid"]
|
||||||
|
|
||||||
|
out = api.com.atproto.repo.create_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.feed.repost",
|
||||||
|
"record": {
|
||||||
|
"$type": "app.bsky.feed.repost",
|
||||||
|
"subject": {"uri": post_uri, "cid": post_cid},
|
||||||
|
"createdAt": getattr(api, "get_current_time_iso", lambda: None)() or None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return getattr(out, "uri", None)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error creating Bluesky repost record")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import logging
|
|||||||
import wx
|
import wx
|
||||||
import config
|
import config
|
||||||
from mysc.repeating_timer import RepeatingTimer
|
from mysc.repeating_timer import RepeatingTimer
|
||||||
|
import arrow
|
||||||
|
import arrow
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from multiplatform_widgets import widgets
|
from multiplatform_widgets import widgets
|
||||||
@@ -32,7 +34,7 @@ class ATProtoSocialHomeTimelinePanel(object):
|
|||||||
self.buffer.name = name
|
self.buffer.name = name
|
||||||
# Ensure controller can resolve current account from the GUI panel
|
# Ensure controller can resolve current account from the GUI panel
|
||||||
self.buffer.account = self.account
|
self.buffer.account = self.account
|
||||||
self.items = [] # list of dicts: {uri, author, text, indexed_at}
|
self.items = [] # list of dicts: {uri, author, handle, display_name, text, indexed_at}
|
||||||
self.cursor = None
|
self.cursor = None
|
||||||
self._auto_timer = None
|
self._auto_timer = None
|
||||||
|
|
||||||
@@ -47,8 +49,14 @@ class ATProtoSocialHomeTimelinePanel(object):
|
|||||||
# The atproto SDK expects params, not raw kwargs
|
# The atproto SDK expects params, not raw kwargs
|
||||||
try:
|
try:
|
||||||
from atproto import models as at_models # type: ignore
|
from atproto import models as at_models # type: ignore
|
||||||
params = at_models.AppBskyFeedGetTimeline.Params(limit=count)
|
# Home: algorithmic/default timeline
|
||||||
res = api.app.bsky.feed.get_timeline(params)
|
try:
|
||||||
|
params = at_models.AppBskyFeedGetTimeline.Params(limit=count)
|
||||||
|
res = api.app.bsky.feed.get_timeline(params)
|
||||||
|
except Exception:
|
||||||
|
# Some SDKs may require explicit algorithm for home; try behavioral
|
||||||
|
params = at_models.AppBskyFeedGetTimeline.Params(limit=count, algorithm="behavioral")
|
||||||
|
res = api.app.bsky.feed.get_timeline(params)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Fallback to plain dict params if typed models unavailable
|
# Fallback to plain dict params if typed models unavailable
|
||||||
res = api.app.bsky.feed.get_timeline({"limit": count})
|
res = api.app.bsky.feed.get_timeline({"limit": count})
|
||||||
@@ -59,17 +67,27 @@ class ATProtoSocialHomeTimelinePanel(object):
|
|||||||
post = getattr(it, "post", None)
|
post = getattr(it, "post", None)
|
||||||
if not post:
|
if not post:
|
||||||
continue
|
continue
|
||||||
|
# No additional client-side filtering; server distinguishes timelines correctly
|
||||||
record = getattr(post, "record", None)
|
record = getattr(post, "record", None)
|
||||||
author = getattr(post, "author", None)
|
author = getattr(post, "author", None)
|
||||||
text = getattr(record, "text", "") if record else ""
|
text = getattr(record, "text", "") if record else ""
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
handle = getattr(author, "handle", "") if author else ""
|
||||||
|
display_name = (
|
||||||
|
getattr(author, "display_name", None)
|
||||||
|
or getattr(author, "displayName", None)
|
||||||
|
or ""
|
||||||
|
) if author else ""
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
indexed_at = getattr(post, "indexed_at", None)
|
||||||
self.items.append({
|
item = {
|
||||||
"uri": getattr(post, "uri", ""),
|
"uri": getattr(post, "uri", ""),
|
||||||
"author": handle,
|
"author": display_name or handle,
|
||||||
|
"handle": handle,
|
||||||
|
"display_name": display_name,
|
||||||
"text": text,
|
"text": text,
|
||||||
"indexed_at": indexed_at,
|
"indexed_at": indexed_at,
|
||||||
})
|
}
|
||||||
|
self._append_item(item, to_top=self._reverse())
|
||||||
|
# Full rerender to ensure column widths and selection
|
||||||
self._render_list(replace=True)
|
self._render_list(replace=True)
|
||||||
return len(self.items)
|
return len(self.items)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -96,26 +114,52 @@ class ATProtoSocialHomeTimelinePanel(object):
|
|||||||
post = getattr(it, "post", None)
|
post = getattr(it, "post", None)
|
||||||
if not post:
|
if not post:
|
||||||
continue
|
continue
|
||||||
|
# No additional client-side filtering
|
||||||
record = getattr(post, "record", None)
|
record = getattr(post, "record", None)
|
||||||
author = getattr(post, "author", None)
|
author = getattr(post, "author", None)
|
||||||
text = getattr(record, "text", "") if record else ""
|
text = getattr(record, "text", "") if record else ""
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
handle = getattr(author, "handle", "") if author else ""
|
||||||
|
display_name = (
|
||||||
|
getattr(author, "display_name", None)
|
||||||
|
or getattr(author, "displayName", None)
|
||||||
|
or ""
|
||||||
|
) if author else ""
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
indexed_at = getattr(post, "indexed_at", None)
|
||||||
new_items.append({
|
new_items.append({
|
||||||
"uri": getattr(post, "uri", ""),
|
"uri": getattr(post, "uri", ""),
|
||||||
"author": handle,
|
"author": display_name or handle,
|
||||||
|
"handle": handle,
|
||||||
|
"display_name": display_name,
|
||||||
"text": text,
|
"text": text,
|
||||||
"indexed_at": indexed_at,
|
"indexed_at": indexed_at,
|
||||||
})
|
})
|
||||||
if not new_items:
|
if not new_items:
|
||||||
return 0
|
return 0
|
||||||
self.items.extend(new_items)
|
for it in new_items:
|
||||||
|
self._append_item(it, to_top=self._reverse())
|
||||||
|
# Render only the newly added slice
|
||||||
self._render_list(replace=False, start=len(self.items) - len(new_items))
|
self._render_list(replace=False, start=len(self.items) - len(new_items))
|
||||||
return len(new_items)
|
return len(new_items)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Failed to load more Bluesky timeline items")
|
log.exception("Failed to load more Bluesky timeline items")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
# Alias to integrate with mainController expectations for ATProto
|
||||||
|
def load_more_posts(self, *args, **kwargs):
|
||||||
|
return self.get_more_items()
|
||||||
|
|
||||||
|
def _reverse(self) -> bool:
|
||||||
|
try:
|
||||||
|
return bool(self.session.settings["general"].get("reverse_timelines", False))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _append_item(self, item: dict, to_top: bool = False):
|
||||||
|
if to_top:
|
||||||
|
self.items.insert(0, item)
|
||||||
|
else:
|
||||||
|
self.items.append(item)
|
||||||
|
|
||||||
def _render_list(self, replace: bool, start: int = 0):
|
def _render_list(self, replace: bool, start: int = 0):
|
||||||
if replace:
|
if replace:
|
||||||
self.buffer.list.clear()
|
self.buffer.list.clear()
|
||||||
@@ -124,14 +168,33 @@ class ATProtoSocialHomeTimelinePanel(object):
|
|||||||
dt = ""
|
dt = ""
|
||||||
if it.get("indexed_at"):
|
if it.get("indexed_at"):
|
||||||
try:
|
try:
|
||||||
# indexed_at is ISO format; show HH:MM or date
|
# Mastodon-like date formatting: relative or full date
|
||||||
dt = str(it["indexed_at"])[:16].replace("T", " ")
|
rel = False
|
||||||
|
try:
|
||||||
|
rel = bool(self.session.settings["general"].get("relative_times", False))
|
||||||
|
except Exception:
|
||||||
|
rel = False
|
||||||
|
ts = arrow.get(str(it["indexed_at"]))
|
||||||
|
if rel:
|
||||||
|
dt = ts.humanize(locale=languageHandler.curLang[:2])
|
||||||
|
else:
|
||||||
|
dt = ts.format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
|
||||||
except Exception:
|
except Exception:
|
||||||
dt = ""
|
try:
|
||||||
|
dt = str(it["indexed_at"])[:16].replace("T", " ")
|
||||||
|
except Exception:
|
||||||
|
dt = ""
|
||||||
text = it.get("text", "").replace("\n", " ")
|
text = it.get("text", "").replace("\n", " ")
|
||||||
if len(text) > 200:
|
if len(text) > 200:
|
||||||
text = text[:197] + "..."
|
text = text[:197] + "..."
|
||||||
self.buffer.list.insert_item(False, it.get("author", ""), text, dt)
|
# Display name and handle like Mastodon: "Display (@handle)"
|
||||||
|
author_col = it.get("author", "")
|
||||||
|
handle = it.get("handle", "")
|
||||||
|
if handle and it.get("display_name"):
|
||||||
|
author_col = f"{it.get('display_name')} (@{handle})"
|
||||||
|
elif handle and not author_col:
|
||||||
|
author_col = f"@{handle}"
|
||||||
|
self.buffer.list.insert_item(False, author_col, text, dt)
|
||||||
|
|
||||||
# For compatibility with controller expectations
|
# For compatibility with controller expectations
|
||||||
def save_positions(self):
|
def save_positions(self):
|
||||||
@@ -151,6 +214,28 @@ class ATProtoSocialHomeTimelinePanel(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_message(self):
|
||||||
|
try:
|
||||||
|
idx = self.buffer.list.get_selected()
|
||||||
|
if idx is None or idx < 0:
|
||||||
|
return ""
|
||||||
|
it = self.items[idx]
|
||||||
|
author = it.get("display_name") or it.get("author") or ""
|
||||||
|
handle = it.get("handle")
|
||||||
|
if handle:
|
||||||
|
author = f"{author} (@{handle})" if author else f"@{handle}"
|
||||||
|
text = it.get("text", "").replace("\n", " ")
|
||||||
|
dt = ""
|
||||||
|
if it.get("indexed_at"):
|
||||||
|
try:
|
||||||
|
dt = str(it["indexed_at"])[:16].replace("T", " ")
|
||||||
|
except Exception:
|
||||||
|
dt = ""
|
||||||
|
parts = [p for p in [author, text, dt] if p]
|
||||||
|
return ", ".join(parts)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
# Auto-refresh support (polling) to simulate near real-time updates
|
# Auto-refresh support (polling) to simulate near real-time updates
|
||||||
def _periodic_refresh(self):
|
def _periodic_refresh(self):
|
||||||
try:
|
try:
|
||||||
@@ -215,7 +300,8 @@ class ATProtoSocialFollowingTimelinePanel(ATProtoSocialHomeTimelinePanel):
|
|||||||
count = 40
|
count = 40
|
||||||
try:
|
try:
|
||||||
api = self.session._ensure_client()
|
api = self.session._ensure_client()
|
||||||
# Use plain dict params to ensure algorithm is passed regardless of SDK models version
|
# Following timeline via reverse-chronological algorithm on get_timeline
|
||||||
|
# Use plain dict to avoid typed-model mismatches across SDK versions
|
||||||
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
|
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
|
||||||
feed = getattr(res, "feed", [])
|
feed = getattr(res, "feed", [])
|
||||||
self.cursor = getattr(res, "cursor", None)
|
self.cursor = getattr(res, "cursor", None)
|
||||||
@@ -228,13 +314,21 @@ class ATProtoSocialFollowingTimelinePanel(ATProtoSocialHomeTimelinePanel):
|
|||||||
author = getattr(post, "author", None)
|
author = getattr(post, "author", None)
|
||||||
text = getattr(record, "text", "") if record else ""
|
text = getattr(record, "text", "") if record else ""
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
handle = getattr(author, "handle", "") if author else ""
|
||||||
|
display_name = (
|
||||||
|
getattr(author, "display_name", None)
|
||||||
|
or getattr(author, "displayName", None)
|
||||||
|
or ""
|
||||||
|
) if author else ""
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
indexed_at = getattr(post, "indexed_at", None)
|
||||||
self.items.append({
|
item = {
|
||||||
"uri": getattr(post, "uri", ""),
|
"uri": getattr(post, "uri", ""),
|
||||||
"author": handle,
|
"author": display_name or handle,
|
||||||
|
"handle": handle,
|
||||||
|
"display_name": display_name,
|
||||||
"text": text,
|
"text": text,
|
||||||
"indexed_at": indexed_at,
|
"indexed_at": indexed_at,
|
||||||
})
|
}
|
||||||
|
self._append_item(item, to_top=self._reverse())
|
||||||
self._render_list(replace=True)
|
self._render_list(replace=True)
|
||||||
return len(self.items)
|
return len(self.items)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -242,3 +336,46 @@ class ATProtoSocialFollowingTimelinePanel(ATProtoSocialHomeTimelinePanel):
|
|||||||
self.buffer.list.clear()
|
self.buffer.list.clear()
|
||||||
self.buffer.list.insert_item(False, _("Error"), _("Could not load timeline."), "")
|
self.buffer.list.insert_item(False, _("Error"), _("Could not load timeline."), "")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def get_more_items(self):
|
||||||
|
if not self.cursor:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
# Pagination via reverse-chronological algorithm on get_timeline
|
||||||
|
res = api.app.bsky.feed.get_timeline({"limit": 40, "cursor": self.cursor, "algorithm": "reverse-chronological"})
|
||||||
|
feed = getattr(res, "feed", [])
|
||||||
|
self.cursor = getattr(res, "cursor", None)
|
||||||
|
new_items = []
|
||||||
|
for it in feed:
|
||||||
|
post = getattr(it, "post", None)
|
||||||
|
if not post:
|
||||||
|
continue
|
||||||
|
record = getattr(post, "record", None)
|
||||||
|
author = getattr(post, "author", None)
|
||||||
|
text = getattr(record, "text", "") if record else ""
|
||||||
|
handle = getattr(author, "handle", "") if author else ""
|
||||||
|
display_name = (
|
||||||
|
getattr(author, "display_name", None)
|
||||||
|
or getattr(author, "displayName", None)
|
||||||
|
or ""
|
||||||
|
) if author else ""
|
||||||
|
indexed_at = getattr(post, "indexed_at", None)
|
||||||
|
new_items.append({
|
||||||
|
"uri": getattr(post, "uri", ""),
|
||||||
|
"author": display_name or handle,
|
||||||
|
"handle": handle,
|
||||||
|
"display_name": display_name,
|
||||||
|
"text": text,
|
||||||
|
"indexed_at": indexed_at,
|
||||||
|
})
|
||||||
|
if not new_items:
|
||||||
|
return 0
|
||||||
|
for it in new_items:
|
||||||
|
self._append_item(it, to_top=self._reverse())
|
||||||
|
self._render_list(replace=False, start=len(self.items) - len(new_items))
|
||||||
|
return len(new_items)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Failed to load more items for following timeline")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|||||||
33
src/wxUI/dialogs/atprotosocial/configuration.py
Normal file
33
src/wxUI/dialogs/atprotosocial/configuration.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import wx
|
||||||
|
import languageHandler
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSettingsDialog(wx.Dialog):
|
||||||
|
def __init__(self, parent=None, ask_before_boost=True):
|
||||||
|
super(AccountSettingsDialog, self).__init__(parent, title=_("Bluesky Account Settings"))
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
|
||||||
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
# Ask before boost/share
|
||||||
|
self.ask_before_boost = wx.CheckBox(panel, wx.ID_ANY, _("Ask confirmation before sharing a post"))
|
||||||
|
self.ask_before_boost.SetValue(bool(ask_before_boost))
|
||||||
|
sizer.Add(self.ask_before_boost, 0, wx.ALL, 8)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
btn_sizer = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL)
|
||||||
|
|
||||||
|
panel.SetSizer(sizer)
|
||||||
|
|
||||||
|
main = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
main.Add(panel, 1, wx.EXPAND | wx.ALL, 10)
|
||||||
|
if btn_sizer:
|
||||||
|
main.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10)
|
||||||
|
self.SetSizerAndFit(main)
|
||||||
|
|
||||||
|
def get_values(self):
|
||||||
|
return {
|
||||||
|
"ask_before_boost": self.ask_before_boost.GetValue(),
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user