summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--LICENSES/BSD-3-Clause.txt26
-rw-r--r--LICENSES/LGPL-2.1-or-later.txt501
-rw-r--r--buteo-plugins/buteo-common/buteo-common.pri4
-rw-r--r--buteo-plugins/buteo-common/buteo-common.pro4
-rw-r--r--buteo-plugins/buteo-plugins.pro4
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro31
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml3
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml3
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp34
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h1
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp47
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp395
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h10
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro4
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml3
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml3
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp83
-rw-r--r--common/common.pri4
-rw-r--r--common/common.pro5
-rw-r--r--common/mastodontextutils.h122
-rw-r--r--eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml71
-rw-r--r--eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro4
-rw-r--r--eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml10
-rw-r--r--eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp4
-rw-r--r--eventsview-plugins/eventsview-plugins.pro4
-rw-r--r--icons/icons.pro4
-rw-r--r--rpm/sailfish-account-mastodon.spec14
-rw-r--r--sailfish-account-mastodon.pro4
-rw-r--r--settings/accounts-translations-plugin/accounts-translations-plugin.pro4
-rw-r--r--settings/accounts-translations-plugin/plugin.cpp4
-rw-r--r--settings/accounts/accounts.pro6
-rw-r--r--settings/accounts/providers/mastodon.provider3
-rw-r--r--settings/accounts/services/mastodon-microblog.service5
-rw-r--r--settings/accounts/services/mastodon-notifications.service32
-rw-r--r--settings/accounts/services/mastodon-sharing.service3
-rw-r--r--settings/accounts/ui/MastodonSettingsDisplay.qml35
-rw-r--r--settings/accounts/ui/mastodon-settings.qml4
-rw-r--r--settings/accounts/ui/mastodon-update.qml7
-rw-r--r--settings/accounts/ui/mastodon.qml9
-rw-r--r--settings/settings.pro4
-rw-r--r--transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml4
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp4
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h4
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp4
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h4
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro4
-rw-r--r--transferengine-plugins/mastodonshareservicestatus.cpp23
-rw-r--r--transferengine-plugins/mastodonshareservicestatus.h5
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonapi.cpp51
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonapi.h6
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp4
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h4
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro4
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp4
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonuploader.h4
-rw-r--r--transferengine-plugins/transferengine-plugins.pro4
57 files changed, 1360 insertions, 293 deletions
diff --git a/.gitignore b/.gitignore
index 7f9abe7..b0f3738 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-documentation.list
+/*.list
Makefile
*.o
*.so
diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000..e6a0aa5
--- /dev/null
+++ b/LICENSES/BSD-3-Clause.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2013 - 2026 Jolla Ltd.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt
new file mode 100644
index 0000000..f6683e7
--- /dev/null
+++ b/LICENSES/LGPL-2.1-or-later.txt
@@ -0,0 +1,501 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Moe Ghoul>, 1 April 1990
+ Moe Ghoul, President of Vice
+
+That's all there is to it!
diff --git a/buteo-plugins/buteo-common/buteo-common.pri b/buteo-plugins/buteo-common/buteo-common.pri
index 192da35..83452ac 100644
--- a/buteo-plugins/buteo-common/buteo-common.pri
+++ b/buteo-plugins/buteo-common/buteo-common.pri
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
INCLUDEPATH += $$PWD
DEPENDPATH += .
diff --git a/buteo-plugins/buteo-common/buteo-common.pro b/buteo-plugins/buteo-common/buteo-common.pro
index c1aa569..c0b84a9 100644
--- a/buteo-plugins/buteo-common/buteo-common.pro
+++ b/buteo-plugins/buteo-common/buteo-common.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = lib
TARGET = mastodonbuteocommon
diff --git a/buteo-plugins/buteo-plugins.pro b/buteo-plugins/buteo-plugins.pro
index 179aebf..ead60e6 100644
--- a/buteo-plugins/buteo-plugins.pro
+++ b/buteo-plugins/buteo-plugins.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = subdirs
SUBDIRS += \
buteo-common \
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro
index 81060b1..d16cc3d 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TARGET = mastodon-notifications-client
QT -= gui
@@ -5,6 +9,31 @@ QT -= gui
include($$PWD/../buteo-common/buteo-common.pri)
include($$PWD/../../common/common.pri)
+TS_FILE = $$OUT_PWD/lipstick-jolla-home-mastodon-notifications.ts
+EE_QM = $$OUT_PWD/lipstick-jolla-home-mastodon-notifications_eng_en.qm
+
+ts.commands += lupdate $$PWD -ts $$TS_FILE
+ts.CONFIG += no_check_exist no_link
+ts.output = $$TS_FILE
+ts.input = .
+
+ts_install.files = $$TS_FILE
+ts_install.path = /usr/share/translations/source
+ts_install.CONFIG += no_check_exist
+
+engineering_english.commands += lrelease -idbased $$TS_FILE -qm $$EE_QM
+engineering_english.CONFIG += no_check_exist no_link
+engineering_english.depends = ts
+engineering_english.input = $$TS_FILE
+engineering_english.output = $$EE_QM
+
+engineering_english_install.path = /usr/share/translations
+engineering_english_install.files = $$EE_QM
+engineering_english_install.CONFIG += no_check_exist
+
+QMAKE_EXTRA_TARGETS += ts engineering_english
+PRE_TARGETDEPS += ts engineering_english
+
CONFIG += link_pkgconfig
PKGCONFIG += mlite5 nemonotifications-qt5
@@ -34,4 +63,4 @@ sync.files = mastodon.Notifications.xml
client.path = /etc/buteo/profiles/client
client.files = mastodon-notifications.xml
-INSTALLS += target sync client
+INSTALLS += target sync client ts_install engineering_english_install
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml
index 8d042d9..3284d61 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<profile name="mastodon-notifications" type="client" >
<field name="Sync Direction" />
</profile>
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml
index 2843180..05d5218 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<profile name="mastodon.Notifications" type="sync" >
<key name="category" value="eventfeed" />
<key name="enabled" value="true" />
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp
index 295d7b9..ddf6686 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp
@@ -20,8 +20,8 @@
#include "mastodondatatypesyncadaptor.h"
#include "mastodonauthutils.h"
-#include "trace.h"
+#include <QtCore/QLoggingCategory>
#include <QtCore/QVariantMap>
#include <QtNetwork/QNetworkRequest>
@@ -36,6 +36,8 @@
#include <SignOn/AuthSession>
#include <SignOn/SessionData>
+Q_LOGGING_CATEGORY(lcMastodonNotificationsSync, "buteo.plugin.mastodon.notifications.sync", QtWarningMsg)
+
MastodonNotificationsDataTypeSyncAdaptor::MastodonNotificationsDataTypeSyncAdaptor(
SocialNetworkSyncAdaptor::DataType dataType,
QObject *parent)
@@ -50,7 +52,7 @@ MastodonNotificationsDataTypeSyncAdaptor::~MastodonNotificationsDataTypeSyncAdap
void MastodonNotificationsDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId)
{
if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) {
- qCWarning(lcSocialPlugin) << "Mastodon" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ qCWarning(lcMastodonNotificationsSync) << "Mastodon" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
<< "sync adaptor was asked to sync" << dataTypeString;
setStatus(SocialNetworkSyncAdaptor::Error);
return;
@@ -58,14 +60,14 @@ void MastodonNotificationsDataTypeSyncAdaptor::sync(const QString &dataTypeStrin
setStatus(SocialNetworkSyncAdaptor::Busy);
updateDataForAccount(accountId);
- qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name();
+ qCDebug(lcMastodonNotificationsSync) << "successfully triggered sync with profile:" << m_accountSyncProfile->name();
}
void MastodonNotificationsDataTypeSyncAdaptor::updateDataForAccount(int accountId)
{
Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this);
if (!account) {
- qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved";
+ qCWarning(lcMastodonNotificationsSync) << "existing account with id" << accountId << "couldn't be retrieved";
setStatus(SocialNetworkSyncAdaptor::Error);
return;
}
@@ -79,6 +81,11 @@ QString MastodonNotificationsDataTypeSyncAdaptor::apiHost(int accountId) const
return m_apiHosts.value(accountId, QStringLiteral("https://mastodon.social"));
}
+QString MastodonNotificationsDataTypeSyncAdaptor::authServiceName() const
+{
+ return syncServiceName();
+}
+
void MastodonNotificationsDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err)
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
@@ -89,7 +96,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::errorHandler(QNetworkReply::Netwo
const int accountId = reply->property("accountId").toInt();
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ qCWarning(lcMastodonNotificationsSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
<< "request with account" << accountId
<< "experienced error:" << err
<< "HTTP:" << httpStatus;
@@ -114,7 +121,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSsl
sslerrs.chop(2);
}
- qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ qCWarning(lcMastodonNotificationsSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
<< "request with account" << sender()->property("accountId").toInt()
<< "experienced ssl errors:" << sslerrs;
sender()->setProperty("isError", QVariant::fromValue<bool>(true));
@@ -122,8 +129,8 @@ void MastodonNotificationsDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSsl
void MastodonNotificationsDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account)
{
- qCInfo(lcSocialPlugin) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id();
- Accounts::Service srv(m_accountManager->service(syncServiceName()));
+ qCInfo(lcMastodonNotificationsSync) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id();
+ Accounts::Service srv(m_accountManager->service(authServiceName()));
account->selectService(srv);
account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue<bool>(true));
account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue<QString>(QString::fromLatin1("sociald-mastodon")));
@@ -139,13 +146,14 @@ void MastodonNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account
return;
}
- Accounts::Service srv(m_accountManager->service(syncServiceName()));
+ Accounts::Service srv(m_accountManager->service(authServiceName()));
account->selectService(srv);
+
SignOn::Identity *identity = account->credentialsId() > 0
? SignOn::Identity::existingIdentity(account->credentialsId())
: 0;
if (!identity) {
- qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials, cannot sign in";
+ qCWarning(lcMastodonNotificationsSync) << "account" << accountId << "has no valid credentials, cannot sign in";
decrementSemaphore(accountId);
return;
}
@@ -155,7 +163,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account
const QString mechanism = accSrv.authData().mechanism();
SignOn::AuthSession *session = identity->createSession(method);
if (!session) {
- qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId;
+ qCWarning(lcMastodonNotificationsSync) << "could not create signon session for account" << accountId;
identity->deleteLater();
decrementSemaphore(accountId);
return;
@@ -183,7 +191,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::signOnError(const SignOn::Error &
SignOn::Identity *identity = session->property("identity").value<SignOn::Identity*>();
const int accountId = account->id();
- qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId
+ qCWarning(lcMastodonNotificationsSync) << "credentials for account with id" << accountId
<< "couldn't be retrieved:" << error.type() << error.message();
if (error.type() == SignOn::Error::UserInteraction) {
@@ -211,7 +219,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::signOnResponse(const SignOn::Sess
accessToken = MastodonAuthUtils::accessToken(data);
if (accessToken.isEmpty()) {
- qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId
+ qCWarning(lcMastodonNotificationsSync) << "signon response for account with id" << accountId
<< "contained no access token; keys:" << data.keys();
}
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h
index 3bb6e23..3c61ade 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h
@@ -48,6 +48,7 @@ public:
protected:
QString apiHost(int accountId) const;
virtual void updateDataForAccount(int accountId);
+ virtual QString authServiceName() const;
virtual void beginSync(int accountId, const QString &accessToken) = 0;
protected Q_SLOTS:
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp
index fe9f989..9dd3724 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp
@@ -22,6 +22,52 @@
#include "mastodonnotificationssyncadaptor.h"
#include "socialnetworksyncadaptor.h"
+#include <QCoreApplication>
+#include <QLocale>
+#include <QTranslator>
+
+namespace {
+class AppTranslator : public QTranslator
+{
+public:
+ explicit AppTranslator(QObject *parent)
+ : QTranslator(parent)
+ {
+ qApp->installTranslator(this);
+ }
+
+ ~AppTranslator() override
+ {
+ qApp->removeTranslator(this);
+ }
+};
+
+void ensureNotificationTranslations()
+{
+ static bool initialized = false;
+ if (initialized) {
+ return;
+ }
+
+ QCoreApplication *app = QCoreApplication::instance();
+ if (!app) {
+ return;
+ }
+
+ AppTranslator *engineeringEnglish = new AppTranslator(app);
+ engineeringEnglish->load(QStringLiteral("lipstick-jolla-home-mastodon-notifications_eng_en"),
+ QStringLiteral("/usr/share/translations"));
+
+ AppTranslator *translator = new AppTranslator(app);
+ translator->load(QLocale(),
+ QStringLiteral("lipstick-jolla-home-mastodon-notifications"),
+ QStringLiteral("-"),
+ QStringLiteral("/usr/share/translations"));
+
+ initialized = true;
+}
+}
+
MastodonNotificationsPlugin::MastodonNotificationsPlugin(const QString& pluginName,
const Buteo::SyncProfile& profile,
Buteo::PluginCbInterface *callbackInterface)
@@ -29,6 +75,7 @@ MastodonNotificationsPlugin::MastodonNotificationsPlugin(const QString& pluginNa
QStringLiteral("mastodon"),
SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications))
{
+ ensureNotificationTranslations();
}
MastodonNotificationsPlugin::~MastodonNotificationsPlugin()
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp
index 7775841..98dbbc8 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp
@@ -19,12 +19,13 @@
****************************************************************************/
#include "mastodonnotificationssyncadaptor.h"
-#include "trace.h"
+#include "mastodontextutils.h"
+#include <QtCore/QCoreApplication>
+#include <QtCore/QLoggingCategory>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
-#include <QtCore/QRegularExpression>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QNetworkRequest>
@@ -50,42 +51,79 @@
)
namespace {
+ Q_LOGGING_CATEGORY(lcMastodonNotifications, "buteo.plugin.mastodon.notifications", QtWarningMsg)
+
const char *const NotificationCategory = "x-nemo.social.mastodon.notification";
const char *const NotificationIdHint = "x-nemo.sociald.notification-id";
const char *const LastFetchedNotificationIdKey = "LastFetchedNotificationId";
const int NotificationsPageLimit = 80;
-
- QString decodeHtmlEntities(QString text)
- {
- text.replace(QStringLiteral("&quot;"), QStringLiteral("\""));
- text.replace(QStringLiteral("&apos;"), QStringLiteral("'"));
- text.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
- text.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
- text.replace(QStringLiteral("&amp;"), QStringLiteral("&"));
- text.replace(QStringLiteral("&nbsp;"), QStringLiteral(" "));
-
- static const QRegularExpression decimalEntity(QStringLiteral("&#(\\d+);"));
- QRegularExpressionMatch match;
- int index = 0;
- while ((index = text.indexOf(decimalEntity, index, &match)) != -1) {
- const uint value = match.captured(1).toUInt();
- const QString replacement = value > 0 ? QString(QChar(value)) : QString();
- text.replace(index, match.capturedLength(0), replacement);
- index += replacement.size();
- }
-
- static const QRegularExpression hexEntity(QStringLiteral("&#x([0-9a-fA-F]+);"));
- index = 0;
- while ((index = text.indexOf(hexEntity, index, &match)) != -1) {
- bool ok = false;
- const uint value = match.captured(1).toUInt(&ok, 16);
- const QString replacement = ok && value > 0 ? QString(QChar(value)) : QString();
- text.replace(index, match.capturedLength(0), replacement);
- index += replacement.size();
- }
-
- return text;
- }
+ const uint NotificationDismissedReason = 1;
+
+ //% "mentioned you"
+ const char *const TrIdMentionedYou = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-mentioned_you");
+ //% "boosted your post"
+ const char *const TrIdBoostedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-boosted_your_post");
+ //% "favourited your post"
+ const char *const TrIdFavouritedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-favourited_your_post");
+ //% "started following you"
+ const char *const TrIdStartedFollowingYou = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-started_following_you");
+ //% "requested to follow you"
+ const char *const TrIdRequestedToFollowYou = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-requested_to_follow_you");
+ //% "interacted with your poll"
+ const char *const TrIdInteractedWithYourPoll = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-interacted_with_your_poll");
+ //% "posted"
+ const char *const TrIdPosted = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-posted");
+ //% "updated a post"
+ const char *const TrIdUpdatedPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-updated_post");
+ //% "signed up"
+ const char *const TrIdSignedUp = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-signed_up");
+ //% "reported an account"
+ const char *const TrIdReportedAccount = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-reported_account");
+ //% "received a moderation warning"
+ const char *const TrIdReceivedModerationWarning = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-received_moderation_warning");
+ //% "quoted your post"
+ const char *const TrIdQuotedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-quoted_your_post");
+ //% "updated a post that quoted you"
+ const char *const TrIdUpdatedQuotedPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-updated_quoted_post");
+ //% "sent you a notification"
+ const char *const TrIdSentNotification = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-sent_notification");
+
+ //% "An admin blocked an instance"
+ const char *const TrIdAdminBlockedInstance = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-admin_blocked_instance");
+ //% "An admin blocked %1"
+ const char *const TrIdAdminBlockedTarget = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-admin_blocked_target");
+ //% "You blocked an instance"
+ const char *const TrIdYouBlockedInstance = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-you_blocked_instance");
+ //% "You blocked %1"
+ const char *const TrIdYouBlockedTarget = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-you_blocked_target");
+ //% "An account was suspended"
+ const char *const TrIdAccountSuspended = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-account_suspended");
+ //% "%1 was suspended"
+ const char *const TrIdTargetSuspended = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-target_suspended");
+ //% "Some follow relationships were severed"
+ const char *const TrIdRelationshipsSevered = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-relationships_severed");
+ //% "%1 (%2 followers, %3 following removed)"
+ const char *const TrIdRelationshipsSummary = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-relationships_summary");
+
+ //% "A moderator sent you a warning"
+ const char *const TrIdModeratorWarningNone = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_none");
+ //% "A moderator disabled your account"
+ const char *const TrIdModeratorWarningDisable = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_disable");
+ //% "A moderator marked specific posts as sensitive"
+ const char *const TrIdModeratorWarningSpecificSensitive = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_specific_sensitive");
+ //% "A moderator deleted specific posts"
+ const char *const TrIdModeratorWarningDeletePosts = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_delete_posts");
+ //% "A moderator marked all your posts as sensitive"
+ const char *const TrIdModeratorWarningAllSensitive = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_all_sensitive");
+ //% "A moderator limited your account"
+ const char *const TrIdModeratorWarningSilence = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_silence");
+ //% "A moderator suspended your account"
+ const char *const TrIdModeratorWarningSuspend = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_suspend");
+
+ //% "Mastodon"
+ const char *const TrIdMastodon = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-mastodon");
+ //% "New notification"
+ const char *const TrIdNewNotification = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-new_notification");
QString displayNameForAccount(const QJsonObject &account)
{
@@ -105,33 +143,112 @@ namespace {
QString actionText(const QString &type)
{
if (type == QLatin1String("mention")) {
- return QStringLiteral("mentioned you");
+ return qtTrId(TrIdMentionedYou);
} else if (type == QLatin1String("reblog")) {
- return QStringLiteral("boosted your post");
+ return qtTrId(TrIdBoostedYourPost);
} else if (type == QLatin1String("favourite")) {
- return QStringLiteral("favourited your post");
+ return qtTrId(TrIdFavouritedYourPost);
} else if (type == QLatin1String("follow")) {
- return QStringLiteral("started following you");
+ return qtTrId(TrIdStartedFollowingYou);
} else if (type == QLatin1String("follow_request")) {
- return QStringLiteral("requested to follow you");
+ return qtTrId(TrIdRequestedToFollowYou);
} else if (type == QLatin1String("poll")) {
- return QStringLiteral("interacted with your poll");
+ return qtTrId(TrIdInteractedWithYourPoll);
} else if (type == QLatin1String("status")) {
- return QStringLiteral("posted");
+ return qtTrId(TrIdPosted);
} else if (type == QLatin1String("update")) {
- return QStringLiteral("updated a post");
+ return qtTrId(TrIdUpdatedPost);
+ } else if (type == QLatin1String("admin.sign_up")) {
+ return qtTrId(TrIdSignedUp);
+ } else if (type == QLatin1String("admin.report")) {
+ return qtTrId(TrIdReportedAccount);
+ } else if (type == QLatin1String("moderation_warning")) {
+ return qtTrId(TrIdReceivedModerationWarning);
+ } else if (type == QLatin1String("quote")) {
+ return qtTrId(TrIdQuotedYourPost);
+ } else if (type == QLatin1String("quoted_update")) {
+ return qtTrId(TrIdUpdatedQuotedPost);
}
- return QStringLiteral("sent you a notification");
+ return qtTrId(TrIdSentNotification);
+ }
+
+ bool useSystemSummary(const QString &notificationType)
+ {
+ return notificationType == QLatin1String("severed_relationships")
+ || notificationType == QLatin1String("moderation_warning");
}
- bool hasActiveNotificationsForAccount(int accountId)
+ QString severedRelationshipsText(const QJsonObject &eventObject)
+ {
+ const QString eventType = eventObject.value(QStringLiteral("type")).toString();
+ const QString targetName = eventObject.value(QStringLiteral("target_name")).toString().trimmed();
+ const int followersCount = eventObject.value(QStringLiteral("followers_count")).toInt();
+ const int followingCount = eventObject.value(QStringLiteral("following_count")).toInt();
+
+ QString action;
+ if (eventType == QLatin1String("domain_block")) {
+ action = targetName.isEmpty()
+ ? qtTrId(TrIdAdminBlockedInstance)
+ : qtTrId(TrIdAdminBlockedTarget).arg(targetName);
+ } else if (eventType == QLatin1String("user_domain_block")) {
+ action = targetName.isEmpty()
+ ? qtTrId(TrIdYouBlockedInstance)
+ : qtTrId(TrIdYouBlockedTarget).arg(targetName);
+ } else if (eventType == QLatin1String("account_suspension")) {
+ action = targetName.isEmpty()
+ ? qtTrId(TrIdAccountSuspended)
+ : qtTrId(TrIdTargetSuspended).arg(targetName);
+ } else {
+ action = qtTrId(TrIdRelationshipsSevered);
+ }
+
+ const int affectedCount = followersCount + followingCount;
+ if (affectedCount <= 0) {
+ return action;
+ }
+
+ return qtTrId(TrIdRelationshipsSummary)
+ .arg(action)
+ .arg(followersCount)
+ .arg(followingCount);
+ }
+
+ QString moderationWarningText(const QJsonObject &warningObject)
+ {
+ const QString warningText = warningObject.value(QStringLiteral("text")).toString().trimmed();
+ if (!warningText.isEmpty()) {
+ return warningText;
+ }
+
+ const QString action = warningObject.value(QStringLiteral("action")).toString();
+ if (action == QLatin1String("none")) {
+ return qtTrId(TrIdModeratorWarningNone);
+ } else if (action == QLatin1String("disable")) {
+ return qtTrId(TrIdModeratorWarningDisable);
+ } else if (action == QLatin1String("mark_statuses_as_sensitive")) {
+ return qtTrId(TrIdModeratorWarningSpecificSensitive);
+ } else if (action == QLatin1String("delete_statuses")) {
+ return qtTrId(TrIdModeratorWarningDeletePosts);
+ } else if (action == QLatin1String("sensitive")) {
+ return qtTrId(TrIdModeratorWarningAllSensitive);
+ } else if (action == QLatin1String("silence")) {
+ return qtTrId(TrIdModeratorWarningSilence);
+ } else if (action == QLatin1String("suspend")) {
+ return qtTrId(TrIdModeratorWarningSuspend);
+ }
+
+ return QString();
+ }
+
+ bool hasActiveNotificationsForAccount(int accountId, const Notification *ignoredNotification = 0)
{
bool hasActiveNotifications = false;
const QList<QObject *> notifications = Notification::notifications();
foreach (QObject *object, notifications) {
Notification *notification = qobject_cast<Notification *>(object);
if (notification
+ && notification != ignoredNotification
&& notification->category() == QLatin1String(NotificationCategory)
&& notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) {
hasActiveNotifications = true;
@@ -185,6 +302,11 @@ MastodonNotificationsSyncAdaptor::~MastodonNotificationsSyncAdaptor()
QString MastodonNotificationsSyncAdaptor::syncServiceName() const
{
+ return QStringLiteral("mastodon-notifications");
+}
+
+QString MastodonNotificationsSyncAdaptor::authServiceName() const
+{
return QStringLiteral("mastodon-microblog");
}
@@ -192,6 +314,7 @@ void MastodonNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialN
{
closeAccountNotifications(oldId);
+ m_accessTokens.remove(oldId);
m_pendingSyncStates.remove(oldId);
m_lastMarkedReadIds.remove(oldId);
saveLastFetchedId(oldId, QString());
@@ -199,6 +322,7 @@ void MastodonNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialN
void MastodonNotificationsSyncAdaptor::beginSync(int accountId, const QString &accessToken)
{
+ m_accessTokens.insert(accountId, accessToken);
m_pendingSyncStates.remove(accountId);
requestUnreadMarker(accountId, accessToken);
}
@@ -206,7 +330,7 @@ void MastodonNotificationsSyncAdaptor::beginSync(int accountId, const QString &a
void MastodonNotificationsSyncAdaptor::finalize(int accountId)
{
if (syncAborted()) {
- qCInfo(lcSocialPlugin) << "sync aborted, won't update notifications";
+ qCInfo(lcMastodonNotifications) << "sync aborted, won't update notifications";
}
Q_UNUSED(accountId)
@@ -214,52 +338,12 @@ void MastodonNotificationsSyncAdaptor::finalize(int accountId)
QString MastodonNotificationsSyncAdaptor::sanitizeContent(const QString &content)
{
- QString plain = content;
- plain.replace(QRegularExpression(QStringLiteral("<\\s*br\\s*/?\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n"));
- plain.replace(QRegularExpression(QStringLiteral("<\\s*/\\s*p\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n"));
- plain.remove(QRegularExpression(QStringLiteral("<[^>]+>"), QRegularExpression::CaseInsensitiveOption));
-
- return decodeHtmlEntities(plain).trimmed();
+ return MastodonTextUtils::sanitizeContent(content);
}
QDateTime MastodonNotificationsSyncAdaptor::parseTimestamp(const QString &timestampString)
{
- QDateTime timestamp;
-
-#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
- timestamp = QDateTime::fromString(timestampString, Qt::ISODateWithMs);
- if (timestamp.isValid()) {
- return timestamp;
- }
-#endif
-
- timestamp = QDateTime::fromString(timestampString, Qt::ISODate);
- if (timestamp.isValid()) {
- return timestamp;
- }
-
- const int timeSeparator = timestampString.indexOf(QLatin1Char('T'));
- const int fractionSeparator = timestampString.indexOf(QLatin1Char('.'), timeSeparator + 1);
- if (timeSeparator > -1 && fractionSeparator > -1) {
- int timezoneSeparator = timestampString.indexOf(QLatin1Char('Z'), fractionSeparator + 1);
- if (timezoneSeparator == -1) {
- timezoneSeparator = timestampString.indexOf(QLatin1Char('+'), fractionSeparator + 1);
- }
- if (timezoneSeparator == -1) {
- timezoneSeparator = timestampString.indexOf(QLatin1Char('-'), fractionSeparator + 1);
- }
-
- QString stripped = timestampString;
- if (timezoneSeparator > -1) {
- stripped.remove(fractionSeparator, timezoneSeparator - fractionSeparator);
- } else {
- stripped.truncate(fractionSeparator);
- }
-
- timestamp = QDateTime::fromString(stripped, Qt::ISODate);
- }
-
- return timestamp;
+ return MastodonTextUtils::parseTimestamp(timestampString);
}
int MastodonNotificationsSyncAdaptor::compareNotificationIds(const QString &left, const QString &right)
@@ -342,7 +426,7 @@ void MastodonNotificationsSyncAdaptor::requestUnreadMarker(int accountId, const
incrementSemaphore(accountId);
setupReplyTimeout(accountId, reply);
} else {
- qCWarning(lcSocialPlugin) << "unable to request notifications marker from Mastodon account with id" << accountId;
+ qCWarning(lcMastodonNotifications) << "unable to request notifications marker from Mastodon account with id" << accountId;
}
}
@@ -365,8 +449,13 @@ void MastodonNotificationsSyncAdaptor::finishedUnreadMarkerHandler()
bool ok = false;
const QJsonObject markerObject = parseJsonObjectReplyData(replyData, &ok);
if (isError || !ok) {
- qCWarning(lcSocialPlugin) << "unable to parse notifications marker data from request with account"
+ qCWarning(lcMastodonNotifications) << "unable to parse notifications marker data from request with account"
<< accountId << ", got:" << QString::fromUtf8(replyData);
+ PendingSyncState fallbackState;
+ fallbackState.accessToken = accessToken;
+ fallbackState.lastFetchedId = loadLastFetchedId(accountId);
+ m_pendingSyncStates.insert(accountId, fallbackState);
+ requestNotifications(accountId, accessToken, fallbackState.lastFetchedId);
decrementSemaphore(accountId);
return;
}
@@ -380,6 +469,7 @@ void MastodonNotificationsSyncAdaptor::finishedUnreadMarkerHandler()
PendingSyncState state;
state.accessToken = accessToken;
+ state.markerKnown = true;
state.unreadFloorId = markerId;
state.lastFetchedId = loadLastFetchedId(accountId);
if (state.lastFetchedId.isEmpty() && !markerId.isEmpty()) {
@@ -424,7 +514,7 @@ void MastodonNotificationsSyncAdaptor::requestNotifications(int accountId,
incrementSemaphore(accountId);
setupReplyTimeout(accountId, reply);
} else {
- qCWarning(lcSocialPlugin) << "unable to request notifications from Mastodon account with id" << accountId;
+ qCWarning(lcMastodonNotifications) << "unable to request notifications from Mastodon account with id" << accountId;
}
}
@@ -452,7 +542,7 @@ void MastodonNotificationsSyncAdaptor::requestMarkRead(int accountId,
incrementSemaphore(accountId);
setupReplyTimeout(accountId, reply);
} else {
- qCWarning(lcSocialPlugin) << "unable to update notifications marker for Mastodon account with id" << accountId;
+ qCWarning(lcMastodonNotifications) << "unable to update notifications marker for Mastodon account with id" << accountId;
}
}
@@ -490,7 +580,10 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
const QJsonArray notifications = parseJsonArrayReplyData(replyData, &ok);
if (!isError && ok) {
if (!notifications.size()) {
- qCDebug(lcSocialPlugin) << "no notifications received for account" << accountId;
+ qCDebug(lcMastodonNotifications) << "no notifications received for account" << accountId;
+ if (state.markerKnown) {
+ closeAccountNotifications(accountId);
+ }
m_pendingSyncStates.remove(accountId);
decrementSemaphore(accountId);
return;
@@ -514,6 +607,10 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
pageMinNotificationId = notificationId;
}
+ if (state.markerKnown) {
+ state.unreadNotificationIds.insert(notificationId);
+ }
+
if (state.maxFetchedId.isEmpty()
|| compareNotificationIds(notificationId, state.maxFetchedId) > 0) {
state.maxFetchedId = notificationId;
@@ -530,6 +627,8 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
const QJsonObject statusObject = statusValue.isObject() && !statusValue.isNull()
? statusValue.toObject()
: QJsonObject();
+ const QJsonObject eventObject = notificationObject.value(QStringLiteral("event")).toObject();
+ const QJsonObject warningObject = notificationObject.value(QStringLiteral("moderation_warning")).toObject();
QDateTime eventTimestamp = parseTimestamp(notificationObject.value(QStringLiteral("created_at")).toString());
if (!eventTimestamp.isValid()) {
@@ -545,9 +644,18 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
const QString statusBody = sanitizeContent(statusObject.value(QStringLiteral("content")).toString());
const QString action = actionText(notificationType);
QString body;
- if (notificationType == QLatin1String("mention")
+ if (notificationType == QLatin1String("severed_relationships")) {
+ body = severedRelationshipsText(eventObject);
+ } else if (notificationType == QLatin1String("moderation_warning")) {
+ const QString warningText = moderationWarningText(warningObject);
+ body = warningText.isEmpty()
+ ? action
+ : QStringLiteral("%1: %2").arg(action, warningText);
+ } else if (notificationType == QLatin1String("mention")
|| notificationType == QLatin1String("status")
- || notificationType == QLatin1String("update")) {
+ || notificationType == QLatin1String("update")
+ || notificationType == QLatin1String("quote")
+ || notificationType == QLatin1String("quoted_update")) {
body = statusBody.isEmpty() ? action : statusBody;
} else {
body = statusBody.isEmpty() ? action : QStringLiteral("%1: %2").arg(action, statusBody);
@@ -567,22 +675,30 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
} else if (url.isEmpty() && !accountName.isEmpty()) {
url = QStringLiteral("%1/@%2").arg(apiHost(accountId), accountName);
}
+ if (useSystemSummary(notificationType)) {
+ url.clear();
+ }
PendingNotification pendingNotification;
pendingNotification.notificationId = notificationId;
- pendingNotification.summary = displayName;
+ pendingNotification.summary = useSystemSummary(notificationType)
+ ? qtTrId(TrIdMastodon)
+ : displayName;
pendingNotification.body = body;
pendingNotification.link = url;
pendingNotification.timestamp = eventTimestamp;
state.pendingNotifications.insert(notificationId, pendingNotification);
}
+ const QString historyBoundaryId = !state.unreadFloorId.isEmpty()
+ ? state.unreadFloorId
+ : state.lastFetchedId;
if (notifications.size() >= NotificationsPageLimit
&& !pageMinNotificationId.isEmpty()
- && (state.unreadFloorId.isEmpty()
- || compareNotificationIds(pageMinNotificationId, state.unreadFloorId) > 0)) {
+ && !historyBoundaryId.isEmpty()
+ && compareNotificationIds(pageMinNotificationId, historyBoundaryId) > 0) {
m_pendingSyncStates.insert(accountId, state);
- requestNotifications(accountId, state.accessToken, state.unreadFloorId, pageMinNotificationId);
+ requestNotifications(accountId, state.accessToken, historyBoundaryId, pageMinNotificationId);
decrementSemaphore(accountId);
return;
}
@@ -599,6 +715,10 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
}
}
+ if (state.markerKnown) {
+ closeAccountNotifications(accountId, state.unreadNotificationIds);
+ }
+
if (!state.maxFetchedId.isEmpty()
&& (state.lastFetchedId.isEmpty()
|| compareNotificationIds(state.maxFetchedId, state.lastFetchedId) > 0)) {
@@ -611,13 +731,13 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
const QString currentMarkerId = m_lastMarkedReadIds.value(accountId);
if (!markerId.isEmpty()
&& !state.accessToken.isEmpty()
- && !hasActiveNotificationsForAccount(accountId)
+ && state.markerKnown
&& (currentMarkerId.isEmpty()
|| compareNotificationIds(markerId, currentMarkerId) > 0)) {
- requestMarkRead(accountId, state.accessToken, markerId);
+ maybeMarkAccountNotificationsRead(accountId, state.accessToken);
}
} else {
- qCWarning(lcSocialPlugin) << "unable to parse notifications data from request with account" << accountId
+ qCWarning(lcMastodonNotifications) << "unable to parse notifications data from request with account" << accountId
<< ", got:" << QString::fromUtf8(replyData);
}
@@ -649,7 +769,7 @@ void MastodonNotificationsSyncAdaptor::finishedMarkReadHandler()
m_lastMarkedReadIds.insert(accountId, lastReadId);
}
} else {
- qCWarning(lcSocialPlugin) << "unable to update notifications marker for account" << accountId
+ qCWarning(lcMastodonNotifications) << "unable to update notifications marker for account" << accountId
<< ", got:" << QString::fromUtf8(replyData);
}
@@ -665,10 +785,10 @@ void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId,
? notificationData.timestamp
: QDateTime::currentDateTimeUtc());
notification->setSummary(notificationData.summary.isEmpty()
- ? QStringLiteral("Mastodon")
+ ? qtTrId(TrIdMastodon)
: notificationData.summary);
notification->setBody(notificationData.body.isEmpty()
- ? QStringLiteral("New notification")
+ ? qtTrId(TrIdNewNotification)
: notificationData.body);
notification->setPreviewSummary(notificationData.summary);
notification->setPreviewBody(notificationData.body);
@@ -686,11 +806,75 @@ void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId,
notification->setRemoteAction(OPEN_URL_ACTION(authorizeInteractionUrl(apiHost(accountId), safeOpenUrl)));
notification->publish();
if (notification->replacesId() == 0) {
- qCWarning(lcSocialPlugin) << "failed to publish Mastodon notification"
+ qCWarning(lcMastodonNotifications) << "failed to publish Mastodon notification"
<< notificationData.notificationId;
}
}
+void MastodonNotificationsSyncAdaptor::notificationClosedWithReason(uint reason)
+{
+ Notification *notification = qobject_cast<Notification *>(sender());
+ removeCachedNotification(notification);
+ if (reason == NotificationDismissedReason) {
+ markReadFromNotification(notification);
+ }
+}
+
+void MastodonNotificationsSyncAdaptor::maybeMarkAccountNotificationsRead(int accountId,
+ const QString &accessToken,
+ Notification *ignoredNotification)
+{
+ if (accountId <= 0 || accessToken.isEmpty()) {
+ return;
+ }
+
+ if (hasActiveNotificationsForAccount(accountId, ignoredNotification)) {
+ return;
+ }
+
+ const QString lastReadId = loadLastFetchedId(accountId);
+ if (lastReadId.isEmpty()) {
+ return;
+ }
+
+ const QString currentMarkerId = m_lastMarkedReadIds.value(accountId);
+ if (!currentMarkerId.isEmpty() && compareNotificationIds(lastReadId, currentMarkerId) <= 0) {
+ return;
+ }
+
+ requestMarkRead(accountId, accessToken, lastReadId);
+}
+
+void MastodonNotificationsSyncAdaptor::markReadFromNotification(Notification *notification)
+{
+ if (!notification) {
+ return;
+ }
+
+ const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt();
+ const QString accessToken = m_accessTokens.value(accountId).trimmed();
+ if (accountId <= 0 || accessToken.isEmpty()) {
+ return;
+ }
+
+ maybeMarkAccountNotificationsRead(accountId, accessToken, notification);
+}
+
+void MastodonNotificationsSyncAdaptor::removeCachedNotification(Notification *notification)
+{
+ if (!notification) {
+ return;
+ }
+
+ const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt();
+ const QString notificationId = notification->hintValue(NotificationIdHint).toString();
+ if (accountId <= 0 || notificationId.isEmpty()) {
+ return;
+ }
+
+ m_notificationObjects.remove(notificationObjectKey(accountId, notificationId));
+}
+
void MastodonNotificationsSyncAdaptor::closeAccountNotifications(int accountId,
const QSet<QString> &keepNotificationIds)
{
@@ -755,6 +939,7 @@ Notification *MastodonNotificationsSyncAdaptor::createNotification(int accountId
notification->setHintValue("x-nemo-priority", 100); // Show on lockscreen
notification->setCategory(QLatin1String(NotificationCategory));
+ connect(notification, SIGNAL(closed(uint)), this, SLOT(notificationClosedWithReason(uint)), Qt::UniqueConnection);
m_notificationObjects.insert(objectKey, notification);
return notification;
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h
index 9711549..0e9106c 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h
@@ -41,6 +41,7 @@ public:
QString syncServiceName() const override;
protected:
+ QString authServiceName() const override;
void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override;
void beginSync(int accountId, const QString &accessToken) override;
void finalize(int accountId) override;
@@ -56,9 +57,11 @@ private:
struct PendingSyncState {
QString accessToken;
+ bool markerKnown = false;
QString unreadFloorId;
QString lastFetchedId;
QString maxFetchedId;
+ QSet<QString> unreadNotificationIds;
QHash<QString, PendingNotification> pendingNotifications;
};
@@ -79,13 +82,20 @@ private:
Notification *findNotification(int accountId, const QString &notificationId);
void closeAccountNotifications(int accountId, const QSet<QString> &keepNotificationIds = QSet<QString>());
static QString notificationObjectKey(int accountId, const QString &notificationId);
+ void maybeMarkAccountNotificationsRead(int accountId,
+ const QString &accessToken,
+ Notification *ignoredNotification = 0);
+ void markReadFromNotification(Notification *notification);
+ void removeCachedNotification(Notification *notification);
private Q_SLOTS:
void finishedUnreadMarkerHandler();
void finishedNotificationsHandler();
void finishedMarkReadHandler();
+ void notificationClosedWithReason(uint reason);
private:
+ QHash<int, QString> m_accessTokens;
QHash<int, PendingSyncState> m_pendingSyncStates;
QHash<int, QString> m_lastMarkedReadIds;
QHash<QString, Notification *> m_notificationObjects;
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro b/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro
index 86387b2..a9f65af 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TARGET = mastodon-posts-client
QT -= gui
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml
index 53bd93a..c1e25ae 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<profile name="mastodon-posts" type="client" >
<field name="Sync Direction" />
</profile>
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml
index 2904fa5..c7e2448 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<profile name="mastodon.Posts" type="sync" >
<key name="category" value="eventfeed" />
<key name="enabled" value="false" />
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp
index deddb0a..160d6cc 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp
@@ -20,48 +20,16 @@
#include "mastodonpostssyncadaptor.h"
#include "trace.h"
+#include "mastodontextutils.h"
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
-#include <QtCore/QRegularExpression>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QNetworkRequest>
namespace {
- QString decodeHtmlEntities(QString text)
- {
- text.replace(QStringLiteral("&quot;"), QStringLiteral("\""));
- text.replace(QStringLiteral("&apos;"), QStringLiteral("'"));
- text.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
- text.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
- text.replace(QStringLiteral("&amp;"), QStringLiteral("&"));
- text.replace(QStringLiteral("&nbsp;"), QStringLiteral(" "));
-
- static const QRegularExpression decimalEntity(QStringLiteral("&#(\\d+);"));
- QRegularExpressionMatch match;
- int index = 0;
- while ((index = text.indexOf(decimalEntity, index, &match)) != -1) {
- const uint value = match.captured(1).toUInt();
- const QString replacement = value > 0 ? QString(QChar(value)) : QString();
- text.replace(index, match.capturedLength(0), replacement);
- index += replacement.size();
- }
-
- static const QRegularExpression hexEntity(QStringLiteral("&#x([0-9a-fA-F]+);"));
- index = 0;
- while ((index = text.indexOf(hexEntity, index, &match)) != -1) {
- bool ok = false;
- const uint value = match.captured(1).toUInt(&ok, 16);
- const QString replacement = ok && value > 0 ? QString(QChar(value)) : QString();
- text.replace(index, match.capturedLength(0), replacement);
- index += replacement.size();
- }
-
- return text;
- }
-
QString displayNameForAccount(const QJsonObject &account)
{
const QString displayName = account.value(QStringLiteral("display_name")).toString().trimmed();
@@ -124,53 +92,12 @@ void MastodonPostsSyncAdaptor::finalize(int accountId)
QString MastodonPostsSyncAdaptor::sanitizeContent(const QString &content)
{
- QString plain = content;
- plain.replace(QRegularExpression(QStringLiteral("<\\s*br\\s*/?\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n"));
- plain.replace(QRegularExpression(QStringLiteral("<\\s*/\\s*p\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n"));
- plain.remove(QRegularExpression(QStringLiteral("<[^>]+>"), QRegularExpression::CaseInsensitiveOption));
-
- return decodeHtmlEntities(plain).trimmed();
+ return MastodonTextUtils::sanitizeContent(content);
}
QDateTime MastodonPostsSyncAdaptor::parseTimestamp(const QString &timestampString)
{
- QDateTime timestamp;
-
-#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
- timestamp = QDateTime::fromString(timestampString, Qt::ISODateWithMs);
- if (timestamp.isValid()) {
- return timestamp;
- }
-#endif
-
- timestamp = QDateTime::fromString(timestampString, Qt::ISODate);
- if (timestamp.isValid()) {
- return timestamp;
- }
-
- // Qt 5.6 cannot parse ISO-8601 timestamps with fractional seconds.
- const int timeSeparator = timestampString.indexOf(QLatin1Char('T'));
- const int fractionSeparator = timestampString.indexOf(QLatin1Char('.'), timeSeparator + 1);
- if (timeSeparator > -1 && fractionSeparator > -1) {
- int timezoneSeparator = timestampString.indexOf(QLatin1Char('Z'), fractionSeparator + 1);
- if (timezoneSeparator == -1) {
- timezoneSeparator = timestampString.indexOf(QLatin1Char('+'), fractionSeparator + 1);
- }
- if (timezoneSeparator == -1) {
- timezoneSeparator = timestampString.indexOf(QLatin1Char('-'), fractionSeparator + 1);
- }
-
- QString stripped = timestampString;
- if (timezoneSeparator > -1) {
- stripped.remove(fractionSeparator, timezoneSeparator - fractionSeparator);
- } else {
- stripped.truncate(fractionSeparator);
- }
-
- timestamp = QDateTime::fromString(stripped, Qt::ISODate);
- }
-
- return timestamp;
+ return MastodonTextUtils::parseTimestamp(timestampString);
}
void MastodonPostsSyncAdaptor::requestPosts(int accountId, const QString &accessToken)
@@ -216,14 +143,14 @@ void MastodonPostsSyncAdaptor::finishedPostsHandler()
bool ok = false;
QJsonArray statuses = parseJsonArrayReplyData(replyData, &ok);
if (!isError && ok) {
+ m_db.removePosts(accountId);
+
if (!statuses.size()) {
qCDebug(lcSocialPlugin) << "no feed posts received for account" << accountId;
decrementSemaphore(accountId);
return;
}
- m_db.removePosts(accountId);
-
const int sinceSpan = m_accountSyncProfile
? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt()
: 7;
diff --git a/common/common.pri b/common/common.pri
index e1d299f..7f593db 100644
--- a/common/common.pri
+++ b/common/common.pri
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
INCLUDEPATH += $$PWD
DEPENDPATH += .
diff --git a/common/common.pro b/common/common.pro
index 02c78ee..c01e571 100644
--- a/common/common.pro
+++ b/common/common.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = lib
QT -= gui
@@ -11,6 +15,7 @@ TARGET = $$qtLibraryTarget($$TARGET)
HEADERS += \
$$PWD/mastodonauthutils.h \
+ $$PWD/mastodontextutils.h \
$$PWD/mastodonpostsdatabase.h
SOURCES += \
diff --git a/common/mastodontextutils.h b/common/mastodontextutils.h
new file mode 100644
index 0000000..bde74c4
--- /dev/null
+++ b/common/mastodontextutils.h
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2013-2026 Jolla Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef MASTODONTEXTUTILS_H
+#define MASTODONTEXTUTILS_H
+
+#include <QtCore/QDateTime>
+#include <QtCore/QRegularExpression>
+#include <QtCore/QString>
+
+namespace MastodonTextUtils {
+
+inline QString decodeHtmlEntities(QString text)
+{
+ text.replace(QStringLiteral("&quot;"), QStringLiteral("\""));
+ text.replace(QStringLiteral("&apos;"), QStringLiteral("'"));
+ text.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
+ text.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
+ text.replace(QStringLiteral("&amp;"), QStringLiteral("&"));
+ text.replace(QStringLiteral("&nbsp;"), QStringLiteral(" "));
+
+ static const QRegularExpression decimalEntity(QStringLiteral("&#(\\d+);"));
+ QRegularExpressionMatch match;
+ int index = 0;
+ while ((index = text.indexOf(decimalEntity, index, &match)) != -1) {
+ bool ok = false;
+ const uint value = match.captured(1).toUInt(&ok, 10);
+ QString replacement;
+ if (ok && value > 0 && value <= 0x10FFFF) {
+ replacement = QString::fromUcs4(&value, 1);
+ }
+ text.replace(index, match.capturedLength(0), replacement);
+ index += replacement.size();
+ }
+
+ static const QRegularExpression hexEntity(QStringLiteral("&#x([0-9a-fA-F]+);"));
+ index = 0;
+ while ((index = text.indexOf(hexEntity, index, &match)) != -1) {
+ bool ok = false;
+ const uint value = match.captured(1).toUInt(&ok, 16);
+ QString replacement;
+ if (ok && value > 0 && value <= 0x10FFFF) {
+ replacement = QString::fromUcs4(&value, 1);
+ }
+ text.replace(index, match.capturedLength(0), replacement);
+ index += replacement.size();
+ }
+
+ return text;
+}
+
+inline QString sanitizeContent(const QString &content)
+{
+ QString plain = content;
+ plain.replace(QRegularExpression(QStringLiteral("<\\s*br\\s*/?\\s*>"), QRegularExpression::CaseInsensitiveOption),
+ QStringLiteral("\n"));
+ plain.replace(QRegularExpression(QStringLiteral("<\\s*/\\s*p\\s*>"), QRegularExpression::CaseInsensitiveOption),
+ QStringLiteral("\n"));
+ plain.remove(QRegularExpression(QStringLiteral("<[^>]+>"), QRegularExpression::CaseInsensitiveOption));
+
+ return decodeHtmlEntities(plain).trimmed();
+}
+
+inline QDateTime parseTimestamp(const QString &timestampString)
+{
+ QDateTime timestamp;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
+ timestamp = QDateTime::fromString(timestampString, Qt::ISODateWithMs);
+ if (timestamp.isValid()) {
+ return timestamp;
+ }
+#endif
+
+ timestamp = QDateTime::fromString(timestampString, Qt::ISODate);
+ if (timestamp.isValid()) {
+ return timestamp;
+ }
+
+ // Qt 5.6 cannot parse ISO-8601 timestamps with fractional seconds.
+ const int timeSeparator = timestampString.indexOf(QLatin1Char('T'));
+ const int fractionSeparator = timestampString.indexOf(QLatin1Char('.'), timeSeparator + 1);
+ if (timeSeparator > -1 && fractionSeparator > -1) {
+ int timezoneSeparator = timestampString.indexOf(QLatin1Char('Z'), fractionSeparator + 1);
+ if (timezoneSeparator == -1) {
+ timezoneSeparator = timestampString.indexOf(QLatin1Char('+'), fractionSeparator + 1);
+ }
+ if (timezoneSeparator == -1) {
+ timezoneSeparator = timestampString.indexOf(QLatin1Char('-'), fractionSeparator + 1);
+ }
+
+ QString stripped = timestampString;
+ if (timezoneSeparator > -1) {
+ stripped.remove(fractionSeparator, timezoneSeparator - fractionSeparator);
+ } else {
+ stripped.truncate(fractionSeparator);
+ }
+
+ timestamp = QDateTime::fromString(stripped, Qt::ISODate);
+ }
+
+ return timestamp;
+}
+
+} // namespace MastodonTextUtils
+
+#endif // MASTODONTEXTUTILS_H
diff --git a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml
index c003950..63b9556 100644
--- a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml
+++ b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml
@@ -1,11 +1,12 @@
-/****************************************************************************
- **
- ** Copyright (C) 2013-2026 Jolla Ltd.
- **
- ****************************************************************************/
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
import QtQuick 2.0
import Sailfish.Silica 1.0
+import Sailfish.Share 1.0
import Sailfish.TextLinking 1.0
import org.nemomobile.lipstick 0.1
import "shared"
@@ -35,10 +36,19 @@ SocialMediaFeedItem {
property var _actionMenu
property real _contextMenuHeight: (_contextMenuOpen && _actionMenu) ? _actionMenu.height : 0
- property string _booster: item.stringValue("boostedBy", "rebloggedBy", "retweeter")
- property string _displayName: item.stringValue("name", "displayName", "display_name")
- property string _accountName: item.stringValue("accountName", "acct", "screenName", "username")
- property string _bodyText: item.stringValue("body", "content", "text")
+ property string _booster: model && model.boostedBy ? model.boostedBy.toString() : ""
+ property string _displayName: model && model.name ? model.name.toString() : ""
+ property string _accountName: model && model.accountName ? model.accountName.toString() : ""
+ property string _bodyText: model && model.body ? model.body.toString() : ""
+ //: Action label shown in Mastodon interaction menu.
+ //% "Share"
+ readonly property string _shareActionText: qsTrId("lipstick-jolla-home-la-mastodon_share")
+ //: Link title used when sharing a Mastodon post.
+ //% "Post from Mastodon"
+ readonly property string _shareLinkTitle: qsTrId("lipstick-jolla-home-la-mastodon_share_link_title")
+ property var _shareAction: ShareAction {
+ title: item._shareActionText
+ }
timestamp: model.timestamp
onRefreshTimeCountChanged: formattedTime = Format.formatDate(model.timestamp, Format.TimeElapsed)
@@ -257,6 +267,10 @@ SocialMediaFeedItem {
return isNaN(parsed) ? -1 : parsed
}
+ function shareStatusUrl() {
+ return item.stringValue("url", "link", "uri")
+ }
+
function topLevelParent() {
var p = item
while (p && p.parent) {
@@ -354,12 +368,12 @@ SocialMediaFeedItem {
width: parent.width
height: Theme.itemSizeMedium
- onXPosChanged: hoveredIndex = xPos < width / 2 ? 0 : 1
+ onXPosChanged: hoveredIndex = Math.max(0, Math.min(2, Math.floor((xPos * 3) / Math.max(1, width))))
onDownChanged: if (!down) hoveredIndex = -1
onClicked: {
xPos = _contentColumn.mapFromItem(actionMenu, actionMenu.mouseX, actionMenu.mouseY).x
- var index = hoveredIndex >= 0 ? hoveredIndex : (xPos < width / 2 ? 0 : 1)
+ var index = hoveredIndex >= 0 ? hoveredIndex : Math.max(0, Math.min(2, Math.floor((xPos * 3) / Math.max(1, width))))
if (!actionEnabled) {
return
}
@@ -371,19 +385,30 @@ SocialMediaFeedItem {
} else {
item.postActions.favourite(accountId, postId)
}
- } else {
+ } else if (index === 1) {
if (item.isReblogged) {
item.postActions.unboost(accountId, postId)
} else {
item.postActions.boost(accountId, postId)
}
+ } else {
+ var shareUrl = item.shareStatusUrl()
+ if (shareUrl.length === 0) {
+ return
+ }
+ item._shareAction.resources = [{
+ "data": shareUrl,
+ "linkTitle": item._shareLinkTitle,
+ "type": "text/x-url"
+ }]
+ item._shareAction.trigger()
}
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
- x: horizontalActions.hoveredIndex === 1 ? parent.width / 2 : 0
- width: parent.width / 2
+ x: (horizontalActions.hoveredIndex >= 0 ? horizontalActions.hoveredIndex : 0) * (parent.width / 3)
+ width: parent.width / 3
height: parent.height
visible: horizontalActions.down && horizontalActions.hoveredIndex >= 0
color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity)
@@ -393,7 +418,7 @@ SocialMediaFeedItem {
anchors.fill: parent
Label {
- width: parent.width / 2
+ width: parent.width / 3
height: parent.height
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -409,7 +434,7 @@ SocialMediaFeedItem {
}
Label {
- width: parent.width / 2
+ width: parent.width / 3
height: parent.height
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -423,6 +448,20 @@ SocialMediaFeedItem {
? Theme.secondaryHighlightColor : Theme.primaryColor))
: Theme.rgba(Theme.secondaryColor, 0.4)
}
+
+ Label {
+ width: parent.width / 3
+ height: parent.height
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: Theme.fontSizeExtraLarge
+ text: "\u260D"
+ color: horizontalActions.actionEnabled
+ ? (((horizontalActions.down && horizontalActions.hoveredIndex === 2)
+ || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 2))
+ ? Theme.secondaryHighlightColor : Theme.primaryColor)
+ : Theme.rgba(Theme.secondaryColor, 0.4)
+ }
}
}
}
diff --git a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro
index 4699324..04be215 100644
--- a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro
+++ b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = lib
TARGET = jollaeventsviewmastodonplugin
TARGET = $$qtLibraryTarget($$TARGET)
diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml
index c8e8713..fac0b89 100644
--- a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml
+++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml
@@ -1,8 +1,8 @@
-/****************************************************************************
- **
- ** Copyright (C) 2013-2026 Jolla Ltd.
- **
- ****************************************************************************/
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
import QtQuick 2.0
import Sailfish.Silica 1.0
diff --git a/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp b/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp
index aa993a0..9ade6e2 100644
--- a/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp
+++ b/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include <QQmlExtensionPlugin>
diff --git a/eventsview-plugins/eventsview-plugins.pro b/eventsview-plugins/eventsview-plugins.pro
index d9b1842..095fd02 100644
--- a/eventsview-plugins/eventsview-plugins.pro
+++ b/eventsview-plugins/eventsview-plugins.pro
@@ -1,2 +1,6 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = subdirs
SUBDIRS += eventsview-plugin-mastodon
diff --git a/icons/icons.pro b/icons/icons.pro
index 510f3dc..89da452 100644
--- a/icons/icons.pro
+++ b/icons/icons.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = aux
THEMENAME = sailfish-default
CONFIG += sailfish-svg2png
diff --git a/rpm/sailfish-account-mastodon.spec b/rpm/sailfish-account-mastodon.spec
index 389b6a9..97e790a 100644
--- a/rpm/sailfish-account-mastodon.spec
+++ b/rpm/sailfish-account-mastodon.spec
@@ -1,8 +1,10 @@
-# Copyright (C) 2013-2026 Jolla Ltd.
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
Name: sailfish-account-mastodon
-License: LGPLv3
-Version: 1.0.0
+License: BSD-3-Clause AND LGPL-2.1-or-later
+Version: 1.0.1
Release: 1
Source0: %{name}-%{version}.tar.bz2
Summary: SailfishOS account plugin for Mastodon
@@ -59,6 +61,7 @@ Translation source files for sailfish-account-mastodon components.
%post
/sbin/ldconfig
%{_libexecdir}/manage-groups add account-mastodon || :
+systemctl-user try-restart msyncd.service || :
%postun
/sbin/ldconfig
@@ -67,12 +70,15 @@ if [ "$1" -eq 0 ]; then
fi
%files
+%license LICENSES/BSD-3-Clause.txt
+%license LICENSES/LGPL-2.1-or-later.txt
%{_libdir}/libmastodoncommon.so.*
%exclude %{_libdir}/libmastodoncommon.so
%{_libdir}/libmastodonbuteocommon.so.*
%exclude %{_libdir}/libmastodonbuteocommon.so
%{_datadir}/accounts/providers/mastodon.provider
%{_datadir}/accounts/services/mastodon-microblog.service
+%{_datadir}/accounts/services/mastodon-notifications.service
%{_datadir}/accounts/services/mastodon-sharing.service
%{_datadir}/accounts/ui/MastodonSettingsDisplay.qml
%{_datadir}/accounts/ui/mastodon.qml
@@ -91,6 +97,7 @@ fi
%{_datadir}/lipstick/eventfeed/mastodon-delegate.qml
%{_datadir}/lipstick/eventfeed/MastodonFeedItem.qml
%{_datadir}/translations/lipstick-jolla-home-mastodon_eng_en.qm
+%{_datadir}/translations/lipstick-jolla-home-mastodon-notifications_eng_en.qm
%{_libdir}/nemo-transferengine/plugins/sharing/libmastodonshareplugin.so
%{_libdir}/nemo-transferengine/plugins/transfer/libmastodontransferplugin.so
@@ -99,3 +106,4 @@ fi
%files -n sailfish-account-mastodon-ts-devel
%{_datadir}/translations/source/settings-accounts-mastodon.ts
%{_datadir}/translations/source/lipstick-jolla-home-mastodon.ts
+%{_datadir}/translations/source/lipstick-jolla-home-mastodon-notifications.ts
diff --git a/sailfish-account-mastodon.pro b/sailfish-account-mastodon.pro
index 3a8f747..91befc3 100644
--- a/sailfish-account-mastodon.pro
+++ b/sailfish-account-mastodon.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = subdirs
SUBDIRS += \
common \
diff --git a/settings/accounts-translations-plugin/accounts-translations-plugin.pro b/settings/accounts-translations-plugin/accounts-translations-plugin.pro
index 48b5a84..aad978f 100644
--- a/settings/accounts-translations-plugin/accounts-translations-plugin.pro
+++ b/settings/accounts-translations-plugin/accounts-translations-plugin.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = lib
TARGET = mastodonaccountstranslationsplugin
TARGET = $$qtLibraryTarget($$TARGET)
diff --git a/settings/accounts-translations-plugin/plugin.cpp b/settings/accounts-translations-plugin/plugin.cpp
index f463a72..4a1c651 100644
--- a/settings/accounts-translations-plugin/plugin.cpp
+++ b/settings/accounts-translations-plugin/plugin.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include <QCoreApplication>
diff --git a/settings/accounts/accounts.pro b/settings/accounts/accounts.pro
index d451438..37982a3 100644
--- a/settings/accounts/accounts.pro
+++ b/settings/accounts/accounts.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = aux
TS_FILE = $$OUT_PWD/settings-accounts-mastodon.ts
@@ -28,6 +32,7 @@ PRE_TARGETDEPS += ts engineering_english
OTHER_FILES += \
$$PWD/providers/mastodon.provider \
$$PWD/services/mastodon-microblog.service \
+ $$PWD/services/mastodon-notifications.service \
$$PWD/services/mastodon-sharing.service \
$$PWD/ui/MastodonSettingsDisplay.qml \
$$PWD/ui/mastodon.qml \
@@ -39,6 +44,7 @@ provider.path = /usr/share/accounts/providers/
services.files += \
$$PWD/services/mastodon-microblog.service \
+ $$PWD/services/mastodon-notifications.service \
$$PWD/services/mastodon-sharing.service
services.path = /usr/share/accounts/services/
diff --git a/settings/accounts/providers/mastodon.provider b/settings/accounts/providers/mastodon.provider
index c02de41..422c231 100644
--- a/settings/accounts/providers/mastodon.provider
+++ b/settings/accounts/providers/mastodon.provider
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<!DOCTYPE provider>
<provider version="1.0" id="mastodon">
<translations>/usr/share/translations/settings-accounts-mastodon</translations>
diff --git a/settings/accounts/services/mastodon-microblog.service b/settings/accounts/services/mastodon-microblog.service
index 6b975f7..527f70f 100644
--- a/settings/accounts/services/mastodon-microblog.service
+++ b/settings/accounts/services/mastodon-microblog.service
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<service id="mastodon-microblog">
<type>microblogging</type>
<translations>/usr/share/translations/settings-accounts-mastodon</translations>
@@ -8,7 +9,7 @@
<provider>mastodon</provider>
<template>
- <setting name="sync_profile_templates" type="as">["mastodon.Posts", "mastodon.Notifications"]</setting>
+ <setting name="sync_profile_templates" type="as">["mastodon.Posts"]</setting>
<group name="auth">
<setting name="method">oauth2</setting>
<setting name="mechanism">web_server</setting>
diff --git a/settings/accounts/services/mastodon-notifications.service b/settings/accounts/services/mastodon-notifications.service
new file mode 100644
index 0000000..f5471c9
--- /dev/null
+++ b/settings/accounts/services/mastodon-notifications.service
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
+<service id="mastodon-notifications">
+ <type>microblogging</type>
+ <translations>/usr/share/translations/settings-accounts-mastodon</translations>
+ <name>Notifications</name>
+ <icon>image://theme/icon-l-mastodon</icon>
+ <provider>mastodon</provider>
+
+ <template>
+ <setting name="sync_profile_templates" type="as">["mastodon.Notifications"]</setting>
+ <group name="auth">
+ <setting name="method">oauth2</setting>
+ <setting name="mechanism">web_server</setting>
+ <group name="oauth2">
+ <group name="web_server">
+ <setting name="Host">mastodon.social</setting>
+ <setting name="AllowedSchemes" type="as">["https"]</setting>
+ <setting name="AuthPath">oauth/authorize</setting>
+ <setting name="TokenPath">oauth/token</setting>
+ <setting name="ResponseType">code</setting>
+ <setting name="Scope" type="as">["read","write"]</setting>
+ <setting name="RedirectUri">http://ipv4.jolla.com/online/status.html</setting>
+ </group>
+ </group>
+ </group>
+ <group name="api">
+ <setting name="Host">https://mastodon.social</setting>
+ </group>
+ </template>
+</service>
diff --git a/settings/accounts/services/mastodon-sharing.service b/settings/accounts/services/mastodon-sharing.service
index 4048b56..fdf342a 100644
--- a/settings/accounts/services/mastodon-sharing.service
+++ b/settings/accounts/services/mastodon-sharing.service
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
-<!-- Copyright (C) 2013-2026 Jolla Ltd. -->
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
<service id="mastodon-sharing">
<type>sharing</type>
<translations>/usr/share/translations/settings-accounts-mastodon</translations>
diff --git a/settings/accounts/ui/MastodonSettingsDisplay.qml b/settings/accounts/ui/MastodonSettingsDisplay.qml
index 1ee276e..13ac06c 100644
--- a/settings/accounts/ui/MastodonSettingsDisplay.qml
+++ b/settings/accounts/ui/MastodonSettingsDisplay.qml
@@ -1,17 +1,21 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
import QtQuick 2.0
import Sailfish.Silica 1.0
import Sailfish.Accounts 1.0
import com.jolla.settings.accounts 1.0
+import com.jolla.settings.accounts.mastodon 1.0
import org.nemomobile.configuration 1.0
StandardAccountSettingsDisplay {
id: root
settingsModified: true
+ property bool postsServiceEnabled: false
function refreshDescriptionEditor() {
var description = root.account.configurationValues("")["description"]
@@ -37,7 +41,8 @@ StandardAccountSettingsDisplay {
var providerDisplayName = root.accountProvider && root.accountProvider.displayName
? root.accountProvider.displayName.toString().trim()
: ""
- return providerDisplayName.length > 0 ? providerDisplayName : "Mastodon"
+ //% "Mastodon"
+ return providerDisplayName.length > 0 ? providerDisplayName : qsTrId("settings-accounts-mastodon-la-provider_name")
}
onAboutToSaveAccount: {
@@ -106,12 +111,25 @@ StandardAccountSettingsDisplay {
id: syncServicesRepeater
TextSwitch {
checked: model.enabled
- text: model.displayName
+ text: model.serviceName === "mastodon-microblog"
+ //% "Posts"
+ ? qsTrId("settings-accounts-mastodon-la-service_posts")
+ : (model.serviceName === "mastodon-notifications"
+ //% "Notifications"
+ ? qsTrId("settings-accounts-mastodon-la-service_notifications")
+ : model.displayName)
description: model.serviceName === "mastodon-microblog"
- ? "Show Mastodon posts in the Events view."
- : ""
+ //% "Show Mastodon posts in the Events view."
+ ? qsTrId("settings-accounts-mastodon-la-service_posts_description")
+ : (model.serviceName === "mastodon-notifications"
+ //% "Show Mastodon notifications."
+ ? qsTrId("settings-accounts-mastodon-la-service_notifications_description")
+ : "")
visible: text.length > 0
onCheckedChanged: {
+ if (model.serviceName === "mastodon-microblog") {
+ root.postsServiceEnabled = checked
+ }
if (checked) {
root.account.enableWithService(model.serviceName)
} else {
@@ -124,8 +142,11 @@ StandardAccountSettingsDisplay {
TextSwitch {
id: eventsSyncSwitch
- text: "Sync Mastodon feed automatically"
- description: "Fetch new posts periodically when browsing Events Mastodon feed."
+ //% "Sync Mastodon feed automatically"
+ text: qsTrId("settings-accounts-mastodon-la-auto_sync_feed")
+ //% "Fetch new posts periodically when browsing Events Mastodon feed."
+ description: qsTrId("settings-accounts-mastodon-la-auto_sync_feed_description")
+ enabled: root.postsServiceEnabled
onCheckedChanged: {
autoSyncConf.value = checked
diff --git a/settings/accounts/ui/mastodon-settings.qml b/settings/accounts/ui/mastodon-settings.qml
index c66cbb7..0538e44 100644
--- a/settings/accounts/ui/mastodon-settings.qml
+++ b/settings/accounts/ui/mastodon-settings.qml
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
import QtQuick 2.0
diff --git a/settings/accounts/ui/mastodon-update.qml b/settings/accounts/ui/mastodon-update.qml
index 98dba8c..2485a83 100644
--- a/settings/accounts/ui/mastodon-update.qml
+++ b/settings/accounts/ui/mastodon-update.qml
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
import QtQuick 2.0
@@ -175,7 +177,8 @@ AccountCredentialsAgent {
var clientId = _valueFromServiceConfig(config, "auth/oauth2/web_server/ClientId")
var clientSecret = _valueFromServiceConfig(config, "auth/oauth2/web_server/ClientSecret")
if (clientId.length === 0 || clientSecret.length === 0) {
- credentialsUpdateError("Missing Mastodon OAuth client credentials")
+ //% "Missing Mastodon OAuth client credentials"
+ credentialsUpdateError(qsTrId("settings-accounts-mastodon-la-missing_client_credentials"))
return
}
diff --git a/settings/accounts/ui/mastodon.qml b/settings/accounts/ui/mastodon.qml
index e7cf255..a4bd512 100644
--- a/settings/accounts/ui/mastodon.qml
+++ b/settings/accounts/ui/mastodon.qml
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
import QtQuick 2.0
@@ -366,12 +368,13 @@ AccountCreationAgent {
function configure() {
hasConfigured = true
- var services = ["mastodon-microblog", "mastodon-sharing"]
+ var services = ["mastodon-microblog", "mastodon-notifications", "mastodon-sharing"]
var providerDisplayName = root.accountProvider && root.accountProvider.displayName
? root.accountProvider.displayName.toString().trim()
: ""
if (providerDisplayName.length === 0) {
- providerDisplayName = "Mastodon"
+ //% "Mastodon"
+ providerDisplayName = qsTrId("settings-accounts-mastodon-la-provider_name")
}
newAccount.displayName = providerDisplayName
diff --git a/settings/settings.pro b/settings/settings.pro
index 84f2a75..4272acf 100644
--- a/settings/settings.pro
+++ b/settings/settings.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = subdirs
SUBDIRS += \
accounts \
diff --git a/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml b/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml
index d2d950c..d859d96 100644
--- a/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml
+++ b/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
import QtQuick 2.6
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp
index 99871cb..919d544 100644
--- a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp
+++ b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include "mastodonplugininfo.h"
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h
index 1d74b74..80fe552 100644
--- a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h
+++ b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef MASTODONPLUGININFO_H
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp
index 0666080..8c139a2 100644
--- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include "mastodonshareplugin.h"
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h
index 9f21919..04d8412 100644
--- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef MASTODONSHAREPLUGIN_H
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
index 0dd1443..59fb7e1 100644
--- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = lib
TARGET = $$qtLibraryTarget(mastodonshareplugin)
CONFIG += plugin
diff --git a/transferengine-plugins/mastodonshareservicestatus.cpp b/transferengine-plugins/mastodonshareservicestatus.cpp
index f3c96ca..2591520 100644
--- a/transferengine-plugins/mastodonshareservicestatus.cpp
+++ b/transferengine-plugins/mastodonshareservicestatus.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include "mastodonshareservicestatus.h"
@@ -27,6 +29,11 @@ MastodonShareServiceStatus::MastodonShareServiceStatus(QObject *parent)
{
}
+QString MastodonShareServiceStatus::authServiceName() const
+{
+ return QStringLiteral("mastodon-microblog");
+}
+
void MastodonShareServiceStatus::signIn(int accountId)
{
Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this);
@@ -36,9 +43,9 @@ void MastodonShareServiceStatus::signIn(int accountId)
return;
}
- const Accounts::Service service(m_accountManager->service(m_serviceName));
+ const Accounts::Service service(m_accountManager->service(authServiceName()));
if (!service.isValid()) {
- qWarning() << Q_FUNC_INFO << "Invalid auth service" << m_serviceName;
+ qWarning() << Q_FUNC_INFO << "Invalid auth service" << authServiceName();
account->deleteLater();
setAccountDetailsState(accountId, Error);
return;
@@ -123,7 +130,7 @@ void MastodonShareServiceStatus::signOnError(const SignOn::Error &error)
<< error.type() << error.message();
if (accountId > 0 && error.type() == SignOn::Error::UserInteraction) {
- setCredentialsNeedUpdate(accountId, m_serviceName);
+ setCredentialsNeedUpdate(accountId, authServiceName());
}
session->disconnect(this);
@@ -186,7 +193,7 @@ void MastodonShareServiceStatus::queryStatus(QueryStatusMode mode)
bool signInActive = false;
Q_FOREACH (Accounts::AccountId id, m_accountManager->accountList()) {
- Accounts::Account *acc = m_accountManager->account(id);
+ Accounts::Account *acc = Accounts::Account::fromId(m_accountManager, id, this);
if (!acc) {
qWarning() << Q_FUNC_INFO << "Failed to get account for id:" << id;
@@ -206,6 +213,7 @@ void MastodonShareServiceStatus::queryStatus(QueryStatusMode mode)
}
if (!service.isValid() || !serviceFound) {
+ acc->deleteLater();
continue;
}
@@ -214,19 +222,23 @@ void MastodonShareServiceStatus::queryStatus(QueryStatusMode mode)
const bool shareServiceEnabled = acc->enabled();
if (!accountEnabled || !shareServiceEnabled) {
acc->selectService(Accounts::Service());
+ acc->deleteLater();
continue;
}
if (acc->value(QStringLiteral("CredentialsNeedUpdate")).toBool()) {
qWarning() << Q_FUNC_INFO << "Credentials need update for account id:" << id;
acc->selectService(Accounts::Service());
+ acc->deleteLater();
continue;
}
if (!m_accountIdToDetailsIdx.contains(id)) {
AccountDetails details;
details.accountId = id;
+ acc->selectService(Accounts::Service());
details.apiHost = MastodonAuthUtils::normalizeApiHost(acc->value(QStringLiteral("api/Host")).toString());
+ acc->selectService(service);
QUrl apiUrl(details.apiHost);
details.providerName = apiUrl.host();
@@ -256,6 +268,7 @@ void MastodonShareServiceStatus::queryStatus(QueryStatusMode mode)
}
acc->selectService(Accounts::Service());
+ acc->deleteLater();
}
if (!signInActive) {
diff --git a/transferengine-plugins/mastodonshareservicestatus.h b/transferengine-plugins/mastodonshareservicestatus.h
index cf2e1f8..be76c37 100644
--- a/transferengine-plugins/mastodonshareservicestatus.h
+++ b/transferengine-plugins/mastodonshareservicestatus.h
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef MASTODONSHARESERVICESTATUS_H
@@ -64,6 +66,7 @@ private:
Error
};
+ QString authServiceName() const;
void setAccountDetailsState(int accountId, AccountDetailsState state);
void signIn(int accountId);
diff --git a/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp b/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp
index baf9dd8..fa973d0 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp
+++ b/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp
@@ -1,8 +1,11 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include "mastodonapi.h"
+#include "mastodonauthutils.h"
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
@@ -18,6 +21,7 @@
MastodonApi::MastodonApi(QNetworkAccessManager *qnam, QObject *parent)
: QObject(parent)
+ , m_cancelRequested(false)
, m_qnam(qnam)
{
}
@@ -26,33 +30,6 @@ MastodonApi::~MastodonApi()
{
}
-QString MastodonApi::normalizeApiHost(const QString &rawHost)
-{
- QString host = rawHost.trimmed();
- if (host.isEmpty()) {
- host = QStringLiteral("https://mastodon.social");
- }
-
- if (!host.startsWith(QLatin1String("https://"))
- && !host.startsWith(QLatin1String("http://"))) {
- host.prepend(QStringLiteral("https://"));
- }
-
- QUrl url(host);
- if (!url.isValid() || url.host().isEmpty()) {
- return QStringLiteral("https://mastodon.social");
- }
-
- QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath
- | QUrl::RemoveQuery
- | QUrl::RemoveFragment));
- if (normalized.endsWith(QLatin1Char('/'))) {
- normalized.chop(1);
- }
-
- return normalized;
-}
-
bool MastodonApi::uploadImage(const QString &filePath,
const QString &statusText,
const QString &mimeType,
@@ -65,7 +42,8 @@ bool MastodonApi::uploadImage(const QString &filePath,
return false;
}
- m_apiHost = normalizeApiHost(apiHost);
+ m_cancelRequested = false;
+ m_apiHost = MastodonAuthUtils::normalizeApiHost(apiHost);
m_accessToken = accessToken;
m_statusText = statusText;
@@ -116,7 +94,8 @@ bool MastodonApi::postStatus(const QString &statusText,
const QString &apiHost,
const QString &accessToken)
{
- m_apiHost = normalizeApiHost(apiHost);
+ m_cancelRequested = false;
+ m_apiHost = MastodonAuthUtils::normalizeApiHost(apiHost);
m_accessToken = accessToken;
m_statusText = statusText;
@@ -172,11 +151,11 @@ void MastodonApi::cancelUpload()
return;
}
+ m_cancelRequested = true;
const QList<QNetworkReply*> replies = m_replies.keys();
Q_FOREACH (QNetworkReply *reply, replies) {
reply->abort();
}
- m_replies.clear();
}
void MastodonApi::replyError(QNetworkReply::NetworkError error)
@@ -205,6 +184,14 @@ void MastodonApi::finished()
reply->deleteLater();
+ if (m_cancelRequested && error == QNetworkReply::OperationCanceledError) {
+ if (m_replies.isEmpty()) {
+ m_cancelRequested = false;
+ emit transferCanceled();
+ }
+ return;
+ }
+
if (apiCall == UPLOAD_MEDIA) {
if (error != QNetworkReply::NoError || httpCode < 200 || httpCode >= 300) {
finishTransfer(error == QNetworkReply::NoError ? QNetworkReply::UnknownNetworkError : error,
@@ -241,6 +228,8 @@ void MastodonApi::finished()
void MastodonApi::finishTransfer(QNetworkReply::NetworkError error, int httpCode, const QByteArray &data)
{
+ m_cancelRequested = false;
+
if (httpCode == 401) {
emit credentialsExpired();
}
diff --git a/transferengine-plugins/mastodontransferplugin/mastodonapi.h b/transferengine-plugins/mastodontransferplugin/mastodonapi.h
index 4ac3d80..df4c87a 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodonapi.h
+++ b/transferengine-plugins/mastodontransferplugin/mastodonapi.h
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef MASTODONAPI_H
@@ -49,11 +51,11 @@ private Q_SLOTS:
void uploadProgress(qint64 received, qint64 total);
private:
- static QString normalizeApiHost(const QString &rawHost);
bool postStatusInternal(const QString &mediaId);
void finishTransfer(QNetworkReply::NetworkError error, int httpCode, const QByteArray &data);
QMap<QNetworkReply*, API_CALL> m_replies;
+ bool m_cancelRequested;
QNetworkAccessManager *m_qnam;
QString m_accessToken;
QString m_apiHost;
diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp
index 064388c..a843df2 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include "mastodontransferplugin.h"
diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h
index 865faff..4d3baaf 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef MASTODONTRANSFERPLUGIN_H
diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
index 8782156..422a889 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = lib
TARGET = $$qtLibraryTarget(mastodontransferplugin)
CONFIG += plugin
diff --git a/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp b/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp
index 6e0a6ea..7b87823 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp
+++ b/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#include "mastodonuploader.h"
diff --git a/transferengine-plugins/mastodontransferplugin/mastodonuploader.h b/transferengine-plugins/mastodontransferplugin/mastodonuploader.h
index f773c23..72d9689 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodonuploader.h
+++ b/transferengine-plugins/mastodontransferplugin/mastodonuploader.h
@@ -1,5 +1,7 @@
/*
- * Copyright (C) 2013-2026 Jolla Ltd.
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef MASTODONUPLOADER_H
diff --git a/transferengine-plugins/transferengine-plugins.pro b/transferengine-plugins/transferengine-plugins.pro
index fef3cf5..e2503a3 100644
--- a/transferengine-plugins/transferengine-plugins.pro
+++ b/transferengine-plugins/transferengine-plugins.pro
@@ -1,2 +1,6 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
TEMPLATE = subdirs
SUBDIRS = mastodonshareplugin mastodontransferplugin