mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-02-04 21:24:34 +01:00
Compare commits
23 Commits
v2026.01.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
643f2ca4b7 | ||
|
|
769483eaf5 | ||
|
|
f433970574 | ||
|
|
aa6d8a28c9 | ||
|
|
99760751eb | ||
|
|
e2156efa14 | ||
|
|
a919d31f7c | ||
|
|
1f7459800a | ||
|
|
3b64f5f35a | ||
|
|
cdb97579e9 | ||
|
|
51d019f035 | ||
| beb676d9ab | |||
| e5822ac8ee | |||
| 320c7a361c | |||
| 7c131b6936 | |||
| 198b1eefb7 | |||
| 363d2082c0 | |||
|
|
d65109d935 | ||
| 9688c20dd9 | |||
| 4d20d7744a | |||
| 29e52288df | |||
| 9ed2f6771e | |||
|
|
0512d53043 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -2,6 +2,14 @@ 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.
|
||||
* 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:
|
||||
|
||||
@@ -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,15 +21,15 @@ 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.2
|
||||
oauthlib==3.3.1
|
||||
packaging==25.0
|
||||
packaging==26.0
|
||||
pillow==12.1.0
|
||||
platform_utils @ git+https://github.com/accessibleapps/platform_utils@e0d79f7b399c4ea677a633d2dde9202350d62c38
|
||||
pluggy==1.6.0
|
||||
psutil==7.2.1
|
||||
psutil==7.2.2
|
||||
pyenchant==3.3.0
|
||||
pypiwin32==223
|
||||
Pypubsub==4.0.7
|
||||
@@ -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
|
||||
|
||||
@@ -126,14 +126,16 @@ class BaseBuffer(base.Buffer):
|
||||
min_id = None
|
||||
# toDo: Implement reverse timelines properly here.
|
||||
if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
|
||||
if self.session.settings["general"]["reverse_timelines"]:
|
||||
min_id = self.session.db[self.name][0].id
|
||||
else:
|
||||
min_id = self.session.db[self.name][-1].id
|
||||
# We use the maximum ID present in the buffer to ensure we only request posts
|
||||
# that are newer than our most recent chronological post.
|
||||
# This prevents old pinned posts from pulling in hundreds of previous statuses.
|
||||
min_id = max(item.id for item in self.session.db[self.name])
|
||||
# loads pinned posts from user accounts.
|
||||
# Load those posts only when there are no items previously loaded.
|
||||
if "-timeline" in self.name and "account_statuses" in self.function and len(self.session.db.get(self.name, [])) == 0:
|
||||
pinned_posts = self.session.api.account_statuses(pinned=True, limit=count, *self.args, **self.kwargs)
|
||||
for p in pinned_posts:
|
||||
p["pinned"] = True
|
||||
pinned_posts.reverse()
|
||||
else:
|
||||
pinned_posts = None
|
||||
@@ -182,10 +184,17 @@ class BaseBuffer(base.Buffer):
|
||||
|
||||
def get_more_items(self):
|
||||
elements = []
|
||||
if self.session.settings["general"]["reverse_timelines"] == False:
|
||||
max_id = self.session.db[self.name][0].id
|
||||
if len(self.session.db[self.name]) == 0:
|
||||
return
|
||||
# We use the minimum ID in the buffer to correctly request the next page of older items.
|
||||
# This prevents old pinned posts from causing us to skip chronological posts.
|
||||
# We try to exclude pinned posts from this calculation as they are usually outliers at the top.
|
||||
unpinned_ids = [item.id for item in self.session.db[self.name] if not getattr(item, "pinned", False)]
|
||||
if unpinned_ids:
|
||||
max_id = min(unpinned_ids)
|
||||
else:
|
||||
max_id = self.session.db[self.name][-1].id
|
||||
max_id = min(item.id for item in self.session.db[self.name])
|
||||
|
||||
try:
|
||||
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
|
||||
except Exception as e:
|
||||
@@ -311,8 +320,10 @@ class BaseBuffer(base.Buffer):
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
||||
if self.can_share() == True:
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.quote, menuitem=menu.quote)
|
||||
else:
|
||||
menu.boost.Enable(False)
|
||||
menu.quote.Enable(False)
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.mute_conversation, menuitem=menu.mute)
|
||||
@@ -442,11 +453,30 @@ class BaseBuffer(base.Buffer):
|
||||
id = item.id
|
||||
if self.session.settings["general"]["boost_mode"] == "ask":
|
||||
answer = mastodon_dialogs.boost_question()
|
||||
if answer == True:
|
||||
if answer == 1:
|
||||
self._direct_boost(id)
|
||||
elif answer == 2:
|
||||
self.quote(item=item)
|
||||
else:
|
||||
self._direct_boost(id)
|
||||
|
||||
def quote(self, event=None, item=None, *args, **kwargs):
|
||||
if item == None:
|
||||
item = self.get_item()
|
||||
if self.can_share(item=item) == False:
|
||||
return output.speak(_("This action is not supported on conversations."))
|
||||
|
||||
title = _("Quote post")
|
||||
caption = _("Write your comment here")
|
||||
post = messages.post(session=self.session, title=title, caption=caption)
|
||||
|
||||
response = post.message.ShowModal()
|
||||
if response == wx.ID_OK:
|
||||
post_data = post.get_data()
|
||||
call_threaded(self.session.send_post, quote_id=item.id, posts=post_data, visibility=post.get_visibility(), language=post.get_language(), **kwargs)
|
||||
if hasattr(post.message, "destroy"):
|
||||
post.message.destroy()
|
||||
|
||||
def _direct_boost(self, id):
|
||||
item = self.session.api_call(call_name="status_reblog", _sound="retweet_send.ogg", id=id)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
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")
|
||||
@@ -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")
|
||||
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")
|
||||
@@ -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")
|
||||
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")
|
||||
@@ -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")
|
||||
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="")
|
||||
@@ -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")
|
||||
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")
|
||||
@@ -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")
|
||||
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"),
|
||||
@@ -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"),
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user