mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 01:17:32 +01:00
Merge next-gen
This commit is contained in:
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -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
|
||||
@@ -29,6 +29,12 @@ jobs:
|
||||
.\scripts\build.ps1
|
||||
mv src/dist scripts\TWBlue64
|
||||
|
||||
- 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
|
||||
|
||||
@@ -48,6 +48,44 @@ This version introduces comprehensive support for the AT Protocol (ATProto), ena
|
||||
|
||||
## 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.
|
||||
* 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
|
||||
|
||||
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:
|
||||
* **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.
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -21,9 +21,9 @@ 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.0
|
||||
numpy==2.4.1
|
||||
oauthlib==3.3.1
|
||||
packaging==25.0
|
||||
pillow==12.1.0
|
||||
@@ -43,13 +43,13 @@ 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
|
||||
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
|
||||
|
||||
@@ -6,3 +6,4 @@ from .users import UserBuffer
|
||||
from .notifications import NotificationsBuffer
|
||||
from .search import SearchBuffer
|
||||
from .community import CommunityBuffer
|
||||
from .announcements import AnnouncementsBuffer
|
||||
165
src/controller/buffers/mastodon/announcements.py
Normal file
165
src/controller/buffers/mastodon/announcements.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
import logging
|
||||
import arrow
|
||||
import widgetUtils
|
||||
import wx
|
||||
import output
|
||||
import languageHandler
|
||||
import config
|
||||
from pubsub import pub
|
||||
from controller.buffers.mastodon.base import BaseBuffer
|
||||
from sessions.mastodon import compose, templates
|
||||
from wxUI import buffers
|
||||
from wxUI.dialogs.mastodon import menus
|
||||
from mysc.thread_utils import call_threaded
|
||||
|
||||
log = logging.getLogger("controller.buffers.mastodon.announcements")
|
||||
|
||||
class AnnouncementsBuffer(BaseBuffer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# We enforce compose_func="compose_announcement"
|
||||
kwargs["compose_func"] = "compose_announcement"
|
||||
super(AnnouncementsBuffer, self).__init__(*args, **kwargs)
|
||||
self.type = "announcementsBuffer"
|
||||
|
||||
def create_buffer(self, parent, name):
|
||||
self.buffer = buffers.mastodon.announcementsPanel(parent, name)
|
||||
|
||||
def get_buffer_name(self):
|
||||
return _("Announcements")
|
||||
|
||||
def bind_events(self):
|
||||
self.buffer.set_focus_function(self.onFocus)
|
||||
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
|
||||
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.dismiss_announcement, self.buffer.dismiss)
|
||||
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
|
||||
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
|
||||
|
||||
def dismiss_announcement(self, event=None, item=None, *args, **kwargs):
|
||||
index = self.buffer.list.get_selected()
|
||||
if index == -1: return
|
||||
item = self.session.db[self.name][index]
|
||||
|
||||
# Optimistic UI update or wait for API? Let's wait for API to be safe, but run threaded.
|
||||
# We need a custom call because 'announcements_dismiss' returns None on success usually.
|
||||
def _do_dismiss():
|
||||
try:
|
||||
self.session.api_call(call_name="announcements_dismiss", id=str(item.id))
|
||||
# If success, update UI in main thread
|
||||
wx.CallAfter(self._on_dismiss_success, index)
|
||||
except Exception as e:
|
||||
log.exception("Error dismissing announcement")
|
||||
self.session.sound.play("error.ogg")
|
||||
|
||||
call_threaded(_do_dismiss)
|
||||
|
||||
def _on_dismiss_success(self, index):
|
||||
if index < len(self.session.db[self.name]):
|
||||
self.session.db[self.name].pop(index)
|
||||
self.buffer.list.remove_item(index)
|
||||
output.speak(_("Announcement dismissed."))
|
||||
|
||||
def show_menu(self, ev, pos=0, *args, **kwargs):
|
||||
if self.buffer.list.get_count() == 0:
|
||||
return
|
||||
# Create a simple menu
|
||||
menu = wx.Menu()
|
||||
dismiss_item = menu.Append(wx.ID_ANY, _("Dismiss"))
|
||||
copy_item = menu.Append(wx.ID_ANY, _("Copy text"))
|
||||
|
||||
self.buffer.Bind(wx.EVT_MENU, self.dismiss_announcement, dismiss_item)
|
||||
self.buffer.Bind(wx.EVT_MENU, self.copy, copy_item)
|
||||
|
||||
if pos != 0:
|
||||
self.buffer.PopupMenu(menu, pos)
|
||||
else:
|
||||
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())
|
||||
|
||||
def url(self, *args, **kwargs):
|
||||
self.dismiss_announcement()
|
||||
|
||||
def get_more_items(self): output.speak(_("This buffer does not support loading more items."), True)
|
||||
|
||||
# Disable social interactions not applicable to announcements
|
||||
def reply(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def share_item(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def toggle_favorite(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def add_to_favorites(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def remove_from_favorites(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def toggle_bookmark(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def mute_conversation(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def vote(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def send_message(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def user_details(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def view_item(self, *args, **kwargs):
|
||||
# We could implement a specific viewer for announcements if needed,
|
||||
# but the default one expects a status object structure.
|
||||
pass
|
||||
|
||||
def copy(self, event=None):
|
||||
item = self.get_item()
|
||||
if item:
|
||||
pub.sendMessage("execute-action", action="copy_to_clipboard")
|
||||
|
||||
def onFocus(self, *args, **kwargs):
|
||||
# Similar logic to BaseBuffer but adapted if needed.
|
||||
# BaseBuffer.onFocus handles reading long posts.
|
||||
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
|
||||
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
|
||||
|
||||
def get_message(self):
|
||||
# Override to use announcement template
|
||||
announcement = self.get_item()
|
||||
if announcement == None:
|
||||
return
|
||||
template = self.session.settings.get("templates", {}).get("announcement", templates.announcement_default_template)
|
||||
t = templates.render_announcement(announcement, template, self.session.settings, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
|
||||
return t
|
||||
|
||||
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
|
||||
current_time = time.time()
|
||||
if self.execution_time == 0 or current_time-self.execution_time >= 300 or mandatory==True:
|
||||
self.execution_time = current_time
|
||||
log.debug("Starting stream for announcements buffer")
|
||||
try:
|
||||
# The announcements API does not accept min_id or limit parameters
|
||||
results = self.session.api.announcements()
|
||||
# Reverse the list so order_buffer processes them according to user preference
|
||||
results.reverse()
|
||||
except Exception as e:
|
||||
log.exception("Error retrieving announcements: %s" % (str(e)))
|
||||
return 0
|
||||
|
||||
# order_buffer handles duplication filtering by ID internally
|
||||
number_of_items = self.session.order_buffer(self.name, results)
|
||||
log.debug("Number of new announcements retrieved: %d" % (number_of_items,))
|
||||
|
||||
self.put_items_on_list(number_of_items)
|
||||
|
||||
if number_of_items > 0 and play_sound == True and self.sound != None and self.session.settings["sound"]["session_mute"] == False:
|
||||
self.session.sound.play(self.sound)
|
||||
|
||||
return number_of_items
|
||||
return 0
|
||||
@@ -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)
|
||||
|
||||
@@ -104,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
|
||||
@@ -160,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:
|
||||
@@ -289,10 +320,13 @@ 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)
|
||||
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)
|
||||
@@ -419,11 +453,30 @@ class BaseBuffer(base.Buffer):
|
||||
id = item.id
|
||||
if self.session.settings["general"]["boost_mode"] == "ask":
|
||||
answer = mastodon_dialogs.boost_question()
|
||||
if answer == True:
|
||||
if answer == 1:
|
||||
self._direct_boost(id)
|
||||
elif answer == 2:
|
||||
self.quote(item=item)
|
||||
else:
|
||||
self._direct_boost(id)
|
||||
|
||||
def quote(self, event=None, item=None, *args, **kwargs):
|
||||
if item == None:
|
||||
item = self.get_item()
|
||||
if self.can_share(item=item) == False:
|
||||
return output.speak(_("This action is not supported on conversations."))
|
||||
|
||||
title = _("Quote post")
|
||||
caption = _("Write your comment here")
|
||||
post = messages.post(session=self.session, title=title, caption=caption)
|
||||
|
||||
response = post.message.ShowModal()
|
||||
if response == wx.ID_OK:
|
||||
post_data = post.get_data()
|
||||
call_threaded(self.session.send_post, quote_id=item.id, posts=post_data, visibility=post.get_visibility(), language=post.get_language(), **kwargs)
|
||||
if hasattr(post.message, "destroy"):
|
||||
post.message.destroy()
|
||||
|
||||
def _direct_boost(self, id):
|
||||
item = self.session.api_call(call_name="status_reblog", _sound="retweet_send.ogg", id=id)
|
||||
|
||||
@@ -612,6 +665,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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -35,6 +35,7 @@ class Handler(object):
|
||||
compose=_("&Post"),
|
||||
reply=_("Re&ply"),
|
||||
share=_("&Boost"),
|
||||
quote=_("&Quote"),
|
||||
fav=_("&Add to favorites"),
|
||||
unfav=_("Remove from favorites"),
|
||||
view=_("&Show post"),
|
||||
@@ -92,6 +93,8 @@ class Handler(object):
|
||||
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="blocks", name="blocked", sessionObject=session, account=name))
|
||||
elif i == 'notifications':
|
||||
pub.sendMessage("createBuffer", buffer_type="NotificationsBuffer", session_type=session.type, buffer_title=_("Notifications"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_notification", function="notifications", name="notifications", sessionObject=session, account=name))
|
||||
elif i == 'announcements':
|
||||
pub.sendMessage("createBuffer", buffer_type="AnnouncementsBuffer", session_type=session.type, buffer_title=_("Announcements"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="announcements", name="announcements", sessionObject=session, account=name, sound="new_event.ogg"))
|
||||
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
|
||||
timelines_position =controller.view.search("timelines", name)
|
||||
for i in session.settings["other_buffers"]["timelines"]:
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -275,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
|
||||
|
||||
@@ -52,10 +52,12 @@ class accountSettingsController(globalSettingsController):
|
||||
post_template = self.config["templates"]["post"]
|
||||
conversation_template = self.config["templates"]["conversation"]
|
||||
person_template = self.config["templates"]["person"]
|
||||
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template)
|
||||
announcement_template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
|
||||
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template, announcement_template=announcement_template)
|
||||
widgetUtils.connect_event(self.dialog.templates.post, widgetUtils.BUTTON_PRESSED, self.edit_post_template)
|
||||
widgetUtils.connect_event(self.dialog.templates.conversation, widgetUtils.BUTTON_PRESSED, self.edit_conversation_template)
|
||||
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
|
||||
widgetUtils.connect_event(self.dialog.templates.announcement, widgetUtils.BUTTON_PRESSED, self.edit_announcement_template)
|
||||
self.dialog.create_other_buffers()
|
||||
buffer_values = self.get_buffers_list()
|
||||
self.dialog.buffers.insert_buffers(buffer_values)
|
||||
@@ -109,6 +111,15 @@ class accountSettingsController(globalSettingsController):
|
||||
self.config.write()
|
||||
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
|
||||
|
||||
def edit_announcement_template(self, *args, **kwargs):
|
||||
template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
|
||||
control = EditTemplate(template=template, type="announcement")
|
||||
result = control.run_dialog()
|
||||
if result != "": # Template has been saved.
|
||||
self.config["templates"]["announcement"] = result
|
||||
self.config.write()
|
||||
self.dialog.templates.announcement.SetLabel(_("Edit template for announcements. Current template: {}").format(result))
|
||||
|
||||
def save_configuration(self):
|
||||
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
|
||||
self.needs_restart = True
|
||||
@@ -204,6 +215,7 @@ class accountSettingsController(globalSettingsController):
|
||||
all_buffers['blocked']=_("Blocked users")
|
||||
all_buffers['muted']=_("Muted users")
|
||||
all_buffers['notifications']=_("Notifications")
|
||||
all_buffers['announcements']=_("Announcements")
|
||||
list_buffers = []
|
||||
hidden_buffers=[]
|
||||
all_buffers_keys = list(all_buffers.keys())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,6 @@ url = string(default="control+win+b")
|
||||
go_home = string(default="control+win+home")
|
||||
go_end = string(default="control+win+end")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
copy_to_clipboard = string(default="control+win+shift+c")
|
||||
@@ -38,3 +37,12 @@ ocr_image = string(default="win+alt+o")
|
||||
open_in_browser = string(default="alt+control+win+return")
|
||||
add_alias=string(default="")
|
||||
vote=string(default="alt+win+shift+v")
|
||||
edit_post=string(default="")
|
||||
open_favs_timeline=string(default="")
|
||||
community_timeline=string(default="")
|
||||
seekLeft=string(default="")
|
||||
seekRight=string(default="")
|
||||
manage_aliases=string(default="")
|
||||
create_filter=string(default="")
|
||||
manage_filters=string(default="")
|
||||
manage_accounts=string(default="")
|
||||
@@ -33,7 +33,6 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="control+win+shift+p")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
copy_to_clipboard = string(default="control+win+shift+c")
|
||||
@@ -57,3 +56,12 @@ ocr_image = string(default="win+alt+o")
|
||||
open_in_browser = string(default="alt+control+win+return")
|
||||
add_alias=string(default="")
|
||||
vote=string(default="alt+win+shift+v")
|
||||
edit_post=string(default="")
|
||||
open_favs_timeline=string(default="")
|
||||
community_timeline=string(default="")
|
||||
seekLeft=string(default="")
|
||||
seekRight=string(default="")
|
||||
manage_aliases=string(default="")
|
||||
create_filter=string(default="")
|
||||
manage_filters=string(default="")
|
||||
manage_accounts=string(default="")
|
||||
@@ -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")
|
||||
@@ -57,5 +56,15 @@ 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")
|
||||
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="")
|
||||
@@ -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")
|
||||
@@ -57,5 +56,15 @@ 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")
|
||||
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="")
|
||||
@@ -57,3 +57,14 @@ 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="")
|
||||
mute_conversation=string(default="alt+win+shift+delete")
|
||||
vote=string(default="alt+win+shift+v")
|
||||
edit_post=string(default="")
|
||||
open_favs_timeline=string(default="")
|
||||
community_timeline=string(default="")
|
||||
seekLeft=string(default="")
|
||||
seekRight=string(default="")
|
||||
manage_aliases=string(default="")
|
||||
create_filter=string(default="")
|
||||
manage_filters=string(default="")
|
||||
manage_accounts=string(default="")
|
||||
@@ -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")
|
||||
@@ -59,4 +58,14 @@ 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")
|
||||
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="")
|
||||
@@ -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"),
|
||||
@@ -54,4 +54,15 @@ 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"),
|
||||
"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"),
|
||||
}
|
||||
@@ -14,7 +14,7 @@ persist_size = integer(default=0)
|
||||
load_cache_in_memory=boolean(default=True)
|
||||
show_screen_names = boolean(default=False)
|
||||
hide_emojis = boolean(default=False)
|
||||
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications'))
|
||||
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications', 'announcements'))
|
||||
boost_mode = string(default="ask")
|
||||
disable_streaming = boolean(default=False)
|
||||
|
||||
@@ -54,6 +54,7 @@ post = string(default="$display_name, $safe_text $image_descriptions $date. $vis
|
||||
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
||||
conversation = string(default="Conversation with $users. Last message: $last_post")
|
||||
notification = string(default="$display_name $text, $date")
|
||||
announcement = string(default="$text. Published $published_at. $read")
|
||||
|
||||
[filters]
|
||||
|
||||
|
||||
@@ -85,3 +85,9 @@ def compose_notification(notification, db, settings, relative_times, show_screen
|
||||
if filtered != None:
|
||||
text = _("hidden by filter {}").format(filtered)
|
||||
return [user, text, ts]
|
||||
|
||||
def compose_announcement(announcement, db, settings, relative_times, show_screen_names, safe=False):
|
||||
# Use the default template or a configured one if available
|
||||
template = settings.get("templates", {}).get("announcement", templates.announcement_default_template)
|
||||
text = templates.render_announcement(announcement, template, settings, relative_times, db["utc_offset"])
|
||||
return [text]
|
||||
@@ -217,35 +217,67 @@ 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")
|
||||
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)
|
||||
# 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:
|
||||
scheduled_at = obj.get("scheduled_at")
|
||||
|
||||
# Prepare media and polls first as they are needed for both standard and quote posts
|
||||
media_ids = []
|
||||
try:
|
||||
poll = None
|
||||
if len(obj["attachments"]) > 0:
|
||||
try:
|
||||
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)
|
||||
except Exception as e:
|
||||
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, lang=language)
|
||||
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=[]):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,3 +3,4 @@ from .base import basePanel
|
||||
from .conversationList import conversationListPanel
|
||||
from .notifications import notificationsPanel
|
||||
from .user import userPanel
|
||||
from .announcements import announcementsPanel
|
||||
|
||||
36
src/wxUI/buffers/mastodon/announcements.py
Normal file
36
src/wxUI/buffers/mastodon/announcements.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import wx
|
||||
from multiplatform_widgets import widgets
|
||||
|
||||
class announcementsPanel(wx.Panel):
|
||||
|
||||
def set_focus_function(self, f):
|
||||
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, f)
|
||||
|
||||
def create_list(self):
|
||||
self.list = widgets.list(self, _("Announcement"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||
self.list.set_windows_size(0, 800)
|
||||
self.list.set_size()
|
||||
|
||||
def __init__(self, parent, name):
|
||||
super(announcementsPanel, self).__init__(parent)
|
||||
self.name = name
|
||||
self.type = "baseBuffer"
|
||||
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.create_list()
|
||||
self.dismiss = wx.Button(self, -1, _("Dismiss"))
|
||||
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
|
||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||
self.SetSizer(self.sizer)
|
||||
self.SetClientSize(self.sizer.CalcMin())
|
||||
|
||||
def set_position(self, reversed=False):
|
||||
if reversed == False:
|
||||
self.list.select_item(self.list.get_count()-1)
|
||||
else:
|
||||
self.list.select_item(0)
|
||||
|
||||
def set_focus_in_list(self):
|
||||
self.list.list.SetFocus()
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
@@ -14,6 +16,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"))
|
||||
@@ -36,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"))
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.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":
|
||||
{"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.13/TWBlue_portable_v2026.01.13.zip"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user