Compare commits

..

86 Commits

Author SHA1 Message Date
7d7a9d72c4 Fixed update file 2023-02-03 12:18:12 -06:00
fcfbae4964 Fixed important issue on updater 2023-02-03 12:16:31 -06:00
aca51a2fb9 Mastodon: Fixed minor issue on notifications handler for streaming API 2023-02-03 11:31:57 -06:00
b4ea6ffcbe Prepare new version release 2023-02-03 10:37:18 -06:00
f69af7aaa1 Twitter: Stop supporting Twitter sessions starting on february 9 2023-02-03 10:29:21 -06:00
a8c5fc8589 Mastodon: Fixed getting more mentions. Closes #508 2023-02-03 10:13:08 -06:00
f87ced817f Fixed visibility setting for replies to dm's. Closes #507 2023-01-29 16:12:37 -06:00
3be01013f4 Mastodon: Use TWBlue user agent, check streaming API health before starting streaming session 2023-01-29 14:34:36 -06:00
fd176f92d3 Mastodon: Set visibility in replies as unlisted by default. Closes #504 2023-01-29 13:11:34 -06:00
97d4fea563 Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!15
2023-01-29 18:51:36 +00:00
b35f2e0fed Translations update from Translations hub 2023-01-29 18:51:35 +00:00
b3851cde95 Updated changelog 2023-01-29 11:55:45 -06:00
4b232d527c Mastodon: Added status updates for subscribed entities to notifications 2023-01-29 11:39:52 -06:00
12b4c8ac23 Merge branch 'next-gen' of github.com:mcv-software/twblue into next-gen 2023-01-05 17:22:22 -06:00
d89c5150f8 Merge branch 'next-gen' of gitlab.mcvsoftware.com:twblue/twblue into next-gen 2023-01-05 17:21:50 -06:00
d17e9ecdac Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!14
2023-01-05 23:21:06 +00:00
76d0866780 Mastodon: TWBlue should be able to ignore sessions if there are errors attempting to log-in 2023-01-05 17:16:34 -06:00
Corentin Bacqué-Cazenave
706616717e Translated using Weblate (French)
Currently translated at 100.0% (941 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2023-01-02 16:39:56 -06:00
7c47d6171a Merge pull request #505 from Mohamed00/NewShortcuts
Mastodon: added new keyboard shortcuts
2022-12-29 13:47:54 -06:00
Mohamed
b743d7af09 Mastodon: added keyboard shortcuts for visibility combo box and sensitive content checkbox 2022-12-29 04:17:58 -05:00
d4219f1705 Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!13
2022-12-26 03:26:06 +00:00
Corentin Bacqué-Cazenave
e25d007149 Translated using Weblate (French)
Currently translated at 100.0% (941 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2022-12-25 11:39:54 -06:00
Nikola Jović
fcb8edbda2 Added translation using Weblate (Serbian) 2022-12-23 13:58:43 -06:00
Nikola Jović
a6ca588115 Translated using Weblate (Serbian)
Currently translated at 100.0% (941 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/sr/
2022-12-23 13:58:43 -06:00
18a7a42b5a Mastodon: Started implementation of read preferences from instance. Currently only content warnings are displayed by taking into accounts values from instance preferences 2022-12-23 13:58:10 -06:00
460cea702b code: updated readme 2022-12-22 11:39:29 -06:00
b14c77b730 Merge branch 'next-gen' of gitlab.mcvsoftware.com:twblue/twblue into next-gen 2022-12-22 11:38:48 -06:00
1e5c7512e4 Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!12
2022-12-22 14:42:48 +00:00
d76dbe318c Merge pull request #503 from CoBC/tr_quote_from
Allow translation of templates text
2022-12-21 11:01:57 -06:00
Nikola Jović
4d4901b029 Translated using Weblate (Serbian)
Currently translated at 95.2% (896 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/sr/
2022-12-21 10:54:17 -06:00
Corentin Bacqué-Cazenave
07128d2e4a Add missing parenthesis 2022-12-21 17:36:05 +01:00
c45ba5e705 Translated using Weblate (Spanish)
Currently translated at 92.2% (284 of 308 strings)

Translation: TWBlue/changelog
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/changelog/es/
2022-12-21 10:24:59 -06:00
Nikola Jović
795cb33efc Translated using Weblate (Serbian)
Currently translated at 89.4% (842 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/sr/
2022-12-21 10:24:59 -06:00
98bcb9d279 Translated using Weblate (Spanish)
Currently translated at 99.8% (940 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/es/
2022-12-21 10:24:59 -06:00
Corentin Bacqué-Cazenave
06cbe0a3b5 Translated using Weblate (French)
Currently translated at 100.0% (941 of 941 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2022-12-21 10:24:59 -06:00
e4f2793aaf Core: Update 'tweet' menu on the menu bar for mastodon sessions 2022-12-21 10:24:44 -06:00
43ae43ce26 core: Fix issues when removing sessions 2022-12-21 10:23:18 -06:00
Corentin Bacqué-Cazenave
7082a5f3ec Translate templates text 2022-12-21 17:21:10 +01:00
c278fba4c7 Code: Delete unneeded code & fixed some typos 2022-12-21 08:45:14 -06:00
cfc8221825 Merge branch 'weblate-twblue-twblue' into 'next-gen'
Translations update from Translations hub

See merge request twblue/twblue!11
2022-12-21 14:23:34 +00:00
Weblate
32c1ed225e Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: TWBlue/changelog
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/changelog/
2022-12-20 17:19:37 -06:00
Weblate
d3b15fcefa Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/
2022-12-20 17:19:36 -06:00
7ec96c47d6 Code: Updated documentation translation catalogs 2022-12-20 17:19:18 -06:00
97812ec8b0 Code: Removed uneeded scripts 2022-12-20 17:13:58 -06:00
d1ca3c9fb2 Fixed merge conflict 2022-12-20 16:52:57 -06:00
Anonymous
492352bc27 Translated using Weblate (Finnish)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fi/
2022-12-20 16:43:48 -06:00
Anonymous
6328c252f7 Translated using Weblate (Hungarian)
Currently translated at 62.8% (500 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/hu/
2022-12-20 16:43:45 -06:00
Anonymous
e1a46f338a Translated using Weblate (French)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2022-12-20 16:43:41 -06:00
Corentin Bacqué-Cazenave
f7e09a05b2 Translated using Weblate (French)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2022-12-20 16:43:38 -06:00
Anonymous
c7116916ba Translated using Weblate (Serbian)
Currently translated at 98.1% (781 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/sr/
2022-12-20 16:43:36 -06:00
Anonymous
f1fbe858e9 Translated using Weblate (Polish)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/pl/
2022-12-20 16:43:32 -06:00
Riku
fc5a1060be Translated using Weblate (Japanese)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ja/
2022-12-20 16:43:29 -06:00
Anonymous
5951276033 Translated using Weblate (Japanese)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ja/
2022-12-20 16:43:28 -06:00
Anonymous
2d761c423f Translated using Weblate (Mongolian)
Currently translated at 45.1% (359 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/mn/
2022-12-20 16:43:24 -06:00
Anonymous
434e2878a7 Translated using Weblate (Russian)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ru/
2022-12-20 16:43:20 -06:00
Anonymous
89cdba5910 Translated using Weblate (Hebrew (Israel))
Currently translated at 10.1% (81 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/he_IL/
2022-12-20 16:43:16 -06:00
Anonymous
e9a885784f Translated using Weblate (Galician)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/gl/
2022-12-20 16:43:12 -06:00
Anonymous
3a968e49aa Translated using Weblate (Catalan)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ca/
2022-12-20 16:43:09 -06:00
Anonymous
c6417962a9 Translated using Weblate (Portuguese)
Currently translated at 90.5% (721 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/pt/
2022-12-20 16:43:05 -06:00
Jonas S. Marques
2f55eca575 Translated using Weblate (Portuguese)
Currently translated at 90.5% (721 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/pt/
2022-12-20 16:43:01 -06:00
Anonymous
1cd66e7f10 Translated using Weblate (Arabic)
Currently translated at 79.0% (629 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ar/
2022-12-20 16:42:59 -06:00
Anonymous
0ddb4e6f32 Translated using Weblate (Croatian)
Currently translated at 95.6% (761 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/hr/
2022-12-20 16:42:55 -06:00
zvonimir stanecic
39aac0a3e7 Translated using Weblate (Croatian)
Currently translated at 95.6% (761 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/hr/
2022-12-20 16:42:52 -06:00
Anonymous
45ab6d953b Translated using Weblate (Italian)
Currently translated at 83.5% (665 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/it/
2022-12-20 16:42:49 -06:00
Anonymous
fdd0d566ad Translated using Weblate (Basque)
Currently translated at 81.2% (647 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/eu/
2022-12-20 16:42:46 -06:00
Anonymous
c606fedda5 Translated using Weblate (Spanish)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/es/
2022-12-20 16:42:42 -06:00
9b0ecdf928 Translated using Weblate (Spanish)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/es/
2022-12-20 16:42:39 -06:00
Anonymous
f5e1ff39be Translated using Weblate (German)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/de/
2022-12-20 16:42:37 -06:00
Steffen Schultz
5dff35fd02 Translated using Weblate (German)
Currently translated at 100.0% (796 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/de/
2022-12-20 16:42:34 -06:00
Anonymous
a5104fd76a Translated using Weblate (Turkish)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/tr/
2022-12-20 16:42:32 -06:00
Anonymous
423b63e486 Translated using Weblate (Romanian)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ro/
2022-12-20 16:42:28 -06:00
Anonymous
25c6db7dd8 Translated using Weblate (Danish)
Currently translated at 86.3% (687 of 796 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/da/
2022-12-20 16:42:25 -06:00
ca40103df7 Core: Keystroke editor will show actions available for current session. Changed wording to use correct terms forh both networks 2022-12-20 13:02:27 -06:00
250b248d25 Core: Update menu bar items when switching between Twitter and Mastodon session to use terms according to the focused network. 2022-12-20 12:21:30 -06:00
efd11b90fb Merge branch 'new-documentation' into next-gen 2022-12-20 09:49:33 -06:00
2a90e7be25 Mastodon: Fixed an issue that prevented TWBlue to open a timeline for users in followers or following buffer. 2022-12-19 17:53:10 -06:00
32a86f1bb4 Core: Updated changelog 2022-12-19 17:34:28 -06:00
b0fa59cc01 Mastodon: Show dialog before dismissing a notification. Mention notifications will make the mention to not be loaded in mentions buffer, as TWBlue reads mentions from the notifications data 2022-12-19 16:50:43 -06:00
b8647c29ea Mastodon: Dismiss notifications from GUI or invisible interface (by using the keystroke to delete an item in current buffer) 2022-12-19 16:21:50 -06:00
1eb9aefbf1 Mastodon: Added notifications in real time from streaming API 2022-12-19 16:07:45 -06:00
d4ebfac317 Core: Skip sessions not yet started when switching accounts in invisible interface 2022-12-19 08:45:05 -06:00
3680349b59 Mastodon: Fix issue when creating user timelines during startup 2022-12-19 02:41:19 -06:00
ec68c7ccae Mastodon: Added initial implementation for notifications buffer (actions not available yet) 2022-12-14 12:12:05 -06:00
4bf155b421 Mastodon: Added compose function for notifications 2022-12-14 12:09:14 -06:00
e63479a261 Mastodon: Add line breaks when new paragraphs are present on posts content 2022-12-14 12:08:02 -06:00
ae0dcc7b21 Generate all versions properly 2022-12-13 15:56:01 -06:00
92 changed files with 63858 additions and 35721 deletions

View File

@@ -120,7 +120,7 @@ twblueWin7:
- cd ..
- move src/twblue.zip artifacts/twblue_windows7_x86.zip
only:
- next-gen
- tags
artifacts:
paths:
- artifacts

View File

@@ -1,21 +1,11 @@
TWBlue -
TWBlue
======
[![Build status](https://ci.appveyor.com/api/projects/status/fml5fu7h1fj8vf6l?svg=true)](https://ci.appveyor.com/project/manuelcortez/twblue)
TW Blue is an app designed to use Twitter simply and efficiently while using minimal system resources.
With this app youll have access to twitter features such as:
TWBlue is a free and open source application that allows you to interact with the main features of Twitter and mastodon from the comfort of a windows software, with 2 different interfaces specially designed for screen reader users.
* Create, reply to, like, retweet and delete tweets,
* Send and delete direct messages,
* See your friends and followers,
* Follow, unfollow, block and report users as spam,
* Open a users timeline, which will allow you to get that users tweets separately,
* Open URLs when attached to a tweet or direct message,
* Play audio tweets
* and more!
See [TWBlue's webpage](http://twblue.es) for more details.
See [TWBlue's webpage](https://twblue.es) for more details.
## Running TWBlue from source
@@ -83,15 +73,7 @@ This dependency has been built using pure basic 4.61. Its source can be found at
* [NSIS,](http://nsis.sourceforge.net/) version 3.04
#### Dependencies required to build the portableApps.com format archive
* [NSIS Portable,](http://portableapps.com/apps/development/nsis_portable) version 3.03
* [PortableApps.com Launcher,](http://portableapps.com/apps/development/portableapps.com_launcher) version 2.2.1
* [PortableApps.com Installer,](http://portableapps.com/apps/development/portableapps.com_installer) version 3.5.11
Important! Install these 3 apps into the same folder, otherwise you won't be able to build the pa.c version. For example: D:\portableApps\NSISPortable, D:\PortableApps\PortableApps.com installer, ...
#### Dependencies to make the spell checker multilingual ####
#### Dependencies to make the spell checker multilingual
In order to add the support for spell checking in more languages than english you need to add some additional dictionaries to pyenchant. These are located on the dictionaries folder under windows-dependencies. Simply copy them to the share/enchant/myspell folder located in your enchant installation. They will be automatically copied when building a binary version.
@@ -136,16 +118,8 @@ If you want to install TWBlue on your computer, you must create the installer fi
### How to generate a translation template
Run the gen_pot.bat file, located in the tools directory. Your python installation must be in your path environment variable. The pot file will appear in the tools directory.
To manage translations in TWBlue, you can install the [Babel package.](https://pypi.org/project/Babel/) You can extract message catalogs and generate the main template file with the following command:
### How to build the portableApps.com archive
If you want to have TWBlue on your PortableApps.com platform, follow these steps:
* Navigate to the src directory, and create a binary version for x86: C:\python37\python setup.py build
* Move the dist directory to the misc\pa.c format\app folder in this repo, and rename it to twblue
* Repeat these steps with Python for x64: C:\python37x64\python setup.py build
* Move the new dist directory to the misc\pa.c format\app folder, and rename it to twblue64
* Run the PortableApps.com Launcher Generator, and follow the wizard. Choose the pa.c format folder and continue to generate the launcher. If the wizard is completed, you will see a file named TWBlue portable.exe inside the pa.c format folder.
* Run the PortableApps.com Installer, and follow the wizard. As in the above step, choose the pa.c format folder. When it completes, you will see a file named TWBluePortable_x.y.paf.exe inside the misc folder, where x.y is the version number.
pybabel extract -o twblue.pot --msgid-bugs-address "manuel@manuelcortez.net" --copyright-holder "MCV software" --input-dirs ..\src
Take into account, though, that we use [weblate](https://weblate.mcvsoftware.com) to track translation work for TWBlue. If you wish to be part of our translation team, please open an issue so we can create an account for you in Weblate.

View File

@@ -2,6 +2,26 @@ TWBlue Changelog
## changes in this version
In this version, TWBlue will no longer support Twitter sessions starting on February 9, due to Twitter's policies prohibiting third-party clients, in addition to the shutdown of the free access to the Twitter API. All Twitter sessions that are active on TWBlue will stop working as of February 9, when the free API access will finally be shut down. It will not be possible to display or add Twitter sessions from the Session manager. From the TWBlue team, we will continue working to improve our support for Mastodon instances and add other social networks in the near future. If you want to keep in touch with the project, you can follow us in our mastodon account, at [@twblue@maaw.social.](https://maaw.social/@twblue)
* In the graphical interface, TWBlue will update menu items, in the menu bar, depending on whether you are focusing a Twitter or Mastodon session. This makes it possible for TWBlue to display the correct terms in each social network. Take into account that there might be unavailable items for the currently active session.
* in the keystroke editor for the invisible interface, TWBlue displays the available shortcuts for the currently active session. Descriptions of those keystrokes are also different for Twitter and mastodon sessions to use correct terms for both networks.
* In the invisible interface, TWBlue will skip sessions that have not been started when using the keyboard shortcut to switch between different accounts.
* Fixed a bug when deleting a session in the session manager dialog. Sessions can now be deleted correctly.
* Mastodon:
* Added basic support to notifications buffer. This buffer shows mastodon notifications in real time. Every notification is attached to a kind of object (posts, users, relationships or polls). At the moment, the only supported action for notification is dismissing, which allows you to remove the notification from the buffer (take into account, though, that mention notifications will remove also the mention in its corresponding buffer, due to the way TWBlue reads mentions from mastodon instances).
* Fixed an issue that was preventing TWBlue to create more than one user timeline during startup.
* Fixed getting more items in mentions buffer. ([#508](https://github.com/mcv-software/twblue/issues/508))
* TWBlue will display properly new paragraphs in mastodon posts.
* In the session manager, Mastodon sessions are now displayed including the instance to avoid confusion.
* TWBlue will now read default visibility preferences when posting new statuses, and display sensitive content. These preferences can be set on the mastodon instance, in the account's preferences section. If you wish to change TWBlue's behavior and have it not read those preferences from your instance, but instead set the default public visibility and hide sensitive content, you can uncheck the Read preferences from instance checkbox in the account options.
* If a mastodon instance is not active or there are errors during login, TWBlue will report it in the log file and will continue with other sessions.
* When replying to someone in a public post, TWBlue will default to "unlisted" as its visibility setting. This is done so replies will not clutter local and federated timelines. This setting might be changed when writing the reply, though. ([#504,](https://github.com/MCV-Software/TWBlue/issues/504))
* TWBlue uses its own user agent in Mastodon sessions, so it will be easier to identify the client for instance admins.
* TWBlue will check if the streaming API endpoints are available before attempting to start Streaming for the current session. Before, TWBlue caused load issues in misconfigured mastodon instances where the streaming API were not available.
## Changes in version 2022.12.13
* per popular request, We will generate a 32-bit portable version of TWBlue available for Windows 7 operating systems. This version will not be supported in our automatic updater, so in case of using such version, you would need to download it manually every time there is a new update. TWBlue will continue to be available for Windows 7 as long as it is possible to build it using Python 3.7.
* Fixed a couple of bugs that were making TWBlue unable to be opened in some computers, related to our translator module and some COM objects handled incorrectly.
* Fixed an issue that was making TWBlue unable to open in certain computers due to errors related to Win32 API'S.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,16 @@
# -*- coding: utf-8 -*-
import datetime
# Make date check for feb 9.
now = datetime.datetime.now()
end_of_twitter = datetime.datetime(2023, 2, 9)
twitter_support_enabled = True
if now >= end_of_twitter:
twitter_support_enabled = False
name = 'TWBlue'
short_name='twblue'
update_url = 'https://twblue.es/updates/updates.php'
mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/updates.json'
mirror_update_url = 'https://raw.githubusercontent.com/mcv-software/TWBlue/next-gen/updates/updates.json'
authors = ["Manuel Cortéz", "José Manuel Delicado"]
authorEmail = "manuel@manuelcortez.net"
copyright = "Copyright (C) 2013-2022, MCV Software."

View File

@@ -3,3 +3,4 @@ from .base import BaseBuffer
from .mentions import MentionsBuffer
from .conversations import ConversationBuffer, ConversationListBuffer
from .users import UserBuffer
from .notifications import NotificationsBuffer

View File

@@ -9,6 +9,7 @@ import config
import sound
import languageHandler
import logging
from mastodon import MastodonNotFoundError
from audio_services import youtube_utils
from controller.buffers.base import base
from controller.mastodon import messages
@@ -72,14 +73,22 @@ class BaseBuffer(base.Buffer):
post.message.destroy()
def get_formatted_message(self):
return self.compose_function(self.get_item(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])[1]
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
return self.compose_function(self.get_item(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)[1]
def get_message(self):
post = self.get_item()
if post == None:
return
template = self.session.settings["templates"]["post"]
# If template is set to hide sensitive media by default, let's change it according to user preferences.
if self.session.settings["general"]["read_preferences_from_instance"] == True:
if self.session.expand_spoilers == True and "$safe_text" in template:
template = template.replace("$safe_text", "$text")
elif self.session.expand_spoilers == False and "$text" in template:
template = template.replace("$text", "$safe_text")
t = templates.render_post(post, template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
@@ -124,7 +133,10 @@ class BaseBuffer(base.Buffer):
else:
post = self.session.db[self.name][0]
output.speak(_("New post in {0}").format(self.get_buffer_name()))
output.speak(" ".join(self.compose_function(post, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
output.speak(" ".join(self.compose_function(post, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)))
elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(_("{0} new posts in {1}.").format(number_of_items, self.get_buffer_name()))
@@ -150,13 +162,16 @@ class BaseBuffer(base.Buffer):
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
@@ -185,27 +200,33 @@ class BaseBuffer(base.Buffer):
if number_of_items == 0 and self.session.settings["general"]["persist_size"] == 0: return
log.debug("The list contains %d items " % (self.buffer.list.get_count(),))
log.debug("Putting %d items on the list" % (number_of_items,))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.buffer.list.get_count() == 0:
for i in list_to_use:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
self.buffer.set_position(self.session.settings["general"]["reverse_timelines"])
elif self.buffer.list.get_count() > 0 and number_of_items > 0:
if self.session.settings["general"]["reverse_timelines"] == False:
items = list_to_use[len(list_to_use)-number_of_items:]
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
else:
items = list_to_use[0:number_of_items]
items.reverse()
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
log.debug("Now the list contains %d items " % (self.buffer.list.get_count(),))
def add_new_item(self, item):
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
if self.session.settings["general"]["reverse_timelines"] == False:
self.buffer.list.insert_item(False, *post)
else:
@@ -214,7 +235,10 @@ class BaseBuffer(base.Buffer):
output.speak(" ".join(post[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"])
def update_item(self, item, position):
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.list.SetItem(position, 1, post[1])
def bind_events(self):
@@ -298,6 +322,9 @@ class BaseBuffer(base.Buffer):
else:
title = _("Reply to {}").format(item.account.username)
caption = _("Write your reply here")
# Set unlisted by default, so we will not clutter other user's buffers with replies.
# see https://github.com/MCV-Software/TWBlue/issues/504
visibility = "unlisted"
if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users:
@@ -350,7 +377,7 @@ class BaseBuffer(base.Buffer):
def share_item(self, *args, **kwargs):
if self.can_share() == False:
return output.speak(_("This action is not supported on conversation posts."))
return output.speak(_("This action is not supported on conversations."))
post = self.get_item()
id = post.id
if self.session.settings["general"]["boost_mode"] == "ask":
@@ -439,6 +466,7 @@ class BaseBuffer(base.Buffer):
self.buffer.list.remove_item(index)
except Exception as e:
self.session.sound.play("error.ogg")
log.exception("")
self.session.db[self.name] = items
def user_details(self):
@@ -472,7 +500,11 @@ class BaseBuffer(base.Buffer):
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.favourited == False:
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
else:
@@ -482,7 +514,11 @@ class BaseBuffer(base.Buffer):
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.bookmarked == False:
call_threaded(self.session.api_call, call_name="status_bookmark", preexec_message=_("Adding to bookmarks..."), _sound="favourite.ogg", id=item.id)
else:
@@ -491,7 +527,11 @@ class BaseBuffer(base.Buffer):
def view_item(self):
post = self.get_item()
# Update object so we can retrieve newer stats
try:
post = self.session.api.status(id=post.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
# print(post)
msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url())

View File

@@ -4,6 +4,7 @@ import logging
import wx
import widgetUtils
import output
from mastodon import MastodonNotFoundError
from controller.mastodon import messages
from controller.buffers.mastodon.base import BaseBuffer
from mysc.thread_utils import call_threaded
@@ -200,7 +201,11 @@ class ConversationBuffer(BaseBuffer):
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
try:
self.post = self.session.api.status(id=self.post.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
# toDo: Implement reverse timelines properly here.
try:
results = []

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import time
import logging
import output
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import utils
@@ -8,6 +9,11 @@ log = logging.getLogger("controller.buffers.mastodon.mentions")
class MentionsBuffer(BaseBuffer):
def get_item(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None and len(self.session.db[self.name]) > index:
return self.session.db[self.name][index]["status"]
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 >= 180 or mandatory==True:
@@ -17,13 +23,12 @@ class MentionsBuffer(BaseBuffer):
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
try:
items = getattr(self.session.api, self.function)(min_id=min_id, limit=count, exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items = getattr(self.session.api, self.function)(min_id=min_id, limit=count, types=["mention"], *self.args, **self.kwargs)
items.reverse()
results = [item.status for item in items if item.get("status") and item.type == "mention"]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
number_of_items = self.session.order_buffer(self.name, items)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
@@ -40,11 +45,11 @@ class MentionsBuffer(BaseBuffer):
else:
max_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items = [item.status for item in items if item.get("status") and item.type == "mention"]
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:
log.exception("Error %s" % (str(e)))
return
print(items)
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
@@ -56,13 +61,55 @@ class MentionsBuffer(BaseBuffer):
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i.status, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
post = self.compose_function(i.status, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def put_items_on_list(self, number_of_items):
list_to_use = self.session.db[self.name]
if number_of_items == 0 and self.session.settings["general"]["persist_size"] == 0: return
log.debug("The list contains %d items " % (self.buffer.list.get_count(),))
log.debug("Putting %d items on the list" % (number_of_items,))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.buffer.list.get_count() == 0:
for i in list_to_use:
post = self.compose_function(i.status, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
self.buffer.set_position(self.session.settings["general"]["reverse_timelines"])
elif self.buffer.list.get_count() > 0 and number_of_items > 0:
if self.session.settings["general"]["reverse_timelines"] == False:
items = list_to_use[len(list_to_use)-number_of_items:]
for i in items:
post = self.compose_function(i.status, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
else:
items = list_to_use[0:number_of_items]
items.reverse()
for i in items:
post = self.compose_function(i.status, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
log.debug("Now the list contains %d items " % (self.buffer.list.get_count(),))
def add_new_item(self, item):
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
post = self.compose_function(item.status, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
if self.session.settings["general"]["reverse_timelines"] == False:
self.buffer.list.insert_item(False, *post)
else:
self.buffer.list.insert_item(True, *post)
if self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(" ".join(post[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"])

View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
import time
import logging
import widgetUtils
import output
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import compose, templates
from wxUI import buffers
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
log = logging.getLogger("controller.buffers.mastodon.notifications")
class NotificationsBuffer(BaseBuffer):
def get_message(self):
notification = self.get_item()
if notification == None:
return
template = self.session.settings["templates"]["notification"]
post_template = self.session.settings["templates"]["post"]
t = templates.render_notification(notification, template, post_template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.notificationsPanel(parent, name)
def onFocus(self, *args, **kwargs):
item = self.get_item()
if self.session.settings["general"]["relative_times"] == True:
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at)
ts = original_date.humanize(locale=languageHandler.getLanguage())
self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 1, ts)
def bind_events(self):
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
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)
def fav(self):
pass
def unfav(self):
pass
def can_share(self):
return False
def destroy_status(self, *args, **kwargs):
index = self.buffer.list.get_selected()
item = self.session.db[self.name][index]
answer = mastodon_dialogs.delete_notification_dialog()
if answer == False:
return
items = self.session.db[self.name]
try:
self.session.api.notifications_dismiss(id=item.id)
items.pop(index)
self.buffer.list.remove_item(index)
output.speak(_("Notification dismissed."))
except Exception as e:
self.session.sound.play("error.ogg")
log.exception("")
self.session.db[self.name] = items

View File

@@ -152,7 +152,7 @@ class Controller(object):
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.edit_keystrokes, menuitem=self.view.keystroke_editor)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.post_tweet, self.view.compose)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.post_reply, self.view.reply)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.post_retweet, self.view.retweet)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.post_retweet, self.view.share)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.add_to_favourites, self.view.fav)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.remove_from_favourites, self.view.unfav)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.view_item, self.view.view)
@@ -234,6 +234,8 @@ class Controller(object):
self.accounts = []
# This saves the current account (important in invisible mode)
self.current_account = ""
# this saves current menu bar layout.
self.menubar_current_handler = ""
# Handlers are special objects as they manage the mapping of available features and events in different social networks.
self.handlers = dict()
self.view.prepare()
@@ -284,6 +286,9 @@ class Controller(object):
self.started = True
self.streams_checker_function = RepeatingTimer(60, self.check_streams)
self.streams_checker_function.start()
if len(self.accounts) > 0:
b = self.get_first_buffer(self.accounts[0])
self.update_menus(handler=self.get_handler(b.session.type))
def create_ignored_session_buffer(self, session):
pub.sendMessage("core.create_account", name=session.get_name(), session_id=session.session_id)
@@ -300,7 +305,6 @@ class Controller(object):
session.start_streaming()
def create_account_buffer(self, name, session_id, logged=False):
self.accounts.append(name)
account = buffers.base.AccountBuffer(self.view.nb, name, name, session_id)
if logged == False:
account.logged = logged
@@ -429,7 +433,8 @@ class Controller(object):
output.speak("Unable to seek.",True)
def edit_keystrokes(self, *args, **kwargs):
editor = keystrokeEditor.KeystrokeEditor()
buffer = self.get_best_buffer()
editor = keystrokeEditor.KeystrokeEditor(buffer.session.type)
if editor.changed == True:
config.keymap.write()
register = False
@@ -676,8 +681,15 @@ class Controller(object):
def buffer_changed(self, *args, **kwargs):
buffer = self.get_current_buffer()
if buffer.account != self.current_account:
old_account = self.current_account
new_account = buffer.account
if new_account != old_account:
self.current_account = buffer.account
new_first_buffer = self.get_first_buffer(new_account)
if new_first_buffer != None and new_first_buffer.session.type != self.menubar_current_handler:
handler = self.get_handler(new_first_buffer.session.type)
self.menubar_current_handler = new_first_buffer.session.type
self.update_menus(handler)
if not hasattr(buffer, "session") or buffer.session == None:
return
muted = autoread = False
@@ -688,6 +700,19 @@ class Controller(object):
self.view.check_menuitem("mute_buffer", muted)
self.view.check_menuitem("autoread", autoread)
def update_menus(self, handler):
if hasattr(handler, "menus"):
for m in list(handler.menus.keys()):
if hasattr(self.view, m):
menu_item = getattr(self.view, m)
if handler.menus[m] == None:
menu_item.Enable(False)
else:
menu_item.Enable(True)
menu_item.SetItemLabel(handler.menus[m])
if hasattr(handler, "item_menu"):
self.view.menubar.SetMenuLabel(1, handler.item_menu)
def fix_wrong_buffer(self):
buf = self.get_best_buffer()
if buf == None:
@@ -1057,8 +1082,9 @@ class Controller(object):
for i in sm.removed_sessions:
if sessions.sessions[i].logged == True:
self.logout_account(sessions.sessions[i].session_id)
self.destroy_buffer(sessions.sessions[i].settings["twitter"]["user_name"], sessions.sessions[i].settings["twitter"]["user_name"])
self.accounts.remove(sessions.sessions[i].settings["twitter"]["user_name"])
self.destroy_buffer(sessions.sessions[i].get_name(), sessions.sessions[i].get_name())
if sessions.sessions[i].get_name() in self.accounts:
self.accounts.remove(sessions.sessions[i].get_name())
sessions.sessions.pop(i)
def update_profile(self, *args, **kwargs):
@@ -1208,7 +1234,7 @@ class Controller(object):
self.notify(buffer.session, sound_to_play)
def toggle_share_settings(self, shareable=True):
self.view.retweet.Enable(shareable)
self.view.share.Enable(shareable)
def check_streams(self):
if self.started == False:
@@ -1239,6 +1265,7 @@ class Controller(object):
elif buff == "direct_messages": sound_to_play = "dm_received.ogg"
elif buff == "sent": sound_to_play = "tweet_send.ogg"
elif buff == "followers" or buff == "following": sound_to_play = "update_followers.ogg"
elif buff == "notifications": sound_to_play = "new_event.ogg"
elif "timeline" in buff: sound_to_play = "tweet_timeline.ogg"
else: sound_to_play = None
if sound_to_play != None and buff not in buffer.session.settings["other_buffers"]["muted_buffers"]:

View File

@@ -16,10 +16,47 @@ class Handler(object):
def __init__(self):
super(Handler, self).__init__()
# Structure to hold names for menu bar items.
# empty names mean the item will be Disabled.
self.menus = dict(
# In application menu.
updateProfile=None,
menuitem_search=_("&Search"),
lists=None,
manageAliases=None,
# In item menu.
compose=_("&Post"),
reply=_("Re&ply"),
share=_("&Boost"),
fav=_("&Add to favorites"),
unfav=_("Remove from favorites"),
view=_("&Show post"),
view_coordinates=None,
view_conversation=_("View conversa&tion"),
ocr=None,
delete=_("&Delete"),
# In user menu.
follow=_("&Actions..."),
timeline=_("&View timeline..."),
dm=_("Direct me&ssage"),
addAlias=None,
addToList=None,
removeFromList=None,
viewLists=None,
details=None,
favs=None,
# In buffer Menu.
trends=None,
filter=None,
manage_filters=None
)
# Name for the "tweet" menu in the menu bar.
self.item_menu = _("&Post")
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
controller.accounts.append(name)
if createAccounts == True:
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
root_position =controller.view.search(name, name)
@@ -48,14 +85,16 @@ class Handler(object):
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Muted users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="mutes", name="muted", sessionObject=session, account=name))
elif i == 'blocked':
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))
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"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=i, parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline".format(i), sessionObject=session, account=name, sound="tweet_timeline.ogg", id=i))
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Timeline for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="{}-timeline".format(i), sessionObject=session, account=name, sound="tweet_timeline.ogg", id=i))
for i in session.settings["other_buffers"]["followers_timelines"]:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Followers for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Followers for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="{}-followers".format(i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
for i in session.settings["other_buffers"]["following_timelines"]:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Following for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-following" % (i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Following for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="{}-following".format(i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
# pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Lists"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="lists", name))
# lists_position =controller.view.search("lists", session.db["user_name"])
# for i in session.settings["other_buffers"]["lists"]:

View File

@@ -185,6 +185,11 @@ class post(messages.basicTweet):
visibility_settings = ["public", "unlisted", "private", "direct"]
return visibility_settings[self.message.visibility.GetSelection()]
def set_visibility(self, setting):
visibility_settings = ["public", "unlisted", "private", "direct"]
visibility_setting = visibility_settings.index(setting)
self.message.visibility.SetSelection(setting)
class viewPost(post):
def __init__(self, post, offset_hours=0, date="", item_url=""):
if post.reblog != None:

View File

@@ -32,6 +32,7 @@ class accountSettingsController(globalSettingsController):
# widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan)
# widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage)
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "read_preferences_from_instance", self.config["general"]["read_preferences_from_instance"])
self.dialog.set_value("general", "show_screen_names", self.config["general"]["show_screen_names"])
self.dialog.set_value("general", "hide_emojis", self.config["general"]["hide_emojis"])
self.dialog.set_value("general", "itemsPerApiCall", self.config["general"]["max_posts_per_call"])
@@ -111,6 +112,7 @@ class accountSettingsController(globalSettingsController):
self.needs_restart = True
log.debug("Triggered app restart due to change in relative times.")
self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time")
self.config["general"]["read_preferences_from_instance"] = self.dialog.get_value("general", "read_preferences_from_instance")
self.config["general"]["show_screen_names"] = self.dialog.get_value("general", "show_screen_names")
self.config["general"]["hide_emojis"] = self.dialog.get_value("general", "hide_emojis")
self.config["general"]["max_posts_per_call"] = self.dialog.get_value("general", "itemsPerApiCall")

View File

@@ -16,10 +16,47 @@ class Handler(object):
def __init__(self):
super(Handler, self).__init__()
# Structure to hold names for menu bar items.
# empty names mean the item will be Disabled.
self.menus = dict(
# In application menu.
updateProfile=_("&Update profile"),
menuitem_search=_("&Search"),
lists=_("&Lists manager"),
manageAliases=_("Manage user aliases"),
# In Item Menu.
compose=_("&Tweet"),
reply=_("Re&ply"),
share=_("&Retweet"),
fav=_("&Like"),
unfav=_("&Unlike"),
view=_("&Show tweet"),
view_coordinates=_("View &address"),
view_conversation=_("View conversa&tion"),
ocr=_("Read text in picture"),
delete=_("&Delete"),
# In user menu.
follow=_("&Actions..."),
timeline=_("&View timeline..."),
dm=_("Direct me&ssage"),
addAlias=_("Add a&lias"),
addToList=_("&Add to list"),
removeFromList=_("R&emove from list"),
viewLists=_("&View lists"),
details=_("Show user &profile"),
favs=_("View likes"),
# In buffer menu.
trends=_("New &trending topics buffer..."),
filter=_("Create a &filter"),
manage_filters=_("&Manage filters"),
)
# Name for the "tweet" menu in the menu bar.
self.item_menu = _("&Tweet")
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
controller.accounts.append(name)
if createAccounts == True:
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
root_position =controller.view.search(name, name)

View File

@@ -1,3 +1 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from .keystrokeEditor import KeystrokeEditor

View File

@@ -0,0 +1 @@
from . import twitter, mastodon

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
actions = {
"up": _(u"Go up in the current buffer"),
"down": _(u"Go down in the current buffer"),
"left": _(u"Go to the previous buffer"),
"right": _(u"Go to the next buffer"),
"next_account": _(u"Focus the next session"),
"previous_account": _(u"Focus the previous session"),
"show_hide": _(u"Show or hide the GUI"),
"post_tweet": _("Make a new post"),
"post_reply": _(u"Reply"),
"post_retweet": _(u"Boost"),
"send_dm": _(u"Send direct message"),
"add_to_favourites": _("Add post to favorites"),
"remove_from_favourites": _(u"Remove post from favorites"),
"toggle_like": _("Add/remove post from favorites"),
"follow": _(u"Open the user actions dialogue"),
# "user_details": _(u"See user details"),
"view_item": _(u"Show post"),
"exit": _(u"Quit"),
"open_timeline": _(u"Open user timeline"),
"remove_buffer": _(u"Destroy buffer"),
"interact": _(u"Interact with the currently focused post."),
"url": _(u"Open URL"),
"open_in_browser": _(u"View in browser"),
"volume_up": _(u"Increase volume by 5%"),
"volume_down": _(u"Decrease volume by 5%"),
"go_home": _(u"Jump to the first element of a buffer"),
"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"),
"delete": _("Delete post"),
"clear_buffer": _(u"Empty the current buffer"),
"repeat_item": _(u"Repeat last item"),
"copy_to_clipboard": _(u"Copy to clipboard"),
# "add_to_list": _(u"Add to list"),
# "remove_from_list": _(u"Remove from list"),
"toggle_buffer_mute": _(u"Mute/unmute the active buffer"),
"toggle_session_mute": _(u"Mute/unmute the current session"),
"toggle_autoread": _(u"toggle the automatic reading of incoming tweets in the active buffer"),
"search": _(u"Search on instance"),
"find": _(u"Find a string in the currently focused buffer"),
"edit_keystrokes": _(u"Show the keystroke editor"),
# "view_user_lists": _(u"Show lists for a specified user"),
"get_more_items": _(u"load previous items"),
# "get_trending_topics": _(u"Create a trending topics buffer"),
"open_conversation": _(u"View conversation"),
"check_for_updates": _(u"Check and download updates"),
"configuration": _(u"Opens the global settings dialogue"),
# "list_manager": _(u"Opens the list manager"),
"accountConfiguration": _(u"Opens the account settings dialogue"),
"audio": _(u"Try to play a media file"),
"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"),
}

View File

@@ -53,7 +53,7 @@ actions = {
"configuration": _(u"Opens the global settings dialogue"),
"list_manager": _(u"Opens the list manager"),
"accountConfiguration": _(u"Opens the account settings dialogue"),
"audio": _(u"Try to play an audio file"),
"audio": _(u"Try to play a media file"),
"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"),

View File

@@ -2,18 +2,19 @@
import widgetUtils
import config
from . import wx_ui
from . import constants
from . import actions
from pubsub import pub
class KeystrokeEditor(object):
def __init__(self):
def __init__(self, session_type="twitter"):
super(KeystrokeEditor, self).__init__()
self.actions = getattr(actions, session_type).actions
self.changed = False # Change it if the keyboard shorcuts are reassigned.
self.dialog = wx_ui.keystrokeEditorDialog()
self.map = config.keymap["keymap"]
# we need to copy the keymap before modify it, for unregistering the old keystrokes if is needed.
self.hold_map = self.map.copy()
self.dialog.put_keystrokes(constants.actions, self.map)
self.dialog.put_keystrokes(self.actions, self.map)
widgetUtils.connect_event(self.dialog.edit, widgetUtils.BUTTON_PRESSED, self.edit_keystroke)
widgetUtils.connect_event(self.dialog.undefine, widgetUtils.BUTTON_PRESSED, self.undefine_keystroke)
widgetUtils.connect_event(self.dialog.execute, widgetUtils.BUTTON_PRESSED, self.execute_action)
@@ -29,7 +30,7 @@ class KeystrokeEditor(object):
if new_keystroke != self.map[action]:
self.changed = True
self.map[action] = new_keystroke
self.dialog.put_keystrokes(constants.actions, self.map)
self.dialog.put_keystrokes(self.actions, self.map)
def undefine_keystroke(self, *args, **kwargs):
action = self.dialog.actions[self.dialog.get_action()]
@@ -40,7 +41,7 @@ class KeystrokeEditor(object):
if answer == widgetUtils.YES:
self.map[action] = ""
self.changed = True
self.dialog.put_keystrokes(constants.actions, self.map)
self.dialog.put_keystrokes(self.actions, self.map)
def set_keystroke(self, keystroke, dialog):
for i in keystroke.split("+"):

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ ignored_clients = list(default=list())
[general]
relative_times = boolean(default=True)
read_preferences_from_instance = boolean(default=True)
max_posts_per_call = integer(default=40)
reverse_timelines = boolean(default=False)
persist_size = integer(default=0)
@@ -48,6 +49,7 @@ speech_reporting = boolean(default=True)
post = string(default="$display_name, $safe_text $image_descriptions $date. $visibility. $source")
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")
[filters]

View File

@@ -3,12 +3,14 @@
import time
import os
import logging
import shutil
import widgetUtils
import sessions
import output
import paths
import config_utils
import config
import application
from pubsub import pub
from tweepy.errors import TweepyException
from controller import settings
@@ -65,12 +67,14 @@ class sessionManagerController(object):
os.exception("Exception thrown while removing malformed session")
continue
if config_test.get("twitter") != None:
if application.twitter_support_enabled == False:
continue
name = _("{account_name} (Twitter)").format(account_name=config_test["twitter"]["user_name"])
if config_test["twitter"]["user_key"] != "" and config_test["twitter"]["user_secret"] != "":
sessionsList.append(name)
self.sessions.append(dict(type="twitter", id=i))
elif config_test.get("mastodon") != None:
name = _("{account_name} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"])
name = _("{account_name}@{instance} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"], instance=config_test["mastodon"]["instance"].replace("https://", ""))
if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "":
sessionsList.append(name)
self.sessions.append(dict(type="mastodon", id=i))
@@ -98,6 +102,8 @@ class sessionManagerController(object):
continue
# Create the session object based in session type.
if i.get("type") == "twitter":
if application.twitter_support_enabled == False:
continue
s = TwitterSession.Session(i.get("id"))
elif i.get("type") == "mastodon":
s = MastodonSession.Session(i.get("id"))

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
""" Base GUI (Wx) class for the Session manager module."""
import wx
import application
from pubsub import pub
from multiplatform_widgets import widgets
@@ -50,9 +51,10 @@ class sessionManagerWindow(wx.Dialog):
def on_new_account(self, *args, **kwargs):
menu = wx.Menu()
if application.twitter_support_enabled:
twitter = menu.Append(wx.ID_ANY, _("Twitter"))
mastodon = menu.Append(wx.ID_ANY, _("Mastodon"))
menu.Bind(wx.EVT_MENU, self.on_new_twitter_account, twitter)
mastodon = menu.Append(wx.ID_ANY, _("Mastodon"))
menu.Bind(wx.EVT_MENU, self.on_new_mastodon_account, mastodon)
self.PopupMenu(menu, self.new.GetPosition())
@@ -64,6 +66,8 @@ class sessionManagerWindow(wx.Dialog):
pub.sendMessage("sessionmanager.new_account", type="mastodon")
def on_new_twitter_account(self, *args, **kwargs):
if application.twitter_support_enabled == False:
return
dlg = wx.MessageDialog(self, _(u"The request to authorize your Twitter account will be opened in your browser. You only need to do this once. Would you like to continue?"), _(u"Authorization"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()

View File

@@ -3,7 +3,7 @@ import arrow
import languageHandler
from . import utils, templates
def compose_post(post, db, relative_times, show_screen_names):
def compose_post(post, db, relative_times, show_screen_names, safe=True):
if show_screen_names == False:
user = post.account.get("display_name")
if user == "":
@@ -16,9 +16,9 @@ def compose_post(post, db, relative_times, show_screen_names):
else:
ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
if post.reblog != None:
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog))
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe))
else:
text = templates.process_text(post)
text = templates.process_text(post, safe=safe)
source = post.get("application", "")
# "" means remote user, None for legacy apps so we should cover both sides.
if source != None and source != "":
@@ -27,7 +27,7 @@ def compose_post(post, db, relative_times, show_screen_names):
source = ""
return [user+", ", text, ts+", ", source]
def compose_user(user, db, relative_times=True, show_screen_names=False):
def compose_user(user, db, relative_times=True, show_screen_names=False, safe=False):
original_date = arrow.get(user.created_at)
if relative_times:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
@@ -38,7 +38,7 @@ def compose_user(user, db, relative_times=True, show_screen_names=False):
name = user.get("username")
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (name, user.acct, user.followers_count, user.following_count, user.statuses_count, ts)]
def compose_conversation(conversation, db, relative_times, show_screen_names):
def compose_conversation(conversation, db, relative_times, show_screen_names, safe=False):
users = []
for account in conversation.accounts:
if account.display_name != "":
@@ -49,3 +49,32 @@ def compose_conversation(conversation, db, relative_times, show_screen_names):
last_post = compose_post(conversation.last_status, db, relative_times, show_screen_names)
text = _("Last message from {}: {}").format(last_post[0], last_post[1])
return [users, text, last_post[-2], last_post[-1]]
def compose_notification(notification, db, relative_times, show_screen_names, safe=False):
if show_screen_names == False:
user = notification.account.get("display_name")
if user == "":
user = notification.account.get("username")
else:
user = notification.account.get("acct")
original_date = arrow.get(notification.created_at)
if relative_times:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
text = "Unknown: %r" % (notification)
if notification.type == "status":
text = _("{username} has posted: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe)))
elif notification.type == "mention":
text = _("{username} has mentionned you: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe)))
elif notification.type == "reblog":
text = _("{username} has boosted: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe)))
elif notification.type == "favourite":
text = _("{username} has added to favorites: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe)))
elif notification.type == "follow":
text = _("{username} has followed you.").format(username=user)
elif notification.type == "poll":
text = _("A poll in which you have voted has expired: {status}").format(status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe)))
elif notification.type == "follow_request":
text = _("{username} wants to follow you.").format(username=user)
return [user, text, ts]

View File

@@ -15,7 +15,6 @@ from pubsub import pub
from mysc.thread_utils import call_threaded
from sessions import base
from sessions.mastodon import utils, streaming
from .wxUI import authorisationDialog
log = logging.getLogger("sessions.mastodonSession")
@@ -30,6 +29,8 @@ class Session(base.baseSession):
self.type = "mastodon"
self.db["pagination_info"] = dict()
self.char_limit = 500
self.post_visibility = "public"
self.expand_spoilers = False
pub.subscribe(self.on_status, "mastodon.status_received")
pub.subscribe(self.on_status_updated, "mastodon.status_updated")
pub.subscribe(self.on_notification, "mastodon.notification_received")
@@ -38,7 +39,7 @@ class Session(base.baseSession):
if self.settings["mastodon"]["access_token"] != None and self.settings["mastodon"]["instance"] != None:
try:
log.debug("Logging in to Mastodon instance {}...".format(self.settings["mastodon"]["instance"]))
self.api = mastodon.Mastodon(access_token=self.settings["mastodon"]["access_token"], api_base_url=self.settings["mastodon"]["instance"], mastodon_version=MASTODON_VERSION)
self.api = mastodon.Mastodon(access_token=self.settings["mastodon"]["access_token"], api_base_url=self.settings["mastodon"]["instance"], mastodon_version=MASTODON_VERSION, user_agent="TWBlue/{}".format(application.version))
if verify_credentials == True:
credentials = self.api.account_verify_credentials()
self.db["user_name"] = credentials["username"]
@@ -47,8 +48,8 @@ class Session(base.baseSession):
self.logged = True
log.debug("Logged.")
self.counter = 0
except IOError:
log.error("The login attempt failed.")
except MastodonError:
log.exception("The login attempt failed.")
self.logged = False
else:
self.logged = False
@@ -99,14 +100,18 @@ class Session(base.baseSession):
offset = time.timezone if (time.localtime().tm_isdst == 0) else time.altzone
offset = offset / 60 / 60 * -1
self.db["utc_offset"] = offset
instance = self.api.instance()
if len(self.supported_languages) == 0:
self.supported_languages = self.api.instance().languages
self.supported_languages = instance.languages
self.get_lists()
self.get_muted_users()
# determine instance custom characters limit.
instance = self.api.instance()
if hasattr(instance, "configuration") and hasattr(instance.configuration, "statuses") and hasattr(instance.configuration.statuses, "max_characters"):
self.char_limit = instance.configuration.statuses.max_characters
# User preferences for some things.
preferences = self.api.preferences()
self.post_visibility = preferences.get("posting:default:visibility")
self.expand_spoilers = preferences.get("reading:expand:spoilers")
self.settings.write()
def get_lists(self):
@@ -221,8 +226,14 @@ class Session(base.baseSession):
if config.app["app-settings"]["no_streaming"]:
return
listener = streaming.StreamListener(session_name=self.get_name(), user_id=self.db["user_id"])
try:
stream_healthy = self.api.stream_healthy()
if stream_healthy == True:
self.user_stream = self.api.stream_user(listener, run_async=True)
self.direct_stream = self.api.stream_direct(listener, run_async=True)
log.debug("Started streams for session {}.".format(self.get_name()))
except Exception as e:
log.exception("Detected streaming unhealthy in {} session.".format(self.get_name()))
def stop_streaming(self):
if config.app["app-settings"]["no_streaming"]:
@@ -279,7 +290,7 @@ class Session(base.baseSession):
obj = None
if notification.type == "mention":
buffers = ["mentions"]
obj = notification.status
obj = notification
elif notification.type == "follow":
buffers = ["followers"]
obj = notification.account
@@ -288,3 +299,7 @@ class Session(base.baseSession):
if num == 0:
buffers.remove(b)
pub.sendMessage("mastodon.new_item", session_name=self.get_name(), item=obj, _buffers=buffers)
# Now, add notification to its buffer.
num = self.order_buffer("notifications", [notification])
if num > 0:
pub.sendMessage("mastodon.new_item", session_name=self.get_name(), item=notification, _buffers=["notifications"])

View File

@@ -12,11 +12,13 @@ from . import utils, compose
post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe_text", "text", "image_descriptions", "visibility"]
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"]
# 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")
def process_date(field, relative_times=True, offset_hours=0):
original_date = arrow.get(field)
@@ -135,3 +137,45 @@ def render_conversation(conversation, template, post_template, relative_times=Fa
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, conversation_variables)
return result
def render_notification(notification, template, post_template, relative_times=False, offset_hours=0):
""" Renders any given notification according to the passed template.
Available data for notifications will be stored in the following variables:
$date: Creation date.
$display_name: User profile name.
$screen_name: User screen name, this is the same name used to reference the user in Twitter.
$text: Notification text, describing the action.
"""
global notification_variables
available_data = dict()
created_at = process_date(notification.created_at, relative_times, offset_hours)
available_data.update(date=created_at)
# user.
display_name = notification.account.display_name
if display_name == "":
display_name = notification.account.username
available_data.update(display_name=display_name, screen_name=notification.account.acct)
text = "Unknown: %r" % (notification)
# Remove date from status, so it won't be rendered twice.
post_template = post_template.replace("$date", "")
if notification.type == "status":
text = _("has posted: {status}").format(status=render_post(notification.status, post_template, relative_times, offset_hours))
elif notification.type == "mention":
text = _("has mentionned you: {status}").format(status=render_post(notification.status, post_template, relative_times, offset_hours))
elif notification.type == "reblog":
text = _("has boosted: {status}").format(status=render_post(notification.status, post_template, relative_times, offset_hours))
elif notification.type == "favourite":
text = _("has added to favorites: {status}").format(status=render_post(notification.status, post_template, relative_times, offset_hours))
elif notification.type == "update":
text = _("has updated a status: {status}").format(status=render_post(notification.status, post_template, relative_times, offset_hours))
elif notification.type == "follow":
text = _("has followed you.")
elif notification.type == "poll":
text = _("A poll in which you have voted has expired: {status}").format(status=render_post(notification.status, post_template, relative_times, offset_hours))
elif notification.type == "follow_request":
text = _("wants to follow you.")
available_data.update(text=text)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, post_variables)
result = result.replace(" . ", "")
return result

View File

@@ -5,12 +5,19 @@ url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>')
class HTMLFilter(HTMLParser):
text = ""
first_paragraph = True
def handle_data(self, data):
self.text += data
def handle_starttag(self, tag, attrs):
if tag == "br":
self.text = self.text+"\n"
elif tag == "p":
if self.first_paragraph:
self.first_paragraph = False
else:
self.text = self.text+"\n\n"
def html_filter(data):
f = HTMLFilter()

View File

@@ -1,35 +0,0 @@
# -*- coding: utf-8 -*-
import wx
class authorisationDialog(wx.Dialog):
def __init__(self):
super(authorisationDialog, self).__init__(parent=None, title=_(u"Authorising account..."))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
static1 = wx.StaticText(panel, wx.NewId(), _("URL of mastodon instance:"))
self.instance = wx.TextCtrl(panel, -1)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(static1, 0, wx.ALL, 5)
sizer1.Add(self.instance, 0, wx.ALL, 5)
sizer.Add(sizer1, 0, wx.ALL, 5)
static2 = wx.StaticText(panel, wx.NewId(), _("Email address:"))
self.email = wx.TextCtrl(panel, -1)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
sizer2.Add(static2, 0, wx.ALL, 5)
sizer2.Add(self.email, 0, wx.ALL, 5)
sizer.Add(sizer2, 0, wx.ALL, 5)
static3 = wx.StaticText(panel, wx.NewId(), _("Password:"))
self.password = wx.TextCtrl(panel, -1)
sizer3 = wx.BoxSizer(wx.HORIZONTAL)
sizer3.Add(static3, 0, wx.ALL, 5)
sizer3.Add(self.password, 0, wx.ALL, 5)
sizer.Add(sizer3, 0, wx.ALL, 5)
self.ok = wx.Button(panel, wx.ID_OK)
self.cancel = wx.Button(panel, wx.ID_CANCEL)
sizer4 = wx.BoxSizer(wx.HORIZONTAL)
sizer4.Add(self.ok, 0, wx.ALL, 5)
sizer4.Add(self.cancel, 0, wx.ALL, 5)
sizer.Add(sizer4, 0, wx.ALL, 5)
panel.SetSizer(sizer)
min = sizer.CalcMin()
self.SetClientSize(min)

View File

@@ -80,11 +80,11 @@ def render_tweet(tweet, template, session, relative_times=False, offset_seconds=
available_data.update(source=tweet.source)
if hasattr(tweet, "retweeted_status"):
if hasattr(tweet.retweeted_status, "quoted_status"):
text = "RT @{}: {} Quote from @{}: {}".format(session.get_user(tweet.retweeted_status.user).screen_name, process_text(tweet.retweeted_status), session.get_user(tweet.retweeted_status.quoted_status.user).screen_name, process_text(tweet.retweeted_status.quoted_status))
text = _("RT @{}: {} Quote from @{}: {}").format(session.get_user(tweet.retweeted_status.user).screen_name, process_text(tweet.retweeted_status), session.get_user(tweet.retweeted_status.quoted_status.user).screen_name, process_text(tweet.retweeted_status.quoted_status))
else:
text = "RT @{}: {}".format(session.get_user(tweet.retweeted_status.user).screen_name, process_text(tweet.retweeted_status))
text = _("RT @{}: {}").format(session.get_user(tweet.retweeted_status.user).screen_name, process_text(tweet.retweeted_status))
elif hasattr(tweet, "quoted_status"):
text = "{} Quote from @{}: {}".format(process_text(tweet), session.get_user(tweet.quoted_status.user).screen_name, process_text(tweet.quoted_status))
text = _("{} Quote from @{}: {}").format(process_text(tweet), session.get_user(tweet.quoted_status.user).screen_name, process_text(tweet.quoted_status))
else:
text = process_text(tweet)
available_data.update(lang=tweet.lang, text=text)

View File

@@ -30,7 +30,7 @@ def progress_callback(total_downloaded, total_size):
if total_downloaded == total_size:
progress_dialog.Destroy()
else:
progress_dialog.Update(old_div((total_downloaded*100),total_size), _(u"Updating... %s of %s") % (str(utils.convert_bytes(total_downloaded)), str(utils.convert_bytes(total_size))))
progress_dialog.Update(int((total_downloaded*100)/total_size), _(u"Updating... %s of %s") % (str(utils.convert_bytes(total_downloaded)), str(utils.convert_bytes(total_size))))
def update_finished():
ms = wx.MessageDialog(None, _(u"The update has been downloaded and installed successfully. Press OK to continue."), _(u"Done!")).ShowModal()

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from .base import basePanel
from .conversationList import conversationListPanel
from .notifications import notificationsPanel
from .user import userPanel

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
import wx
from multiplatform_widgets import widgets
class notificationsPanel(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, _("Text"), _("Date"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
self.list.set_windows_size(0, 320)
self.list.set_windows_size(2, 110)
self.list.set_size()
def __init__(self, parent, name):
super(notificationsPanel, self).__init__(parent)
self.name = name
self.type = "baseBuffer"
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.create_list()
self.post = wx.Button(self, -1, _("Post"))
self.dismiss = wx.Button(self, -1, _("Dismiss"))
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(self.post, 0, wx.ALL, 5)
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
self.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()

View File

@@ -22,6 +22,8 @@ class generalAccount(wx.Panel, baseDialog.BaseWXDialog):
sizer.Add(autocompletionSizer, 0, wx.ALL, 5)
self.relative_time = wx.CheckBox(self, wx.ID_ANY, _("Relative timestamps"))
sizer.Add(self.relative_time, 0, wx.ALL, 5)
self.read_preferences_from_instance = wx.CheckBox(self, wx.ID_ANY, _("Read preferences from instance (default visibility when publishing and displaying sensitive content)"))
sizer.Add(self.read_preferences_from_instance, 0, wx.ALL, 5)
itemsPerCallBox = wx.BoxSizer(wx.HORIZONTAL)
itemsPerCallBox.Add(wx.StaticText(self, -1, _("Items on each API call")), 0, wx.ALL, 5)
self.itemsPerApiCall = wx.SpinCtrl(self, wx.ID_ANY)

View File

@@ -18,6 +18,14 @@ def delete_post_dialog():
dlg.Destroy()
return result
def delete_notification_dialog():
result = False
dlg = wx.MessageDialog(None, _("Are you sure you want to dismiss this notification? If you dismiss a mention notification, it also disappears from your mentions buffer. The post is not going to be deleted from the instance, though."), _("Dismiss"), wx.ICON_QUESTION|wx.YES_NO)
if dlg.ShowModal() == wx.ID_YES:
result = True
dlg.Destroy()
return result
def clear_list():
result = False
dlg = wx.MessageDialog(None, _("Do you really want to empty this buffer? It's items will be removed from the list but not from the instance"), _(u"Empty buffer"), wx.ICON_QUESTION|wx.YES_NO)

View File

@@ -43,13 +43,13 @@ class Post(wx.Dialog):
main_sizer.Add(post_actions_sizer, 1, wx.EXPAND, 0)
visibility_sizer = wx.BoxSizer(wx.HORIZONTAL)
post_actions_sizer.Add(visibility_sizer, 1, wx.EXPAND, 0)
label_1 = wx.StaticText(self, wx.ID_ANY, _("Visibility"))
label_1 = wx.StaticText(self, wx.ID_ANY, _("&Visibility"))
visibility_sizer.Add(label_1, 0, 0, 0)
self.visibility = wx.ComboBox(self, wx.ID_ANY, choices=[_("Public"), _("Not listed"), _("Followers only"), _("Direct")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
self.visibility.SetSelection(0)
visibility_sizer.Add(self.visibility, 0, 0, 0)
self.add = wx.Button(self, wx.ID_ANY, _("A&dd"))
self.sensitive = wx.CheckBox(self, wx.ID_ANY, _("Sensitive content"))
self.sensitive = wx.CheckBox(self, wx.ID_ANY, _("S&ensitive content"))
self.sensitive.SetValue(False)
self.sensitive.Bind(wx.EVT_CHECKBOX, self.on_sensitivity_changed)
main_sizer.Add(self.sensitive, 0, wx.ALL, 5)

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from builtins import range
import wx
import wx.adv
import application
@@ -11,88 +9,88 @@ class mainFrame(wx.Frame):
### MENU
def makeMenus(self):
""" Creates, bind and returns the menu bar for the application. Also in this function, the accel table is created."""
menuBar = wx.MenuBar()
self.menubar = wx.MenuBar()
# Application menu
app = wx.Menu()
self.manage_accounts = app.Append(wx.ID_ANY, _(u"&Manage accounts"))
self.updateProfile = app.Append(wx.ID_ANY, _(u"&Update profile"))
self.show_hide = app.Append(wx.ID_ANY, _(u"&Hide window"))
self.menuitem_search = app.Append(wx.ID_ANY, _(u"&Search"))
self.lists = app.Append(wx.ID_ANY, _(u"&Lists manager"))
self.manageAliases = app.Append(wx.ID_ANY, _("Manage user aliases"))
self.keystroke_editor = app.Append(wx.ID_ANY, _(u"&Edit keystrokes"))
self.account_settings = app.Append(wx.ID_ANY, _(u"Account se&ttings"))
self.prefs = app.Append(wx.ID_PREFERENCES, _(u"&Global settings"))
self.close = app.Append(wx.ID_EXIT, _(u"E&xit"))
self.menubar_application = wx.Menu()
self.manage_accounts = self.menubar_application.Append(wx.ID_ANY, _(u"&Manage accounts"))
self.updateProfile = self.menubar_application.Append(wx.ID_ANY, _("&Update profile"))
self.show_hide = self.menubar_application.Append(wx.ID_ANY, _(u"&Hide window"))
self.menuitem_search = self.menubar_application.Append(wx.ID_ANY, _(u"&Search"))
self.lists = self.menubar_application.Append(wx.ID_ANY, _(u"&Lists manager"))
self.manageAliases = self.menubar_application.Append(wx.ID_ANY, _("Manage user aliases"))
self.keystroke_editor = self.menubar_application.Append(wx.ID_ANY, _(u"&Edit keystrokes"))
self.account_settings = self.menubar_application.Append(wx.ID_ANY, _(u"Account se&ttings"))
self.prefs = self.menubar_application.Append(wx.ID_PREFERENCES, _(u"&Global settings"))
self.close = self.menubar_application.Append(wx.ID_EXIT, _(u"E&xit"))
# Tweet menu
tweet = wx.Menu()
self.compose = tweet.Append(wx.ID_ANY, _(u"&Tweet"))
self.reply = tweet.Append(wx.ID_ANY, _(u"Re&ply"))
self.retweet = tweet.Append(wx.ID_ANY, _(u"&Retweet"))
self.fav = tweet.Append(wx.ID_ANY, _(u"&Like"))
self.unfav = tweet.Append(wx.ID_ANY, _(u"&Unlike"))
self.view = tweet.Append(wx.ID_ANY, _(u"&Show tweet"))
self.view_coordinates = tweet.Append(wx.ID_ANY, _(u"View &address"))
self.view_conversation = tweet.Append(wx.ID_ANY, _(u"View conversa&tion"))
self.ocr = tweet.Append(wx.ID_ANY, _(u"Read text in picture"))
self.delete = tweet.Append(wx.ID_ANY, _(u"&Delete"))
self.menubar_item = wx.Menu()
self.compose = self.menubar_item.Append(wx.ID_ANY, _(u"&Tweet"))
self.reply = self.menubar_item.Append(wx.ID_ANY, _(u"Re&ply"))
self.share = self.menubar_item.Append(wx.ID_ANY, _(u"&Retweet"))
self.fav = self.menubar_item.Append(wx.ID_ANY, _(u"&Like"))
self.unfav = self.menubar_item.Append(wx.ID_ANY, _(u"&Unlike"))
self.view = self.menubar_item.Append(wx.ID_ANY, _(u"&Show tweet"))
self.view_coordinates = self.menubar_item.Append(wx.ID_ANY, _(u"View &address"))
self.view_conversation = self.menubar_item.Append(wx.ID_ANY, _(u"View conversa&tion"))
self.ocr = self.menubar_item.Append(wx.ID_ANY, _(u"Read text in picture"))
self.delete = self.menubar_item.Append(wx.ID_ANY, _(u"&Delete"))
# User menu
user = wx.Menu()
self.follow = user.Append(wx.ID_ANY, _(u"&Actions..."))
self.timeline = user.Append(wx.ID_ANY, _(u"&View timeline..."))
self.dm = user.Append(wx.ID_ANY, _(u"Direct me&ssage"))
self.addAlias = user.Append(wx.ID_ANY, _("Add a&lias"))
self.addToList = user.Append(wx.ID_ANY, _(u"&Add to list"))
self.removeFromList = user.Append(wx.ID_ANY, _(u"R&emove from list"))
self.viewLists = user.Append(wx.ID_ANY, _(u"&View lists"))
self.details = user.Append(wx.ID_ANY, _(u"Show user &profile"))
self.favs = user.Append(wx.ID_ANY, _(u"V&iew likes"))
self.menubar_user = wx.Menu()
self.follow = self.menubar_user.Append(wx.ID_ANY, _(u"&Actions..."))
self.timeline = self.menubar_user.Append(wx.ID_ANY, _(u"&View timeline..."))
self.dm = self.menubar_user.Append(wx.ID_ANY, _(u"Direct me&ssage"))
self.addAlias = self.menubar_user.Append(wx.ID_ANY, _("Add a&lias"))
self.addToList = self.menubar_user.Append(wx.ID_ANY, _(u"&Add to list"))
self.removeFromList = self.menubar_user.Append(wx.ID_ANY, _(u"R&emove from list"))
self.viewLists = self.menubar_user.Append(wx.ID_ANY, _(u"&View lists"))
self.details = self.menubar_user.Append(wx.ID_ANY, _(u"Show user &profile"))
self.favs = self.menubar_user.Append(wx.ID_ANY, _(u"V&iew likes"))
# buffer menu
buffer = wx.Menu()
self.update_buffer = buffer.Append(wx.ID_ANY, _(u"&Update buffer"))
self.trends = buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer..."))
self.filter = buffer.Append(wx.ID_ANY, _(u"Create a &filter"))
self.manage_filters = buffer.Append(wx.ID_ANY, _(u"&Manage filters"))
self.find = buffer.Append(wx.ID_ANY, _(u"Find a string in the currently focused buffer..."))
self.load_previous_items = buffer.Append(wx.ID_ANY, _(u"&Load previous items"))
buffer.AppendSeparator()
self.mute_buffer = buffer.AppendCheckItem(wx.ID_ANY, _(u"&Mute"))
self.autoread = buffer.AppendCheckItem(wx.ID_ANY, _(u"&Autoread"))
self.clear = buffer.Append(wx.ID_ANY, _(u"&Clear buffer"))
self.deleteTl = buffer.Append(wx.ID_ANY, _(u"&Destroy"))
self.menubar_buffer = wx.Menu()
self.update_buffer = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Update buffer"))
self.trends = self.menubar_buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer..."))
self.filter = self.menubar_buffer.Append(wx.ID_ANY, _(u"Create a &filter"))
self.manage_filters = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Manage filters"))
self.find = self.menubar_buffer.Append(wx.ID_ANY, _(u"Find a string in the currently focused buffer..."))
self.load_previous_items = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Load previous items"))
self.menubar_buffer.AppendSeparator()
self.mute_buffer = self.menubar_buffer.AppendCheckItem(wx.ID_ANY, _(u"&Mute"))
self.autoread = self.menubar_buffer.AppendCheckItem(wx.ID_ANY, _(u"&Autoread"))
self.clear = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Clear buffer"))
self.deleteTl = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Destroy"))
# audio menu
audio = wx.Menu()
self.seekLeft = audio.Append(wx.ID_ANY, _(u"&Seek back 5 seconds"))
self.seekRight = audio.Append(wx.ID_ANY, _(u"&Seek forward 5 seconds"))
self.menubar_audio = wx.Menu()
self.seekLeft = self.menubar_audio.Append(wx.ID_ANY, _(u"&Seek back 5 seconds"))
self.seekRight = self.menubar_audio.Append(wx.ID_ANY, _(u"&Seek forward 5 seconds"))
# Help Menu
help = wx.Menu()
self.doc = help.Append(-1, _(u"&Documentation"))
self.sounds_tutorial = help.Append(wx.ID_ANY, _(u"Sounds &tutorial"))
self.changelog = help.Append(wx.ID_ANY, _(u"&What's new in this version?"))
self.check_for_updates = help.Append(wx.ID_ANY, _(u"&Check for updates"))
self.reportError = help.Append(wx.ID_ANY, _(u"&Report an error"))
self.visit_website = help.Append(-1, _(u"{0}'s &website").format(application.name,))
self.get_soundpacks = help.Append(-1, _(u"Get soundpacks for TWBlue"))
self.about = help.Append(-1, _(u"About &{0}").format(application.name,))
self.menubar_help = wx.Menu()
self.doc = self.menubar_help.Append(-1, _(u"&Documentation"))
self.sounds_tutorial = self.menubar_help.Append(wx.ID_ANY, _(u"Sounds &tutorial"))
self.changelog = self.menubar_help.Append(wx.ID_ANY, _(u"&What's new in this version?"))
self.check_for_updates = self.menubar_help.Append(wx.ID_ANY, _(u"&Check for updates"))
self.reportError = self.menubar_help.Append(wx.ID_ANY, _(u"&Report an error"))
self.visit_website = self.menubar_help.Append(-1, _(u"{0}'s &website").format(application.name,))
self.get_soundpacks = self.menubar_help.Append(-1, _(u"Get soundpacks for TWBlue"))
self.about = self.menubar_help.Append(-1, _(u"About &{0}").format(application.name,))
# Add all to the menu Bar
menuBar.Append(app, _(u"&Application"))
menuBar.Append(tweet, _(u"&Tweet"))
menuBar.Append(user, _(u"&User"))
menuBar.Append(buffer, _(u"&Buffer"))
menuBar.Append(audio, _(u"&Audio"))
menuBar.Append(help, _(u"&Help"))
self.menubar.Append(self.menubar_application, _(u"&Application"))
self.menubar.Append(self.menubar_item, _("&Tweet"))
self.menubar.Append(self.menubar_user, _(u"&User"))
self.menubar.Append(self.menubar_buffer, _(u"&Buffer"))
self.menubar.Append(self.menubar_audio, _(u"&Audio"))
self.menubar.Append(self.menubar_help, _(u"&Help"))
self.accel_tbl = wx.AcceleratorTable([
(wx.ACCEL_CTRL, ord('N'), self.compose.GetId()),
(wx.ACCEL_CTRL, ord('R'), self.reply.GetId()),
(wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('R'), self.retweet.GetId()),
(wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('R'), self.share.GetId()),
(wx.ACCEL_CTRL, ord('F'), self.fav.GetId()),
(wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('F'), self.unfav.GetId()),
(wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('V'), self.view.GetId()),
@@ -110,7 +108,6 @@ class mainFrame(wx.Frame):
])
self.SetAcceleratorTable(self.accel_tbl)
return menuBar
### MAIN
def __init__(self):
@@ -119,7 +116,8 @@ class mainFrame(wx.Frame):
self.panel = wx.Panel(self)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.SetTitle(application.name)
self.SetMenuBar(self.makeMenus())
self.makeMenus()
self.SetMenuBar(self.menubar)
self.nb = wx.Treebook(self.panel, wx.ID_ANY)
self.buffers = {}

View File

@@ -1,57 +0,0 @@
#!/bin/bash
# Define paths for a regular use, if there are not paths for python32 or 64, these commands will be taken.
pythonpath32="/C/python27x86"
pythonpath64="/C/python27"
nsispath=$PROGRAMFILES/NSIS
help () {
echo -e "$0 | usage:"
echo -e "$0 | \t./build_twblue.sh [-py32path <path to python for 32 bits> | -py64path <path for python on 64 bits> | -nsyspath <path to nsys> | -h]"
}
# parsing options from the command line
while [[ $# > 1 ]]
do
key="$1"
shift
case $key in
-py32path)
pythonpath32="$1"
shift
;;
-py64path)
pythonpath64="$1"
shift
;;
-nsispath)
nsispath="$1"
shift
;;
-help)
help
;;
*)
help
esac
done
cd ../src
if [ -d build/ ];
then
rm -rf build
fi
if [ -d dist/ ];
then
rm -rf dist
fi
$pythonpath32/python.exe "setup.py" "py2exe" "--quiet"
mv -f dist ../scripts/TWBlue
rm -rf build
$pythonpath64/python.exe "setup.py" "py2exe" "--quiet"
mv -f dist ../scripts/TWBlue64
rm -rf build
cd ../scripts
#$nsispath/Unicode/makensis.exe "twblue.nsi"
#rm -rf TWBlue
#rm -rf TWBlue64

View File

@@ -1,41 +0,0 @@
import sys, os, os.path, glob, argparse
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-v', '--verbose', action='store_true', default=False, dest='verbose')
verbose = parser.parse_args().verbose
srcpath = os.path.normpath(os.path.join(sys.path[0], "../src"))
file_count = 0
dir_count = 0
filenames = []
for (path, subdirs, files) in os.walk(srcpath):
filenames.extend(glob.glob(os.path.join(path, "*.pyc")))
# filenames.extend(glob.glob(os.path.join(path, "*.py")))
for filename in filenames:
try:
os.remove(filename)
if verbose: print "Removed " + filename
file_count += 1
except:
if verbose: print "Can't remove " + filename
#Remove empty directories.
if verbose: print "Removing empty directories..."
run_again = True
while run_again:
run_again = False
removals = []
for (path, subdirs, files) in os.walk(srcpath, topdown=False):
if len(subdirs) == 0 and len(files) == 0:
removals.append(path)
for path in removals:
try:
os.rmdir(path)
run_again = True
if verbose: print "Removed directory " + path
dir_count += 1
except:
if verbose: print "Can't remove directory " + path
print
print "{0} file(s), {1} dir(s) removed.".format(file_count, dir_count)

View File

@@ -1,13 +0,0 @@
#!/bin/bash
mkdir ../src/documentation
for i in `ls ../documentation`
do
if test -d ../documentation/$i
then
mkdir ../src/documentation/$i
pandoc -s ../documentation/$i/changes.md -o ../src/documentation/$i/changes.html
pandoc -s ../documentation/$i/manual.md -o ../src/documentation/$i/manual.html
cp ../documentation/license.txt ../src/documentation/license.txt
fi
done
exit

View File

@@ -1,5 +0,0 @@
@echo off
echo Generating application translation strings...
C:\python27\python.exe pygettext.py -v -d twblue ../src/*.pyw ../src/*.py ../src/*/*.py ../src/*/*.pyw ../src/*/*/*.py ../src/*/*/*.pyw ../src/*/*/*/*.py ../src/*/*/*/*.pyw ../src/*/*/*/*/*.py ../src/*/*/*/*/*.pyw
C:\python27\python.exe pygettext.py -v -d twblue-documentation ../doc/strings.py
C:\python27\python.exe pygettext.py -v -d twblue-changelog ../doc/changelog.py

View File

@@ -1,631 +0,0 @@
#! /usr/bin/env python3
# -*- coding: iso-8859-1 -*-
# Originally written by Barry Warsaw <barry@python.org>
#
# Minimally patched to make it even more xgettext compatible
# by Peter Funk <pf@artcom-gmbh.de>
#
# 2002-11-22 J<>rgen Hermann <jh@web.de>
# Added checks that _() only contains string literals, and
# command line args are resolved to module lists, i.e. you
# can now pass a filename, a module or package name, or a
# directory (including globbing chars, important for Win32).
# Made docstring fit in 80 chars wide displays using pydoc.
#
# for selftesting
try:
import fintl
_ = fintl.gettext
except ImportError:
_ = lambda s: s
__doc__ = _("""pygettext -- Python equivalent of xgettext(1)
Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
internationalization of C programs. Most of these tools are independent of
the programming language and can be used from within Python programs.
Martin von Loewis' work[1] helps considerably in this regard.
There's one problem though; xgettext is the program that scans source code
looking for message strings, but it groks only C (or C++). Python
introduces a few wrinkles, such as dual quoting characters, triple quoted
strings, and raw strings. xgettext understands none of this.
Enter pygettext, which uses Python's standard tokenize module to scan
Python source code, generating .pot files identical to what GNU xgettext[2]
generates for C and C++ code. From there, the standard GNU tools can be
used.
A word about marking Python strings as candidates for translation. GNU
xgettext recognizes the following keywords: gettext, dgettext, dcgettext,
and gettext_noop. But those can be a lot of text to include all over your
code. C and C++ have a trick: they use the C preprocessor. Most
internationalized C source includes a #define for gettext() to _() so that
what has to be written in the source is much less. Thus these are both
translatable strings:
gettext("Translatable String")
_("Translatable String")
Python of course has no preprocessor so this doesn't work so well. Thus,
pygettext searches only for _() by default, but see the -k/--keyword flag
below for how to augment this.
[1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
[2] http://www.gnu.org/software/gettext/gettext.html
NOTE: pygettext attempts to be option and feature compatible with GNU
xgettext where ever possible. However some options are still missing or are
not fully implemented. Also, xgettext's use of command line switches with
option arguments is broken, and in these cases, pygettext just defines
additional switches.
Usage: pygettext [options] inputfile ...
Options:
-a
--extract-all
Extract all strings.
-d name
--default-domain=name
Rename the default output file from messages.pot to name.pot.
-E
--escape
Replace non-ASCII characters with octal escape sequences.
-D
--docstrings
Extract module, class, method, and function docstrings. These do
not need to be wrapped in _() markers, and in fact cannot be for
Python to consider them docstrings. (See also the -X option).
-h
--help
Print this help message and exit.
-k word
--keyword=word
Keywords to look for in addition to the default set, which are:
%(DEFAULTKEYWORDS)s
You can have multiple -k flags on the command line.
-K
--no-default-keywords
Disable the default set of keywords (see above). Any keywords
explicitly added with the -k/--keyword option are still recognized.
--no-location
Do not write filename/lineno location comments.
-n
--add-location
Write filename/lineno location comments indicating where each
extracted string is found in the source. These lines appear before
each msgid. The style of comments is controlled by the -S/--style
option. This is the default.
-o filename
--output=filename
Rename the default output file from messages.pot to filename. If
filename is `-' then the output is sent to standard out.
-p dir
--output-dir=dir
Output files will be placed in directory dir.
-S stylename
--style stylename
Specify which style to use for location comments. Two styles are
supported:
Solaris # File: filename, line: line-number
GNU #: filename:line
The style name is case insensitive. GNU style is the default.
-v
--verbose
Print the names of the files being processed.
-V
--version
Print the version of pygettext and exit.
-w columns
--width=columns
Set width of output to columns.
-x filename
--exclude-file=filename
Specify a file that contains a list of strings that are not be
extracted from the input files. Each string to be excluded must
appear on a line by itself in the file.
-X filename
--no-docstrings=filename
Specify a file that contains a list of files (one per line) that
should not have their docstrings extracted. This is only useful in
conjunction with the -D option above.
If `inputfile' is -, standard input is read.
""")
import os
import importlib.machinery
import importlib.util
import sys
import glob
import time
import getopt
import token
import tokenize
__version__ = '1.5'
default_keywords = ['_']
DEFAULTKEYWORDS = ', '.join(default_keywords)
EMPTYSTRING = ''
# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
# there.
pot_header = _('''\
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\\n"
"POT-Creation-Date: %(time)s\\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
"Language-Team: LANGUAGE <LL@li.org>\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=%(charset)s\\n"
"Content-Transfer-Encoding: %(encoding)s\\n"
"Generated-By: pygettext.py %(version)s\\n"
''')
def usage(code, msg=''):
print(__doc__ % globals(), file=sys.stderr)
if msg:
print(msg, file=sys.stderr)
sys.exit(code)
def make_escapes(pass_nonascii):
global escapes, escape
if pass_nonascii:
# Allow non-ascii characters to pass through so that e.g. 'msgid
# "H<>he"' would result not result in 'msgid "H\366he"'. Otherwise we
# escape any character outside the 32..126 range.
mod = 128
escape = escape_ascii
else:
mod = 256
escape = escape_nonascii
escapes = [r"\%03o" % i for i in range(mod)]
for i in range(32, 127):
escapes[i] = chr(i)
escapes[ord('\\')] = r'\\'
escapes[ord('\t')] = r'\t'
escapes[ord('\r')] = r'\r'
escapes[ord('\n')] = r'\n'
escapes[ord('\"')] = r'\"'
def escape_ascii(s, encoding):
return ''.join(escapes[ord(c)] if ord(c) < 128 else c for c in s)
def escape_nonascii(s, encoding):
return ''.join(escapes[b] for b in s.encode(encoding))
def is_literal_string(s):
return s[0] in '\'"' or (s[0] in 'rRuU' and s[1] in '\'"')
def safe_eval(s):
# unwrap quotes, safely
return eval(s, {'__builtins__':{}}, {})
def normalize(s, encoding):
# This converts the various Python string types into a format that is
# appropriate for .po files, namely much closer to C style.
lines = s.split('\n')
if len(lines) == 1:
s = '"' + escape(s, encoding) + '"'
else:
if not lines[-1]:
del lines[-1]
lines[-1] = lines[-1] + '\n'
for i in range(len(lines)):
lines[i] = escape(lines[i], encoding)
lineterm = '\\n"\n"'
s = '""\n"' + lineterm.join(lines) + '"'
return s
def containsAny(str, set):
"""Check whether 'str' contains ANY of the chars in 'set'"""
return 1 in [c in str for c in set]
def getFilesForName(name):
"""Get a list of module files for a filename, a module or package name,
or a directory.
"""
if not os.path.exists(name):
# check for glob chars
if containsAny(name, "*?[]"):
files = glob.glob(name)
list = []
for file in files:
list.extend(getFilesForName(file))
return list
# try to find module or package
try:
spec = importlib.util.find_spec(name)
name = spec.origin
except ImportError:
name = None
if not name:
return []
if os.path.isdir(name):
# find all python files in directory
list = []
# get extension for python source files
_py_ext = importlib.machinery.SOURCE_SUFFIXES[0]
for root, dirs, files in os.walk(name):
# don't recurse into CVS directories
if 'CVS' in dirs:
dirs.remove('CVS')
# add all *.py files to list
list.extend(
[os.path.join(root, file) for file in files
if os.path.splitext(file)[1] == _py_ext]
)
return list
elif os.path.exists(name):
# a single file
return [name]
return []
class TokenEater:
def __init__(self, options):
self.__options = options
self.__messages = {}
self.__state = self.__waiting
self.__data = []
self.__lineno = -1
self.__freshmodule = 1
self.__curfile = None
self.__enclosurecount = 0
def __call__(self, ttype, tstring, stup, etup, line):
# dispatch
## import token
## print('ttype:', token.tok_name[ttype], 'tstring:', tstring,
## file=sys.stderr)
self.__state(ttype, tstring, stup[0])
def __waiting(self, ttype, tstring, lineno):
opts = self.__options
# Do docstring extractions, if enabled
if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
# module docstring?
if self.__freshmodule:
if ttype == tokenize.STRING and is_literal_string(tstring):
self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
self.__freshmodule = 0
elif ttype not in (tokenize.COMMENT, tokenize.NL):
self.__freshmodule = 0
return
# class or func/method docstring?
if ttype == tokenize.NAME and tstring in ('class', 'def'):
self.__state = self.__suiteseen
return
if ttype == tokenize.NAME and tstring in opts.keywords:
self.__state = self.__keywordseen
def __suiteseen(self, ttype, tstring, lineno):
# skip over any enclosure pairs until we see the colon
if ttype == tokenize.OP:
if tstring == ':' and self.__enclosurecount == 0:
# we see a colon and we're not in an enclosure: end of def
self.__state = self.__suitedocstring
elif tstring in '([{':
self.__enclosurecount += 1
elif tstring in ')]}':
self.__enclosurecount -= 1
def __suitedocstring(self, ttype, tstring, lineno):
# ignore any intervening noise
if ttype == tokenize.STRING and is_literal_string(tstring):
self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
self.__state = self.__waiting
elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
tokenize.COMMENT):
# there was no class docstring
self.__state = self.__waiting
def __keywordseen(self, ttype, tstring, lineno):
if ttype == tokenize.OP and tstring == '(':
self.__data = []
self.__lineno = lineno
self.__state = self.__openseen
else:
self.__state = self.__waiting
def __openseen(self, ttype, tstring, lineno):
if ttype == tokenize.OP and tstring == ')':
# We've seen the last of the translatable strings. Record the
# line number of the first line of the strings and update the list
# of messages seen. Reset state for the next batch. If there
# were no strings inside _(), then just ignore this entry.
if self.__data:
self.__addentry(EMPTYSTRING.join(self.__data))
self.__state = self.__waiting
elif ttype == tokenize.STRING and is_literal_string(tstring):
self.__data.append(safe_eval(tstring))
elif ttype not in [tokenize.COMMENT, token.INDENT, token.DEDENT,
token.NEWLINE, tokenize.NL]:
# warn if we see anything else than STRING or whitespace
print(_(
'*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"'
) % {
'token': tstring,
'file': self.__curfile,
'lineno': self.__lineno
}, file=sys.stderr)
self.__state = self.__waiting
def __addentry(self, msg, lineno=None, isdocstring=0):
if lineno is None:
lineno = self.__lineno
if not msg in self.__options.toexclude:
entry = (self.__curfile, lineno)
self.__messages.setdefault(msg, {})[entry] = isdocstring
def set_filename(self, filename):
self.__curfile = filename
self.__freshmodule = 1
def write(self, fp):
options = self.__options
timestamp = time.strftime('%Y-%m-%d %H:%M%z')
encoding = fp.encoding if fp.encoding else 'UTF-8'
print(pot_header % {'time': timestamp, 'version': __version__,
'charset': encoding,
'encoding': '8bit'}, file=fp)
# Sort the entries. First sort each particular entry's keys, then
# sort all the entries by their first item.
reverse = {}
for k, v in self.__messages.items():
keys = sorted(v.keys())
reverse.setdefault(tuple(keys), []).append((k, v))
rkeys = sorted(reverse.keys())
for rkey in rkeys:
rentries = reverse[rkey]
rentries.sort()
for k, v in rentries:
# If the entry was gleaned out of a docstring, then add a
# comment stating so. This is to aid translators who may wish
# to skip translating some unimportant docstrings.
isdocstring = any(v.values())
# k is the message string, v is a dictionary-set of (filename,
# lineno) tuples. We want to sort the entries in v first by
# file name and then by line number.
v = sorted(v.keys())
if not options.writelocations:
pass
# location comments are different b/w Solaris and GNU:
elif options.locationstyle == options.SOLARIS:
for filename, lineno in v:
d = {'filename': filename, 'lineno': lineno}
print(_(
'# File: %(filename)s, line: %(lineno)d') % d, file=fp)
elif options.locationstyle == options.GNU:
# fit as many locations on one line, as long as the
# resulting line length doesn't exceed 'options.width'
locline = '#:'
for filename, lineno in v:
d = {'filename': filename, 'lineno': lineno}
s = _(' %(filename)s:%(lineno)d') % d
if len(locline) + len(s) <= options.width:
locline = locline + s
else:
print(locline, file=fp)
locline = "#:" + s
if len(locline) > 2:
print(locline, file=fp)
if isdocstring:
print('#, docstring', file=fp)
print('msgid', normalize(k, encoding), file=fp)
print('msgstr ""\n', file=fp)
def main():
global default_keywords
try:
opts, args = getopt.getopt(
sys.argv[1:],
'ad:DEhk:Kno:p:S:Vvw:x:X:',
['extract-all', 'default-domain=', 'escape', 'help',
'keyword=', 'no-default-keywords',
'add-location', 'no-location', 'output=', 'output-dir=',
'style=', 'verbose', 'version', 'width=', 'exclude-file=',
'docstrings', 'no-docstrings',
])
except getopt.error as msg:
usage(1, msg)
# for holding option values
class Options:
# constants
GNU = 1
SOLARIS = 2
# defaults
extractall = 0 # FIXME: currently this option has no effect at all.
escape = 0
keywords = []
outpath = ''
outfile = 'messages.pot'
writelocations = 1
locationstyle = GNU
verbose = 0
width = 78
excludefilename = ''
docstrings = 0
nodocstrings = {}
options = Options()
locations = {'gnu' : options.GNU,
'solaris' : options.SOLARIS,
}
# parse options
for opt, arg in opts:
if opt in ('-h', '--help'):
usage(0)
elif opt in ('-a', '--extract-all'):
options.extractall = 1
elif opt in ('-d', '--default-domain'):
options.outfile = arg + '.pot'
elif opt in ('-E', '--escape'):
options.escape = 1
elif opt in ('-D', '--docstrings'):
options.docstrings = 1
elif opt in ('-k', '--keyword'):
options.keywords.append(arg)
elif opt in ('-K', '--no-default-keywords'):
default_keywords = []
elif opt in ('-n', '--add-location'):
options.writelocations = 1
elif opt in ('--no-location',):
options.writelocations = 0
elif opt in ('-S', '--style'):
options.locationstyle = locations.get(arg.lower())
if options.locationstyle is None:
usage(1, _('Invalid value for --style: %s') % arg)
elif opt in ('-o', '--output'):
options.outfile = arg
elif opt in ('-p', '--output-dir'):
options.outpath = arg
elif opt in ('-v', '--verbose'):
options.verbose = 1
elif opt in ('-V', '--version'):
print(_('pygettext.py (xgettext for Python) %s') % __version__)
sys.exit(0)
elif opt in ('-w', '--width'):
try:
options.width = int(arg)
except ValueError:
usage(1, _('--width argument must be an integer: %s') % arg)
elif opt in ('-x', '--exclude-file'):
options.excludefilename = arg
elif opt in ('-X', '--no-docstrings'):
fp = open(arg)
try:
while 1:
line = fp.readline()
if not line:
break
options.nodocstrings[line[:-1]] = 1
finally:
fp.close()
# calculate escapes
make_escapes(not options.escape)
# calculate all keywords
options.keywords.extend(default_keywords)
# initialize list of strings to exclude
if options.excludefilename:
try:
fp = open(options.excludefilename)
options.toexclude = fp.readlines()
fp.close()
except IOError:
print(_(
"Can't read --exclude-file: %s") % options.excludefilename, file=sys.stderr)
sys.exit(1)
else:
options.toexclude = []
# resolve args to module lists
expanded = []
for arg in args:
if arg == '-':
expanded.append(arg)
else:
expanded.extend(getFilesForName(arg))
args = expanded
# slurp through all the files
eater = TokenEater(options)
for filename in args:
if filename == '-':
if options.verbose:
print(_('Reading standard input'))
fp = sys.stdin.buffer
closep = 0
else:
if options.verbose:
print(_('Working on %s') % filename)
fp = open(filename, 'rb')
closep = 1
try:
eater.set_filename(filename)
try:
tokens = tokenize.tokenize(fp.readline)
for _token in tokens:
eater(*_token)
except tokenize.TokenError as e:
print('%s: %s, line %d, column %d' % (
e.args[0], filename, e.args[1][0], e.args[1][1]),
file=sys.stderr)
finally:
if closep:
fp.close()
# write the output
if options.outfile == '-':
fp = sys.stdout
closep = 0
else:
if options.outpath:
options.outfile = os.path.join(options.outpath, options.outfile)
fp = open(options.outfile, 'w')
closep = 1
try:
eater.write(fp)
finally:
if closep:
fp.close()
if __name__ == '__main__':
main()
# some more test strings
# this one creates a warning
_('*** Seen unexpected token "%(token)s"') % {'token': 'test'}
_('more' 'than' 'one' 'string')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{"current_version": "2022.8.28",
"description": "Added support for creating threads, upload videos and polls to Twitter.",
{"current_version": "2023.2.3",
"description": "Stops support for Twitter session on Feb 9, updates to mastodon sessions. Please avoid using autoupdate as it will not work.",
"date": "unknown",
"downloads":
{"Windows32": "https://twblue.es/pubs/twblue_x86.zip",