From bbef9d988b1056e2d9f20d339b9a16a7268869c9 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 8 Mar 2025 20:50:43 -0600 Subject: [PATCH 01/25] Fixed a string --- src/wxUI/dialogs/mastodon/filters/create_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wxUI/dialogs/mastodon/filters/create_filter.py b/src/wxUI/dialogs/mastodon/filters/create_filter.py index 8329b16c..a0d770b5 100644 --- a/src/wxUI/dialogs/mastodon/filters/create_filter.py +++ b/src/wxUI/dialogs/mastodon/filters/create_filter.py @@ -31,7 +31,7 @@ class FilterKeywordPanel(wx.Panel): button_sizer.Add(self.add_button, 0, wx.RIGHT, 5) button_sizer.Add(self.remove_button, 0) main_sizer = wx.BoxSizer(wx.VERTICAL) - main_sizer.Add(wx.StaticText(self, label=_("Palabras clave a filtrar:")), 0, wx.BOTTOM, 5) + main_sizer.Add(wx.StaticText(self, label=_("Keywords to filter:")), 0, wx.BOTTOM, 5) main_sizer.Add(list_panel, 1, wx.EXPAND | wx.BOTTOM, 5) main_sizer.Add(input_sizer, 0, wx.EXPAND | wx.BOTTOM, 5) main_sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT) From ccaa0d98ff3a577d0efc0e01e5753f019551a3ff Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 11 Jan 2026 00:37:59 -0600 Subject: [PATCH 02/25] fix: make buffers visible on screen. Maximize window and apply visual fixes. Closes #886 --- src/wxUI/buffers/mastodon/base.py | 10 +++++----- src/wxUI/buffers/mastodon/conversationList.py | 10 +++++----- src/wxUI/buffers/mastodon/notifications.py | 6 +++--- src/wxUI/buffers/mastodon/user.py | 4 ++-- src/wxUI/view.py | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/wxUI/buffers/mastodon/base.py b/src/wxUI/buffers/mastodon/base.py index c3aacc70..9352a57a 100644 --- a/src/wxUI/buffers/mastodon/base.py +++ b/src/wxUI/buffers/mastodon/base.py @@ -9,10 +9,10 @@ class basePanel(wx.Panel): def create_list(self): self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES) - self.list.set_windows_size(0, 60) - self.list.set_windows_size(1, 320) - self.list.set_windows_size(2, 110) - self.list.set_windows_size(3, 84) + self.list.set_windows_size(0, 200) + self.list.set_windows_size(1, 600) + self.list.set_windows_size(2, 200) + self.list.set_windows_size(3, 200) self.list.set_size() def __init__(self, parent, name): @@ -35,7 +35,7 @@ class basePanel(wx.Panel): btnSizer.Add(self.bookmark, 0, wx.ALL, 5) btnSizer.Add(self.dm, 0, wx.ALL, 5) self.sizer.Add(btnSizer, 0, wx.ALL, 5) - self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5) + self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5) self.SetSizer(self.sizer) self.SetClientSize(self.sizer.CalcMin()) diff --git a/src/wxUI/buffers/mastodon/conversationList.py b/src/wxUI/buffers/mastodon/conversationList.py index 75318bfc..2405bf29 100644 --- a/src/wxUI/buffers/mastodon/conversationList.py +++ b/src/wxUI/buffers/mastodon/conversationList.py @@ -9,10 +9,10 @@ class conversationListPanel(wx.Panel): def create_list(self): self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES) - self.list.set_windows_size(0, 60) - self.list.set_windows_size(1, 320) - self.list.set_windows_size(2, 110) - self.list.set_windows_size(3, 84) + self.list.set_windows_size(0, 200) + self.list.set_windows_size(1, 600) + self.list.set_windows_size(2, 200) + self.list.set_windows_size(3, 200) self.list.set_size() def __init__(self, parent, name): @@ -27,7 +27,7 @@ class conversationListPanel(wx.Panel): btnSizer.Add(self.post, 0, wx.ALL, 5) btnSizer.Add(self.reply, 0, wx.ALL, 5) self.sizer.Add(btnSizer, 0, wx.ALL, 5) - self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5) + self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5) self.SetSizer(self.sizer) self.SetClientSize(self.sizer.CalcMin()) diff --git a/src/wxUI/buffers/mastodon/notifications.py b/src/wxUI/buffers/mastodon/notifications.py index 1e8e2ddb..f19e211a 100644 --- a/src/wxUI/buffers/mastodon/notifications.py +++ b/src/wxUI/buffers/mastodon/notifications.py @@ -9,8 +9,8 @@ class notificationsPanel(wx.Panel): def create_list(self): self.list = widgets.list(self, _("Text"), _("Date"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES) - self.list.set_windows_size(0, 320) - self.list.set_windows_size(2, 110) + self.list.set_windows_size(0, 600) + self.list.set_windows_size(1, 200) self.list.set_size() def __init__(self, parent, name): @@ -25,7 +25,7 @@ class notificationsPanel(wx.Panel): btnSizer.Add(self.post, 0, wx.ALL, 5) btnSizer.Add(self.dismiss, 0, wx.ALL, 5) self.sizer.Add(btnSizer, 0, wx.ALL, 5) - self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5) + self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5) self.SetSizer(self.sizer) self.SetClientSize(self.sizer.CalcMin()) diff --git a/src/wxUI/buffers/mastodon/user.py b/src/wxUI/buffers/mastodon/user.py index cb3f5038..86958c65 100644 --- a/src/wxUI/buffers/mastodon/user.py +++ b/src/wxUI/buffers/mastodon/user.py @@ -6,7 +6,7 @@ class userPanel(wx.Panel): def create_list(self): self.list = widgets.list(self, _("User"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES) - self.list.set_windows_size(0, 320) + self.list.set_windows_size(0, 600) self.list.set_size() def __init__(self, parent, name): @@ -23,7 +23,7 @@ class userPanel(wx.Panel): btnSizer.Add(self.actions, 0, wx.ALL, 5) btnSizer.Add(self.message, 0, wx.ALL, 5) self.sizer.Add(btnSizer, 0, wx.ALL, 5) - self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5) + self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5) self.SetSizer(self.sizer) self.SetClientSize(self.sizer.CalcMin()) diff --git a/src/wxUI/view.py b/src/wxUI/view.py index 25daa1a2..693bfe9b 100644 --- a/src/wxUI/view.py +++ b/src/wxUI/view.py @@ -134,9 +134,9 @@ class mainFrame(wx.Frame): self.buffers[name] = buffer.GetId() def prepare(self): - self.sizer.Add(self.nb, 0, wx.ALL, 5) + self.sizer.Add(self.nb, 1, wx.ALL | wx.EXPAND, 5) self.panel.SetSizer(self.sizer) -# self.Maximize() + self.Maximize() self.sizer.Layout() self.SetClientSize(self.sizer.CalcMin()) # print self.GetSize() From cb0bb4cf27912819f14f16751b1ab85f9852ab82 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 11 Jan 2026 01:22:29 -0600 Subject: [PATCH 03/25] feat: implement mute conversation support for Mastodon This feature allows users to visually hide muted conversations from the Home timeline. It includes: - Automatic filtering of muted posts in Home. - Immediate visual removal of posts when muting a conversation. - New 'mute_conversation' action in context menu and keyboard shortcuts. - Default shortcut: Alt+Win+Shift+Delete - Win10/11 shortcut: Ctrl+Alt+Win+Backspace --- src/controller/buffers/mastodon/base.py | 39 +++++++++++++++++++++++++ src/keymaps/Windows 10.keymap | 1 + src/keymaps/Windows11.keymap | 1 + src/keymaps/base.template | 4 ++- src/keymaps/default.keymap | 1 + src/keystrokeEditor/actions/mastodon.py | 1 + src/sessions/mastodon/utils.py | 5 ++++ src/wxUI/dialogs/mastodon/menus.py | 2 ++ 8 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/controller/buffers/mastodon/base.py b/src/controller/buffers/mastodon/base.py index 2deff612..1595f932 100644 --- a/src/controller/buffers/mastodon/base.py +++ b/src/controller/buffers/mastodon/base.py @@ -40,9 +40,31 @@ class BaseBuffer(base.Buffer): self.buffer.account = account self.bind_events() self.sound = sound + pub.subscribe(self.on_mute_cleanup, "mastodon.mute_cleanup") if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name or "searchterm" in self.name: self.finished_timeline = False + def on_mute_cleanup(self, conversation_id, session_name): + if self.name != "home_timeline": + return + if session_name != self.session.get_name(): + return + items_to_remove = [] + for index, item in enumerate(self.session.db[self.name]): + c_id = None + if hasattr(item, "conversation_id"): + c_id = item.conversation_id + elif isinstance(item, dict): + c_id = item.get("conversation_id") + + if c_id == conversation_id: + items_to_remove.append(index) + + items_to_remove.sort(reverse=True) + for index in items_to_remove: + self.session.db[self.name].pop(index) + self.buffer.list.remove_item(index) + def create_buffer(self, parent, name): self.buffer = buffers.mastodon.basePanel(parent, name) @@ -293,6 +315,7 @@ class BaseBuffer(base.Buffer): menu.boost.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) widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl) widgetUtils.connect_event(menu, widgetUtils.MENU, self.audio, menuitem=menu.play) widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view) @@ -612,6 +635,22 @@ class BaseBuffer(base.Buffer): else: call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id) + def mute_conversation(self, event=None, item=None, *args, **kwargs): + if item == None: + item = self.get_item() + if item.reblog != None: + item = item.reblog + try: + item = self.session.api.status(item.id) + except MastodonNotFoundError: + output.speak(_("No status found with that ID")) + return + if item.muted == False: + call_threaded(self.session.api_call, call_name="status_mute", preexec_message=_("Muting conversation..."), _sound="favourite.ogg", id=item.id) + pub.sendMessage("mastodon.mute_cleanup", conversation_id=item.conversation_id, session_name=self.session.get_name()) + else: + call_threaded(self.session.api_call, call_name="status_unmute", preexec_message=_("Unmuting conversation..."), _sound="favourite.ogg", id=item.id) + def view_item(self, item=None): if item == None: item = self.get_item() diff --git a/src/keymaps/Windows 10.keymap b/src/keymaps/Windows 10.keymap index 426c598f..c12d78ee 100644 --- a/src/keymaps/Windows 10.keymap +++ b/src/keymaps/Windows 10.keymap @@ -57,5 +57,6 @@ update_buffer = string(default="control+alt+shift+u") ocr_image = string(default="win+alt+o") open_in_browser = string(default="alt+control+win+return") add_alias=string(default="") +mute_conversation=string(default="control+alt+win+back") find = string(default="control+win+{") vote=string(default="alt+win+shift+v") \ No newline at end of file diff --git a/src/keymaps/Windows11.keymap b/src/keymaps/Windows11.keymap index 9ed24488..c4301cff 100644 --- a/src/keymaps/Windows11.keymap +++ b/src/keymaps/Windows11.keymap @@ -57,5 +57,6 @@ update_buffer = string(default="control+alt+shift+u") ocr_image = string(default="win+alt+o") open_in_browser = string(default="alt+control+win+return") add_alias=string(default="") +mute_conversation=string(default="control+alt+win+back") find = string(default="control+win+{") vote=string(default="alt+win+shift+v") \ No newline at end of file diff --git a/src/keymaps/base.template b/src/keymaps/base.template index f95ffcc1..15429f06 100644 --- a/src/keymaps/base.template +++ b/src/keymaps/base.template @@ -56,4 +56,6 @@ configuration = string(default="control+win+o") accountConfiguration = string(default="control+win+shift+o") update_buffer = string(default="control+win+shift+u") open_in_browser = string(default="alt+control+win+return") -add_alias=string(default="") \ No newline at end of file +add_alias=string(default="") +mute_conversation=string(default="alt+win+shift+delete") +vote=string(default="alt+win+shift+v") \ No newline at end of file diff --git a/src/keymaps/default.keymap b/src/keymaps/default.keymap index 8e4bf9b2..30e6bcbb 100644 --- a/src/keymaps/default.keymap +++ b/src/keymaps/default.keymap @@ -59,4 +59,5 @@ update_buffer = string(default="control+win+shift+u") ocr_image = string(default="win+alt+o") 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") \ No newline at end of file diff --git a/src/keystrokeEditor/actions/mastodon.py b/src/keystrokeEditor/actions/mastodon.py index 2dea1a9e..3f07f152 100644 --- a/src/keystrokeEditor/actions/mastodon.py +++ b/src/keystrokeEditor/actions/mastodon.py @@ -54,4 +54,5 @@ actions = { "update_buffer": _(u"Updates the buffer and retrieves possible lost items there."), "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"), } \ No newline at end of file diff --git a/src/sessions/mastodon/utils.py b/src/sessions/mastodon/utils.py index 05a6303f..589ee23f 100644 --- a/src/sessions/mastodon/utils.py +++ b/src/sessions/mastodon/utils.py @@ -140,6 +140,11 @@ def evaluate_filters(post: dict, current_context: str) -> str | None: - None if no applicable filters are found, meaning the post should be shown normally. """ filters = post.get("filtered", None) + + # Automatically hide muted conversations from home timeline. + if current_context == "home" and post.get("muted") == True: + return "hide" + if filters == None: return warn_filter_title = None diff --git a/src/wxUI/dialogs/mastodon/menus.py b/src/wxUI/dialogs/mastodon/menus.py index 7947d942..62dd3407 100644 --- a/src/wxUI/dialogs/mastodon/menus.py +++ b/src/wxUI/dialogs/mastodon/menus.py @@ -14,6 +14,8 @@ class base(wx.Menu): self.Append(self.fav) self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites")) self.Append(self.unfav) + self.mute = wx.MenuItem(self, wx.ID_ANY, _(u"Mute/Unmute conversation")) + self.Append(self.mute) self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL")) self.Append(self.openUrl) self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in instance")) From 31bab4cf8a6558a8daa633a1b7fefd3a43121b1a Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 11 Jan 2026 02:49:46 -0600 Subject: [PATCH 04/25] feat(mastodon): Add support for scheduled posts - Added UI controls (checkbox, date/time pickers) to Post dialog - Implemented validation logic (min 5 mins future) - Updated session handler to pass scheduled_at to API --- src/controller/mastodon/messages.py | 7 +++ src/sessions/mastodon/session.py | 5 +- src/wxUI/dialogs/mastodon/postDialogs.py | 66 +++++++++++++++++++++++- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/controller/mastodon/messages.py b/src/controller/mastodon/messages.py index 2500759a..48b2b678 100644 --- a/src/controller/mastodon/messages.py +++ b/src/controller/mastodon/messages.py @@ -65,6 +65,13 @@ class post(messages.basicMessage): postdata = dict(text=text, attachments=attachments, sensitive=self.message.sensitive.GetValue(), spoiler_text=None) if postdata.get("sensitive") == True: postdata.update(spoiler_text=self.message.spoiler.GetValue()) + + # Check for scheduled post + if hasattr(self.message, 'get_scheduled_at'): + scheduled_at = self.message.get_scheduled_at() + if scheduled_at: + postdata['scheduled_at'] = scheduled_at + self.thread.append(postdata) self.attachments = [] if update_gui: diff --git a/src/sessions/mastodon/session.py b/src/sessions/mastodon/session.py index 47cc47aa..658fd3c4 100644 --- a/src/sessions/mastodon/session.py +++ b/src/sessions/mastodon/session.py @@ -222,9 +222,10 @@ class Session(base.baseSession): 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: 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) + 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) @@ -241,7 +242,7 @@ class Session(base.baseSession): 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) + 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: diff --git a/src/wxUI/dialogs/mastodon/postDialogs.py b/src/wxUI/dialogs/mastodon/postDialogs.py index c9051705..c3532ec1 100644 --- a/src/wxUI/dialogs/mastodon/postDialogs.py +++ b/src/wxUI/dialogs/mastodon/postDialogs.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- import wx +import wx.adv +import datetime class Post(wx.Dialog): def __init__(self, caption=_("Post"), text="", languages=[], *args, **kwds): @@ -60,6 +62,28 @@ class Post(wx.Dialog): self.sensitive.SetValue(False) self.sensitive.Bind(wx.EVT_CHECKBOX, self.on_sensitivity_changed) main_sizer.Add(self.sensitive, 0, wx.ALL, 5) + + # Scheduled post section + scheduled_box = wx.BoxSizer(wx.HORIZONTAL) + self.scheduled = wx.CheckBox(self, wx.ID_ANY, _("Schedule &post")) + self.scheduled.SetValue(False) + self.scheduled.Bind(wx.EVT_CHECKBOX, self.on_schedule_changed) + scheduled_box.Add(self.scheduled, 0, wx.ALL, 5) + + # Default to now + 6 minutes to be safe for the 5 minute minimum + future_dt = wx.DateTime.Now() + future_dt.Add(wx.TimeSpan(0, 6, 0, 0)) + + self.date_picker = wx.adv.DatePickerCtrl(self, wx.ID_ANY, dt=future_dt, style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY) + self.date_picker.Enable(False) + scheduled_box.Add(self.date_picker, 0, wx.ALL, 5) + + self.time_picker = wx.adv.TimePickerCtrl(self, wx.ID_ANY, dt=future_dt) + self.time_picker.Enable(False) + scheduled_box.Add(self.time_picker, 0, wx.ALL, 5) + + main_sizer.Add(scheduled_box, 0, wx.ALL, 5) + spoiler_box = wx.BoxSizer(wx.HORIZONTAL) spoiler_label = wx.StaticText(self, wx.ID_ANY, _("Content warning")) self.spoiler = wx.TextCtrl(self, wx.ID_ANY) @@ -80,8 +104,9 @@ class Post(wx.Dialog): text_actions_sizer.Add(self.translate, 0, 0, 0) btn_sizer = wx.StdDialogButtonSizer() main_sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4) - self.send = wx.Button(self, wx.ID_OK, "") + self.send = wx.Button(self, wx.ID_ANY, _("&Send")) self.send.SetDefault() + self.send.Bind(wx.EVT_BUTTON, self.validate_and_send) btn_sizer.AddButton(self.send) self.close = wx.Button(self, wx.ID_CLOSE, "") btn_sizer.AddButton(self.close) @@ -95,13 +120,50 @@ class Post(wx.Dialog): """ Allows to react to certain keyboard events from the text control. """ shift=event.ShiftDown() if event.GetKeyCode() == wx.WXK_RETURN and shift==False and hasattr(self,'send'): - self.EndModal(wx.ID_OK) + self.validate_and_send() else: event.Skip() + def validate_and_send(self, event=None): + scheduled_at = self.get_scheduled_at() + if scheduled_at: + min_time = datetime.datetime.now() + datetime.timedelta(minutes=5) + if scheduled_at < min_time: + wx.MessageDialog(self, + _("Scheduled posts must be set at least 5 minutes in the future. Please adjust the time."), + _("Invalid scheduled time"), + wx.ICON_ERROR | wx.OK).ShowModal() + return + self.EndModal(wx.ID_OK) + def on_sensitivity_changed(self, *args, **kwargs): self.spoiler.Enable(self.sensitive.GetValue()) + def on_schedule_changed(self, *args, **kwargs): + enabled = self.scheduled.GetValue() + self.date_picker.Enable(enabled) + self.time_picker.Enable(enabled) + + def get_scheduled_at(self): + if not self.scheduled.GetValue(): + return None + + # Get date from date picker + wx_date = self.date_picker.GetValue() + # Get time from time picker + wx_time = self.time_picker.GetValue() + + # Combine into a python datetime object + dt = datetime.datetime( + wx_date.GetYear(), + wx_date.GetMonth() + 1, # wx.DateTime months are 0-11 + wx_date.GetDay(), + wx_time.GetHour(), + wx_time.GetMinute(), + wx_time.GetSecond() + ) + return dt + def set_title(self, chars): self.SetTitle(_("Post - {} characters").format(chars)) From 4df58f0880387f821b4bfae59af02574a047c6c5 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 01:05:16 -0600 Subject: [PATCH 05/25] Updaed changelog --- doc/changelog.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index dac500cc..6decc5d6 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,31 @@ TWBlue Changelog ## changes in this version +* Core: + * Fixed a critical issue where buffers were not visible on screen in certain configurations. Now the main window maximizes correctly and visual fixes have been applied to ensure content is accessible. ([#886](https://github.com/mcv-software/twblue/issues/886)) + * Keyboard shortcut improvements: Several shortcuts have been added and fixed to improve efficiency and avoid conflicts: + * New shortcuts for the autocomplete users manager and menu bar items. ([#842](https://github.com/mcv-software/twblue/issues/842)) + * Added a shortcut for the "Restore Template" button in the template editor dialog. ([#841](https://github.com/mcv-software/twblue/issues/841)) + * New shortcuts for user list and poll dialogs. + * Resolved a conflict with the 's' key shortcut used for seeking media. + * Updated the shortcut for marking an account as a "Bot" to avoid conflict with the biography field. +* Mastodon: + * **Post Editing:** It is finally possible to edit Mastodon posts from TWBlue! You can now correct errors in your posts. ([#859](https://github.com/mcv-software/twblue/issues/859)) + * Safety warning: if you edit a post containing a poll, votes will be reset. + * Polls are now correctly displayed as attachments within the edit dialog. + * **Scheduled Posts:** It is now possible to schedule your posts to be published at a later time! + * Added a "Schedule post" checkbox to the post dialog with date and time pickers. + * Implemented validation to ensure posts are scheduled at least 5 minutes in the future, as required by Mastodon. + * The default time is automatically set to 6 minutes in the future for convenience. + * **Quoted Posts:** Significantly improved the reading and display of quoted posts. TWBlue now structures and reads this content more clearly. ([#860](https://github.com/mcv-software/twblue/issues/860)) + * **Content Cleaning:** Implemented a more robust HTML filter to remove junk elements or unnecessary CSS classes from post text, offering a cleaner reading experience. + * **Mute Conversation:** Enhanced the "Mute Conversation" feature. + * Posts from muted conversations will now be visually hidden from the Home timeline immediately upon muting, ensuring a cleaner experience. + * Added a new invisible shortcut to toggle mute on the focused conversation: `Alt+Windows+Shift+Delete` (Default) or `Control+Alt+Windows+Backspace` (Windows 10/11). + * The action is also available in the context menu of the post. + +## Changes in version 2025.3.8 + In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author. * Mastodon: From 15a9df2ca9cb751c42647d7d3ab7da588e21d612 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 01:53:03 -0600 Subject: [PATCH 06/25] feat(mastodon): Add support for server announcements Implemented a new 'Announcements' buffer to view instance-wide news. Features include: - New buffer and UI panel for announcements. - Support for templates and rendering of announcement content. - 'Dismiss' functionality (mapped to Enter/Return) to mark announcements as read. - Integrated into account settings for buffer management. --- doc/changelog.md | 3 + src/controller/buffers/mastodon/__init__.py | 1 + .../buffers/mastodon/announcements.py | 165 ++++++++++++++++++ src/controller/mastodon/handler.py | 2 + src/controller/mastodon/settings.py | 1 + src/mastodon.defaults | 2 +- src/sessions/mastodon/compose.py | 8 +- src/sessions/mastodon/templates.py | 22 +++ src/wxUI/buffers/mastodon/__init__.py | 3 +- src/wxUI/buffers/mastodon/announcements.py | 36 ++++ 10 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 src/controller/buffers/mastodon/announcements.py create mode 100644 src/wxUI/buffers/mastodon/announcements.py diff --git a/doc/changelog.md b/doc/changelog.md index 6decc5d6..54f2f66f 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -24,6 +24,9 @@ TWBlue Changelog * Posts from muted conversations will now be visually hidden from the Home timeline immediately upon muting, ensuring a cleaner experience. * Added a new invisible shortcut to toggle mute on the focused conversation: `Alt+Windows+Shift+Delete` (Default) or `Control+Alt+Windows+Backspace` (Windows 10/11). * The action is also available in the context menu of the post. + * **Announcements:** Added support for viewing server announcements. + * New dedicated buffer for "Announcements" where you can read instance-wide news. + * Added ability to dismiss (mark as read) announcements directly from the buffer. ## Changes in version 2025.3.8 diff --git a/src/controller/buffers/mastodon/__init__.py b/src/controller/buffers/mastodon/__init__.py index 0df3110d..9486c467 100644 --- a/src/controller/buffers/mastodon/__init__.py +++ b/src/controller/buffers/mastodon/__init__.py @@ -6,3 +6,4 @@ from .users import UserBuffer from .notifications import NotificationsBuffer from .search import SearchBuffer from .community import CommunityBuffer +from .announcements import AnnouncementsBuffer \ No newline at end of file diff --git a/src/controller/buffers/mastodon/announcements.py b/src/controller/buffers/mastodon/announcements.py new file mode 100644 index 00000000..a3ffbd54 --- /dev/null +++ b/src/controller/buffers/mastodon/announcements.py @@ -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 diff --git a/src/controller/mastodon/handler.py b/src/controller/mastodon/handler.py index a6e1e301..3f5086d7 100644 --- a/src/controller/mastodon/handler.py +++ b/src/controller/mastodon/handler.py @@ -92,6 +92,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"]: diff --git a/src/controller/mastodon/settings.py b/src/controller/mastodon/settings.py index be615baa..a59543b2 100644 --- a/src/controller/mastodon/settings.py +++ b/src/controller/mastodon/settings.py @@ -204,6 +204,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()) diff --git a/src/mastodon.defaults b/src/mastodon.defaults index 525bfc84..0bab3a92 100644 --- a/src/mastodon.defaults +++ b/src/mastodon.defaults @@ -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) diff --git a/src/sessions/mastodon/compose.py b/src/sessions/mastodon/compose.py index b9d9534f..15c3aebf 100644 --- a/src/sessions/mastodon/compose.py +++ b/src/sessions/mastodon/compose.py @@ -84,4 +84,10 @@ def compose_notification(notification, db, settings, relative_times, show_screen filtered = utils.evaluate_filters(post=notification, current_context="notifications") if filtered != None: text = _("hidden by filter {}").format(filtered) - return [user, text, ts] \ No newline at end of file + 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] \ No newline at end of file diff --git a/src/sessions/mastodon/templates.py b/src/sessions/mastodon/templates.py index 2674bea3..99de0dde 100644 --- a/src/sessions/mastodon/templates.py +++ b/src/sessions/mastodon/templates.py @@ -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 diff --git a/src/wxUI/buffers/mastodon/__init__.py b/src/wxUI/buffers/mastodon/__init__.py index 32791133..aa0305da 100644 --- a/src/wxUI/buffers/mastodon/__init__.py +++ b/src/wxUI/buffers/mastodon/__init__.py @@ -2,4 +2,5 @@ from .base import basePanel from .conversationList import conversationListPanel from .notifications import notificationsPanel -from .user import userPanel \ No newline at end of file +from .user import userPanel +from .announcements import announcementsPanel diff --git a/src/wxUI/buffers/mastodon/announcements.py b/src/wxUI/buffers/mastodon/announcements.py new file mode 100644 index 00000000..d6639925 --- /dev/null +++ b/src/wxUI/buffers/mastodon/announcements.py @@ -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() From 7a9337c07a43c131e9d0e9c401bd5cd0135de250 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 02:04:20 -0600 Subject: [PATCH 07/25] feat(mastodon): Add support for editing announcement templates Updated the template editor and account settings to allow customization of announcement display: - Added default announcement template to mastodon.defaults. - Updated templateEditor to recognize announcement variables. - Added 'Edit template for announcements' button to account configuration dialog. - Implemented template saving logic in settings controller. --- src/controller/mastodon/settings.py | 13 ++++++++++++- src/controller/mastodon/templateEditor.py | 4 +++- src/mastodon.defaults | 1 + src/wxUI/dialogs/mastodon/configuration.py | 8 +++++--- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/controller/mastodon/settings.py b/src/controller/mastodon/settings.py index a59543b2..db9e6544 100644 --- a/src/controller/mastodon/settings.py +++ b/src/controller/mastodon/settings.py @@ -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 diff --git a/src/controller/mastodon/templateEditor.py b/src/controller/mastodon/templateEditor.py index c4620303..330a304c 100644 --- a/src/controller/mastodon/templateEditor.py +++ b/src/controller/mastodon/templateEditor.py @@ -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 diff --git a/src/mastodon.defaults b/src/mastodon.defaults index 0bab3a92..b6b06af3 100644 --- a/src/mastodon.defaults +++ b/src/mastodon.defaults @@ -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] diff --git a/src/wxUI/dialogs/mastodon/configuration.py b/src/wxUI/dialogs/mastodon/configuration.py index 9138ea63..4fada629 100644 --- a/src/wxUI/dialogs/mastodon/configuration.py +++ b/src/wxUI/dialogs/mastodon/configuration.py @@ -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): From 512e2e16846fbdc4914697d65cf6cebf9cd08a00 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 02:31:45 -0600 Subject: [PATCH 08/25] docs: update changelog and release notes for upcoming release --- doc/changelog.md | 2 ++ release-notes.md | 31 +++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 54f2f66f..3e041b4b 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,8 @@ TWBlue Changelog ## changes in this version +In this version, we have focused on expanding content management capabilities within Mastodon. It is now possible to edit sent posts and schedule them for future publication. Additionally, support for reading quoted posts has been implemented, and a new buffer for server announcements is available. On the Core side, visual stability has been prioritized to ensure proper window display, along with an expansion of keyboard shortcuts. + * Core: * Fixed a critical issue where buffers were not visible on screen in certain configurations. Now the main window maximizes correctly and visual fixes have been applied to ensure content is accessible. ([#886](https://github.com/mcv-software/twblue/issues/886)) * Keyboard shortcut improvements: Several shortcuts have been added and fixed to improve efficiency and avoid conflicts: diff --git a/release-notes.md b/release-notes.md index 760bb94a..a1221b24 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,10 +1,29 @@ ## Changelog -In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author. +In this version, we have focused on expanding content management capabilities within Mastodon. It is now possible to edit sent posts and schedule them for future publication. Additionally, support for reading quoted posts has been implemented, and a new buffer for server announcements is available. On the Core side, visual stability has been prioritized to ensure proper window display, along with an expansion of keyboard shortcuts. +* Core: + * Fixed a critical issue where buffers were not visible on screen in certain configurations. Now the main window maximizes correctly and visual fixes have been applied to ensure content is accessible. ([#886](https://github.com/mcv-software/twblue/issues/886)) + * Keyboard shortcut improvements: Several shortcuts have been added and fixed to improve efficiency and avoid conflicts: + * New shortcuts for the autocomplete users manager and menu bar items. ([#842](https://github.com/mcv-software/twblue/issues/842)) + * Added a shortcut for the "Restore Template" button in the template editor dialog. ([#841](https://github.com/mcv-software/twblue/issues/841)) + * New shortcuts for user list and poll dialogs. + * Resolved a conflict with the 's' key shortcut used for seeking media. + * Updated the shortcut for marking an account as a "Bot" to avoid conflict with the biography field. * Mastodon: - * Added filters support to TWBlue. Filters are only implemented in posts for the moment. TWBlue will, depending in the selected settings, hide behind a content warning or completely ignore a post based on filters. Also it is possible to add, delete or edit filters from the buffer menu in the menu bar. - * A language selector has been added for posting in TWBlue. It is now possible to choose the language in which a post will be made, which will be useful for content filtering and other language-dependent features. The default language can be chosen based on your Mastodon account’s language, the language of the post you’re replying to, or, if no automatic selection is possible, TWBlue’s own language will be used by default. - * TWBlue now supports announcing (via a new template variable for posts) pinned posts. You can edit your posts template and use the $pinned variable. When reading the post, TWBlue will indicate if the post is pinned. Also, when loading an user timeline, pinned posts will be loaded at the top or bottom of the buffer according to local settings. - * TWBlue should be able to display all posts in the post displayer dialog. - * reading long posts in the graphical user interface should work better. + * **Post Editing:** It is finally possible to edit Mastodon posts from TWBlue! You can now correct errors in your posts. ([#859](https://github.com/mcv-software/twblue/issues/859)) + * Safety warning: if you edit a post containing a poll, votes will be reset. + * Polls are now correctly displayed as attachments within the edit dialog. + * **Scheduled Posts:** It is now possible to schedule your posts to be published at a later time! + * Added a "Schedule post" checkbox to the post dialog with date and time pickers. + * Implemented validation to ensure posts are scheduled at least 5 minutes in the future, as required by Mastodon. + * The default time is automatically set to 6 minutes in the future for convenience. + * **Quoted Posts:** Significantly improved the reading and display of quoted posts. TWBlue now structures and reads this content more clearly. ([#860](https://github.com/mcv-software/twblue/issues/860)) + * **Content Cleaning:** Implemented a more robust HTML filter to remove junk elements or unnecessary CSS classes from post text, offering a cleaner reading experience. + * **Mute Conversation:** Enhanced the "Mute Conversation" feature. + * Posts from muted conversations will now be visually hidden from the Home timeline immediately upon muting, ensuring a cleaner experience. + * Added a new invisible shortcut to toggle mute on the focused conversation: `Alt+Windows+Shift+Delete` (Default) or `Control+Alt+Windows+Backspace` (Windows 10/11). + * The action is also available in the context menu of the post. + * **Announcements:** Added support for viewing server announcements. + * New dedicated buffer for "Announcements" where you can read instance-wide news. + * Added ability to dismiss (mark as read) announcements directly from the buffer. \ No newline at end of file From 04843616b358cf5ba5b5b7a03675580b48f78883 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 20:35:07 -0600 Subject: [PATCH 09/25] feat: update updates.json for version 2026.01.12 and remove win32 support --- updates/updates.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/updates/updates.json b/updates/updates.json index 3d50ef19..ca6b1178 100644 --- a/updates/updates.json +++ b/updates/updates.json @@ -1,7 +1,9 @@ -{"current_version": "2025.03.08", -"description": "Initial filter support, added pinned posts. Fixed some minor issues.", -"date": "unknown", +{ +"current_version": "2026.01.12", +"description": "Added support for editing and scheduling Mastodon posts, improved quoted posts reading, and added server announcements buffer. Includes visual stability fixes and keyboard shortcut improvements.", +"date": "2026-01-12", "downloads": -{"Windows32": "https://github.com/MCV-Software/TWBlue/releases/download/v2025.03.08/TWBlue_portable_v2024.05.23.zip", -"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2025.03.08/TWBlue_portable_v2025.03.08.zip"} +{ +"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2026.01.12/TWBlue_portable_v2026.01.12.zip" +} } \ No newline at end of file From 04dca7681bb562c2c32c4d2548834df657c6628f Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 20:42:15 -0600 Subject: [PATCH 10/25] Revert updater for a while --- updates/updates.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates/updates.json b/updates/updates.json index ca6b1178..adad1429 100644 --- a/updates/updates.json +++ b/updates/updates.json @@ -1,5 +1,5 @@ { -"current_version": "2026.01.12", +"current_version": "2025.03.08", "description": "Added support for editing and scheduling Mastodon posts, improved quoted posts reading, and added server announcements buffer. Includes visual stability fixes and keyboard shortcut improvements.", "date": "2026-01-12", "downloads": From 0512d53043f7e98b1b274a622ab2085f076c8c78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 03:47:36 +0000 Subject: [PATCH 11/25] build(deps): bump numpy from 2.4.0 to 2.4.1 Bumps [numpy](https://github.com/numpy/numpy) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.4.0...v2.4.1) --- updated-dependencies: - dependency-name: numpy dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e140ed10..58f9d444 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ libretranslatepy==2.1.4 lief==0.15.1 Markdown==3.10 Mastodon.py==2.1.4 -numpy==2.4.0 +numpy==2.4.1 oauthlib==3.3.1 packaging==25.0 pillow==12.1.0 From da493a88ea3f8f3247d82d6c7b6b7fef816a46e0 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 23:06:35 -0600 Subject: [PATCH 12/25] change: Update python version to latest 3.13 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35adceba..7e0bf808 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - name: Get python interpreter uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.13' - name: Install python packages run: python -m pip install -r requirements.txt From 302c22ab9f17ffd81bb7f3112fc63d3350317800 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 23:17:21 -0600 Subject: [PATCH 13/25] Updated setuptools to attempt a build --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e140ed10..23fc9118 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ requests==2.32.5 requests-oauthlib==2.0.0 requests-toolbelt==1.0.0 rfc3986==2.0.0 -setuptools==69.0.0 +setuptools==80.9.0 six==1.17.0 sniffio==1.3.1 sound_lib @ git+https://github.com/accessibleapps/sound_lib@a439f0943fb95ee7b6ba24f51a686f47c4ad66b2 From 9ed2f6771ee5db5dc562f7cae6f22c42c478504a Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 12 Jan 2026 23:44:21 -0600 Subject: [PATCH 14/25] Fix setup.py with cx_freeze 8 API changes --- src/setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/setup.py b/src/setup.py index b1a0069f..34b27cb4 100644 --- a/src/setup.py +++ b/src/setup.py @@ -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, From 29e52288df79de76d6614fbc8708dfa6323b0c3b Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Jan 2026 00:01:19 -0600 Subject: [PATCH 15/25] Update release workflow to install nsis prior to generating the installer --- .github/workflows/release.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e0bf808..f9182db6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,10 +29,11 @@ jobs: .\scripts\build.ps1 mv src/dist scripts\TWBlue64 - - name: make installer - run: | - cd scripts - makensis twblue.nsi + - name: Setup NSIS + working-directory: scripts + uses: joncloud/makensis-action@v5 + with: + script-file: twblue.nsi - name: Create portable working-directory: scripts\TWBlue64 From 4d20d7744a04794314688ad8851ead7efcc81067 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Jan 2026 00:04:00 -0600 Subject: [PATCH 16/25] Update workflow (again) --- .github/workflows/release.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9182db6..90fbc780 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,11 +29,13 @@ jobs: .\scripts\build.ps1 mv src/dist scripts\TWBlue64 - - name: Setup NSIS - working-directory: scripts - uses: joncloud/makensis-action@v5 - with: - script-file: twblue.nsi + - name: Install NSIS + run: choco install nsis + + - name: make installer + run: | + cd scripts + makensis twblue.nsi - name: Create portable working-directory: scripts\TWBlue64 From 9688c20dd9407c9837dd710f1a3ae9fbe5529fa2 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Jan 2026 00:15:19 -0600 Subject: [PATCH 17/25] Added nsis to github actions' path --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90fbc780..60b39b53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,9 @@ jobs: - name: Install NSIS run: choco install nsis + - name: Add NSIS to PATH + run: echo "C:\Program Files (x86)\NSIS" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: make installer run: | cd scripts From 363d2082c0d3c0ae222d91de022c4c71c39c98ae Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Jan 2026 00:43:46 -0600 Subject: [PATCH 18/25] release a new version in our updates system --- updates/updates.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/updates/updates.json b/updates/updates.json index adad1429..6de8a861 100644 --- a/updates/updates.json +++ b/updates/updates.json @@ -1,9 +1,9 @@ { -"current_version": "2025.03.08", +"current_version": "2026.01.13", "description": "Added support for editing and scheduling Mastodon posts, improved quoted posts reading, and added server announcements buffer. Includes visual stability fixes and keyboard shortcut improvements.", "date": "2026-01-12", "downloads": { -"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2026.01.12/TWBlue_portable_v2026.01.12.zip" +"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2026.01.13/TWBlue_portable_v2026.01.13.zip" } } \ No newline at end of file From 7c131b6936498b4a6499063301b60b12861e98e4 Mon Sep 17 00:00:00 2001 From: Manuel cortez Date: Fri, 16 Jan 2026 13:59:02 -0600 Subject: [PATCH 19/25] keystroke editor: expand available actions and update keymaps --- doc/changelog.md | 5 +++++ src/keymaps/Chicken Nugget.keymap | 12 ++++++++++-- src/keymaps/Qwitter.keymap | 12 ++++++++++-- src/keymaps/Windows 10.keymap | 12 ++++++++++-- src/keymaps/Windows11.keymap | 12 ++++++++++-- src/keymaps/base.template | 11 ++++++++++- src/keymaps/default.keymap | 12 ++++++++++-- src/keystrokeEditor/actions/mastodon.py | 12 +++++++++++- 8 files changed, 76 insertions(+), 12 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 3e041b4b..508fbc44 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,11 @@ TWBlue Changelog ## changes in this version +* Core: + * Expanded the keystroke editor actions list. Now, many previously hidden or unassignable actions are available to be mapped to custom keyboard shortcuts. + +## Changes in version 2026.01.13 + In this version, we have focused on expanding content management capabilities within Mastodon. It is now possible to edit sent posts and schedule them for future publication. Additionally, support for reading quoted posts has been implemented, and a new buffer for server announcements is available. On the Core side, visual stability has been prioritized to ensure proper window display, along with an expansion of keyboard shortcuts. * Core: diff --git a/src/keymaps/Chicken Nugget.keymap b/src/keymaps/Chicken Nugget.keymap index 9d95a47f..29832008 100644 --- a/src/keymaps/Chicken Nugget.keymap +++ b/src/keymaps/Chicken Nugget.keymap @@ -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") @@ -37,4 +36,13 @@ update_buffer = string(default="control+win+shift+u") 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") \ No newline at end of file +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="") \ No newline at end of file diff --git a/src/keymaps/Qwitter.keymap b/src/keymaps/Qwitter.keymap index 7a8bc30a..ddd1fe6d 100644 --- a/src/keymaps/Qwitter.keymap +++ b/src/keymaps/Qwitter.keymap @@ -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") @@ -56,4 +55,13 @@ update_buffer = string(default="control+win+shift+u") 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") \ No newline at end of file +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="") \ No newline at end of file diff --git a/src/keymaps/Windows 10.keymap b/src/keymaps/Windows 10.keymap index c12d78ee..3e969518 100644 --- a/src/keymaps/Windows 10.keymap +++ b/src/keymaps/Windows 10.keymap @@ -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") @@ -59,4 +58,13 @@ open_in_browser = string(default="alt+control+win+return") add_alias=string(default="") mute_conversation=string(default="control+alt+win+back") find = string(default="control+win+{") -vote=string(default="alt+win+shift+v") \ No newline at end of file +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="") \ No newline at end of file diff --git a/src/keymaps/Windows11.keymap b/src/keymaps/Windows11.keymap index c4301cff..779c3b3b 100644 --- a/src/keymaps/Windows11.keymap +++ b/src/keymaps/Windows11.keymap @@ -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") @@ -59,4 +58,13 @@ open_in_browser = string(default="alt+control+win+return") add_alias=string(default="") mute_conversation=string(default="control+alt+win+back") find = string(default="control+win+{") -vote=string(default="alt+win+shift+v") \ No newline at end of file +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="") \ No newline at end of file diff --git a/src/keymaps/base.template b/src/keymaps/base.template index 15429f06..ad514d8d 100644 --- a/src/keymaps/base.template +++ b/src/keymaps/base.template @@ -58,4 +58,13 @@ update_buffer = string(default="control+win+shift+u") 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") \ No newline at end of file +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="") \ No newline at end of file diff --git a/src/keymaps/default.keymap b/src/keymaps/default.keymap index 30e6bcbb..fa70c756 100644 --- a/src/keymaps/default.keymap +++ b/src/keymaps/default.keymap @@ -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") @@ -60,4 +59,13 @@ ocr_image = string(default="win+alt+o") 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") \ No newline at end of file +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="") \ No newline at end of file diff --git a/src/keystrokeEditor/actions/mastodon.py b/src/keystrokeEditor/actions/mastodon.py index 3f07f152..613687c3 100644 --- a/src/keystrokeEditor/actions/mastodon.py +++ b/src/keystrokeEditor/actions/mastodon.py @@ -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"), } \ No newline at end of file From 320c7a361cf82b76bae6f564fb801e455e030eb8 Mon Sep 17 00:00:00 2001 From: Manuel cortez Date: Wed, 21 Jan 2026 08:44:13 -0600 Subject: [PATCH 20/25] Fix HTML entity decoding when editing Mastodon posts (#893) --- doc/changelog.md | 2 ++ src/controller/mastodon/messages.py | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 508fbc44..72875542 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -4,6 +4,8 @@ TWBlue Changelog * Core: * Expanded the keystroke editor actions list. Now, many previously hidden or unassignable actions are available to be mapped to custom keyboard shortcuts. +* Mastodon: + * Fixed an issue where HTML entities were not decoded when editing a post. ([#893](https://github.com/mcv-software/twblue/issues/893)) ## Changes in version 2026.01.13 diff --git a/src/controller/mastodon/messages.py b/src/controller/mastodon/messages.py index 48b2b678..63c6a7da 100644 --- a/src/controller/mastodon/messages.py +++ b/src/controller/mastodon/messages.py @@ -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 From e5822ac8eec52db121f6faabd2fc9627ac13b27a Mon Sep 17 00:00:00 2001 From: Manuel cortez Date: Wed, 21 Jan 2026 10:57:00 -0600 Subject: [PATCH 21/25] mastodon: feat: implement support for sending quoted posts --- doc/changelog.md | 1 + src/controller/buffers/mastodon/base.py | 23 ++++++++- src/controller/mastodon/handler.py | 1 + src/sessions/mastodon/session.py | 65 ++++++++++++++++++------- src/wxUI/dialogs/mastodon/dialogs.py | 40 +++++++++++++-- src/wxUI/dialogs/mastodon/menus.py | 4 ++ 6 files changed, 112 insertions(+), 22 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 72875542..92745afa 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -5,6 +5,7 @@ TWBlue Changelog * Core: * Expanded the keystroke editor actions list. Now, many previously hidden or unassignable actions are available to be mapped to custom keyboard shortcuts. * Mastodon: + * Added support for sending quoted posts! You can now quote other users' posts from the context menu or the new Boost dialog. ([#860](https://github.com/mcv-software/twblue/issues/860)) * Fixed an issue where HTML entities were not decoded when editing a post. ([#893](https://github.com/mcv-software/twblue/issues/893)) ## Changes in version 2026.01.13 diff --git a/src/controller/buffers/mastodon/base.py b/src/controller/buffers/mastodon/base.py index 1595f932..91fb03e7 100644 --- a/src/controller/buffers/mastodon/base.py +++ b/src/controller/buffers/mastodon/base.py @@ -311,8 +311,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 +444,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) diff --git a/src/controller/mastodon/handler.py b/src/controller/mastodon/handler.py index 3f5086d7..89e45a04 100644 --- a/src/controller/mastodon/handler.py +++ b/src/controller/mastodon/handler.py @@ -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"), diff --git a/src/sessions/mastodon/session.py b/src/sessions/mastodon/session.py index 658fd3c4..8a205862 100644 --- a/src/sessions/mastodon/session.py +++ b/src/sessions/mastodon/session.py @@ -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. diff --git a/src/wxUI/dialogs/mastodon/dialogs.py b/src/wxUI/dialogs/mastodon/dialogs.py index 1f945d82..865dd36a 100644 --- a/src/wxUI/dialogs/mastodon/dialogs.py +++ b/src/wxUI/dialogs/mastodon/dialogs.py @@ -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 diff --git a/src/wxUI/dialogs/mastodon/menus.py b/src/wxUI/dialogs/mastodon/menus.py index 62dd3407..b7277ab4 100644 --- a/src/wxUI/dialogs/mastodon/menus.py +++ b/src/wxUI/dialogs/mastodon/menus.py @@ -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")) From beb676d9ab2d37a6f1ff01ad563e850b62e71692 Mon Sep 17 00:00:00 2001 From: Manuel cortez Date: Wed, 21 Jan 2026 12:00:50 -0600 Subject: [PATCH 22/25] mastodon: fix: ensure pagination works correctly with pinned posts --- src/controller/buffers/mastodon/base.py | 23 ++++++++++++++------ src/controller/buffers/mastodon/community.py | 16 ++++++++------ src/controller/buffers/mastodon/mentions.py | 20 +++++++++++++---- src/controller/buffers/mastodon/search.py | 5 +---- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/controller/buffers/mastodon/base.py b/src/controller/buffers/mastodon/base.py index 91fb03e7..85ac3bf8 100644 --- a/src/controller/buffers/mastodon/base.py +++ b/src/controller/buffers/mastodon/base.py @@ -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: diff --git a/src/controller/buffers/mastodon/community.py b/src/controller/buffers/mastodon/community.py index d0223431..aa319274 100644 --- a/src/controller/buffers/mastodon/community.py +++ b/src/controller/buffers/mastodon/community.py @@ -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: diff --git a/src/controller/buffers/mastodon/mentions.py b/src/controller/buffers/mastodon/mentions.py index 8a3d397c..db5f4a0c 100644 --- a/src/controller/buffers/mastodon/mentions.py +++ b/src/controller/buffers/mastodon/mentions.py @@ -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: diff --git a/src/controller/buffers/mastodon/search.py b/src/controller/buffers/mastodon/search.py index e583a9ad..04f3a1df 100644 --- a/src/controller/buffers/mastodon/search.py +++ b/src/controller/buffers/mastodon/search.py @@ -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: From 51d019f0356ef00acae6ff2f76e2318671cda469 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:12:41 +0000 Subject: [PATCH 23/25] build(deps): bump coverage from 7.13.1 to 7.13.2 Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.1 to 7.13.2. - [Release notes](https://github.com/coveragepy/coveragepy/releases) - [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst) - [Commits](https://github.com/coveragepy/coveragepy/compare/7.13.1...7.13.2) --- updated-dependencies: - dependency-name: coverage dependency-version: 7.13.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b9f863c..2b784ae9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ chardet==5.2.0 charset-normalizer==3.4.4 colorama==0.4.6 configobj==5.0.9 -coverage==7.13.1 +coverage==7.13.2 cx-Freeze==8.5.3 cx-Logging==3.2.1 decorator==5.2.1 From cdb97579e9b692fa8f244d090a58a8fec14eb5c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:12:42 +0000 Subject: [PATCH 24/25] build(deps): bump types-python-dateutil Bumps [types-python-dateutil](https://github.com/typeshed-internal/stub_uploader) from 2.9.0.20251115 to 2.9.0.20260124. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-version: 2.9.0.20260124 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b9f863c..47049cd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,7 @@ sniffio==1.3.1 sound_lib @ git+https://github.com/accessibleapps/sound_lib@a439f0943fb95ee7b6ba24f51a686f47c4ad66b2 sqlitedict==2.1.0 twitter-text-parser==3.0.0 -types-python-dateutil==2.9.0.20251115 +types-python-dateutil==2.9.0.20260124 urllib3==2.6.3 win-inet-pton==1.1.0 winpaths==0.2 From a919d31f7cd6edf3feeabcf5163588c4e20dd532 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:36:26 +0000 Subject: [PATCH 25/25] build(deps): bump markdown from 3.10 to 3.10.1 Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.10 to 3.10.1. - [Release notes](https://github.com/Python-Markdown/markdown/releases) - [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md) - [Commits](https://github.com/Python-Markdown/markdown/compare/3.10.0...3.10.1) --- updated-dependencies: - dependency-name: markdown dependency-version: 3.10.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da28d49c..4c90f77e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ iniconfig==2.3.0 libloader @ git+https://github.com/accessibleapps/libloader@bc94811c095b2e57a036acd88660be9a33260267 libretranslatepy==2.1.4 lief==0.15.1 -Markdown==3.10 +Markdown==3.10.1 Mastodon.py==2.1.4 numpy==2.4.1 oauthlib==3.3.1