Refactor y eliminar patrones raros.

This commit is contained in:
Jesús Pavón Abián
2026-02-01 20:40:09 +01:00
parent 0d8395c6fc
commit 6f0514fd6a
29 changed files with 583 additions and 127 deletions

View File

@@ -98,6 +98,24 @@ def format_error_message(error_description: str, details: str | None = None) ->
logger.info("Blueski messages module loaded (placeholders).")
class post(base_messages.basicMessage):
def __init__(self, session: Any, title: str, caption: str, text: str = "", *args, **kwargs):
self.session = session
self.title = title
self.message = postDialogs.Post(caption=caption, text=text, *args, **kwargs)
try:
self.message.SetTitle(title)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
except Exception:
pass
def get_data(self):
return self.message.get_payload()
def text_processor(self):
pass
def _g(obj: Any, key: str, default: Any = None) -> Any:
if isinstance(obj, dict):
return obj.get(key, default)

View File

@@ -190,28 +190,20 @@ class BaseBuffer(base.Buffer):
pub.sendMessage("execute-action", action="copy_to_clipboard")
def on_post(self, evt):
from wxUI.dialogs.blueski import postDialogs
dlg = postDialogs.Post(caption=_("New Post"))
if dlg.ShowModal() == wx.ID_OK:
text, files, cw, langs = dlg.get_payload()
if not text and not files:
dlg.Destroy()
return
def do_send():
try:
uri = self.session.send_message(message=text, files=files, cw_text=cw, langs=langs)
if uri:
wx.CallAfter(self.session.sound.play, "tweet_send.ogg")
wx.CallAfter(output.speak, _("Sent."))
if hasattr(self, "start_stream"):
wx.CallAfter(self.start_stream, False, False)
else:
wx.CallAfter(output.speak, _("Failed to send post."), True)
except Exception:
log.exception("Error sending Bluesky post")
wx.CallAfter(output.speak, _("An error occurred while posting."), True)
call_threaded(do_send)
dlg.Destroy()
dlg = blueski_messages.post(session=self.session, title=_("New Post"), caption=_("New Post"))
if dlg.message.ShowModal() == wx.ID_OK:
text, files, cw, langs = dlg.get_data()
self._send_post_async(
text=text,
files=files,
cw_text=cw,
langs=langs,
success_message=_("Sent."),
error_message=_("An error occurred while posting."),
sound="tweet_send.ogg",
refresh_args=(False, False),
)
dlg.message.Destroy()
def on_reply(self, evt):
item = self.get_item()
@@ -237,38 +229,71 @@ class BaseBuffer(base.Buffer):
handle = g(author, "handle", "")
initial_text = f"@{handle} " if handle and not handle.startswith("@") else (f"{handle} " if handle else "")
from wxUI.dialogs.blueski import postDialogs
dlg = postDialogs.Post(caption=_("Reply"), text=initial_text)
if dlg.ShowModal() == wx.ID_OK:
text, files, cw, langs = dlg.get_payload()
if not text and not files:
dlg.Destroy()
return
def do_send():
try:
uri_resp = self.session.send_message(
message=text,
files=files,
reply_to=uri,
reply_to_cid=reply_cid,
cw_text=cw,
langs=langs
)
if uri_resp:
wx.CallAfter(self.session.sound.play, "reply_send.ogg")
wx.CallAfter(output.speak, _("Reply sent."))
if getattr(self, "type", "") == "conversation":
try:
wx.CallAfter(self.start_stream, True, False)
except Exception:
pass
else:
wx.CallAfter(output.speak, _("Failed to send reply."), True)
except Exception:
log.exception("Error sending Bluesky reply")
wx.CallAfter(output.speak, _("An error occurred while replying."), True)
call_threaded(do_send)
dlg.Destroy()
dlg = blueski_messages.post(session=self.session, title=_("Reply"), caption=_("Reply"), text=initial_text)
if dlg.message.ShowModal() == wx.ID_OK:
text, files, cw, langs = dlg.get_data()
refresh_args = (True, False) if getattr(self, "type", "") == "conversation" else None
self._send_post_async(
text=text,
files=files,
cw_text=cw,
langs=langs,
reply_to=uri,
reply_to_cid=reply_cid,
success_message=_("Reply sent."),
error_message=_("An error occurred while replying."),
sound="reply_send.ogg",
refresh_args=refresh_args,
)
dlg.message.Destroy()
def _send_post_async(
self,
*,
text,
files,
cw_text,
langs,
reply_to=None,
reply_to_cid=None,
success_message="",
error_message="",
sound=None,
refresh_args=None,
):
if not text and not files:
return
def do_send():
try:
uri_resp = self.session.send_message(
message=text,
files=files,
reply_to=reply_to,
reply_to_cid=reply_to_cid,
cw_text=cw_text,
langs=langs,
)
if uri_resp:
if sound:
wx.CallAfter(self.session.sound.play, sound)
if success_message:
wx.CallAfter(output.speak, success_message)
if refresh_args and hasattr(self, "start_stream"):
try:
wx.CallAfter(self.start_stream, *refresh_args)
except Exception:
pass
else:
wx.CallAfter(output.speak, _("Failed to send post."), True)
except Exception:
log.exception("Error sending Bluesky post")
if error_message:
wx.CallAfter(output.speak, error_message, True)
else:
wx.CallAfter(output.speak, _("An error occurred while posting."), True)
call_threaded(do_send)
def on_repost(self, evt):
self.share_item(confirm=True)

View File

@@ -6,3 +6,4 @@ from .users import UserBuffer
from .notifications import NotificationsBuffer
from .search import SearchBuffer
from .community import CommunityBuffer
from .announcements import AnnouncementsBuffer

View File

@@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
import time
import logging
import arrow
import widgetUtils
import wx
import output
import languageHandler
import config
from pubsub import pub
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import compose, templates
from wxUI import buffers
from wxUI.dialogs.mastodon import menus
from mysc.thread_utils import call_threaded
log = logging.getLogger("controller.buffers.mastodon.announcements")
class AnnouncementsBuffer(BaseBuffer):
def __init__(self, *args, **kwargs):
# We enforce compose_func="compose_announcement"
kwargs["compose_func"] = "compose_announcement"
super(AnnouncementsBuffer, self).__init__(*args, **kwargs)
self.type = "announcementsBuffer"
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.announcementsPanel(parent, name)
def get_buffer_name(self):
return _("Announcements")
def bind_events(self):
self.buffer.set_focus_function(self.onFocus)
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.dismiss_announcement, self.buffer.dismiss)
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)
def dismiss_announcement(self, event=None, item=None, *args, **kwargs):
index = self.buffer.list.get_selected()
if index == -1: return
item = self.session.db[self.name][index]
# Optimistic UI update or wait for API? Let's wait for API to be safe, but run threaded.
# We need a custom call because 'announcements_dismiss' returns None on success usually.
def _do_dismiss():
try:
self.session.api_call(call_name="announcements_dismiss", id=str(item.id))
# If success, update UI in main thread
wx.CallAfter(self._on_dismiss_success, index)
except Exception as e:
log.exception("Error dismissing announcement")
self.session.sound.play("error.ogg")
call_threaded(_do_dismiss)
def _on_dismiss_success(self, index):
if index < len(self.session.db[self.name]):
self.session.db[self.name].pop(index)
self.buffer.list.remove_item(index)
output.speak(_("Announcement dismissed."))
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
# Create a simple menu
menu = wx.Menu()
dismiss_item = menu.Append(wx.ID_ANY, _("Dismiss"))
copy_item = menu.Append(wx.ID_ANY, _("Copy text"))
self.buffer.Bind(wx.EVT_MENU, self.dismiss_announcement, dismiss_item)
self.buffer.Bind(wx.EVT_MENU, self.copy, copy_item)
if pos != 0:
self.buffer.PopupMenu(menu, pos)
else:
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())
def url(self, *args, **kwargs):
self.dismiss_announcement()
def get_more_items(self): output.speak(_("This buffer does not support loading more items."), True)
# Disable social interactions not applicable to announcements
def reply(self, *args, **kwargs):
pass
def share_item(self, *args, **kwargs):
pass
def toggle_favorite(self, *args, **kwargs):
pass
def add_to_favorites(self, *args, **kwargs):
pass
def remove_from_favorites(self, *args, **kwargs):
pass
def toggle_bookmark(self, *args, **kwargs):
pass
def mute_conversation(self, *args, **kwargs):
pass
def vote(self, *args, **kwargs):
pass
def send_message(self, *args, **kwargs):
pass
def user_details(self, *args, **kwargs):
pass
def view_item(self, *args, **kwargs):
# We could implement a specific viewer for announcements if needed,
# but the default one expects a status object structure.
pass
def copy(self, event=None):
item = self.get_item()
if item:
pub.sendMessage("execute-action", action="copy_to_clipboard")
def onFocus(self, *args, **kwargs):
# Similar logic to BaseBuffer but adapted if needed.
# BaseBuffer.onFocus handles reading long posts.
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
def get_message(self):
# Override to use announcement template
announcement = self.get_item()
if announcement == None:
return
template = self.session.settings.get("templates", {}).get("announcement", templates.announcement_default_template)
t = templates.render_announcement(announcement, template, self.session.settings, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 300 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for announcements buffer")
try:
# The announcements API does not accept min_id or limit parameters
results = self.session.api.announcements()
# Reverse the list so order_buffer processes them according to user preference
results.reverse()
except Exception as e:
log.exception("Error retrieving announcements: %s" % (str(e)))
return 0
# order_buffer handles duplication filtering by ID internally
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of new announcements retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and play_sound == True and self.sound != None and self.session.settings["sound"]["session_mute"] == False:
self.session.sound.play(self.sound)
return number_of_items
return 0

View File

@@ -126,14 +126,16 @@ class BaseBuffer(base.Buffer):
min_id = None
# toDo: Implement reverse timelines properly here.
if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
if self.session.settings["general"]["reverse_timelines"]:
min_id = self.session.db[self.name][0].id
else:
min_id = self.session.db[self.name][-1].id
# We use the maximum ID present in the buffer to ensure we only request posts
# that are newer than our most recent chronological post.
# This prevents old pinned posts from pulling in hundreds of previous statuses.
min_id = max(item.id for item in self.session.db[self.name])
# loads pinned posts from user accounts.
# Load those posts only when there are no items previously loaded.
if "-timeline" in self.name and "account_statuses" in self.function and len(self.session.db.get(self.name, [])) == 0:
pinned_posts = self.session.api.account_statuses(pinned=True, limit=count, *self.args, **self.kwargs)
for p in pinned_posts:
p["pinned"] = True
pinned_posts.reverse()
else:
pinned_posts = None
@@ -182,10 +184,17 @@ class BaseBuffer(base.Buffer):
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
if len(self.session.db[self.name]) == 0:
return
# We use the minimum ID in the buffer to correctly request the next page of older items.
# This prevents old pinned posts from causing us to skip chronological posts.
# We try to exclude pinned posts from this calculation as they are usually outliers at the top.
unpinned_ids = [item.id for item in self.session.db[self.name] if not getattr(item, "pinned", False)]
if unpinned_ids:
max_id = min(unpinned_ids)
else:
max_id = self.session.db[self.name][-1].id
max_id = min(item.id for item in self.session.db[self.name])
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:
@@ -311,8 +320,10 @@ class BaseBuffer(base.Buffer):
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.quote, menuitem=menu.quote)
else:
menu.boost.Enable(False)
menu.quote.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.mute_conversation, menuitem=menu.mute)
@@ -442,11 +453,30 @@ class BaseBuffer(base.Buffer):
id = item.id
if self.session.settings["general"]["boost_mode"] == "ask":
answer = mastodon_dialogs.boost_question()
if answer == True:
if answer == 1:
self._direct_boost(id)
elif answer == 2:
self.quote(item=item)
else:
self._direct_boost(id)
def quote(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if self.can_share(item=item) == False:
return output.speak(_("This action is not supported on conversations."))
title = _("Quote post")
caption = _("Write your comment here")
post = messages.post(session=self.session, title=title, caption=caption)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, quote_id=item.id, posts=post_data, visibility=post.get_visibility(), language=post.get_language(), **kwargs)
if hasattr(post.message, "destroy"):
post.message.destroy()
def _direct_boost(self, id):
item = self.session.api_call(call_name="status_reblog", _sound="retweet_send.ogg", id=id)

View File

@@ -33,10 +33,7 @@ class CommunityBuffer(base.BaseBuffer):
min_id = None
# toDo: Implement reverse timelines properly here.
if self.name in self.session.db and len(self.session.db[self.name]) > 0:
if self.session.settings["general"]["reverse_timelines"]:
min_id = self.session.db[self.name][0].id
else:
min_id = self.session.db[self.name][-1].id
min_id = max(item.id for item in self.session.db[self.name])
try:
results = self.community_api.timeline(timeline=self.timeline, min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
@@ -55,10 +52,15 @@ class CommunityBuffer(base.BaseBuffer):
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
if len(self.session.db[self.name]) == 0:
return
unpinned_ids = [item.id for item in self.session.db[self.name] if not getattr(item, "pinned", False)]
if unpinned_ids:
max_id = min(unpinned_ids)
else:
max_id = self.session.db[self.name][-1].id
max_id = min(item.id for item in self.session.db[self.name])
try:
items = self.community_api.timeline(timeline=self.timeline, max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:

View File

@@ -42,10 +42,22 @@ class MentionsBuffer(BaseBuffer):
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
else:
max_id = self.session.db[self.name][-1].id
if len(self.session.db[self.name]) == 0:
return
# In mentions buffer, items are notification objects which don't have 'pinned' attribute directly.
# But we check the status attached to the notification if it exists.
# However, notifications are strictly chronological usually. Pinned mentions don't exist?
# But let's stick to the safe ID extraction.
# The logic here is tricky because self.session.db stores notification objects, but sometimes just dicts?
# Let's assume they are objects with 'id' attribute.
# Notifications don't have 'pinned', so we just take the min ID.
# But wait, did I change this file previously to use min()? Yes.
# Is there any case where a notification ID is "pinned" (old)? No.
# So min() should be fine here. But for consistency with other buffers if any weird logic exists...
# Actually, let's keep min() as notifications don't support pinning.
max_id = min(item.id for item in self.session.db[self.name])
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], types=["mention"], *self.args, **self.kwargs)
except Exception as e:

View File

@@ -33,10 +33,7 @@ class SearchBuffer(BaseBuffer):
self.execution_time = current_time
min_id = None
if self.name in self.session.db and len(self.session.db[self.name]) > 0:
if self.session.settings["general"]["reverse_timelines"]:
min_id = self.session.db[self.name][0].id
else:
min_id = self.session.db[self.name][-1].id
min_id = max(item.id for item in self.session.db[self.name])
try:
results = getattr(self.session.api, self.function)(min_id=min_id, **self.kwargs)
except Exception as mess:

View File

@@ -35,6 +35,7 @@ class Handler(object):
compose=_("&Post"),
reply=_("Re&ply"),
share=_("&Boost"),
quote=_("&Quote"),
fav=_("&Add to favorites"),
unfav=_("Remove from favorites"),
view=_("&Show post"),
@@ -92,6 +93,8 @@ class Handler(object):
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="blocks", name="blocked", sessionObject=session, account=name))
elif i == 'notifications':
pub.sendMessage("createBuffer", buffer_type="NotificationsBuffer", session_type=session.type, buffer_title=_("Notifications"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_notification", function="notifications", name="notifications", sessionObject=session, account=name))
elif i == 'announcements':
pub.sendMessage("createBuffer", buffer_type="AnnouncementsBuffer", session_type=session.type, buffer_title=_("Announcements"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="announcements", name="announcements", sessionObject=session, account=name, sound="new_event.ogg"))
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)
for i in session.settings["other_buffers"]["timelines"]:

View File

@@ -10,7 +10,7 @@ import languageHandler
from twitter_text import parse_tweet, config
from mastodon import MastodonError
from controller import messages
from sessions.mastodon import templates
from sessions.mastodon import templates, utils
from wxUI.dialogs.mastodon import postDialogs
from extra.autocompletionUsers import completion
from . import userList
@@ -282,10 +282,7 @@ class editPost(post):
# Extract text from post
if item.reblog != None:
item = item.reblog
text = item.content
# Remove HTML tags from content
import re
text = re.sub('<[^<]+?>', '', text)
text = utils.html_filter(item.content)
# Initialize parent class
super(editPost, self).__init__(session, title, caption, text=text, *args, **kwargs)
# Store the post ID for editing

View File

@@ -52,10 +52,12 @@ class accountSettingsController(globalSettingsController):
post_template = self.config["templates"]["post"]
conversation_template = self.config["templates"]["conversation"]
person_template = self.config["templates"]["person"]
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template)
announcement_template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template, announcement_template=announcement_template)
widgetUtils.connect_event(self.dialog.templates.post, widgetUtils.BUTTON_PRESSED, self.edit_post_template)
widgetUtils.connect_event(self.dialog.templates.conversation, widgetUtils.BUTTON_PRESSED, self.edit_conversation_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
widgetUtils.connect_event(self.dialog.templates.announcement, widgetUtils.BUTTON_PRESSED, self.edit_announcement_template)
self.dialog.create_other_buffers()
buffer_values = self.get_buffers_list()
self.dialog.buffers.insert_buffers(buffer_values)
@@ -109,6 +111,15 @@ class accountSettingsController(globalSettingsController):
self.config.write()
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
def edit_announcement_template(self, *args, **kwargs):
template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
control = EditTemplate(template=template, type="announcement")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["announcement"] = result
self.config.write()
self.dialog.templates.announcement.SetLabel(_("Edit template for announcements. Current template: {}").format(result))
def save_configuration(self):
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
self.needs_restart = True
@@ -204,6 +215,7 @@ class accountSettingsController(globalSettingsController):
all_buffers['blocked']=_("Blocked users")
all_buffers['muted']=_("Muted users")
all_buffers['notifications']=_("Notifications")
all_buffers['announcements']=_("Announcements")
list_buffers = []
hidden_buffers=[]
all_buffers_keys = list(all_buffers.keys())

View File

@@ -2,7 +2,7 @@
import re
import wx
from typing import List
from sessions.mastodon.templates import post_variables, conversation_variables, person_variables
from sessions.mastodon.templates import post_variables, conversation_variables, person_variables, announcement_variables
from wxUI.dialogs import templateDialogs
class EditTemplate(object):
@@ -13,6 +13,8 @@ class EditTemplate(object):
self.variables = post_variables
elif type == "conversation":
self.variables = conversation_variables
elif type == "announcement":
self.variables = announcement_variables
else:
self.variables = person_variables
self.template: str = template

View File

@@ -23,7 +23,6 @@ url = string(default="control+win+b")
go_home = string(default="control+win+home")
go_end = string(default="control+win+end")
delete = string(default="control+win+delete")
edit_post = string(default="")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
@@ -38,3 +37,12 @@ ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -33,7 +33,6 @@ go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="control+win+shift+p")
delete = string(default="control+win+delete")
edit_post = string(default="")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
@@ -57,3 +56,12 @@ ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -33,7 +33,6 @@ go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="alt+win+delete")
edit_post = string(default="")
clear_buffer = string(default="alt+win+shift+delete")
repeat_item = string(default="alt+win+space")
copy_to_clipboard = string(default="alt+win+shift+c")
@@ -60,3 +59,12 @@ add_alias=string(default="")
mute_conversation=string(default="control+alt+win+back")
find = string(default="control+win+{")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -33,7 +33,6 @@ go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="alt+win+delete")
edit_post = string(default="")
clear_buffer = string(default="alt+win+shift+delete")
repeat_item = string(default="control+alt+win+space")
copy_to_clipboard = string(default="alt+win+shift+c")
@@ -60,3 +59,12 @@ add_alias=string(default="")
mute_conversation=string(default="control+alt+win+back")
find = string(default="control+win+{")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -59,3 +59,12 @@ open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
mute_conversation=string(default="alt+win+shift+delete")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -34,7 +34,6 @@ go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="control+win+delete")
edit_post = string(default="")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
@@ -61,3 +60,12 @@ open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
mute_conversation=string(default="alt+win+shift+delete")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -29,7 +29,7 @@ actions = {
"go_end": _(u"Jump to the last element of the current buffer"),
"go_page_up": _(u"Jump 20 elements up in the current buffer"),
"go_page_down": _(u"Jump 20 elements down in the current buffer"),
# "update_profile": _(u"Edit profile"),
"update_profile": _(u"Edit profile"),
"delete": _("Delete post"),
"clear_buffer": _(u"Empty the current buffer"),
"repeat_item": _(u"Repeat last item"),
@@ -55,4 +55,14 @@ actions = {
"ocr_image": _(u"Extracts the text from a picture and displays the result in a dialog."),
"add_alias": _("Adds an alias to an user"),
"mute_conversation": _("Mute/Unmute conversation"),
"edit_post": _(u"Edit the selected post"),
"vote": _(u"Vote in the selected poll"),
"open_favs_timeline": _(u"Open favorites timeline"),
"community_timeline": _(u"Open local/federated timeline"),
"seekLeft": _(u"Seek media backward"),
"seekRight": _(u"Seek media forward"),
"manage_aliases": _(u"Manage user aliases"),
"create_filter": _(u"Create a new filter"),
"manage_filters": _(u"Manage filters"),
"manage_accounts": _(u"Manage accounts"),
}

View File

@@ -14,7 +14,7 @@ persist_size = integer(default=0)
load_cache_in_memory=boolean(default=True)
show_screen_names = boolean(default=False)
hide_emojis = boolean(default=False)
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications'))
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications', 'announcements'))
boost_mode = string(default="ask")
disable_streaming = boolean(default=False)
@@ -54,6 +54,7 @@ post = string(default="$display_name, $safe_text $image_descriptions $date. $vis
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
conversation = string(default="Conversation with $users. Last message: $last_post")
notification = string(default="$display_name $text, $date")
announcement = string(default="$text. Published $published_at. $read")
[filters]

View File

@@ -85,3 +85,9 @@ def compose_notification(notification, db, settings, relative_times, show_screen
if filtered != None:
text = _("hidden by filter {}").format(filtered)
return [user, text, ts]
def compose_announcement(announcement, db, settings, relative_times, show_screen_names, safe=False):
# Use the default template or a configured one if available
template = settings.get("templates", {}).get("announcement", templates.announcement_default_template)
text = templates.render_announcement(announcement, template, settings, relative_times, db["utc_offset"])
return [text]

View File

@@ -217,38 +217,69 @@ class Session(base.baseSession):
self.sound.play(_sound)
return val
def send_post(self, reply_to=None, visibility=None, language=None, posts=[]):
def _send_quote_post(self, text, quote_id, visibility, sensitive, spoiler_text, language, scheduled_at, in_reply_to_id=None, media_ids=[], poll=None):
"""Internal helper to send a quote post using direct API call."""
params = {
'status': text,
'visibility': visibility,
'quoted_status_id': quote_id,
}
if in_reply_to_id:
params['in_reply_to_id'] = in_reply_to_id
if sensitive:
params['sensitive'] = sensitive
if spoiler_text:
params['spoiler_text'] = spoiler_text
if language:
params['language'] = language
if scheduled_at:
if hasattr(scheduled_at, 'isoformat'):
params['scheduled_at'] = scheduled_at.isoformat()
else:
params['scheduled_at'] = scheduled_at
if media_ids:
params['media_ids'] = media_ids
if poll:
params['poll'] = poll
# Use the internal API request method directly
return self.api._Mastodon__api_request('POST', '/api/v1/statuses', params)
def send_post(self, reply_to=None, quote_id=None, visibility=None, language=None, posts=[]):
""" Convenience function to send a thread. """
in_reply_to_id = reply_to
for obj in posts:
text = obj.get("text")
scheduled_at = obj.get("scheduled_at")
if len(obj["attachments"]) == 0:
# Prepare media and polls first as they are needed for both standard and quote posts
media_ids = []
poll = None
if len(obj["attachments"]) > 0:
try:
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, visibility=visibility, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language, scheduled_at=scheduled_at)
# If it fails, let's basically send an event with all passed info so we will catch it later.
except Exception as e:
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
return
if item != None:
in_reply_to_id = item["id"]
else:
media_ids = []
try:
poll = None
if len(obj["attachments"]) == 1 and obj["attachments"][0]["type"] == "poll":
poll = self.api.make_poll(options=obj["attachments"][0]["options"], expires_in=obj["attachments"][0]["expires_in"], multiple=obj["attachments"][0]["multiple"], hide_totals=obj["attachments"][0]["hide_totals"])
else:
for i in obj["attachments"]:
media = self.api_call("media_post", media_file=i["file"], description=i["description"], synchronous=True)
media_ids.append(media.id)
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language, scheduled_at=scheduled_at)
if item != None:
in_reply_to_id = item["id"]
except Exception as e:
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, language=language)
return
try:
if quote_id:
item = self._send_quote_post(text, quote_id, visibility, obj["sensitive"], obj["spoiler_text"], language, scheduled_at, in_reply_to_id, media_ids, poll)
self.sound.play("tweet_send.ogg")
else:
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language, scheduled_at=scheduled_at)
if item != None:
in_reply_to_id = item["id"]
except Exception as e:
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, language=language)
return
def edit_post(self, post_id, posts=[]):
""" Convenience function to edit a post. Only the first item in posts list is used as threads cannot be edited.

View File

@@ -13,12 +13,14 @@ post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe
person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"]
conversation_variables = ["users", "last_post"]
notification_variables = ["display_name", "screen_name", "text", "date"]
announcement_variables = ["text", "published_at", "updated_at", "starts_at", "ends_at", "read"]
# Default, translatable templates.
post_default_template = _("$display_name, $text $image_descriptions $date. $source")
dm_sent_default_template = _("Dm to $recipient_display_name, $text $date")
person_default_template = _("$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
notification_default_template = _("$display_name $text, $date")
announcement_default_template = _("$text. Published $published_at. $read")
def process_date(field, relative_times=True, offset_hours=0):
original_date = arrow.get(field)
@@ -185,3 +187,23 @@ def render_notification(notification, template, post_template, settings, relativ
result = Template(_(template)).safe_substitute(**available_data)
result = result.replace(" . ", "")
return result
def render_announcement(announcement, template, settings, relative_times=False, offset_hours=0):
""" Renders any given announcement according to the passed template. """
global announcement_variables
available_data = dict()
# Process dates
for date_field in ["published_at", "updated_at", "starts_at", "ends_at"]:
if hasattr(announcement, date_field) and getattr(announcement, date_field) is not None:
available_data[date_field] = process_date(getattr(announcement, date_field), relative_times, offset_hours)
else:
available_data[date_field] = ""
available_data["text"] = utils.html_filter(announcement.content)
if announcement.read:
available_data["read"] = _("Read")
else:
available_data["read"] = _("Unread")
result = Template(_(template)).safe_substitute(**available_data)
return result

View File

@@ -3,7 +3,7 @@ import sys
import application
import platform
import os
from cx_Freeze import setup, Executable, winmsvcr
from cx_Freeze import setup, Executable
from requests import certs
def get_architecture_files():
@@ -34,7 +34,7 @@ def find_accessible_output2_datafiles():
base = None
if sys.platform == 'win32':
base = 'Win32GUI'
base = 'GUI'
build_exe_options = dict(
build_exe="dist",
@@ -51,8 +51,6 @@ executables = [
Executable('main.py', base=base, target_name="twblue")
]
winmsvcr.FILES = ()
winmsvcr.FILES_TO_DUPLICATE = ()
setup(name=application.name,
version=application.version,
description=application.description,

View File

@@ -3,3 +3,4 @@ from .base import basePanel
from .conversationList import conversationListPanel
from .notifications import notificationsPanel
from .user import userPanel
from .announcements import announcementsPanel

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import wx
from multiplatform_widgets import widgets
class announcementsPanel(wx.Panel):
def set_focus_function(self, f):
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, f)
def create_list(self):
self.list = widgets.list(self, _("Announcement"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
self.list.set_windows_size(0, 800)
self.list.set_size()
def __init__(self, parent, name):
super(announcementsPanel, self).__init__(parent)
self.name = name
self.type = "baseBuffer"
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.create_list()
self.dismiss = wx.Button(self, -1, _("Dismiss"))
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
self.SetSizer(self.sizer)
self.SetClientSize(self.sizer.CalcMin())
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):
self.list.list.SetFocus()

View File

@@ -47,7 +47,7 @@ class generalAccount(wx.Panel, baseDialog.BaseWXDialog):
self.SetSizer(sizer)
class templates(wx.Panel, baseDialog.BaseWXDialog):
def __init__(self, parent, post_template, conversation_template, person_template):
def __init__(self, parent, post_template, conversation_template, person_template, announcement_template):
super(templates, self).__init__(parent)
sizer = wx.BoxSizer(wx.VERTICAL)
self.post = wx.Button(self, wx.ID_ANY, _("Edit template for &posts. Current template: {}").format(post_template))
@@ -56,6 +56,8 @@ class templates(wx.Panel, baseDialog.BaseWXDialog):
sizer.Add(self.conversation, 0, wx.ALL, 5)
self.person = wx.Button(self, wx.ID_ANY, _("Edit template for p&ersons. Current template: {}").format(person_template))
sizer.Add(self.person, 0, wx.ALL, 5)
self.announcement = wx.Button(self, wx.ID_ANY, _("Edit template for &announcements. Current template: {}").format(announcement_template))
sizer.Add(self.announcement, 0, wx.ALL, 5)
self.SetSizer(sizer)
class sound(wx.Panel):
@@ -152,8 +154,8 @@ class configurationDialog(baseDialog.BaseWXDialog):
self.buffers = other_buffers(self.notebook)
self.notebook.AddPage(self.buffers, _(u"Buffers"))
def create_templates(self, post_template, conversation_template, person_template):
self.templates = templates(self.notebook, post_template=post_template, conversation_template=conversation_template, person_template=person_template)
def create_templates(self, post_template, conversation_template, person_template, announcement_template):
self.templates = templates(self.notebook, post_template=post_template, conversation_template=conversation_template, person_template=person_template, announcement_template=announcement_template)
self.notebook.AddPage(self.templates, _("Templates"))
def create_sound(self, output_devices, input_devices, soundpacks):

View File

@@ -2,11 +2,43 @@
import wx
import application
class BoostDialog(wx.Dialog):
def __init__(self):
super(BoostDialog, self).__init__(None, title=_("Boost"))
p = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
lbl = wx.StaticText(p, wx.ID_ANY, _("What would you like to do with this post?"))
sizer.Add(lbl, 0, wx.ALL, 10)
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.btn_boost = wx.Button(p, wx.ID_ANY, _("Boost"))
self.btn_quote = wx.Button(p, wx.ID_ANY, _("Quote"))
self.btn_cancel = wx.Button(p, wx.ID_CANCEL, _("Cancel"))
btn_sizer.Add(self.btn_boost, 0, wx.ALL, 5)
btn_sizer.Add(self.btn_quote, 0, wx.ALL, 5)
btn_sizer.Add(self.btn_cancel, 0, wx.ALL, 5)
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
p.SetSizer(sizer)
sizer.Fit(self)
self.btn_boost.Bind(wx.EVT_BUTTON, self.on_boost)
self.btn_quote.Bind(wx.EVT_BUTTON, self.on_quote)
self.result = 0
def on_boost(self, event):
self.result = 1
self.EndModal(wx.ID_OK)
def on_quote(self, event):
self.result = 2
self.EndModal(wx.ID_OK)
def boost_question():
result = False
dlg = wx.MessageDialog(None, _("Would you like to share this post?"), _("Boost"), wx.YES_NO|wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_YES:
result = True
dlg = BoostDialog()
dlg.ShowModal()
result = dlg.result
dlg.Destroy()
return result

View File

@@ -6,6 +6,8 @@ class base(wx.Menu):
super(base, self).__init__()
self.boost = wx.MenuItem(self, wx.ID_ANY, _("&Boost"))
self.Append(self.boost)
self.quote = wx.MenuItem(self, wx.ID_ANY, _("&Quote"))
self.Append(self.quote)
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
self.Append(self.reply)
self.edit = wx.MenuItem(self, wx.ID_ANY, _(u"&Edit"))
@@ -38,6 +40,8 @@ class notification(wx.Menu):
if item in valid_types:
self.boost = wx.MenuItem(self, wx.ID_ANY, _("&Boost"))
self.Append(self.boost)
self.quote = wx.MenuItem(self, wx.ID_ANY, _("&Quote"))
self.Append(self.quote)
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
self.Append(self.reply)
self.edit = wx.MenuItem(self, wx.ID_ANY, _(u"&Edit"))