Feat: display users who boosted or favorited a post.

This commit is contained in:
Manuel Cortez 2024-05-17 13:58:54 -06:00
parent fd1a64c7b8
commit aee2a3b8b2
No known key found for this signature in database
GPG Key ID: 9E0735CA15EFE790
8 changed files with 165 additions and 75 deletions

View File

@ -7,6 +7,7 @@ TWBlue Changelog
* The translation module has been rewritten. Now, instead of offering translations with Google Translator, the user can choose between [LibreTranslate,](https://github.com/LibreTranslate/LibreTranslate) which requires no configuration thanks to the [instance of the NVDA Spanish community;](https://translate.nvda.es) or translate using [DeepL,](https://deepl.com) for which it is necessary to create an account on DeepL and [subscribe to a DeepL API Free plan](https://support.deepl.com/hc/en-us/articles/360021200939-DeepL-API-Free) to obtain the API key which can be used to translate up to 500000 characters every month. The API key can be entered in the global options dialog, under a new tab called translation services. When translating a text, the translation engine can be changed. When changing the translation engine, the target language must be selected again before translation takes place. * The translation module has been rewritten. Now, instead of offering translations with Google Translator, the user can choose between [LibreTranslate,](https://github.com/LibreTranslate/LibreTranslate) which requires no configuration thanks to the [instance of the NVDA Spanish community;](https://translate.nvda.es) or translate using [DeepL,](https://deepl.com) for which it is necessary to create an account on DeepL and [subscribe to a DeepL API Free plan](https://support.deepl.com/hc/en-us/articles/360021200939-DeepL-API-Free) to obtain the API key which can be used to translate up to 500000 characters every month. The API key can be entered in the global options dialog, under a new tab called translation services. When translating a text, the translation engine can be changed. When changing the translation engine, the target language must be selected again before translation takes place.
* TWBlue should be able to switch to Windows 11 Keymap when running under Windows 11. ([#494](https://github.com/mcv-software/twblue/issues/494)) * TWBlue should be able to switch to Windows 11 Keymap when running under Windows 11. ([#494](https://github.com/mcv-software/twblue/issues/494))
* Mastodon: * Mastodon:
* When viewing a post, a button displays the number of boosts and times it has been added to favorites. Clicking on that button will open a list of users who have interacted with the post. From that list, it is possible to view profiles and perform common user actions.
* Fixed an error that caused TWBlue to be unable to properly display the user action dialog from the followers or following buffer. ([#575](https://github.com/mcv-software/twblue/issues/575)) * Fixed an error that caused TWBlue to be unable to properly display the user action dialog from the followers or following buffer. ([#575](https://github.com/mcv-software/twblue/issues/575))
## changes in version 2024.01.05 ## changes in version 2024.01.05

View File

@ -550,7 +550,7 @@ class BaseBuffer(base.Buffer):
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(item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item)) msg = messages.viewPost(self.session, 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

@ -10,6 +10,7 @@ from controller import messages
from sessions.mastodon import templates from sessions.mastodon import templates
from wxUI.dialogs.mastodon import postDialogs from wxUI.dialogs.mastodon import postDialogs
from extra.autocompletionUsers import completion from extra.autocompletionUsers import completion
from . import userList
def character_count(post_text, post_cw, character_limit=500): def character_count(post_text, post_cw, character_limit=500):
# We will use text for counting character limit only. # We will use text for counting character limit only.
@ -235,9 +236,11 @@ class post(messages.basicMessage):
self.message.visibility.SetSelection(setting) self.message.visibility.SetSelection(setting)
class viewPost(post): class viewPost(post):
def __init__(self, post, offset_hours=0, date="", item_url=""): def __init__(self, session, post, offset_hours=0, date="", item_url=""):
self.session = session
if post.reblog != None: if post.reblog != None:
post = post.reblog post = post.reblog
self.post_id = post.id
author = post.account.display_name if post.account.display_name != "" else post.account.username author = post.account.display_name if post.account.display_name != "" else post.account.username
title = _(u"Post from {}").format(author) title = _(u"Post from {}").format(author)
image_description = templates.process_image_descriptions(post.media_attachments) image_description = templates.process_image_descriptions(post.media_attachments)
@ -264,12 +267,24 @@ class viewPost(post):
widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share) widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share)
self.item_url = item_url self.item_url = item_url
widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate) widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate)
widgetUtils.connect_event(self.message.boosts_button, widgetUtils.BUTTON_PRESSED, self.on_boosts)
widgetUtils.connect_event(self.message.favorites_button, widgetUtils.BUTTON_PRESSED, self.on_favorites)
self.message.ShowModal() self.message.ShowModal()
# We won't need text_processor in this dialog, so let's avoid it. # We won't need text_processor in this dialog, so let's avoid it.
def text_processor(self): def text_processor(self):
pass pass
def on_boosts(self, *args, **kwargs):
users = self.session.api.status_reblogged_by(self.post_id)
title = _("people who boosted this post")
user_list = userList.MastodonUserList(session=self.session, users=users, title=title)
def on_favorites(self, *args, **kwargs):
users = self.session.api.status_favourited_by(self.post_id)
title = _("people who favorited this post")
user_list = userList.MastodonUserList(session=self.session, users=users, title=title)
def share(self, *args, **kwargs): def share(self, *args, **kwargs):
if hasattr(self, "item_url"): if hasattr(self, "item_url"):
output.copy(self.item_url) output.copy(self.item_url)

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from mastodon import MastodonError
from wxUI.dialogs.mastodon import showUserProfile
from controller.userList import UserListController
from . import userActions
class MastodonUserList(UserListController):
def process_users(self, users):
return [dict(id=user.id, display_name=f"{user.display_name} (@{user.acct})", acct=user.acct) for user in users]
def on_actions(self, *args, **kwargs):
user = self.dialog.user_list.GetSelection()
user_account = self.users[user]
u = userActions.userActions(self.session, [user_account.get("acct")])
def on_details(self, *args, **kwargs):
user = self.dialog.user_list.GetSelection()
user_id = self.users[user].get("id")
try:
user_object = self.session.api.account(user_id)
except MastodonError:
return
dlg = showUserProfile.ShowUserProfile(user_object)
dlg.ShowModal()

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import widgetUtils
from pubsub import pub
from wxUI.dialogs import userList
class UserListController(object):
def __init__(self, users, session, title):
super(UserListController, self).__init__()
self.session = session
self.users = self.process_users(users)
self.dialog = userList.UserListDialog(title=title, users=[user.get("display_name", user.get("acct")) for user in self.users])
widgetUtils.connect_event(self.dialog.actions_button, widgetUtils.BUTTON_PRESSED, self.on_actions)
widgetUtils.connect_event(self.dialog.details_button, widgetUtils.BUTTON_PRESSED, self.on_details)
self.dialog.ShowModal()
def process_users(self, users):
return {}
def on_actions(self):
pass
def on_details(self, *args, **kwargs):
pass

View File

@ -1,7 +0,0 @@
ffmpeg -hwaccel qsv -c:v h264_qsv -i "$1" -map 0 -c copy -c:v hevc_qsv -preset slow -global_quality 22 -look_ahead 1 "final/${1%.*}.mkv"
#!/bin/bash
for i in *.wmv;
do
ffmpeg -i "$i" -c:v libx264 -c:a aac -vf scale=640:480 -crf 20 "final/$i.mp4"
done

View File

@ -164,80 +164,81 @@ class Post(wx.Dialog):
def unable_to_attach_poll(self, *args, **kwargs): def unable_to_attach_poll(self, *args, **kwargs):
return wx.MessageDialog(self, _("You can add a poll or media files. In order to add your poll, please remove other attachments first."), _("Error adding poll"), wx.ICON_ERROR).ShowModal() return wx.MessageDialog(self, _("You can add a poll or media files. In order to add your poll, please remove other attachments first."), _("Error adding poll"), wx.ICON_ERROR).ShowModal()
import wx
class viewPost(wx.Dialog): class viewPost(wx.Dialog):
def set_title(self, lenght): def set_title(self, length):
self.SetTitle(_("Post - %i characters ") % (lenght,)) self.SetTitle(_("Post - %i characters ") % length)
def __init__(self, text="", boosts_count=0, favs_count=0, source="", date="", privacy="", *args, **kwargs): def __init__(self, text="", boosts_count=0, favs_count=0, source="", date="", privacy="", *args, **kwargs):
super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850, 850)) super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850, 850))
self.init_ui(text, boosts_count, favs_count, source, date, privacy)
def init_ui(self, text, boosts_count, favs_count, source, date, privacy):
panel = wx.Panel(self) panel = wx.Panel(self)
label = wx.StaticText(panel, -1, _("Post")) main_sizer = wx.BoxSizer(wx.VERTICAL)
self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180)) main_sizer.Add(self.create_text_section(panel, text), 1, wx.EXPAND | wx.ALL, 5)
self.text.SetFocus() main_sizer.Add(self.create_image_description_section(panel), 1, wx.EXPAND | wx.ALL, 5)
textBox = wx.BoxSizer(wx.HORIZONTAL) main_sizer.Add(self.create_info_section(panel, privacy, boosts_count, favs_count, source, date), 0, wx.EXPAND | wx.ALL, 5)
textBox.Add(label, 0, wx.ALL, 5) main_sizer.Add(self.create_buttons_section(panel), 0, wx.ALIGN_RIGHT | wx.ALL, 5)
textBox.Add(self.text, 1, wx.EXPAND, 5) panel.SetSizer(main_sizer)
mainBox = wx.BoxSizer(wx.VERTICAL) self.SetClientSize(main_sizer.CalcMin())
mainBox.Add(textBox, 0, wx.ALL, 5)
label2 = wx.StaticText(panel, -1, _("Image description")) def create_text_section(self, panel, text):
self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180)) sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Post")), wx.VERTICAL)
self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY | wx.TE_MULTILINE)
sizer.Add(self.text, 1, wx.EXPAND | wx.ALL, 5)
return sizer
def create_image_description_section(self, panel):
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Image description")), wx.VERTICAL)
self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY | wx.TE_MULTILINE)
self.image_description.Enable(False) self.image_description.Enable(False)
iBox = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.image_description, 1, wx.EXPAND | wx.ALL, 5)
iBox.Add(label2, 0, wx.ALL, 5) return sizer
iBox.Add(self.image_description, 1, wx.EXPAND, 5)
mainBox.Add(iBox, 0, wx.ALL, 5) def create_info_section(self, panel, privacy, boosts_count, favs_count, source, date):
privacyLabel = wx.StaticText(panel, -1, _("Privacy")) sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Information")), wx.VERTICAL)
privacy = wx.TextCtrl(panel, -1, privacy, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) flex_sizer = wx.FlexGridSizer(cols=3, hgap=10, vgap=10)
privacyBox = wx.BoxSizer(wx.HORIZONTAL) flex_sizer.AddGrowableCol(1)
privacyBox.Add(privacyLabel, 0, wx.ALL, 5) flex_sizer.Add(wx.StaticText(panel, -1, _("Privacy")), 0, wx.ALIGN_CENTER_VERTICAL)
privacyBox.Add(privacy, 0, wx.ALL, 5) flex_sizer.Add(wx.TextCtrl(panel, -1, privacy, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
boostsCountLabel = wx.StaticText(panel, -1, _(u"Boosts: ")) flex_sizer.Add(self.create_boosts_section(panel, boosts_count), 1, wx.EXPAND | wx.ALL, 5)
boostsCount = wx.TextCtrl(panel, -1, str(boosts_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) flex_sizer.Add(self.create_favorites_section(panel, favs_count), 1, wx.EXPAND | wx.ALL, 5)
boostBox = wx.BoxSizer(wx.HORIZONTAL) flex_sizer.Add(wx.StaticText(panel, -1, _("Source")), 0, wx.ALIGN_CENTER_VERTICAL)
boostBox.Add(boostsCountLabel, 0, wx.ALL, 5) flex_sizer.Add(wx.TextCtrl(panel, -1, source, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
boostBox.Add(boostsCount, 0, wx.ALL, 5) flex_sizer.Add(wx.StaticText(panel, -1, _("Date")), 0, wx.ALIGN_CENTER_VERTICAL)
favsCountLabel = wx.StaticText(panel, -1, _("Favorites: ")) flex_sizer.Add(wx.TextCtrl(panel, -1, date, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
favsCount = wx.TextCtrl(panel, -1, str(favs_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) sizer.Add(flex_sizer, 1, wx.EXPAND | wx.ALL, 5)
favsBox = wx.BoxSizer(wx.HORIZONTAL) return sizer
favsBox.Add(favsCountLabel, 0, wx.ALL, 5)
favsBox.Add(favsCount, 0, wx.ALL, 5) def create_boosts_section(self, panel, boosts_count):
sourceLabel = wx.StaticText(panel, -1, _("Source: ")) sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Boosts")), wx.VERTICAL)
source = wx.TextCtrl(panel, -1, source, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) self.boosts_button = wx.Button(panel, -1, str(boosts_count))
sourceBox = wx.BoxSizer(wx.HORIZONTAL) self.boosts_button.SetToolTip(_("View users who boosted this post"))
sourceBox.Add(sourceLabel, 0, wx.ALL, 5) sizer.Add(self.boosts_button, 1, wx.EXPAND | wx.ALL, 5)
sourceBox.Add(source, 0, wx.ALL, 5) return sizer
dateLabel = wx.StaticText(panel, -1, _(u"Date: "))
date = wx.TextCtrl(panel, -1, date, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) def create_favorites_section(self, panel, favs_count):
dateBox = wx.BoxSizer(wx.HORIZONTAL) sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Favorites")), wx.VERTICAL)
dateBox.Add(dateLabel, 0, wx.ALL, 5) self.favorites_button = wx.Button(panel, -1, str(favs_count))
dateBox.Add(date, 0, wx.ALL, 5) self.favorites_button.SetToolTip(_("View users who favorited this post"))
infoBox = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.favorites_button, 1, wx.EXPAND | wx.ALL, 5)
infoBox.Add(privacyBox, 0, wx.ALL, 5) return sizer
infoBox.Add(boostBox, 0, wx.ALL, 5)
infoBox.Add(favsBox, 0, wx.ALL, 5) def create_buttons_section(self, panel):
infoBox.Add(sourceBox, 0, wx.ALL, 5) sizer = wx.BoxSizer(wx.HORIZONTAL)
mainBox.Add(infoBox, 0, wx.ALL, 5)
mainBox.Add(dateBox, 0, wx.ALL, 5)
self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard")) self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard"))
self.share.Enable(False) self.share.Enable(False)
self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize) self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
self.translateButton = wx.Button(panel, -1, _(u"&Translate..."), size=wx.DefaultSize) self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
cancelButton = wx.Button(panel, wx.ID_CANCEL, _(u"C&lose"), size=wx.DefaultSize) cancelButton = wx.Button(panel, wx.ID_CANCEL, _("C&lose"))
cancelButton.SetDefault() cancelButton.SetDefault()
buttonsBox = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.share, 0, wx.ALL, 5)
buttonsBox.Add(self.share, 0, wx.ALL, 5) sizer.Add(self.spellcheck, 0, wx.ALL, 5)
buttonsBox.Add(self.spellcheck, 0, wx.ALL, 5) sizer.Add(self.translateButton, 0, wx.ALL, 5)
buttonsBox.Add(self.translateButton, 0, wx.ALL, 5) sizer.Add(cancelButton, 0, wx.ALL, 5)
buttonsBox.Add(cancelButton, 0, wx.ALL, 5) return sizer
mainBox.Add(buttonsBox, 0, wx.ALL, 5)
selectId = wx.ID_ANY
self.Bind(wx.EVT_MENU, self.onSelect, id=selectId)
self.accel_tbl = wx.AcceleratorTable([
(wx.ACCEL_CTRL, ord('A'), selectId),
])
self.SetAcceleratorTable(self.accel_tbl)
panel.SetSizer(mainBox)
self.SetClientSize(mainBox.CalcMin())
def set_text(self, text): def set_text(self, text):
self.text.ChangeValue(text) self.text.ChangeValue(text)

View File

@ -0,0 +1,32 @@
import wx
class UserListDialog(wx.Dialog):
def __init__(self, parent=None, title="", users=[]):
super(UserListDialog, self).__init__(parent=parent, title=title, size=(400, 300))
self.users = users
self.init_ui()
def init_ui(self):
panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL)
title_text = wx.StaticText(panel, label=self.GetTitle())
title_font = title_text.GetFont()
title_font.PointSize += 2
title_font = title_font.Bold()
title_text.SetFont(title_font)
main_sizer.Add(title_text, 0, wx.ALIGN_CENTER | wx.TOP, 10)
user_list_box = wx.StaticBox(panel, wx.ID_ANY, "Users")
user_list_sizer = wx.StaticBoxSizer(user_list_box, wx.VERTICAL)
self.user_list = wx.ListBox(panel, wx.ID_ANY, choices=self.users, style=wx.LB_SINGLE)
user_list_sizer.Add(self.user_list, 1, wx.EXPAND | wx.ALL, 10)
main_sizer.Add(user_list_sizer, 1, wx.EXPAND | wx.ALL, 15)
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.actions_button = wx.Button(panel, wx.ID_ANY, "Actions")
buttons_sizer.Add(self.actions_button, 0, wx.RIGHT, 10)
self.details_button = wx.Button(panel, wx.ID_ANY, _("View profile"))
buttons_sizer.Add(self.details_button, 0, wx.RIGHT, 10)
close_button = wx.Button(panel, wx.ID_CANCEL, "Close")
buttons_sizer.Add(close_button, 0)
main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15)
panel.SetSizer(main_sizer)
# self.SetSizerAndFit(main_sizer)