Mastodon: Added actions for notifications. closes #517

This commit is contained in:
Manuel Cortez 2024-01-05 15:49:18 -06:00
parent 3907777c91
commit cdcbcf754a
No known key found for this signature in database
GPG Key ID: 9E0735CA15EFE790
5 changed files with 228 additions and 63 deletions

View File

@ -4,17 +4,19 @@ TWBlue Changelog
* Core: * Core:
* The TWBlue website will no longer be available on the twblue.es domain. Beginning in January 2024, TWBlue will live at https://twblue.mcvsoftware.com. Also, we will start releasing versions on [gitHub releases](https://github.com/mcv-software/twblue/releases) so it will be easier to track specific versions. * The TWBlue website will no longer be available on the twblue.es domain. Beginning in January 2024, TWBlue will live at https://twblue.mcvsoftware.com. Also, we will start releasing versions on [gitHub releases](https://github.com/mcv-software/twblue/releases) so it will be easier to track specific versions.
* As of the first release of TWBlue in 2024, we will officially stop generating 32-bit (X86) compatible binaries due to the increasing difficulty of generating versions compatible with this architecture in modern Python versions. * As of the first release of TWBlue in 2024, we will officially stop generating 32-bit (X86) compatible binaries due to the increasing difficulty of generating versions compatible with this architecture in modern Python.
* TWBlue should be more reliable when checking for updates. * TWBlue should be more reliable when checking for updates.
* If running from source, automatic updates will not be checked as this works only for distribution. ([#540](https://github.com/MCV-Software/TWBlue/pull/540)) * If running from source, automatic updates will not be checked as this works only for distribution. ([#540](https://github.com/MCV-Software/TWBlue/pull/540))
* Fixed the 'report an error' item in the help menu. Now this item redirects to our gitHub issue tracker. ([#524](https://github.com/MCV-Software/TWBlue/pull/524)) * Fixed the 'report an error' item in the help menu. Now this item redirects to our gitHub issue tracker. ([#524](https://github.com/MCV-Software/TWBlue/pull/524))
* Mastodon: * Mastodon:
* Implemented actions for the notifications buffer on a mastodon instance. Actions can be performed from the contextual menu on every notification, or by using invisible keystrokes. ([#517](https://github.com/mcv-software/twblue(issues/517))
* Implemented update profile dialog. ([#547](https://github.com/MCV-Software/TWBlue/pull/547)) * Implemented update profile dialog. ([#547](https://github.com/MCV-Software/TWBlue/pull/547))
* It is possible to display an user profile from the user menu within the menu bar, or by using the invisible keystroke for user details. ([#555](https://github.com/MCV-Software/TWBlue/pull/555)) * It is possible to display an user profile from the user menu within the menu bar, or by using the invisible keystroke for user details. ([#555](https://github.com/MCV-Software/TWBlue/pull/555))
* Added possibility to vote in polls. * Added possibility to vote in polls. This is mapped to Alt+Win+Shift+V in the invisible keymaps for windows 10/11.
* Added posts search. Take into account that Mastodon instances should be configured with full text search enabled. Search for posts only include posts the logged-in user has interacted with. ([#541](https://github.com/MCV-Software/TWBlue/pull/541)) * Added posts search. Take into account that Mastodon instances should be configured with full text search enabled. Search for posts only include posts the logged-in user has interacted with. ([#541](https://github.com/MCV-Software/TWBlue/pull/541))
* Added user autocompletion settings in account settings dialog, so it is possible to ask TWBlue to scan mastodon accounts and add people from followers and following buffers. For now, user autocompletion can be used only when composing new posts or replies. * Added user autocompletion settings in account settings dialog, so it is possible to ask TWBlue to scan mastodon accounts and add people from followers and following buffers. For now, user autocompletion can be used only when composing new posts or replies.
* TWBlue should be able to ignore deleted direct messages or messages from deleted accounts. Previously, a direct message that no longer existed in the instance caused errors when loading the direct messages buffer and could potentially affect notifications as well. * TWBlue should be able to ignore deleted direct messages or messages from deleted accounts. Previously, a direct message that no longer existed in the instance caused errors when loading the direct messages buffer and could potentially affect notifications as well.
* TWBlue should be able to ignore notifications from deleted accounts or posts.
## changes in version 2023.4.13 ## changes in version 2023.4.13

View File

@ -263,7 +263,10 @@ class BaseBuffer(base.Buffer):
menu = menus.base() menu = menus.base()
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply) widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions) widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost) if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
else:
menu.boost.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav) 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.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl) widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
@ -310,14 +313,16 @@ class BaseBuffer(base.Buffer):
if index > -1 and self.session.db.get(self.name) != None: if index > -1 and self.session.db.get(self.name) != None:
return self.session.db[self.name][index] return self.session.db[self.name][index]
def can_share(self): def can_share(self, item=None):
post = self.get_item() if item == None:
if post.visibility == "direct": item = self.get_item()
if item.visibility == "direct":
return False return False
return True return True
def reply(self, *args): def reply(self, item=None, *args, **kwargs):
item = self.get_item() if item == None:
item = self.get_item()
visibility = item.visibility visibility = item.visibility
if visibility == "direct": if visibility == "direct":
title = _("Conversation with {}").format(item.account.username) title = _("Conversation with {}").format(item.account.username)
@ -352,8 +357,9 @@ class BaseBuffer(base.Buffer):
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()
def send_message(self, *args, **kwargs): def send_message(self, item=None, *args, **kwargs):
item = self.get_item() if item == None:
item = self.get_item()
title = _("Conversation with {}").format(item.account.username) title = _("Conversation with {}").format(item.account.username)
caption = _("Write your message here") caption = _("Write your message here")
if item.reblog != None: if item.reblog != None:
@ -378,11 +384,12 @@ class BaseBuffer(base.Buffer):
if hasattr(post.message, "destroy"): if hasattr(post.message, "destroy"):
post.message.destroy() post.message.destroy()
def share_item(self, *args, **kwargs): def share_item(self, item=None, *args, **kwargs):
if self.can_share() == False: if item == None:
item = self.get_item()
if self.can_share(item=item) == False:
return output.speak(_("This action is not supported on conversations.")) return output.speak(_("This action is not supported on conversations."))
post = self.get_item() id = item.id
id = post.id
if self.session.settings["general"]["boost_mode"] == "ask": if self.session.settings["general"]["boost_mode"] == "ask":
answer = mastodon_dialogs.boost_question() answer = mastodon_dialogs.boost_question()
if answer == True: if answer == True:
@ -407,12 +414,11 @@ class BaseBuffer(base.Buffer):
pub.sendMessage("toggleShare", shareable=can_share) pub.sendMessage("toggleShare", shareable=can_share)
self.buffer.boost.Enable(can_share) self.buffer.boost.Enable(can_share)
def audio(self, url='', *args, **kwargs): def audio(self, item=None, *args, **kwargs):
if sound.URLPlayer.player.is_playing(): if sound.URLPlayer.player.is_playing():
return sound.URLPlayer.stop_audio() return sound.URLPlayer.stop_audio()
item = self.get_item()
if item == None: if item == None:
return item = self.get_item()
urls = utils.get_media_urls(item) urls = utils.get_media_urls(item)
if len(urls) == 1: if len(urls) == 1:
url=urls[0] url=urls[0]
@ -428,25 +434,25 @@ class BaseBuffer(base.Buffer):
# except: # except:
# log.error("Exception while executing audio method.") # log.error("Exception while executing audio method.")
def url(self, url='', announce=True, *args, **kwargs): def url(self, announce=True, item=None, *args, **kwargs):
if url == '': if item == None:
post = self.get_item() item = self.get_item()
if post.reblog != None: if item.reblog != None:
urls = utils.find_urls(post.reblog) urls = utils.find_urls(item.reblog)
else: else:
urls = utils.find_urls(post) urls = utils.find_urls(item)
if len(urls) == 1: if len(urls) == 1:
url=urls[0] url=urls[0]
elif len(urls) > 1: elif len(urls) > 1:
urls_list = urlList.urlList() urls_list = urlList.urlList()
urls_list.populate_list(urls) urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK: if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string() url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy() if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '': if url != '':
if announce: if announce:
output.speak(_(u"Opening URL..."), True) output.speak(_(u"Opening URL..."), True)
webbrowser.open_new_tab(url) webbrowser.open_new_tab(url)
def clear_list(self): def clear_list(self):
dlg = commonMessageDialogs.clear_list() dlg = commonMessageDialogs.clear_list()
@ -476,31 +482,37 @@ class BaseBuffer(base.Buffer):
item = self.get_item() item = self.get_item()
pass pass
def get_item_url(self): def get_item_url(self, item=None):
post = self.get_item() if item == None:
if post.reblog != None: item = self.get_item()
return post.reblog.url if item.reblog != None:
return post.url return item.reblog.url
return item.url
def open_in_browser(self, *args, **kwargs): def open_in_browser(self, item=None, *args, **kwargs):
url = self.get_item_url() if item == None:
item = self.get_item()
url = self.get_item_url(item=item)
output.speak(_("Opening item in web browser...")) output.speak(_("Opening item in web browser..."))
webbrowser.open(url) webbrowser.open(url)
def add_to_favorites(self): def add_to_favorites(self, item=None):
item = self.get_item() if item == None:
item = self.get_item()
if item.reblog != None: if item.reblog != None:
item = item.reblog item = item.reblog
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id) call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
def remove_from_favorites(self): def remove_from_favorites(self, item=None):
item = self.get_item() if item == None:
item = self.get_item()
if item.reblog != None: if item.reblog != None:
item = item.reblog item = item.reblog
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id) call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_favorite(self, *args, **kwargs): def toggle_favorite(self, item=None, *args, **kwargs):
item = self.get_item() if item == None:
item = self.get_item()
if item.reblog != None: if item.reblog != None:
item = item.reblog item = item.reblog
try: try:
@ -513,8 +525,9 @@ class BaseBuffer(base.Buffer):
else: else:
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id) call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_bookmark(self, *args, **kwargs): def toggle_bookmark(self, item=None, *args, **kwargs):
item = self.get_item() if item == None:
item = self.get_item()
if item.reblog != None: if item.reblog != None:
item = item.reblog item = item.reblog
try: try:
@ -527,16 +540,17 @@ class BaseBuffer(base.Buffer):
else: else:
call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id) call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id)
def view_item(self): def view_item(self, item=None):
post = self.get_item() if item == None:
item = self.get_item()
# Update object so we can retrieve newer stats # Update object so we can retrieve newer stats
try: try:
post = self.session.api.status(id=post.id) item = self.session.api.status(id=item.id)
except MastodonNotFoundError: except MastodonNotFoundError:
output.speak(_("No status found with that ID")) output.speak(_("No status found with that ID"))
return return
# print(post) # print(post)
msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url()) msg = messages.viewPost(item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item))
def ocr_image(self): def ocr_image(self):
post = self.get_item() post = self.get_item()

View File

@ -3,15 +3,23 @@ import time
import logging import logging
import widgetUtils import widgetUtils
import output import output
from pubsub import pub
from controller.buffers.mastodon.base import BaseBuffer from controller.buffers.mastodon.base import BaseBuffer
from controller.mastodon import messages
from sessions.mastodon import compose, templates from sessions.mastodon import compose, templates
from wxUI import buffers from wxUI import buffers
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
from wxUI.dialogs.mastodon import menus
from mysc.thread_utils import call_threaded
log = logging.getLogger("controller.buffers.mastodon.notifications") log = logging.getLogger("controller.buffers.mastodon.notifications")
class NotificationsBuffer(BaseBuffer): class NotificationsBuffer(BaseBuffer):
def __init__(self, *args, **kwargs):
super(NotificationsBuffer, self).__init__(*args, **kwargs)
self.type = "notificationsBuffer"
def get_message(self): def get_message(self):
notification = self.get_item() notification = self.get_item()
if notification == None: if notification == None:
@ -36,16 +44,90 @@ class NotificationsBuffer(BaseBuffer):
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.destroy_status, self.buffer.dismiss) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.destroy_status, self.buffer.dismiss)
def fav(self):
pass
def unfav(self):
pass
def vote(self): def vote(self):
pass pass
def can_share(self): def can_share(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
return super(NotificationsBuffer, self).can_share(item=item.status)
return False
def add_to_favorites(self):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).add_to_favorites(item=item.status)
def remove_from_favorites(self):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).remove_from_favorites(item=item.status)
def toggle_favorite(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).toggle_favorite(item=item.status)
def toggle_bookmark(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).toggle_bookmark(item=item.status)
def reply(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).reply(item=item.status)
def share_item(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).share_item(item=item.status)
def url(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).url(item=item.status, *args, **kwargs)
def audio(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).audio(item=item.status)
def view_item(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).view_item(item=item.status)
else:
pub.sendMessage("execute-action", action="user_details")
def open_in_browser(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).open_in_browser(item=item.status)
def send_message(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).send_message(item=item.status)
else:
item = self.get_item()
title = _("New conversation with {}").format(item.account.username)
caption = _("Write your message here")
users_str = "@{} ".format(item.account.acct)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
post.message.visibility.SetSelection(3)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct")
if hasattr(post.message, "destroy"):
post.message.destroy()
def is_post(self):
post_types = ["status", "mention", "reblog", "favourite", "update", "poll"]
item = self.get_item()
if item.type in post_types:
return True
return False return False
def destroy_status(self, *args, **kwargs): def destroy_status(self, *args, **kwargs):
@ -64,3 +146,29 @@ class NotificationsBuffer(BaseBuffer):
self.session.sound.play("error.ogg") self.session.sound.play("error.ogg")
log.exception("") log.exception("")
self.session.db[self.name] = items self.session.db[self.name] = items
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
notification = self.get_item()
menu = menus.notification(notification.type)
if self.is_post():
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
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)
else:
menu.boost.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.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)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.copy, menuitem=menu.copy)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.destroy_status, menuitem=menu.remove)
if hasattr(menu, "openInBrowser"):
widgetUtils.connect_event(menu, widgetUtils.MENU, self.open_in_browser, menuitem=menu.openInBrowser)
if pos != 0:
self.buffer.PopupMenu(menu, pos)
else:
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())

View File

@ -142,6 +142,17 @@ class Handler(object):
users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]] users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users: if item.account.acct not in users:
users.insert(0, item.account.acct) users.insert(0, item.account.acct)
elif buffer.type == "notificationsBuffer":
if buffer.is_post():
status = item.status
if status.reblog != None:
users = [user.acct for user in status.reblog.mentions if user.id != buffer.session.db["user_id"]]
if status.reblog.account.acct not in users and status.account.id != buffer.session.db["user_id"]:
users.insert(0, status.reblog.account.acct)
else:
users = [user.acct for user in status.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users:
users.insert(0, item.account.acct)
u = userActions.userActions(buffer.session, users) u = userActions.userActions(buffer.session, users)
def search(self, controller, session, value): def search(self, controller, session, value):
@ -316,6 +327,8 @@ class Handler(object):
log.debug(f"Opening user profile. dictionary: {item}") log.debug(f"Opening user profile. dictionary: {item}")
mentionedUsers = list() mentionedUsers = list()
holdUser = item.account if item.get('account') else None holdUser = item.account if item.get('account') else None
if hasattr(item, "type") and item.type in ["status", "mention", "reblog", "favourite", "update", "poll"]: # statuses in Notification buffers
item = item.status
if item.get('username'): # account dict if item.get('username'): # account dict
holdUser = item holdUser = item
elif isinstance(item.get('mentions'), list): elif isinstance(item.get('mentions'), list):

View File

@ -26,3 +26,31 @@ class base(wx.Menu):
self.Append(self.remove) self.Append(self.remove)
self.userActions = wx.MenuItem(self, wx.ID_ANY, _(u"&User actions...")) self.userActions = wx.MenuItem(self, wx.ID_ANY, _(u"&User actions..."))
self.Append(self.userActions) self.Append(self.userActions)
class notification(wx.Menu):
def __init__(self, item="status"):
super(notification, self).__init__()
valid_types = ["status", "mention", "reblog", "favourite", "update", "poll"]
if item in valid_types:
self.boost = wx.MenuItem(self, wx.ID_ANY, _("&Boost"))
self.Append(self.boost)
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
self.Append(self.reply)
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
self.Append(self.fav)
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
self.Append(self.unfav)
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
self.Append(self.openUrl)
self.play = wx.MenuItem(self, wx.ID_ANY, _(u"&Play audio"))
self.Append(self.play)
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in instance"))
self.Append(self.openInBrowser)
self.view = wx.MenuItem(self, wx.ID_ANY, _(u"&Show post"))
self.Append(self.view)
self.copy = wx.MenuItem(self, wx.ID_ANY, _(u"&Copy to clipboard"))
self.Append(self.copy)
self.remove = wx.MenuItem(self, wx.ID_ANY, _(u"&Dismiss"))
self.Append(self.remove)
self.userActions = wx.MenuItem(self, wx.ID_ANY, _(u"&User actions..."))
self.Append(self.userActions)