diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt
new file mode 100644
index 0000000..0c97efd
--- /dev/null
+++ b/LICENSES/AGPL-3.0-or-later.txt
@@ -0,0 +1,235 @@
+GNU AFFERO GENERAL PUBLIC LICENSE
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+ Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, 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 them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+ TERMS AND CONDITIONS
+
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+
+1. Source Code.
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+2. Basic Permissions.
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+
+4. Conveying Verbatim Copies.
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+
+7. Additional Terms.
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
+
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+
+If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
+
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Affero 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 Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
+
+You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see .
diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt
new file mode 100644
index 0000000..17cb286
--- /dev/null
+++ b/LICENSES/GPL-2.0-only.txt
@@ -0,0 +1,117 @@
+GNU GENERAL PUBLIC LICENSE
+Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, 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 or use pieces of it in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's 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 give any other recipients of the Program a copy of this License along with the Program.
+
+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 Program or any portion of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, 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 Program, 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 Program.
+
+In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
+
+ a) 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; or,
+
+ b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for making modifications to it. For an executable work, 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 executable. However, as a special exception, the source code 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.
+
+If distribution of executable or 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 counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program 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.
+
+5. 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 Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program 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 to this License.
+
+7. 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 Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program 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 Program.
+
+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.
+
+8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program 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.
+
+9. The Free Software Foundation may publish revised and/or new versions of the 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 Program 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 Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, 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
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 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 Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. 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 program's name and an idea of what it does. Copyright (C) yyyy name of author
+
+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
diff --git a/app/build.gradle b/app/build.gradle
index 55fd1eb..8147914 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,9 +13,9 @@ import com.github.spotbugs.snom.Effort
import com.github.spotbugs.snom.SpotBugsTask
plugins {
- id "org.jetbrains.kotlin.plugin.compose" version "2.2.0"
+ id "org.jetbrains.kotlin.plugin.compose" version "2.2.10"
id "org.jetbrains.kotlin.kapt"
- id 'com.google.devtools.ksp' version '2.2.0-2.0.2'
+ id 'com.google.devtools.ksp' version '2.2.10-2.0.2'
}
apply plugin: 'com.android.application'
@@ -28,22 +28,22 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
apply plugin: 'kotlinx-serialization'
android {
- compileSdk 35
+ compileSdkVersion 35
- namespace 'com.nextcloud.talk'
+ namespace = 'com.nextcloud.talk'
defaultConfig {
minSdkVersion 26
targetSdkVersion 35
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
// xx .xxx .xx .xx
- versionCode 210020090
- versionName "21.2.0"
+ versionCode 220000190
+ versionName "22.0.1"
flavorDimensions "default"
- renderscriptTargetApi 19
+ renderscriptTargetApi = 19
renderscriptSupportModeEnabled true
productFlavors {
@@ -65,7 +65,7 @@ android {
}
// Enabling multidex support.
- multiDexEnabled true
+ multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
@@ -82,6 +82,11 @@ android {
}
}
+ sourceSets {
+ // Adds exported schema location as test app assets.
+ getByName("androidTest").assets.srcDir("$projectDir/schemas")
+ }
+
testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}"
testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}"
testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}"
@@ -95,6 +100,7 @@ android {
unitTests.all {
useJUnitPlatform()
}
+ unitTests.returnDefaultValues = true
}
buildTypes {
@@ -129,7 +135,7 @@ android {
}
buildFeatures {
- viewBinding true
+ viewBinding = true
buildConfig = true
compose = true
}
@@ -139,10 +145,10 @@ android {
}
lint {
- abortOnError false
+ abortOnError = false
disable 'MissingTranslation','PrivateResource'
- htmlOutput file("$project.buildDir/reports/lint/lint.html")
- htmlReport true
+ htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile
+ htmlReport = true
}
}
kapt {
@@ -152,10 +158,10 @@ kapt {
ext {
androidxCameraVersion = "1.4.2"
coilKtVersion = "2.7.0"
- daggerVersion = "2.56.2"
+ daggerVersion = "2.57.1"
emojiVersion = "1.5.0"
fidoVersion = "4.1.0-patch2"
- lifecycleVersion = '2.9.1'
+ lifecycleVersion = '2.9.3'
okhttpVersion = "4.12.0"
markwonVersion = "4.6.2"
materialDialogsVersion = "3.3.0"
@@ -163,10 +169,10 @@ ext {
prismVersion = "2.0.0"
retrofit2Version = "3.0.0"
roomVersion = "2.7.2"
- workVersion = "2.10.2"
- espressoVersion = "3.6.1"
+ workVersion = "2.10.3"
+ espressoVersion = "3.7.0"
androidxTestVersion = "1.5.0"
- media3_version = "1.7.1"
+ media3_version = "1.8.0"
coroutines_version = "1.10.2"
mockitoKotlinVersion = "6.0.0"
}
@@ -179,22 +185,24 @@ configurations.configureEach {
}
dependencies {
+ implementation "androidx.room:room-testing-android:${roomVersion}"
+ implementation 'androidx.compose.foundation:foundation-layout:1.9.0'
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0'
- spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.11'
+ spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.14'
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8")
- implementation("androidx.compose.runtime:runtime:1.8.3")
+ implementation("androidx.compose.runtime:runtime:1.9.0")
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.7'
implementation 'androidx.datastore:datastore-preferences:1.1.7'
- implementation 'androidx.test.ext:junit-ktx:1.2.1'
+ implementation 'androidx.test.ext:junit-ktx:1.3.0'
implementation fileTree(include: ['*'], dir: 'libs')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
implementation 'androidx.appcompat:appcompat:1.7.1'
- implementation 'com.google.android.material:material:1.12.0'
+ implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "com.vanniktech:emoji-google:0.21.0"
implementation "androidx.emoji2:emoji2:${emojiVersion}"
@@ -210,6 +218,7 @@ dependencies {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
})
implementation 'org.conscrypt:conscrypt-android:2.5.3'
+ implementation "com.github.nextcloud-deps:qrcodescanner:0.1.2.4" // "com.github.blikoon:QRCodeScanner:0.1.2"
implementation "androidx.camera:camera-core:${androidxCameraVersion}"
implementation "androidx.camera:camera-camera2:${androidxCameraVersion}"
@@ -236,7 +245,7 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
implementation 'com.bluelinelabs:logansquare:1.3.7'
- implementation 'com.fasterxml.jackson.core:jackson-core:2.19.1'
+ implementation 'com.fasterxml.jackson.core:jackson-core:2.20.0'
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
@@ -251,7 +260,7 @@ dependencies {
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'org.greenrobot:eventbus:3.3.1'
- implementation 'net.zetetic:sqlcipher-android:4.9.0'
+ implementation 'net.zetetic:sqlcipher-android:4.10.0'
implementation "androidx.room:room-runtime:${roomVersion}"
implementation "androidx.room:room-rxjava2:${roomVersion}"
@@ -261,7 +270,7 @@ dependencies {
implementation "org.parceler:parceler-api:$parcelerVersion"
implementation 'com.github.ddB0515.FlexibleAdapter:flexible-adapter:5.1.1'
implementation 'com.github.ddB0515.FlexibleAdapter:flexible-adapter-ui:5.1.1'
- implementation 'org.apache.commons:commons-lang3:3.17.0'
+ implementation 'org.apache.commons:commons-lang3:3.18.0'
implementation 'com.github.wooplr:Spotlight:1.3'
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'com.github.nextcloud-deps:ChatKit:0.4.2'
@@ -303,14 +312,14 @@ dependencies {
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.activity:activity-ktx:1.10.1'
- implementation 'com.github.nextcloud.android-common:ui:0.27.0'
+ implementation 'com.github.nextcloud.android-common:ui:0.28.0'
implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0'
gplayImplementation 'com.google.android.gms:play-services-base:18.6.0'
gplayImplementation "com.google.firebase:firebase-messaging:24.1.2"
//compose
- implementation(platform("androidx.compose:compose-bom:2025.06.01"))
+ implementation(platform("androidx.compose:compose-bom:2025.08.01"))
implementation("androidx.compose.ui:ui")
implementation 'androidx.compose.material3:material3:1.3.2'
implementation("androidx.compose.ui:ui-tooling-preview")
@@ -318,18 +327,19 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")
//tests
- androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.3")
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.4'
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.9.0")
debugImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation 'junit:junit:4.13.2'
- testImplementation 'org.mockito:mockito-core:5.18.0'
+ testImplementation 'org.mockito:mockito-core:5.19.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
- androidTestImplementation "androidx.test:core:1.6.1"
+ androidTestImplementation "androidx.test:core:1.7.0"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
- androidTestImplementation 'androidx.test:core-ktx:1.6.1'
- androidTestImplementation 'org.mockito:mockito-android:5.18.0'
+ androidTestImplementation 'androidx.test:core-ktx:1.7.0'
+ androidTestImplementation 'org.mockito:mockito-android:5.19.0'
androidTestImplementation "androidx.work:work-testing:${workVersion}"
// Espresso core
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
@@ -343,11 +353,15 @@ dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2')
- androidTestImplementation(platform("androidx.compose:compose-bom:2025.06.01"))
+ androidTestImplementation(platform("androidx.compose:compose-bom:2025.08.01"))
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
- testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.3'
+ testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.4' // DO NOT REMOVE
+ testImplementation "androidx.room:room-testing:${roomVersion}"
+ testImplementation("com.squareup.okhttp3:mockwebserver:$okhttpVersion")
+ testImplementation("com.google.dagger:hilt-android-testing:2.57.1")
+ testImplementation("org.robolectric:robolectric:4.16")
}
tasks.register('installGitHooks', Copy) {
@@ -371,14 +385,14 @@ tasks.withType(SpotBugsTask).configureEach { task ->
dependsOn "compile${variantNameCap}Sources"
excludeFilter = file("${project.rootDir}/spotbugs-filter.xml")
- classes = fileTree("$project.buildDir/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/")
+ classes = fileTree(layout.buildDirectory.get().asFile.toString()+"/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/")
reports {
xml {
required = true
}
html {
required = true
- outputLocation = file("$project.buildDir/reports/spotbugs/spotbugs.html")
+ outputLocation = layout.buildDirectory.file("reports/spotbugs/spotbugs.html")
stylesheet = 'fancy.xsl'
}
}
diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json
index cdff3df..84c3039 100644
--- a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json
+++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json
@@ -1,709 +1,146 @@
{
- "formatVersion": 1,
- "database": {
- "version": 10,
- "identityHash": "c07a2543aa583e08e7b3208f44fcc7ac",
- "entities": [
- {
- "tableName": "User",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "username",
- "columnName": "username",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "baseUrl",
- "columnName": "baseUrl",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "token",
- "columnName": "token",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "displayName",
- "columnName": "displayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "pushConfigurationState",
- "columnName": "pushConfigurationState",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "capabilities",
- "columnName": "capabilities",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "serverVersion",
- "columnName": "serverVersion",
- "affinity": "TEXT",
- "notNull": false,
- "defaultValue": "''"
- },
- {
- "fieldPath": "clientCertificate",
- "columnName": "clientCertificate",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "externalSignalingServer",
- "columnName": "externalSignalingServer",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "current",
- "columnName": "current",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "scheduledForDeletion",
- "columnName": "scheduledForDeletion",
- "affinity": "INTEGER",
- "notNull": true
- }
+ "formatVersion": 1,
+ "database": {
+ "version": 10,
+ "identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039",
+ "entities": [
+ {
+ "tableName": "User",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "pushConfigurationState",
+ "columnName": "pushConfigurationState",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "serverVersion",
+ "columnName": "serverVersion",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "clientCertificate",
+ "columnName": "clientCertificate",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "externalSignalingServer",
+ "columnName": "externalSignalingServer",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForDeletion",
+ "columnName": "scheduledForDeletion",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ArbitraryStorage",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "accountIdentifier",
+ "columnName": "accountIdentifier",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "storageObject",
+ "columnName": "object",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "accountIdentifier",
+ "key"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
],
- "primaryKey": {
- "autoGenerate": true,
- "columnNames": [
- "id"
- ]
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "ArbitraryStorage",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
- "fields": [
- {
- "fieldPath": "accountIdentifier",
- "columnName": "accountIdentifier",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "key",
- "columnName": "key",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "storageObject",
- "columnName": "object",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "value",
- "columnName": "value",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "accountIdentifier",
- "key"
- ]
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "Conversations",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT, `name` TEXT, `displayName` TEXT, `description` TEXT, `type` TEXT, `lastPing` INTEGER NOT NULL, `participantType` TEXT, `hasPassword` INTEGER NOT NULL, `sessionId` TEXT, `actorId` TEXT, `actorType` TEXT, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `unreadMention` INTEGER NOT NULL, `lastMessageJson` TEXT, `objectType` TEXT, `notificationLevel` TEXT, `readOnly` TEXT, `lobbyState` TEXT, `lobbyTimer` INTEGER, `lastReadMessage` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `hasCall` INTEGER NOT NULL, `callFlag` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `canLeaveConversation` INTEGER, `canDeleteConversation` INTEGER, `unreadMentionDirect` INTEGER, `notificationCalls` INTEGER, `permissions` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `status` TEXT, `statusIcon` TEXT, `statusMessage` TEXT, `statusClearAt` INTEGER, `callRecording` INTEGER NOT NULL, `avatarVersion` TEXT, `isCustomAvatar` INTEGER, `callStartTime` INTEGER, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "internalId",
- "columnName": "internalId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "accountId",
- "columnName": "accountId",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "token",
- "columnName": "token",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "displayName",
- "columnName": "displayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "description",
- "columnName": "description",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "lastPing",
- "columnName": "lastPing",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "participantType",
- "columnName": "participantType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "hasPassword",
- "columnName": "hasPassword",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "sessionId",
- "columnName": "sessionId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "actorId",
- "columnName": "actorId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "actorType",
- "columnName": "actorType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "favorite",
- "columnName": "isFavorite",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "lastActivity",
- "columnName": "lastActivity",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "unreadMessages",
- "columnName": "unreadMessages",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "unreadMention",
- "columnName": "unreadMention",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "lastMessageJson",
- "columnName": "lastMessageJson",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "objectType",
- "columnName": "objectType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "notificationLevel",
- "columnName": "notificationLevel",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "conversationReadOnlyState",
- "columnName": "readOnly",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "lobbyState",
- "columnName": "lobbyState",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "lobbyTimer",
- "columnName": "lobbyTimer",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "lastReadMessage",
- "columnName": "lastReadMessage",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "lastCommonReadMessage",
- "columnName": "lastCommonReadMessage",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "hasCall",
- "columnName": "hasCall",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "callFlag",
- "columnName": "callFlag",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "canStartCall",
- "columnName": "canStartCall",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "canLeaveConversation",
- "columnName": "canLeaveConversation",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "canDeleteConversation",
- "columnName": "canDeleteConversation",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "unreadMentionDirect",
- "columnName": "unreadMentionDirect",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "notificationCalls",
- "columnName": "notificationCalls",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "permissions",
- "columnName": "permissions",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "messageExpiration",
- "columnName": "messageExpiration",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "status",
- "columnName": "status",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "statusIcon",
- "columnName": "statusIcon",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "statusMessage",
- "columnName": "statusMessage",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "statusClearAt",
- "columnName": "statusClearAt",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "callRecording",
- "columnName": "callRecording",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "avatarVersion",
- "columnName": "avatarVersion",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "hasCustomAvatar",
- "columnName": "isCustomAvatar",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "callStartTime",
- "columnName": "callStartTime",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "recordingConsentRequired",
- "columnName": "recordingConsent",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "remoteServer",
- "columnName": "remoteServer",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "remoteToken",
- "columnName": "remoteToken",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "internalId"
- ]
- },
- "indices": [
- {
- "name": "index_Conversations_accountId",
- "unique": false,
- "columnNames": [
- "accountId"
- ],
- "orders": [],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "User",
- "onDelete": "CASCADE",
- "onUpdate": "CASCADE",
- "columns": [
- "accountId"
- ],
- "referencedColumns": [
- "id"
- ]
- }
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b2dab0ea495c45c9c9ee6e64ba74039')"
]
- },
- {
- "tableName": "ChatMessages",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `id` INTEGER NOT NULL, `internalConversationId` TEXT, `actorType` TEXT, `actorId` TEXT, `actorDisplayName` TEXT, `timestamp` INTEGER NOT NULL, `systemMessage` TEXT, `messageType` TEXT, `isReplyable` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `expirationTimestamp` INTEGER NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `markdown` INTEGER, `lastEditActorType` TEXT, `lastEditActorId` TEXT, `lastEditActorDisplayName` TEXT, `lastEditTimestamp` INTEGER, `deleted` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "internalId",
- "columnName": "internalId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "accountId",
- "columnName": "accountId",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "token",
- "columnName": "token",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "internalConversationId",
- "columnName": "internalConversationId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "actorType",
- "columnName": "actorType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "actorId",
- "columnName": "actorId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "actorDisplayName",
- "columnName": "actorDisplayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "timestamp",
- "columnName": "timestamp",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "systemMessageType",
- "columnName": "systemMessage",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "messageType",
- "columnName": "messageType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "replyable",
- "columnName": "isReplyable",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "message",
- "columnName": "message",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "messageParameters",
- "columnName": "messageParameters",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "expirationTimestamp",
- "columnName": "expirationTimestamp",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "parentMessageId",
- "columnName": "parent",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "reactions",
- "columnName": "reactions",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "reactionsSelf",
- "columnName": "reactionsSelf",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "renderMarkdown",
- "columnName": "markdown",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "lastEditActorType",
- "columnName": "lastEditActorType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "lastEditActorId",
- "columnName": "lastEditActorId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "lastEditActorDisplayName",
- "columnName": "lastEditActorDisplayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "lastEditTimestamp",
- "columnName": "lastEditTimestamp",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "deleted",
- "columnName": "deleted",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "internalId"
- ]
- },
- "indices": [
- {
- "name": "index_ChatMessages_internalId",
- "unique": true,
- "columnNames": [
- "internalId"
- ],
- "orders": [],
- "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
- },
- {
- "name": "index_ChatMessages_internalConversationId",
- "unique": false,
- "columnNames": [
- "internalConversationId"
- ],
- "orders": [],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "Conversations",
- "onDelete": "CASCADE",
- "onUpdate": "CASCADE",
- "columns": [
- "internalConversationId"
- ],
- "referencedColumns": [
- "internalId"
- ]
- }
- ]
- },
- {
- "tableName": "ChatBlocks",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "internalConversationId",
- "columnName": "internalConversationId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "accountId",
- "columnName": "accountId",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "token",
- "columnName": "token",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "oldestMessageId",
- "columnName": "oldestMessageId",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "newestMessageId",
- "columnName": "newestMessageId",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "hasHistory",
- "columnName": "hasHistory",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": true,
- "columnNames": [
- "id"
- ]
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "Conversations",
- "onDelete": "CASCADE",
- "onUpdate": "CASCADE",
- "columns": [
- "internalConversationId"
- ],
- "referencedColumns": [
- "internalId"
- ]
- }
- ]
- }
- ],
- "views": [],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c07a2543aa583e08e7b3208f44fcc7ac')"
- ]
- }
+ }
}
\ No newline at end of file
diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json
new file mode 100644
index 0000000..391c430
--- /dev/null
+++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json
@@ -0,0 +1,746 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 18,
+ "identityHash": "c5e3716925065d7419fb23efabf6691f",
+ "entities": [
+ {
+ "tableName": "User",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "pushConfigurationState",
+ "columnName": "pushConfigurationState",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "serverVersion",
+ "columnName": "serverVersion",
+ "affinity": "TEXT",
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "clientCertificate",
+ "columnName": "clientCertificate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "externalSignalingServer",
+ "columnName": "externalSignalingServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForDeletion",
+ "columnName": "scheduledForDeletion",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ArbitraryStorage",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "accountIdentifier",
+ "columnName": "accountIdentifier",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "storageObject",
+ "columnName": "object",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "accountIdentifier",
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "Conversations",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarVersion",
+ "columnName": "avatarVersion",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callFlag",
+ "columnName": "callFlag",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callRecording",
+ "columnName": "callRecording",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callStartTime",
+ "columnName": "callStartTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canDeleteConversation",
+ "columnName": "canDeleteConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canLeaveConversation",
+ "columnName": "canLeaveConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canStartCall",
+ "columnName": "canStartCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCall",
+ "columnName": "hasCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasPassword",
+ "columnName": "hasPassword",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCustomAvatar",
+ "columnName": "isCustomAvatar",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favorite",
+ "columnName": "isFavorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastActivity",
+ "columnName": "lastActivity",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastCommonReadMessage",
+ "columnName": "lastCommonReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastMessage",
+ "columnName": "lastMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastPing",
+ "columnName": "lastPing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastReadMessage",
+ "columnName": "lastReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyState",
+ "columnName": "lobbyState",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyTimer",
+ "columnName": "lobbyTimer",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageExpiration",
+ "columnName": "messageExpiration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationCalls",
+ "columnName": "notificationCalls",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationLevel",
+ "columnName": "notificationLevel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectType",
+ "columnName": "objectType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectId",
+ "columnName": "objectId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "participantType",
+ "columnName": "participantType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationReadOnlyState",
+ "columnName": "readOnly",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recordingConsentRequired",
+ "columnName": "recordingConsent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteServer",
+ "columnName": "remoteServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteToken",
+ "columnName": "remoteToken",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusClearAt",
+ "columnName": "statusClearAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "statusIcon",
+ "columnName": "statusIcon",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "statusMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMention",
+ "columnName": "unreadMention",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMentionDirect",
+ "columnName": "unreadMentionDirect",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMessages",
+ "columnName": "unreadMessages",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasArchived",
+ "columnName": "hasArchived",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasSensitive",
+ "columnName": "hasSensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasImportant",
+ "columnName": "hasImportant",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Conversations_accountId",
+ "unique": false,
+ "columnNames": [
+ "accountId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "User",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "accountId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatMessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isThread",
+ "columnName": "isThread",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorDisplayName",
+ "columnName": "actorDisplayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTimestamp",
+ "columnName": "expirationTimestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyable",
+ "columnName": "isReplyable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isTemporary",
+ "columnName": "isTemporary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastEditActorDisplayName",
+ "columnName": "lastEditActorDisplayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorId",
+ "columnName": "lastEditActorId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorType",
+ "columnName": "lastEditActorType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditTimestamp",
+ "columnName": "lastEditTimestamp",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "renderMarkdown",
+ "columnName": "markdown",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "messageParameters",
+ "columnName": "messageParameters",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "messageType",
+ "columnName": "messageType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentMessageId",
+ "columnName": "parent",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "reactionsSelf",
+ "columnName": "reactionsSelf",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "referenceId",
+ "columnName": "referenceId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sendStatus",
+ "columnName": "sendStatus",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "silent",
+ "columnName": "silent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "systemMessageType",
+ "columnName": "systemMessage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatMessages_internalId",
+ "unique": true,
+ "columnNames": [
+ "internalId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
+ },
+ {
+ "name": "index_ChatMessages_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatBlocks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "oldestMessageId",
+ "columnName": "oldestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "newestMessageId",
+ "columnName": "newestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasHistory",
+ "columnName": "hasHistory",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatBlocks_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5e3716925065d7419fb23efabf6691f')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json
new file mode 100644
index 0000000..c11f384
--- /dev/null
+++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json
@@ -0,0 +1,746 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 19,
+ "identityHash": "c5e3716925065d7419fb23efabf6691f",
+ "entities": [
+ {
+ "tableName": "User",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "pushConfigurationState",
+ "columnName": "pushConfigurationState",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "serverVersion",
+ "columnName": "serverVersion",
+ "affinity": "TEXT",
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "clientCertificate",
+ "columnName": "clientCertificate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "externalSignalingServer",
+ "columnName": "externalSignalingServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForDeletion",
+ "columnName": "scheduledForDeletion",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ArbitraryStorage",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "accountIdentifier",
+ "columnName": "accountIdentifier",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "storageObject",
+ "columnName": "object",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "accountIdentifier",
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "Conversations",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarVersion",
+ "columnName": "avatarVersion",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callFlag",
+ "columnName": "callFlag",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callRecording",
+ "columnName": "callRecording",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callStartTime",
+ "columnName": "callStartTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canDeleteConversation",
+ "columnName": "canDeleteConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canLeaveConversation",
+ "columnName": "canLeaveConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canStartCall",
+ "columnName": "canStartCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCall",
+ "columnName": "hasCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasPassword",
+ "columnName": "hasPassword",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCustomAvatar",
+ "columnName": "isCustomAvatar",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favorite",
+ "columnName": "isFavorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastActivity",
+ "columnName": "lastActivity",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastCommonReadMessage",
+ "columnName": "lastCommonReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastMessage",
+ "columnName": "lastMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastPing",
+ "columnName": "lastPing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastReadMessage",
+ "columnName": "lastReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyState",
+ "columnName": "lobbyState",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyTimer",
+ "columnName": "lobbyTimer",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageExpiration",
+ "columnName": "messageExpiration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationCalls",
+ "columnName": "notificationCalls",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationLevel",
+ "columnName": "notificationLevel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectType",
+ "columnName": "objectType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectId",
+ "columnName": "objectId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "participantType",
+ "columnName": "participantType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationReadOnlyState",
+ "columnName": "readOnly",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recordingConsentRequired",
+ "columnName": "recordingConsent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteServer",
+ "columnName": "remoteServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteToken",
+ "columnName": "remoteToken",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusClearAt",
+ "columnName": "statusClearAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "statusIcon",
+ "columnName": "statusIcon",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "statusMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMention",
+ "columnName": "unreadMention",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMentionDirect",
+ "columnName": "unreadMentionDirect",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMessages",
+ "columnName": "unreadMessages",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasArchived",
+ "columnName": "hasArchived",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasSensitive",
+ "columnName": "hasSensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasImportant",
+ "columnName": "hasImportant",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Conversations_accountId",
+ "unique": false,
+ "columnNames": [
+ "accountId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "User",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "accountId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatMessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isThread",
+ "columnName": "isThread",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorDisplayName",
+ "columnName": "actorDisplayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTimestamp",
+ "columnName": "expirationTimestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyable",
+ "columnName": "isReplyable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isTemporary",
+ "columnName": "isTemporary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastEditActorDisplayName",
+ "columnName": "lastEditActorDisplayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorId",
+ "columnName": "lastEditActorId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorType",
+ "columnName": "lastEditActorType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditTimestamp",
+ "columnName": "lastEditTimestamp",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "renderMarkdown",
+ "columnName": "markdown",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "messageParameters",
+ "columnName": "messageParameters",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "messageType",
+ "columnName": "messageType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentMessageId",
+ "columnName": "parent",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "reactionsSelf",
+ "columnName": "reactionsSelf",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "referenceId",
+ "columnName": "referenceId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sendStatus",
+ "columnName": "sendStatus",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "silent",
+ "columnName": "silent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "systemMessageType",
+ "columnName": "systemMessage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatMessages_internalId",
+ "unique": true,
+ "columnNames": [
+ "internalId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
+ },
+ {
+ "name": "index_ChatMessages_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatBlocks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "oldestMessageId",
+ "columnName": "oldestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "newestMessageId",
+ "columnName": "newestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasHistory",
+ "columnName": "hasHistory",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatBlocks_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5e3716925065d7419fb23efabf6691f')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json
new file mode 100644
index 0000000..68198eb
--- /dev/null
+++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json
@@ -0,0 +1,751 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 20,
+ "identityHash": "7330dad871a0b42e36931ffe8c7d4bcf",
+ "entities": [
+ {
+ "tableName": "User",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "pushConfigurationState",
+ "columnName": "pushConfigurationState",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "serverVersion",
+ "columnName": "serverVersion",
+ "affinity": "TEXT",
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "clientCertificate",
+ "columnName": "clientCertificate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "externalSignalingServer",
+ "columnName": "externalSignalingServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForDeletion",
+ "columnName": "scheduledForDeletion",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ArbitraryStorage",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "accountIdentifier",
+ "columnName": "accountIdentifier",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "storageObject",
+ "columnName": "object",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "accountIdentifier",
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "Conversations",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, `messageDraft` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarVersion",
+ "columnName": "avatarVersion",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callFlag",
+ "columnName": "callFlag",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callRecording",
+ "columnName": "callRecording",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callStartTime",
+ "columnName": "callStartTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canDeleteConversation",
+ "columnName": "canDeleteConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canLeaveConversation",
+ "columnName": "canLeaveConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canStartCall",
+ "columnName": "canStartCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCall",
+ "columnName": "hasCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasPassword",
+ "columnName": "hasPassword",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCustomAvatar",
+ "columnName": "isCustomAvatar",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favorite",
+ "columnName": "isFavorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastActivity",
+ "columnName": "lastActivity",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastCommonReadMessage",
+ "columnName": "lastCommonReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastMessage",
+ "columnName": "lastMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastPing",
+ "columnName": "lastPing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastReadMessage",
+ "columnName": "lastReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyState",
+ "columnName": "lobbyState",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyTimer",
+ "columnName": "lobbyTimer",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageExpiration",
+ "columnName": "messageExpiration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationCalls",
+ "columnName": "notificationCalls",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationLevel",
+ "columnName": "notificationLevel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectType",
+ "columnName": "objectType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectId",
+ "columnName": "objectId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "participantType",
+ "columnName": "participantType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationReadOnlyState",
+ "columnName": "readOnly",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recordingConsentRequired",
+ "columnName": "recordingConsent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteServer",
+ "columnName": "remoteServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteToken",
+ "columnName": "remoteToken",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusClearAt",
+ "columnName": "statusClearAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "statusIcon",
+ "columnName": "statusIcon",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "statusMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMention",
+ "columnName": "unreadMention",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMentionDirect",
+ "columnName": "unreadMentionDirect",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMessages",
+ "columnName": "unreadMessages",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasArchived",
+ "columnName": "hasArchived",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasSensitive",
+ "columnName": "hasSensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasImportant",
+ "columnName": "hasImportant",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageDraft",
+ "columnName": "messageDraft",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Conversations_accountId",
+ "unique": false,
+ "columnNames": [
+ "accountId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "User",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "accountId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatMessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isThread",
+ "columnName": "isThread",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorDisplayName",
+ "columnName": "actorDisplayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTimestamp",
+ "columnName": "expirationTimestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyable",
+ "columnName": "isReplyable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isTemporary",
+ "columnName": "isTemporary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastEditActorDisplayName",
+ "columnName": "lastEditActorDisplayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorId",
+ "columnName": "lastEditActorId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorType",
+ "columnName": "lastEditActorType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditTimestamp",
+ "columnName": "lastEditTimestamp",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "renderMarkdown",
+ "columnName": "markdown",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "messageParameters",
+ "columnName": "messageParameters",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "messageType",
+ "columnName": "messageType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentMessageId",
+ "columnName": "parent",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "reactionsSelf",
+ "columnName": "reactionsSelf",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "referenceId",
+ "columnName": "referenceId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sendStatus",
+ "columnName": "sendStatus",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "silent",
+ "columnName": "silent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "systemMessageType",
+ "columnName": "systemMessage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatMessages_internalId",
+ "unique": true,
+ "columnNames": [
+ "internalId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
+ },
+ {
+ "name": "index_ChatMessages_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatBlocks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "oldestMessageId",
+ "columnName": "oldestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "newestMessageId",
+ "columnName": "newestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasHistory",
+ "columnName": "hasHistory",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatBlocks_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7330dad871a0b42e36931ffe8c7d4bcf')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json
new file mode 100644
index 0000000..ebd9449
--- /dev/null
+++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json
@@ -0,0 +1,761 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 21,
+ "identityHash": "8077a29304b3d28882e4b37fb10d0081",
+ "entities": [
+ {
+ "tableName": "User",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "pushConfigurationState",
+ "columnName": "pushConfigurationState",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "serverVersion",
+ "columnName": "serverVersion",
+ "affinity": "TEXT",
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "clientCertificate",
+ "columnName": "clientCertificate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "externalSignalingServer",
+ "columnName": "externalSignalingServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForDeletion",
+ "columnName": "scheduledForDeletion",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ArbitraryStorage",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "accountIdentifier",
+ "columnName": "accountIdentifier",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "storageObject",
+ "columnName": "object",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "accountIdentifier",
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "Conversations",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, `messageDraft` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarVersion",
+ "columnName": "avatarVersion",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callFlag",
+ "columnName": "callFlag",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callRecording",
+ "columnName": "callRecording",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "callStartTime",
+ "columnName": "callStartTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canDeleteConversation",
+ "columnName": "canDeleteConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canLeaveConversation",
+ "columnName": "canLeaveConversation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canStartCall",
+ "columnName": "canStartCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCall",
+ "columnName": "hasCall",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasPassword",
+ "columnName": "hasPassword",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasCustomAvatar",
+ "columnName": "isCustomAvatar",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favorite",
+ "columnName": "isFavorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastActivity",
+ "columnName": "lastActivity",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastCommonReadMessage",
+ "columnName": "lastCommonReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastMessage",
+ "columnName": "lastMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastPing",
+ "columnName": "lastPing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastReadMessage",
+ "columnName": "lastReadMessage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyState",
+ "columnName": "lobbyState",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lobbyTimer",
+ "columnName": "lobbyTimer",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageExpiration",
+ "columnName": "messageExpiration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationCalls",
+ "columnName": "notificationCalls",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationLevel",
+ "columnName": "notificationLevel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectType",
+ "columnName": "objectType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "objectId",
+ "columnName": "objectId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "participantType",
+ "columnName": "participantType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationReadOnlyState",
+ "columnName": "readOnly",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recordingConsentRequired",
+ "columnName": "recordingConsent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteServer",
+ "columnName": "remoteServer",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remoteToken",
+ "columnName": "remoteToken",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusClearAt",
+ "columnName": "statusClearAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "statusIcon",
+ "columnName": "statusIcon",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "statusMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMention",
+ "columnName": "unreadMention",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMentionDirect",
+ "columnName": "unreadMentionDirect",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unreadMessages",
+ "columnName": "unreadMessages",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasArchived",
+ "columnName": "hasArchived",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasSensitive",
+ "columnName": "hasSensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasImportant",
+ "columnName": "hasImportant",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageDraft",
+ "columnName": "messageDraft",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Conversations_accountId",
+ "unique": false,
+ "columnNames": [
+ "accountId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "User",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "accountId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatMessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `threadTitle` TEXT, `threadReplies` INTEGER, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "internalId",
+ "columnName": "internalId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isThread",
+ "columnName": "isThread",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorDisplayName",
+ "columnName": "actorDisplayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorId",
+ "columnName": "actorId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actorType",
+ "columnName": "actorType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTimestamp",
+ "columnName": "expirationTimestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyable",
+ "columnName": "isReplyable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isTemporary",
+ "columnName": "isTemporary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastEditActorDisplayName",
+ "columnName": "lastEditActorDisplayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorId",
+ "columnName": "lastEditActorId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditActorType",
+ "columnName": "lastEditActorType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastEditTimestamp",
+ "columnName": "lastEditTimestamp",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "renderMarkdown",
+ "columnName": "markdown",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "messageParameters",
+ "columnName": "messageParameters",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "messageType",
+ "columnName": "messageType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentMessageId",
+ "columnName": "parent",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "reactions",
+ "columnName": "reactions",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "reactionsSelf",
+ "columnName": "reactionsSelf",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "referenceId",
+ "columnName": "referenceId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sendStatus",
+ "columnName": "sendStatus",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "silent",
+ "columnName": "silent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "systemMessageType",
+ "columnName": "systemMessage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "threadTitle",
+ "columnName": "threadTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "threadReplies",
+ "columnName": "threadReplies",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "internalId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatMessages_internalId",
+ "unique": true,
+ "columnNames": [
+ "internalId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
+ },
+ {
+ "name": "index_ChatMessages_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ChatBlocks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "internalConversationId",
+ "columnName": "internalConversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "threadId",
+ "columnName": "threadId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "oldestMessageId",
+ "columnName": "oldestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "newestMessageId",
+ "columnName": "newestMessageId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasHistory",
+ "columnName": "hasHistory",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ChatBlocks_internalConversationId",
+ "unique": false,
+ "columnNames": [
+ "internalConversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Conversations",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "internalConversationId"
+ ],
+ "referencedColumns": [
+ "internalId"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8077a29304b3d28882e4b37fb10d0081')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt
index 6c87b61..01067b1 100644
--- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt
+++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt
@@ -25,6 +25,9 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import java.lang.Boolean
+import kotlin.Long
+import kotlin.String
@RunWith(AndroidJUnit4::class)
class ChatBlocksDaoTest {
@@ -50,12 +53,82 @@ class ChatBlocksDaoTest {
fun closeDb() = db.close()
@Test
- fun testGetConnectedChatBlocks() =
+ fun testGetChatBlocksContainingMessageId() =
runTest {
- usersDao.saveUser(createUserEntity("account1", "Account 1"))
+ val user = createUserEntity("account1", "Account 1")
+ usersDao.saveUser(user)
val account1 = usersDao.getUserWithUserId("account1").blockingGet()
conversationsDao.upsertConversations(
+ accountId = user.id,
+ listOf(
+ createConversationEntity(
+ accountId = account1.id,
+ "abc",
+ roomName = "Conversation One"
+ ),
+ createConversationEntity(
+ accountId = account1.id,
+ "def",
+ roomName = "Conversation Two"
+ )
+ )
+ )
+
+ val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
+
+ val chatBlock1 = ChatBlockEntity(
+ internalConversationId = conversation1.internalId,
+ accountId = conversation1.accountId,
+ token = conversation1.token,
+ threadId = 123,
+ oldestMessageId = 50,
+ newestMessageId = 60,
+ hasHistory = true
+ )
+
+ val chatBlock2 = ChatBlockEntity(
+ internalConversationId = conversation1.internalId,
+ accountId = conversation1.accountId,
+ token = conversation1.token,
+ threadId = 123,
+ oldestMessageId = 10,
+ newestMessageId = 20,
+ hasHistory = true
+ )
+
+ val chatBlock3 = ChatBlockEntity(
+ internalConversationId = conversation1.internalId,
+ accountId = conversation1.accountId,
+ token = conversation1.token,
+ threadId = null,
+ oldestMessageId = 50,
+ newestMessageId = 60,
+ hasHistory = true
+ )
+
+ chatBlocksDao.upsertChatBlock(chatBlock1)
+ chatBlocksDao.upsertChatBlock(chatBlock2)
+ chatBlocksDao.upsertChatBlock(chatBlock3)
+
+ val chatBlocksOfThread = chatBlocksDao.getChatBlocksContainingMessageId(
+ internalConversationId = conversation1.internalId,
+ threadId = 123,
+ messageId = 55
+ )
+
+ assertEquals(1, chatBlocksOfThread.first().size)
+ }
+
+ @Test
+ fun testGetConnectedChatBlocks() =
+ runTest {
+ val user = createUserEntity("account1", "Account 1")
+ usersDao.saveUser(user)
+ val account1 = usersDao.getUserWithUserId("account1").blockingGet()
+
+ conversationsDao.upsertConversations(
+ account1.id,
listOf(
createConversationEntity(
accountId = account1.id,
@@ -77,6 +150,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 50,
newestMessageId = 60,
hasHistory = true
@@ -86,6 +160,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 10,
newestMessageId = 20,
hasHistory = true
@@ -95,6 +170,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 45,
newestMessageId = 55,
hasHistory = true
@@ -104,6 +180,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 52,
newestMessageId = 58,
hasHistory = true
@@ -113,6 +190,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 1,
newestMessageId = 99,
hasHistory = true
@@ -122,6 +200,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 59,
newestMessageId = 70,
hasHistory = true
@@ -131,6 +210,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId,
accountId = conversation1.accountId,
token = conversation1.token,
+ threadId = null,
oldestMessageId = 80,
newestMessageId = 90,
hasHistory = true
@@ -140,6 +220,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation2.internalId,
accountId = conversation2.accountId,
token = conversation2.token,
+ threadId = null,
oldestMessageId = 53,
newestMessageId = 57,
hasHistory = true
@@ -156,14 +237,94 @@ class ChatBlocksDaoTest {
chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation)
val results = chatBlocksDao.getConnectedChatBlocks(
- conversation1.internalId,
- searchedChatBlock.oldestMessageId,
- searchedChatBlock.newestMessageId
+ internalConversationId = conversation1.internalId,
+ threadId = null,
+ oldestMessageId = searchedChatBlock.oldestMessageId,
+ newestMessageId = searchedChatBlock.newestMessageId
)
assertEquals(5, results.first().size)
}
+ @Test
+ fun testGetConnectedChatBlocksWithThreadsScenario() =
+ runTest {
+ val user = createUserEntity("account1", "Account 1")
+ usersDao.saveUser(user)
+ val account1 = usersDao.getUserWithUserId("account1").blockingGet()
+
+ conversationsDao.upsertConversations(
+ account1.id,
+ listOf(
+ createConversationEntity(
+ accountId = account1.id,
+ "abc",
+ roomName = "Conversation One"
+ ),
+ createConversationEntity(
+ accountId = account1.id,
+ "def",
+ roomName = "Conversation Two"
+ )
+ )
+ )
+
+ val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0]
+
+ val searchedChatBlock = ChatBlockEntity(
+ internalConversationId = conversation1.internalId,
+ accountId = conversation1.accountId,
+ token = conversation1.token,
+ threadId = 123,
+ oldestMessageId = 50,
+ newestMessageId = 60,
+ hasHistory = true
+ )
+
+ val chatBlockOverlap1 = ChatBlockEntity(
+ internalConversationId = conversation1.internalId,
+ accountId = conversation1.accountId,
+ token = conversation1.token,
+ threadId = null,
+ oldestMessageId = 45,
+ newestMessageId = 55,
+ hasHistory = true
+ )
+
+ val chatBlockOverlap2 = ChatBlockEntity(
+ internalConversationId = conversation1.internalId,
+ accountId = conversation1.accountId,
+ token = conversation1.token,
+ threadId = 123,
+ oldestMessageId = 59,
+ newestMessageId = 70,
+ hasHistory = true
+ )
+
+ chatBlocksDao.upsertChatBlock(searchedChatBlock)
+
+ chatBlocksDao.upsertChatBlock(chatBlockOverlap1)
+ chatBlocksDao.upsertChatBlock(chatBlockOverlap2)
+
+ val resultsForThreadIdNull = chatBlocksDao.getConnectedChatBlocks(
+ internalConversationId = conversation1.internalId,
+ threadId = null,
+ oldestMessageId = searchedChatBlock.oldestMessageId,
+ newestMessageId = searchedChatBlock.newestMessageId
+ )
+
+ assertEquals(1, resultsForThreadIdNull.first().size)
+
+ val resultsForThreadId123 = chatBlocksDao.getConnectedChatBlocks(
+ internalConversationId = conversation1.internalId,
+ threadId = 123,
+ oldestMessageId = searchedChatBlock.oldestMessageId,
+ newestMessageId = searchedChatBlock.newestMessageId
+ )
+
+ assertEquals(2, resultsForThreadId123.first().size)
+ }
+
private fun createUserEntity(userId: String, userName: String) =
UserEntity(
userId = userId,
@@ -176,8 +337,8 @@ class ChatBlocksDaoTest {
serverVersion = null,
clientCertificate = null,
externalSignalingServer = null,
- current = java.lang.Boolean.FALSE,
- scheduledForDeletion = java.lang.Boolean.FALSE
+ current = Boolean.FALSE,
+ scheduledForDeletion = Boolean.FALSE
)
private fun createConversationEntity(accountId: Long, token: String, roomName: String) =
diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt
index a6f8386..6bcce26 100644
--- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt
+++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt
@@ -66,6 +66,7 @@ class ChatMessagesDaoTest {
// Problem: lets say we want to update the conv list -> We don#t know the primary keys!
// with account@token that would be easier!
conversationsDao.upsertConversations(
+ account1.id,
listOf(
createConversationEntity(
accountId = account1.id,
@@ -140,7 +141,11 @@ class ChatMessagesDaoTest {
assertEquals("are", conv1chatMessage3.message)
val chatMessagesConv1Since =
- chatMessagesDao.getMessagesForConversationSince(conversation1.internalId, conv1chatMessage3.id)
+ chatMessagesDao.getMessagesForConversationSince(
+ conversation1.internalId,
+ conv1chatMessage3.id,
+ null
+ )
assertEquals(3, chatMessagesConv1Since.first().size)
assertEquals("are", chatMessagesConv1Since.first()[0].message)
assertEquals("some", chatMessagesConv1Since.first()[1].message)
@@ -150,7 +155,8 @@ class ChatMessagesDaoTest {
chatMessagesDao.getMessagesForConversationBeforeAndEqual(
conversation1.internalId,
conv1chatMessage3.id,
- 3
+ 3,
+ null
)
assertEquals(3, chatMessagesConv1To.first().size)
assertEquals("hello", chatMessagesConv1To.first()[2].message)
diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt
new file mode 100644
index 0000000..978bab2
--- /dev/null
+++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.data.database.migrations
+
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.nextcloud.talk.data.source.local.Migrations
+import com.nextcloud.talk.data.source.local.TalkDatabase
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class MigrationsTest {
+ companion object {
+ private const val TEST_DB = "migration-test"
+ private const val INIT_VERSION = 10 // last version before update to offline first
+ private val TAG = MigrationsTest::class.java.simpleName
+ }
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ TalkDatabase::class.java
+ )
+
+ @Test
+ @Throws(IOException::class)
+ @Suppress("SpreadOperator")
+ fun migrateAll() {
+ helper.createDatabase(TEST_DB, INIT_VERSION).apply {
+ close()
+ }
+
+ Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ TalkDatabase::class.java,
+ TEST_DB
+ ).addMigrations(*TalkDatabase.MIGRATIONS).build().apply {
+ openHelper.writableDatabase.close()
+ }
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate10To11() {
+ helper.createDatabase(TEST_DB, 10).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 11, true, Migrations.MIGRATION_10_11)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate11To12() {
+ helper.createDatabase(TEST_DB, 11).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 12, true, Migrations.MIGRATION_11_12)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate12To13() {
+ helper.createDatabase(TEST_DB, 12).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 13, true, Migrations.MIGRATION_12_13)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate13To14() {
+ helper.createDatabase(TEST_DB, 13).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 14, true, Migrations.MIGRATION_13_14)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate14To15() {
+ helper.createDatabase(TEST_DB, 14).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 15, true, Migrations.MIGRATION_14_15)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate15To16() {
+ helper.createDatabase(TEST_DB, 15).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 16, true, Migrations.MIGRATION_15_16)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate17To19() {
+ helper.createDatabase(TEST_DB, 17).apply {
+ close()
+ }
+ helper.runMigrationsAndValidate(TEST_DB, 19, true, Migrations.MIGRATION_17_19)
+ }
+}
diff --git a/app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt b/app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt
new file mode 100644
index 0000000..97f6f4c
--- /dev/null
+++ b/app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2025 Marcel Hibbe
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.utils
+
+import android.graphics.Color
+import org.junit.Assert
+import org.junit.Test
+
+class ColorGeneratorTest {
+
+ @Test
+ fun testUsernameToColor() {
+ usernameToColorHexHelper("", "#0082c9")
+ usernameToColorHexHelper(",", "#1e78c1")
+ usernameToColorHexHelper(".", "#c98879")
+ usernameToColorHexHelper("admin", "#d09e6d")
+ usernameToColorHexHelper("123e4567-e89b-12d3-a456-426614174000", "#bc5c91")
+ usernameToColorHexHelper("Akeel Robertson", "#9750a4")
+ usernameToColorHexHelper("Brayden Truong", "#d09e6d")
+ usernameToColorHexHelper("Daphne Roy", "#9750a4")
+ usernameToColorHexHelper("Ellena Wright Frederic Conway", "#c37285")
+ usernameToColorHexHelper("Gianluca Hills", "#d6b461")
+ usernameToColorHexHelper("Haseeb Stephens", "#d6b461")
+ usernameToColorHexHelper("Idris Mac", "#9750a4")
+ usernameToColorHexHelper("Kristi Fisher", "#0082c9")
+ usernameToColorHexHelper("Lillian Wall", "#bc5c91")
+ usernameToColorHexHelper("Lorelai Taylor", "#ddcb55")
+ usernameToColorHexHelper("Madina Knight", "#9750a4")
+ usernameToColorHexHelper("Meeting", "#c98879")
+ usernameToColorHexHelper("Private Circle", "#c37285")
+ usernameToColorHexHelper("Rae Hope", "#795aab")
+ usernameToColorHexHelper("Santiago Singleton", "#bc5c91")
+ usernameToColorHexHelper("Sid Combs", "#d09e6d")
+ usernameToColorHexHelper("TestCircle", "#499aa2")
+ usernameToColorHexHelper("Tom Mörtel", "#248eb5")
+ usernameToColorHexHelper("Vivienne Jacobs", "#1e78c1")
+ usernameToColorHexHelper("Zaki Cortes", "#6ea68f")
+ usernameToColorHexHelper("a user", "#5b64b3")
+ usernameToColorHexHelper("admin@cloud.example.com", "#9750a4")
+ usernameToColorHexHelper("another user", "#ddcb55")
+ usernameToColorHexHelper("asd", "#248eb5")
+ usernameToColorHexHelper("bar", "#0082c9")
+ usernameToColorHexHelper("foo", "#d09e6d")
+ usernameToColorHexHelper("wasd", "#b6469d")
+ usernameToColorHexHelper("مرحبا بالعالم", "#c98879")
+ usernameToColorHexHelper("🙈", "#b6469d")
+ }
+
+ private fun usernameToColorHexHelper(username: String, expectedHexColor: String) {
+ val userColorInt = ColorGenerator.usernameToColor(username) // returns Int
+ val userHexColor = intToHex(userColorInt)
+
+ Assert.assertEquals(expectedHexColor.lowercase(), userHexColor.lowercase())
+ }
+
+ private fun intToHex(colorInt: Int): String {
+ val r = Color.red(colorInt)
+ val g = Color.green(colorInt)
+ val b = Color.blue(colorInt)
+ return String.format("#%02x%02x%02x", r, g, b)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b823553..384b7c1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -258,6 +258,10 @@
android:name=".lock.LockedActivity"
android:theme="@style/AppTheme" />
+
+
diff --git a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
index abbade3..d3a12a0 100644
--- a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
@@ -8,6 +8,7 @@
*/
package com.nextcloud.talk.account
+import android.Manifest
import android.accounts.Account
import android.annotation.SuppressLint
import android.content.Intent
@@ -21,8 +22,13 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
+import androidx.core.os.bundleOf
import autodagger.AutoInjector
+import com.blikoon.qrcodescanner.QrCodeActivity
+import com.github.dhaval2404.imagepicker.util.PermissionUtil
+import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
@@ -47,6 +53,7 @@ import io.reactivex.schedulers.Schedulers
import java.security.cert.CertificateException
import javax.inject.Inject
+@Suppress("TooManyFunctions")
@AutoInjector(NextcloudTalkApplication::class)
class ServerSelectionActivity : BaseActivity() {
@@ -120,6 +127,8 @@ class ServerSelectionActivity : BaseActivity() {
}
binding.certTextView.setOnClickListener { onCertClick() }
+ binding.scanQr.setOnClickListener { onScan() }
+
if (ApplicationWideMessageHolder.getInstance().messageType != null) {
if (ApplicationWideMessageHolder.getInstance().messageType
== ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
@@ -390,6 +399,52 @@ class ServerSelectionActivity : BaseActivity() {
}
}
+ private val requestCameraPermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
+ if (isGranted) {
+ // Permission was granted
+ startQRScanner()
+ }
+ }
+
+ fun onScan() {
+ if (PermissionUtil.isPermissionGranted(this, Manifest.permission.CAMERA)) {
+ startQRScanner()
+ } else {
+ requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ }
+
+ private fun startQRScanner() {
+ val intent = Intent(this, QrCodeActivity::class.java)
+ qrScanResultLauncher.launch(intent)
+ }
+
+ private val qrScanResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ val data = result.data
+
+ if (data == null) {
+ return@registerForActivityResult
+ }
+
+ val resultData = data.getStringExtra(QR_URI)
+
+ if (resultData == null || !resultData.startsWith("nc")) {
+ Snackbar.make(binding.root, getString(R.string.qr_code_error), Snackbar.LENGTH_SHORT).show()
+ return@registerForActivityResult
+ }
+
+ val intent = Intent(this, WebViewLoginActivity::class.java)
+ val bundle = bundleOf().apply {
+ putString(BundleKeys.KEY_FROM_QR, resultData)
+ }
+ intent.putExtras(bundle)
+ startActivity(intent)
+ }
+ }
+
public override fun onDestroy() {
super.onDestroy()
dispose()
@@ -408,5 +463,6 @@ class ServerSelectionActivity : BaseActivity() {
companion object {
private val TAG = ServerSelectionActivity::class.java.simpleName
const val MIN_SERVER_MAJOR_VERSION = 13
+ private const val QR_URI = "com.blikoon.qrcodescanner.got_qr_scan_relult"
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt
index 38a6c2a..e2fcb5a 100644
--- a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt
@@ -1,9 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
- * SPDX-FileCopyrightText: 2023 Marcel Hibbe
- * SPDX-FileCopyrightText: 2022 Andy Scherzinger
- * SPDX-FileCopyrightText: 2017 Mario Danic
+ * SPDX-FileCopyrightText: 2025 Julius Linus
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.account
@@ -63,6 +61,7 @@ import java.security.cert.X509Certificate
import java.util.Locale
import javax.inject.Inject
+@Suppress("ReturnCount", "LongMethod")
@AutoInjector(NextcloudTalkApplication::class)
class WebViewLoginActivity : BaseActivity() {
@@ -115,10 +114,9 @@ class WebViewLoginActivity : BaseActivity() {
setContentView(binding.root)
actionBar?.hide()
initSystemBars()
-
+ assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
handleIntent()
- setupWebView()
}
private fun handleIntent() {
@@ -133,11 +131,18 @@ class WebViewLoginActivity : BaseActivity() {
if (extras.containsKey(BundleKeys.KEY_PASSWORD)) {
password = extras.getString(BundleKeys.KEY_PASSWORD)
}
+
+ if (extras.containsKey(BundleKeys.KEY_FROM_QR)) {
+ extras.getString(BundleKeys.KEY_FROM_QR)?.let {
+ parseAndLoginFromWebView(it)
+ }
+ } else {
+ setupWebView()
+ }
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
- assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
binding.webview.settings.allowFileAccess = false
binding.webview.settings.allowFileAccessFromFileURLs = false
binding.webview.settings.javaScriptEnabled = true
@@ -289,22 +294,18 @@ class WebViewLoginActivity : BaseActivity() {
}
@SuppressLint("DiscouragedPrivateApi")
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "WebViewClientOnReceivedSslError")
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
try {
val sslCertificate = error.certificate
val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate")
f.isAccessible = true
val cert = f[sslCertificate] as X509Certificate
- if (cert == null) {
- handler.cancel()
- } else {
- try {
- trustManager.checkServerTrusted(arrayOf(cert), "generic")
- handler.proceed()
- } catch (exception: CertificateException) {
- eventBus.post(CertificateEvent(cert, trustManager, handler))
- }
+ try {
+ trustManager.checkServerTrusted(arrayOf(cert), "generic")
+ handler.proceed()
+ } catch (exception: CertificateException) {
+ eventBus.post(CertificateEvent(cert, trustManager, handler))
}
} catch (exception: Exception) {
handler.cancel()
@@ -332,12 +333,16 @@ class WebViewLoginActivity : BaseActivity() {
dispose()
cookieManager.cookieStore.removeAll()
- if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) {
+ if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, loginData.serverUrl!!)
+ .blockingGet()
+ ) {
Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.")
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
startAccountRemovalWorkerAndRestartApp()
- } else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) {
+ } else if (userManager.checkIfUserExists(loginData.username!!, loginData.serverUrl!!)
+ .blockingGet()
+ ) {
if (reauthorizeAccount) {
updateUserAndRestartApp(loginData)
} else {
@@ -347,6 +352,9 @@ class WebViewLoginActivity : BaseActivity() {
} else {
startAccountVerification(loginData)
}
+ } else {
+ Log.e(TAG, "Login Data was null")
+ restartApp()
}
}
@@ -356,9 +364,9 @@ class WebViewLoginActivity : BaseActivity() {
bundle.putString(KEY_TOKEN, loginData.token)
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
var protocol = ""
- if (baseUrl!!.startsWith("http://")) {
+ if (loginData.serverUrl!!.startsWith("http://")) {
protocol = "http://"
- } else if (baseUrl!!.startsWith("https://")) {
+ } else if (loginData.serverUrl!!.startsWith("https://")) {
protocol = "https://"
}
if (!TextUtils.isEmpty(protocol)) {
@@ -416,17 +424,17 @@ class WebViewLoginActivity : BaseActivity() {
return null
}
for (value in values) {
- if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
+ if (value.startsWith("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.username = URLDecoder.decode(
- value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
+ value.substring(("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length)
)
- } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
+ } else if (value.startsWith("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.token = URLDecoder.decode(
- value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
+ value.substring(("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length)
)
- } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
+ } else if (value.startsWith("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.serverUrl = URLDecoder.decode(
- value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
+ value.substring(("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length)
)
} else {
return null
diff --git a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt
new file mode 100644
index 0000000..d2ddf88
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt
@@ -0,0 +1,161 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.account.data
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import com.nextcloud.talk.account.data.io.LocalLoginDataSource
+import com.nextcloud.talk.account.data.model.LoginCompletion
+import com.nextcloud.talk.account.data.model.LoginResponse
+import com.nextcloud.talk.account.data.network.NetworkLoginDataSource
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import java.net.URLDecoder
+
+@Suppress("TooManyFunctions", "ReturnCount")
+class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLoginDataSource) {
+
+ companion object {
+ val TAG: String = LoginRepository::class.java.simpleName
+ private const val INTERVAL = 250L
+ private const val HTTP_OK = 200
+ private const val USER_KEY = "user:"
+ private const val SERVER_KEY = "server:"
+ private const val PASS_KEY = "password:"
+ private const val PREFIX = "nc://login/"
+ private const val MAX_ARGS = 3
+ }
+
+ private var shouldReauthorizeUser = false
+ private var shouldLoop = true
+
+ suspend fun pollLogin(response: LoginResponse): LoginCompletion? =
+ withContext(Dispatchers.IO) {
+ while (shouldLoop) {
+ val loginData = network.performLoginFlowV2(response)
+ if (loginData == null) {
+ break
+ }
+
+ if (loginData.status == HTTP_OK) {
+ return@withContext loginData
+ }
+
+ delay(INTERVAL) // No response yet, retry
+ }
+ return@withContext null
+ }
+
+ /**
+ * Entry point for QR scanner
+ *
+ */
+ fun startLoginFlowFromQR(dataString: String, reAuth: Boolean = false): LoginCompletion? {
+ shouldReauthorizeUser = reAuth
+
+ if (!dataString.startsWith(PREFIX)) {
+ Log.e(TAG, "Invalid login URL detected")
+ return null
+ }
+
+ val data = dataString.removePrefix(PREFIX)
+ val values = data.split('&')
+
+ if (values.size !in 1..MAX_ARGS) {
+ Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}")
+ return null
+ }
+
+ var server = ""
+ var loginName = ""
+ var appPassword = ""
+ values.forEach { value ->
+ when {
+ value.startsWith(USER_KEY) -> {
+ loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8")
+ }
+
+ value.startsWith(PASS_KEY) -> {
+ appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8")
+ }
+
+ value.startsWith(SERVER_KEY) -> {
+ server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8")
+ }
+ }
+ }
+
+ return if (server.isNotEmpty() && loginName.isNotEmpty() && appPassword.isNotEmpty()) {
+ LoginCompletion(HTTP_OK, server, loginName, appPassword)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Entry point to the login process
+ */
+ suspend fun startLoginFlow(baseUrl: String, reAuth: Boolean = false): LoginResponse? =
+ withContext(Dispatchers.IO) {
+ shouldReauthorizeUser = reAuth
+ val response = network.anonymouslyPostLoginRequest(baseUrl)
+ return@withContext response
+ }
+
+ /**
+ * Ends normal login process by canceling the polling
+ */
+ fun cancelLoginFlow() {
+ shouldLoop = false
+ }
+
+ /**
+ * Returns bundle if user is not scheduled for deletion or doesn't already exist, null otherwise
+ */
+ fun parseAndLogin(loginData: LoginCompletion): Bundle? {
+ if (local.checkIfUserIsScheduledForDeletion(loginData)) {
+ // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
+ local.startAccountRemovalWorker()
+ return null
+ } else if (local.checkIfUserExists(loginData)) {
+ if (shouldReauthorizeUser) {
+ local.updateUser(loginData)
+ } else {
+ Log.w(TAG, "Tried to add an account that account already exists. Skipped user creation.")
+ }
+
+ return null
+ } else {
+ return startAccountVerification(loginData)
+ }
+ }
+
+ private fun startAccountVerification(loginData: LoginCompletion): Bundle {
+ val bundle = Bundle()
+ bundle.putString(KEY_USERNAME, loginData.loginName)
+ bundle.putString(KEY_TOKEN, loginData.appPassword)
+ bundle.putString(KEY_BASE_URL, loginData.server)
+ var protocol = ""
+ if (loginData.server.startsWith("http://")) {
+ protocol = "http://"
+ } else if (loginData.server.startsWith("https://")) {
+ protocol = "https://"
+ }
+ if (!TextUtils.isEmpty(protocol)) {
+ bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
+ }
+
+ return bundle
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt
new file mode 100644
index 0000000..c7672e7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt
@@ -0,0 +1,45 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.account.data.io
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import com.nextcloud.talk.account.data.model.LoginCompletion
+import com.nextcloud.talk.jobs.AccountRemovalWorker
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.preferences.AppPreferences
+
+// local datasource for communicating with room through account manager
+// crucial for making sure the login process interacts with the db as expected.
+class LocalLoginDataSource(val userManager: UserManager, val appPreferences: AppPreferences, val context: Context) {
+
+ fun updateUser(loginData: LoginCompletion) {
+ val currentUser = userManager.currentUser.blockingGet()
+ if (currentUser != null) {
+ currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
+ currentUser.token = loginData.appPassword
+ userManager.updateOrCreateUser(currentUser)
+ }
+ }
+
+ fun startAccountRemovalWorker(): LiveData {
+ val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
+ WorkManager.getInstance(context).enqueue(accountRemovalWork)
+
+ return WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
+ }
+
+ fun checkIfUserIsScheduledForDeletion(data: LoginCompletion): Boolean =
+ userManager.checkIfUserIsScheduledForDeletion(data.loginName, data.server).blockingGet()
+
+ fun checkIfUserExists(data: LoginCompletion): Boolean =
+ userManager.checkIfUserExists(data.loginName, data.server).blockingGet()
+}
diff --git a/app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt b/app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt
new file mode 100644
index 0000000..346a5ff
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt
@@ -0,0 +1,12 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.account.data.model
+
+data class LoginResponse(val token: String, val pollUrl: String, val loginUrl: String)
+
+data class LoginCompletion(val status: Int, val server: String, val loginName: String, val appPassword: String)
diff --git a/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt
new file mode 100644
index 0000000..aadd601
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt
@@ -0,0 +1,122 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.account.data.network
+
+import android.util.Log
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import com.nextcloud.talk.account.data.model.LoginCompletion
+import com.nextcloud.talk.account.data.model.LoginResponse
+import okhttp3.FormBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import java.io.IOException
+import javax.net.ssl.SSLHandshakeException
+
+// This class handles the network and polling logic in isolation, which makes it easier to test
+// Login and Authentication is critical, thus it needs to be working properly.
+class NetworkLoginDataSource(val okHttpClient: OkHttpClient) {
+
+ companion object {
+ val TAG: String = NetworkLoginDataSource::class.java.simpleName
+ }
+
+ fun anonymouslyPostLoginRequest(baseUrl: String): LoginResponse? {
+ val url = "$baseUrl/index.php/login/v2"
+ var result: LoginResponse? = null
+ runCatching {
+ val response = getResponseOfAnonymouslyPostLoginRequest(url)
+ val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject
+ val loginUrl: String = getLoginUrl(jsonObject)
+ val token = jsonObject.getAsJsonObject("poll").get("token").asString
+ val pollUrl = jsonObject.getAsJsonObject("poll").get("endpoint").asString
+ result = LoginResponse(token, pollUrl, loginUrl)
+ }.getOrElse { e ->
+ when (e) {
+ is SSLHandshakeException,
+ is NullPointerException,
+ is IOException -> {
+ Log.e(TAG, "Error caught at anonymouslyPostLoginRequest: $e")
+ }
+
+ else -> throw e
+ }
+ }
+
+ return result
+ }
+
+ private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? {
+ val request = Request.Builder()
+ .url(url)
+ .post(FormBody.Builder().build())
+ .addHeader("Clear-Site-Data", "cookies")
+ .build()
+
+ okHttpClient.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) {
+ throw IOException("Unexpected code $response")
+ }
+ return response.body?.string()
+ }
+ }
+
+ private fun getLoginUrl(response: JsonObject): String {
+ var result: String? = response.get("login").asString
+ if (result == null) {
+ result = ""
+ }
+
+ return result
+ }
+
+ fun performLoginFlowV2(response: LoginResponse): LoginCompletion? {
+ val requestBody: RequestBody = FormBody.Builder()
+ .add("token", response.token)
+ .build()
+
+ val request = Request.Builder()
+ .url(response.pollUrl)
+ .post(requestBody)
+ .build()
+
+ var result: LoginCompletion? = null
+ runCatching {
+ okHttpClient.newCall(request).execute()
+ .use { response ->
+ val status: Int = response.code
+ val responseBody = response.body?.string()
+
+ result = if (response.isSuccessful && responseBody?.isNotEmpty() == true) {
+ val jsonObject = JsonParser.parseString(responseBody).asJsonObject
+ val server: String = jsonObject.get("server").asString
+ val loginName: String = jsonObject.get("loginName").asString
+ val appPassword: String = jsonObject.get("appPassword").asString
+
+ LoginCompletion(status, server, loginName, appPassword)
+ } else {
+ LoginCompletion(status, "", "", "")
+ }
+ }
+ }.getOrElse { e ->
+ when (e) {
+ is NullPointerException,
+ is SSLHandshakeException,
+ is IllegalStateException,
+ is IOException -> {
+ Log.e(TAG, "Error caught at performLoginFlowV2: $e")
+ }
+
+ else -> throw e
+ }
+ }
+
+ return result
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt
new file mode 100644
index 0000000..e9560c5
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt
@@ -0,0 +1,92 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.account.viewmodels
+
+import android.os.Bundle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.talk.account.data.LoginRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRepository) : ViewModel() {
+
+ companion object {
+ private val TAG = BrowserLoginActivityViewModel::class.java.simpleName
+ }
+
+ sealed class InitialLoginViewState {
+ data object None : InitialLoginViewState()
+ data class InitialLoginRequestSuccess(val loginUrl: String) : InitialLoginViewState()
+ data object InitialLoginRequestError : InitialLoginViewState()
+ }
+
+ private val _initialLoginRequestState = MutableStateFlow(InitialLoginViewState.None)
+ val initialLoginRequestState: StateFlow = _initialLoginRequestState
+
+ sealed class PostLoginViewState {
+ data object None : PostLoginViewState()
+ data object PostLoginRestartApp : PostLoginViewState()
+ data object PostLoginError : PostLoginViewState()
+ data class PostLoginContinue(val data: Bundle) : PostLoginViewState()
+ }
+
+ private val _postLoginState = MutableStateFlow(PostLoginViewState.None)
+ val postLoginState: StateFlow = _postLoginState
+
+ fun loginNormally(baseUrl: String, reAuth: Boolean = false) {
+ viewModelScope.launch {
+ val response = repository.startLoginFlow(baseUrl, reAuth)
+
+ if (response == null) {
+ _initialLoginRequestState.value = InitialLoginViewState.InitialLoginRequestError
+ return@launch
+ }
+
+ _initialLoginRequestState.value =
+ InitialLoginViewState.InitialLoginRequestSuccess(response.loginUrl)
+
+ val loginCompletionResponse = repository.pollLogin(response)
+
+ if (loginCompletionResponse == null) {
+ _postLoginState.value = PostLoginViewState.PostLoginError
+ return@launch
+ }
+
+ val bundle = repository.parseAndLogin(loginCompletionResponse)
+ if (bundle == null) {
+ _postLoginState.value = PostLoginViewState.PostLoginRestartApp
+ return@launch
+ }
+
+ _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
+ }
+ }
+
+ fun loginWithQR(dataString: String, reAuth: Boolean = false) {
+ viewModelScope.launch {
+ val loginCompletionResponse = repository.startLoginFlowFromQR(dataString, reAuth)
+ if (loginCompletionResponse == null) {
+ _postLoginState.value = PostLoginViewState.PostLoginError
+ return@launch
+ }
+
+ val bundle = repository.parseAndLogin(loginCompletionResponse)
+ if (bundle == null) {
+ _postLoginState.value = PostLoginViewState.PostLoginRestartApp
+ return@launch
+ }
+
+ _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
+ }
+ }
+
+ fun cancelLogin() = repository.cancelLoginFlow()
+}
diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
index c85ad6f..0b203e3 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
@@ -17,7 +17,6 @@ import android.text.TextUtils
import android.util.Log
import android.view.View
import android.view.ViewGroup
-import android.view.WindowInsets
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.webkit.SslErrorHandler
@@ -25,6 +24,8 @@ import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.R
@@ -119,18 +120,23 @@ open class BaseActivity : AppCompatActivity() {
* May be aligned with android-common lib in the future: .../ui/util/extensions/AppCompatActivityExtensions.kt
*/
fun initSystemBars() {
- window.decorView.setOnApplyWindowInsetsListener { view, insets ->
+ val decorView = window.decorView
+ decorView.setOnApplyWindowInsetsListener { view, insets ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- val statusBarHeight = insets.getInsets(WindowInsets.Type.statusBars()).top
- view.setPadding(0, statusBarHeight, 0, 0)
+ val systemBars = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars() or
+ WindowInsetsCompat.Type.displayCutout()
+ )
val color = ResourcesCompat.getColor(resources, R.color.bg_default, context.theme)
view.setBackgroundColor(color)
+ view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
} else {
colorizeStatusBar()
colorizeNavigationBar()
}
insets
}
+ ViewCompat.requestApplyInsets(decorView)
}
open fun colorizeStatusBar() {
diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
index c5af596..4d5d3f7 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
@@ -10,7 +10,6 @@
package com.nextcloud.talk.activities
import android.app.KeyguardManager
-import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.ContactsContract
@@ -93,7 +92,7 @@ class MainActivity :
}
fun lockScreenIfConditionsApply() {
- val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+ val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) {
val lockIntent = Intent(context, LockedActivity::class.java)
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt
index a51124b..745070b 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt
@@ -22,6 +22,7 @@ import android.view.View
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
+import coil.dispose
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
@@ -182,7 +183,9 @@ class ConversationItem(
}
private fun showAvatar(holder: ConversationItemViewHolder) {
+ holder.binding.dialogAvatar.dispose()
holder.binding.dialogAvatar.visibility = View.VISIBLE
+
var shouldLoadAvatar = shouldLoadAvatar(holder)
if (ConversationEnums.ConversationType.ROOM_SYSTEM == model.type) {
holder.binding.dialogAvatar.loadSystemAvatar()
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt
index 0fd5b3c..5de0e72 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt
@@ -221,6 +221,10 @@ class MentionAutocompleteItem(
if (statusMessage.isNullOrEmpty()) {
holder.binding.conversationInfoStatusMessage.setText(R.string.dnd)
}
+ } else if (status != null && status == StatusType.BUSY.string) {
+ if (statusMessage.isNullOrEmpty()) {
+ holder.binding.conversationInfoStatusMessage.setText(R.string.busy)
+ }
} else if (status != null && status == StatusType.AWAY.string) {
if (statusMessage.isNullOrEmpty()) {
holder.binding.conversationInfoStatusMessage.setText(R.string.away)
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt
index c71a596..251ec8d 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt
@@ -276,6 +276,10 @@ class ParticipantItem(
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
holder.binding.conversationInfoStatusMessage.setText(R.string.dnd)
}
+ } else if (model.status != null && model.status == StatusType.BUSY.string) {
+ if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
+ holder.binding.conversationInfoStatusMessage.setText(R.string.busy)
+ }
} else if (model.status != null && model.status == StatusType.AWAY.string) {
if (model.statusMessage == null || model.statusMessage!!.isEmpty()) {
holder.binding.conversationInfoStatusMessage.setText(R.string.away)
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt
index 12bbd1e..8ce5187 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt
@@ -22,21 +22,21 @@ interface AdjustableMessageHolderInterface {
val binding: ViewBinding
- fun adjustIfNoteToSelf(viewHolder: AdjustableMessageHolderInterface, currentConversation: ConversationModel?) {
+ fun adjustIfNoteToSelf(currentConversation: ConversationModel?) {
if (currentConversation?.type == ConversationType.NOTE_TO_SELF) {
- when (viewHolder.binding.javaClass) {
+ when (this.binding.javaClass) {
ItemCustomOutcomingTextMessageBinding::class.java ->
- (viewHolder.binding as ItemCustomOutcomingTextMessageBinding).bubble
+ (this.binding as ItemCustomOutcomingTextMessageBinding).bubble
ItemCustomOutcomingDeckCardMessageBinding::class.java ->
- (viewHolder.binding as ItemCustomOutcomingDeckCardMessageBinding).bubble
+ (this.binding as ItemCustomOutcomingDeckCardMessageBinding).bubble
ItemCustomOutcomingLinkPreviewMessageBinding::class.java ->
- (viewHolder.binding as ItemCustomOutcomingLinkPreviewMessageBinding).bubble
+ (this.binding as ItemCustomOutcomingLinkPreviewMessageBinding).bubble
ItemCustomOutcomingPollMessageBinding::class.java ->
- (viewHolder.binding as ItemCustomOutcomingPollMessageBinding).bubble
+ (this.binding as ItemCustomOutcomingPollMessageBinding).bubble
ItemCustomOutcomingVoiceMessageBinding::class.java ->
- (viewHolder.binding as ItemCustomOutcomingVoiceMessageBinding).bubble
+ (this.binding as ItemCustomOutcomingVoiceMessageBinding).bubble
ItemCustomOutcomingLocationMessageBinding::class.java ->
- (viewHolder.binding as ItemCustomOutcomingLocationMessageBinding).bubble
+ (this.binding as ItemCustomOutcomingLocationMessageBinding).bubble
else -> null
}?.let {
RelativeLayout.LayoutParams(binding.root.layoutParams).apply {
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt
index 262ca69..c419f7b 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt
@@ -12,4 +12,5 @@ interface CommonMessageInterface {
fun onLongClickReactions(chatMessage: ChatMessage)
fun onClickReaction(chatMessage: ChatMessage, emoji: String)
fun onOpenMessageActionsDialog(chatMessage: ChatMessage)
+ fun openThread(chatMessage: ChatMessage)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt
index 503a7b5..5bef49b 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt
@@ -182,7 +182,7 @@ class IncomingDeckCardViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -229,10 +229,18 @@ class IncomingDeckCardViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView
+ binding.messageQuote.quotedChatMessageView
)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
index b199317..250b34e 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
@@ -110,6 +110,19 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
+ val chatActivity = commonMessageInterface as ChatActivity
+ val showThreadButton = chatActivity.conversationThreadId == null && message.isThread
+ if (showThreadButton) {
+ binding.reactions.threadButton.visibility = View.VISIBLE
+ binding.reactions.threadButton.setContent {
+ ThreadButtonComposable(
+ onButtonClick = { openThread(message) }
+ )
+ }
+ } else {
+ binding.reactions.threadButton.visibility = View.GONE
+ }
+
Reaction().showReactions(
message,
::clickOnReaction,
@@ -129,6 +142,10 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
val actorName = message.actorDisplayName
if (!actorName.isNullOrBlank()) {
@@ -157,7 +174,7 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -204,10 +221,18 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView
+ binding.messageQuote.quotedChatMessageView
)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
index 3983098..f03c095 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
@@ -142,7 +142,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -189,10 +189,18 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView
+ binding.messageQuote.quotedChatMessageView
)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
index 60d0785..83a06e3 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
@@ -81,6 +81,15 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
setPollPreview(message)
+ val chatActivity = commonMessageInterface as ChatActivity
+ Thread().showThreadPreview(
+ chatActivity,
+ message,
+ threadBinding = binding.threadTitleWrapper,
+ reactionsBinding = binding.reactions,
+ openThread = { openThread(message) }
+ )
+
Reaction().showReactions(
message,
::clickOnReaction,
@@ -100,6 +109,10 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
private fun setPollPreview(message: ChatMessage) {
var pollId: String? = null
var pollName: String? = null
@@ -164,7 +177,7 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -211,9 +224,17 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView
+ binding.messageQuote.quotedChatMessageView
)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
index 572bd9e..7c13d01 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
@@ -17,6 +17,7 @@ import android.widget.ProgressBar;
import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
+import com.nextcloud.talk.databinding.ItemThreadTitleBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.chat.data.model.ChatMessage;
import com.nextcloud.talk.utils.TextMatchers;
@@ -138,4 +139,7 @@ public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
@Override
public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; }
+
+ @Override
+ public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; }
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt
index 4d1b7fc..ae3f691 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt
@@ -146,6 +146,9 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
}
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageText.text = processedMessageText
+ // just for debugging:
+ // binding.messageText.text =
+ // SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")")
} else {
binding.messageText.visibility = View.GONE
binding.checkboxContainer.visibility = View.VISIBLE
@@ -159,16 +162,35 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
+
// parent message handling
- if (!message.isDeleted && message.parentMessageId != null) {
- processParentMessage(message)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
- } else {
- binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+ val chatActivity = commonMessageInterface as ChatActivity
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ processParentMessage(message)
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? ->
+ commonMessageInterface.onOpenMessageActionsDialog(message)
+ true
}
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
+ Thread().showThreadPreview(
+ chatActivity,
+ message,
+ threadBinding = binding.threadTitleWrapper,
+ reactionsBinding = binding.reactions,
+ openThread = { openThread(message) }
+ )
+
Reaction().showReactions(
message,
::clickOnReaction,
@@ -291,6 +313,10 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
val actorName = message.actorDisplayName
if (!actorName.isNullOrBlank()) {
@@ -369,7 +395,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView,
+ binding.messageQuote.quotedChatMessageView,
R.color.high_emphasis_text
)
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
index 89a0a2e..97d01f6 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
@@ -304,7 +304,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -351,10 +351,20 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView
+ binding.messageQuote.quotedChatMessageView
)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
index bd6384f..acfad29 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
@@ -10,8 +10,10 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
+import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import coil.load
+import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding
@@ -74,12 +76,18 @@ class LinkPreview {
}
val referenceThumbUrl = reference.openGraphObject?.thumb
+ var backgroundId = R.drawable.link_text_background
if (!referenceThumbUrl.isNullOrEmpty()) {
binding.referenceThumbImage.visibility = View.VISIBLE
binding.referenceThumbImage.load(referenceThumbUrl)
} else {
+ backgroundId = R.drawable.link_text_no_preview_background
binding.referenceThumbImage.visibility = View.GONE
}
+ binding.referenceMetadataContainer.background = ContextCompat.getDrawable(
+ binding.referenceMetadataContainer.context,
+ backgroundId
+ )
binding.referenceWrapper.setOnClickListener {
val browserIntent = Intent(Intent.ACTION_VIEW, referenceLink!!.toUri())
@@ -95,7 +103,6 @@ class LinkPreview {
binding.referenceDescription.visibility = View.GONE
binding.referenceLink.visibility = View.GONE
binding.referenceThumbImage.visibility = View.GONE
- binding.referenceIndentedSideBar.visibility = View.GONE
}
override fun onComplete() {
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt
index 2aec3b1..e4433f3 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt
@@ -170,7 +170,7 @@ class OutcomingDeckCardViewHolder(outcomingView: View) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -217,10 +217,18 @@ class OutcomingDeckCardViewHolder(outcomingView: View) :
viewThemeUtils.talk.themeParentMessage(
parentChatMessage,
message,
- binding.messageQuote.quoteColoredView
+ binding.messageQuote.quotedChatMessageView
)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
index 8d553da..fcf78cf 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
@@ -127,6 +127,19 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
+ val chatActivity = commonMessageInterface as ChatActivity
+ val showThreadButton = chatActivity.conversationThreadId == null && message.isThread
+ if (showThreadButton) {
+ binding.reactions.threadButton.visibility = View.VISIBLE
+ binding.reactions.threadButton.setContent {
+ ThreadButtonComposable(
+ onButtonClick = { openThread(message) }
+ )
+ }
+ } else {
+ binding.reactions.threadButton.visibility = View.GONE
+ }
+
Reaction().showReactions(
message,
::clickOnReaction,
@@ -146,7 +159,11 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -188,9 +205,21 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
- viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
+ viewThemeUtils.talk.themeParentMessage(
+ parentChatMessage,
+ message,
+ binding.messageQuote.quotedChatMessageView
+ )
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
index d0d38c8..6c91605 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
@@ -196,7 +196,7 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
})
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -238,9 +238,21 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
- viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
+ viewThemeUtils.talk.themeParentMessage(
+ parentChatMessage,
+ message,
+ binding.messageQuote.quotedChatMessageView
+ )
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
index 450f36a..ac952cd 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
@@ -39,9 +39,10 @@ import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
- MessageHolders.OutcomingTextMessageViewHolder(outcomingView, payload) {
+ MessageHolders.OutcomingTextMessageViewHolder(outcomingView, payload),
+ AdjustableMessageHolderInterface {
- private val binding: ItemCustomOutcomingPollMessageBinding = ItemCustomOutcomingPollMessageBinding.bind(itemView)
+ override val binding: ItemCustomOutcomingPollMessageBinding = ItemCustomOutcomingPollMessageBinding.bind(itemView)
@Inject
lateinit var context: Context
@@ -103,6 +104,15 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
setPollPreview(message)
+ val chatActivity = commonMessageInterface as ChatActivity
+ Thread().showThreadPreview(
+ chatActivity,
+ message,
+ threadBinding = binding.threadTitleWrapper,
+ reactionsBinding = binding.reactions,
+ openThread = { openThread(message) }
+ )
+
Reaction().showReactions(
message,
::clickOnReaction,
@@ -122,6 +132,10 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
private fun setPollPreview(message: ChatMessage) {
var pollId: String? = null
var pollName: String? = null
@@ -158,7 +172,7 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
}
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -200,9 +214,21 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
- viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
+ viewThemeUtils.talk.themeParentMessage(
+ parentChatMessage,
+ message,
+ binding.messageQuote.quotedChatMessageView
+ )
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
index eb7d544..17b1c55 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
@@ -16,6 +16,7 @@ import android.widget.ProgressBar;
import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
+import com.nextcloud.talk.databinding.ItemThreadTitleBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.chat.data.model.ChatMessage;
import com.nextcloud.talk.utils.TextMatchers;
@@ -133,6 +134,9 @@ public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder
@Override
public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; }
+ @Override
+ public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; }
+
@NonNull
@Override
public EmojiTextView getMessageCaption() { return binding.messageCaption; }
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt
index 669ecfc..755d32a 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt
@@ -160,6 +160,9 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.messageTime.layoutParams = layoutParams
viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT)
binding.messageText.text = processedMessageText
+ // just for debugging:
+ // binding.messageText.text =
+ // SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")")
} else {
binding.messageText.visibility = View.GONE
binding.checkboxContainer.visibility = View.VISIBLE
@@ -174,12 +177,23 @@ class OutcomingTextMessageViewHolder(itemView: View) :
}
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
setBubbleOnChatMessage(message)
+
// parent message handling
- if (!message.isDeleted && message.parentMessageId != null) {
- processParentMessage(message)
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
- } else {
- binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+ val chatActivity = commonMessageInterface as ChatActivity
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ processParentMessage(message)
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? ->
+ commonMessageInterface.onOpenMessageActionsDialog(message)
+ true
}
binding.checkMark.visibility = View.INVISIBLE
@@ -195,8 +209,6 @@ class OutcomingTextMessageViewHolder(itemView: View) :
updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent))
}
- val chatActivity = commonMessageInterface as ChatActivity
-
chatActivity.lifecycleScope.launch {
if (message.isTemporary && !networkMonitor.isOnline.value) {
updateStatus(
@@ -208,6 +220,14 @@ class OutcomingTextMessageViewHolder(itemView: View) :
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
+ Thread().showThreadPreview(
+ chatActivity,
+ message,
+ threadBinding = binding.threadTitleWrapper,
+ reactionsBinding = binding.reactions,
+ openThread = { openThread(message) }
+ )
+
Reaction().showReactions(
message,
::clickOnReaction,
@@ -345,6 +365,10 @@ class OutcomingTextMessageViewHolder(itemView: View) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
@Suppress("Detekt.TooGenericExceptionCaught")
private fun processParentMessage(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
@@ -389,7 +413,11 @@ class OutcomingTextMessageViewHolder(itemView: View) :
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
- viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
+ viewThemeUtils.talk.themeParentMessage(
+ parentChatMessage,
+ message,
+ binding.messageQuote.quotedChatMessageView
+ )
binding.messageQuote.quotedChatMessageView.setOnClickListener {
chatActivity.jumpToQuotedMessage(parentChatMessage)
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
index 184c096..09ef5c0 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
@@ -307,7 +307,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
binding.progressBar.visibility = View.VISIBLE
}
- @Suppress("Detekt.TooGenericExceptionCaught")
+ @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod")
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
@@ -349,9 +349,21 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
- viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
+ viewThemeUtils.talk.themeParentMessage(
+ parentChatMessage,
+ message,
+ binding.messageQuote.quotedChatMessageView
+ )
- binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+ binding.messageQuote.quotedChatMessageView.visibility =
+ if (!message.isDeleted &&
+ message.parentMessageId != null &&
+ message.parentMessageId != chatActivity.conversationThreadId
+ ) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt
index 605ac20..9449526 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt
@@ -28,8 +28,10 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.ItemThreadTitleBinding
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
import com.nextcloud.talk.extensions.loadChangelogBotAvatar
import com.nextcloud.talk.extensions.loadFederatedUserAvatar
@@ -78,6 +80,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
var okHttpClient: OkHttpClient? = null
open var progressBar: ProgressBar? = null
open var reactionsBinding: ReactionsInsideMessageBinding? = null
+ open var threadsBinding: ItemThreadTitleBinding? = null
var fileViewerUtils: FileViewerUtils? = null
var clickView: View? = null
@@ -150,6 +153,16 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
messageText.text = ""
}
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
+
+ val chatActivity = commonMessageInterface as ChatActivity
+ Thread().showThreadPreview(
+ chatActivity,
+ message,
+ threadBinding = threadsBinding!!,
+ reactionsBinding = reactionsBinding!!,
+ openThread = { openThread(message) }
+ )
+
val paddingSide = DisplayUtils.convertDpToPixel(HORIZONTAL_REACTION_PADDING, context!!).toInt()
Reaction().showReactions(
message,
@@ -203,6 +216,10 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
+ private fun openThread(chatMessage: ChatMessage) {
+ commonMessageInterface.openThread(chatMessage)
+ }
+
override fun getPayloadForImageLoader(message: ChatMessage?): Any? {
if (message!!.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_NAME)) {
previewContainer.visibility = View.GONE
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt
index baf82d2..883642d 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt
@@ -14,6 +14,7 @@ import android.text.SpannableString
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
+import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
@@ -76,6 +77,10 @@ class SystemMessageViewHolder(itemView: View) :
R.drawable.shape_grouped_incoming_message
)
ViewCompat.setBackground(background, bubbleDrawable)
+ binding.messageText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ resources.getDimension(R.dimen.chat_system_message_text_size)
+ )
var messageString: Spannable = SpannableString(message.text)
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
for (key in message.messageParameters!!.keys) {
@@ -89,7 +94,13 @@ class SystemMessageViewHolder(itemView: View) :
} else {
individualMap["name"]
}
- messageString = DisplayUtils.searchAndColor(messageString, searchText!!, mentionColor)
+ messageString =
+ DisplayUtils.searchAndColor(
+ messageString,
+ searchText!!,
+ mentionColor,
+ resources.getDimensionPixelSize(R.dimen.chat_system_message_text_size)
+ )
if (individualMap["link"] != null) {
val displayName = individualMap["name"] ?: ""
val link = (user.baseUrl + individualMap["link"])
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java
index 3d76091..a51dde4 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java
@@ -39,19 +39,19 @@ public class TalkMessagesListAdapter extends MessagesListAda
holderInstance.assignCommonMessageInterface(chatActivity);
} else if (holder instanceof OutcomingTextMessageViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity);
- holderInstance.adjustIfNoteToSelf(holderInstance, chatActivity.getCurrentConversation());
+ holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
} else if (holder instanceof IncomingLocationMessageViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity);
} else if (holder instanceof OutcomingLocationMessageViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity);
- holderInstance.adjustIfNoteToSelf(holderInstance, chatActivity.getCurrentConversation());
+ holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
} else if (holder instanceof IncomingLinkPreviewMessageViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity);
} else if (holder instanceof OutcomingLinkPreviewMessageViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity);
- holderInstance.adjustIfNoteToSelf(holderInstance, chatActivity.getCurrentConversation());
+ holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
} else if (holder instanceof IncomingVoiceMessageViewHolder holderInstance) {
holderInstance.assignVoiceMessageInterface(chatActivity);
@@ -59,7 +59,7 @@ public class TalkMessagesListAdapter extends MessagesListAda
} else if (holder instanceof OutcomingVoiceMessageViewHolder holderInstance) {
holderInstance.assignVoiceMessageInterface(chatActivity);
holderInstance.assignCommonMessageInterface(chatActivity);
- holderInstance.adjustIfNoteToSelf(holderInstance, chatActivity.getCurrentConversation());
+ holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
} else if (holder instanceof PreviewMessageViewHolder holderInstance) {
holderInstance.assignPreviewMessageInterface(chatActivity);
@@ -72,7 +72,13 @@ public class TalkMessagesListAdapter extends MessagesListAda
holderInstance.assignCommonMessageInterface(chatActivity);
} else if (holder instanceof OutcomingDeckCardViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity);
- holderInstance.adjustIfNoteToSelf(holderInstance, chatActivity.getCurrentConversation());
+ holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
+
+ } else if (holder instanceof IncomingPollMessageViewHolder holderInstance) {
+ holderInstance.assignCommonMessageInterface(chatActivity);
+ } else if (holder instanceof OutcomingPollMessageViewHolder holderInstance) {
+ holderInstance.assignCommonMessageInterface(chatActivity);
+ holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation());
}
super.onBindViewHolder(holder, position);
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt
new file mode 100644
index 0000000..f2f6820
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt
@@ -0,0 +1,45 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2022 Marcel Hibbe
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.adapters.messages
+
+import android.view.View
+import com.nextcloud.talk.R
+import com.nextcloud.talk.chat.ChatActivity
+import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
+import com.nextcloud.talk.chat.data.model.ChatMessage
+import com.nextcloud.talk.databinding.ItemThreadTitleBinding
+
+class Thread {
+
+ fun showThreadPreview(
+ chatActivity: ChatActivity,
+ message: ChatMessage,
+ threadBinding: ItemThreadTitleBinding,
+ reactionsBinding: ReactionsInsideMessageBinding,
+ openThread: (message: ChatMessage) -> Unit
+ ) {
+ val isFirstMessageOfThreadInNormalChat = chatActivity.conversationThreadId == null && message.isThread
+ if (isFirstMessageOfThreadInNormalChat) {
+ threadBinding.threadTitleLayout.visibility = View.VISIBLE
+
+ threadBinding.threadTitleLayout.findViewById(R.id.threadTitle).text =
+ message.threadTitle
+
+ reactionsBinding.threadButton.visibility = View.VISIBLE
+
+ reactionsBinding.threadButton.setContent {
+ ThreadButtonComposable(
+ message.threadReplies ?: 0,
+ onButtonClick = { openThread(message) }
+ )
+ }
+ } else {
+ threadBinding.threadTitleLayout.visibility = View.GONE
+ reactionsBinding.threadButton.visibility = View.GONE
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt
new file mode 100644
index 0000000..22481bd
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt
@@ -0,0 +1,88 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Marcel Hibbe
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.nextcloud.talk.R
+
+@Composable
+fun ThreadButtonComposable(replyAmount: Int = 0, onButtonClick: () -> Unit = {}) {
+ val replyAmountText = if (replyAmount == 0) {
+ stringResource(R.string.thread_reply)
+ } else {
+ pluralStringResource(
+ R.plurals.thread_replies,
+ replyAmount,
+ replyAmount
+ )
+ }
+
+ OutlinedButton(
+ onClick = onButtonClick,
+ modifier = Modifier
+ .padding(8.dp)
+ .height(24.dp),
+ shape = RoundedCornerShape(9.dp),
+ border = BorderStroke(1.dp, colorResource(R.color.grey_600)),
+ contentPadding = PaddingValues(0.dp),
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = colorResource(R.color.grey_600)
+ )
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_reply),
+ contentDescription = stringResource(R.string.open_thread),
+ modifier = Modifier
+ .size(20.dp)
+ .padding(start = 5.dp, end = 2.dp),
+ tint = colorResource(R.color.grey_600)
+ )
+ Text(
+ text = replyAmountText,
+ modifier = Modifier
+ .padding(end = 5.dp)
+ )
+ }
+}
+
+@Preview
+@Composable
+fun ThreadButtonPreviewMultipleReplies() {
+ ThreadButtonComposable(2)
+}
+
+@Preview
+@Composable
+fun ThreadButtonPreviewOneReply() {
+ ThreadButtonComposable(1)
+}
+
+@Preview
+@Composable
+fun ThreadButtonPreviewZeroReplies() {
+ ThreadButtonComposable(0)
+}
diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
index a2e58dc..a0cf017 100644
--- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
+++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
@@ -19,6 +19,8 @@ import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.participants.TalkBanOverall
import com.nextcloud.talk.models.json.profile.ProfileOverall
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall
+import com.nextcloud.talk.models.json.threads.ThreadOverall
+import com.nextcloud.talk.models.json.threads.ThreadsOverall
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -146,7 +148,8 @@ interface NcApiCoroutines {
@Field("actorDisplayName") actorDisplayName: String,
@Field("replyTo") replyTo: Int,
@Field("silent") sendWithoutNotification: Boolean,
- @Field("referenceId") referenceId: String
+ @Field("referenceId") referenceId: String,
+ @Field("threadTitle") threadTitle: String?
): ChatOverallSingleMessage
@FormUrlEncoded
@@ -274,7 +277,8 @@ interface NcApiCoroutines {
suspend fun getContextOfChatMessage(
@Header("Authorization") authorization: String,
@Url url: String,
- @Query("limit") limit: Int
+ @Query("limit") limit: Int,
+ @Query("threadId") threadId: Int?
): ChatOverall
@GET
@@ -285,4 +289,22 @@ interface NcApiCoroutines {
@DELETE
suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
+
+ @GET
+ suspend fun getThreads(
+ @Header("Authorization") authorization: String,
+ @Url url: String,
+ @Query("limit") limit: Int?
+ ): ThreadsOverall
+
+ @GET
+ suspend fun getThread(@Header("Authorization") authorization: String, @Url url: String): ThreadOverall
+
+ @FormUrlEncoded
+ @POST
+ suspend fun setThreadNotificationLevel(
+ @Header("Authorization") authorization: String,
+ @Url url: String,
+ @Field("level") level: Int
+ ): ThreadOverall
}
diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt
index e8617bf..ddf9859 100644
--- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt
+++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt
@@ -50,7 +50,7 @@ fun ParticipantTile(
modifier: Modifier = Modifier,
isVoiceOnlyCall: Boolean
) {
- val colorInt = ColorGenerator.shared.usernameToColor(participantUiState.nick)
+ val colorInt = ColorGenerator.usernameToColor(participantUiState.nick)
BoxWithConstraints(
modifier = modifier
diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
index f018524..aecdd39 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
@@ -24,12 +24,14 @@ import android.content.pm.PackageManager
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.graphics.drawable.Drawable
+import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.ContactsContract
import android.provider.MediaStore
+import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.util.Log
@@ -38,6 +40,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.AbsListView
import android.widget.FrameLayout
@@ -56,7 +59,13 @@ import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
@@ -126,6 +135,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
+import com.nextcloud.talk.contextchat.ContextChatView
+import com.nextcloud.talk.contextchat.ContextChatViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
@@ -148,17 +159,18 @@ import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
+import com.nextcloud.talk.models.json.threads.ThreadInfo
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.signaling.SignalingMessageReceiver
import com.nextcloud.talk.signaling.SignalingMessageSender
+import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
import com.nextcloud.talk.translate.ui.TranslateActivity
import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.ui.PlaybackSpeedControl
import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
-import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.ui.dialog.DateTimeCompose
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@@ -193,10 +205,12 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPENED_VIA_NOTIFICATION
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
import com.nextcloud.talk.utils.rx.DisposableSet
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
@@ -275,10 +289,14 @@ class ChatActivity :
lateinit var chatViewModel: ChatViewModel
lateinit var conversationInfoViewModel: ConversationInfoViewModel
+ lateinit var contextChatViewModel: ContextChatViewModel
lateinit var messageInputViewModel: MessageInputViewModel
private var chatMenu: Menu? = null
+ private var overflowMenuHostView: ComposeView? = null
+ private var isThreadMenuExpanded by mutableStateOf(false)
+
private val startSelectContactForResult = registerForActivityResult(
ActivityResultContracts
.StartActivityForResult()
@@ -308,28 +326,27 @@ class ChatActivity :
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
executeIfResultOk(it) { intent ->
runBlocking {
- val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
- id?.let {
- startContextChatWindowForMessage(id)
+ val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
+ val threadId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_THREAD_ID)
+ messageId?.let {
+ startContextChatWindowForMessage(messageId, threadId)
}
}
}
}
- private fun startContextChatWindowForMessage(id: String?) {
+ private fun startContextChatWindowForMessage(messageId: String?, threadId: String?) {
binding.genericComposeView.apply {
- val shouldDismiss = mutableStateOf(false)
setContent {
- val bundle = bundleOf()
- bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
- bundle.putString(BundleKeys.KEY_BASE_URL, conversationUser!!.baseUrl)
- bundle.putString(KEY_ROOM_TOKEN, roomToken)
- bundle.putString(BundleKeys.KEY_MESSAGE_ID, id)
- bundle.putString(
- KEY_CONVERSATION_NAME,
- currentConversation!!.displayName
+ contextChatViewModel.getContextForChatMessages(
+ credentials = credentials!!,
+ baseUrl = conversationUser!!.baseUrl!!,
+ token = roomToken,
+ threadId = threadId,
+ messageId = messageId!!,
+ title = currentConversation!!.displayName
)
- ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
+ ContextChatView(context, contextChatViewModel)
}
}
Log.d(TAG, "Should open something else")
@@ -350,6 +367,9 @@ class ChatActivity :
var sessionIdAfterRoomJoined: String? = null
lateinit var roomToken: String
+ var conversationThreadId: Long? = null
+ var openedViaNotification: Boolean = false
+ var conversationThreadInfo: ThreadInfo? = null
var conversationUser: User? = null
lateinit var spreedCapabilities: SpreedCapability
var chatApiVersion: Int = 1
@@ -391,8 +411,13 @@ class ChatActivity :
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
- val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
- startActivity(intent)
+ if (!openedViaNotification && isChatThread()) {
+ isEnabled = false
+ onBackPressedDispatcher.onBackPressed()
+ } else {
+ val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
+ startActivity(intent)
+ }
}
}
@@ -461,18 +486,20 @@ class ChatActivity :
setContentView(binding.root)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.chat_container)) { view, insets ->
- val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
- val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
+ ViewCompat.setOnApplyWindowInsetsListener(binding.chatContainer) { view, insets ->
+ val systemBarInsets = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars() or
+ WindowInsetsCompat.Type.displayCutout()
+ )
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
- val bottomPadding = if (isKeyboardVisible) imeInsets.bottom else navBarInsets.bottom
+ val bottomPadding = if (isKeyboardVisible) imeInsets.bottom else systemBarInsets.bottom
view.setPadding(
- view.paddingLeft,
- statusBarInsets.top,
- view.paddingRight,
+ systemBarInsets.left,
+ systemBarInsets.top,
+ systemBarInsets.right,
bottomPadding
)
WindowInsetsCompat.CONSUMED
@@ -489,14 +516,27 @@ class ChatActivity :
conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
+ contextChatViewModel = ViewModelProvider(this, viewModelFactory)[ContextChatViewModel::class.java]
+
val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
chatViewModel.initData(
credentials!!,
urlForChatting,
- roomToken
+ roomToken,
+ conversationThreadId
)
+ conversationThreadId?.let {
+ val threadUrl = ApiUtils.getUrlForThread(
+ version = 1,
+ baseUrl = conversationUser!!.baseUrl,
+ token = roomToken,
+ threadId = it.toInt()
+ )
+ chatViewModel.getThread(credentials, threadUrl)
+ }
+
messageInputFragment = getMessageInputFragment()
messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
messageInputViewModel.setData(chatViewModel.getChatRepository())
@@ -521,6 +561,7 @@ class ChatActivity :
return MessageInputFragment().apply {
arguments = Bundle().apply {
putString(CONVERSATION_INTERNAL_ID, internalId)
+ putString(BundleKeys.KEY_SHARED_TEXT, sharedText)
}
}
}
@@ -549,6 +590,14 @@ class ChatActivity :
roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
+ conversationThreadId = if (extras?.containsKey(KEY_THREAD_ID) == true) {
+ extras.getLong(KEY_THREAD_ID)
+ } else {
+ null
+ }
+
+ openedViaNotification = extras?.getBoolean(KEY_OPENED_VIA_NOTIFICATION) ?: false
+
sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
Log.d(TAG, " roomToken = $roomToken")
@@ -671,7 +720,8 @@ class ChatActivity :
joinRoomWithPassword()
if (conversationUser?.userId != "?" &&
- hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
+ hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG) &&
+ !isChatThread()
) {
binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
}
@@ -937,6 +987,7 @@ class ChatActivity :
var chatMessageList = triple.third
chatMessageList = handleSystemMessages(chatMessageList)
+ chatMessageList = handleThreadMessages(chatMessageList)
if (chatMessageList.isEmpty()) {
return@onEach
}
@@ -1107,6 +1158,11 @@ class ChatActivity :
chatViewModel.getVoiceRecordingInProgress.observe(this) { voiceRecordingInProgress ->
VibrationUtils.vibrateShort(context)
+ if (voiceRecordingInProgress) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
binding.voiceRecordingLock.visibility = if (
voiceRecordingInProgress &&
chatViewModel.getVoiceRecordingLocked.value != true
@@ -1133,6 +1189,7 @@ class ChatActivity :
chatMenu?.removeItem(R.id.conversation_event)
}
+
is ChatViewModel.UnbindRoomUiState.Error -> {
Snackbar.make(
binding.root,
@@ -1140,7 +1197,8 @@ class ChatActivity :
Snackbar.LENGTH_LONG
).show()
}
- else -> { }
+
+ else -> {}
}
}
@@ -1234,6 +1292,25 @@ class ChatActivity :
}
}
}
+
+ this.lifecycleScope.launch {
+ chatViewModel.threadRetrieveState.collect { uiState ->
+ when (uiState) {
+ ChatViewModel.ThreadRetrieveUiState.None -> {
+ }
+
+ is ChatViewModel.ThreadRetrieveUiState.Error -> {
+ Log.e(TAG, "Error when retrieving thread", uiState.exception)
+ Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
+ }
+
+ is ChatViewModel.ThreadRetrieveUiState.Success -> {
+ conversationThreadInfo = uiState.thread
+ invalidateOptionsMenu()
+ }
+ }
+ }
+ }
}
private fun removeUnreadMessagesMarker() {
@@ -1503,6 +1580,7 @@ class ChatActivity :
} while (true && pos >= 0)
}
+ @Suppress("LongMethod")
private fun initMessageHolders(): MessageHolders {
val messageHolders = MessageHolders()
val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, viewThemeUtils)
@@ -2271,11 +2349,26 @@ class ChatActivity :
BuildConfig.APPLICATION_ID,
File(file.absolutePath)
)
- uploadFile(shareUri.toString(), false)
+ uploadFile(
+ fileUri = shareUri.toString(),
+ isVoiceMessage = false,
+ caption = "",
+ roomToken = roomToken,
+ replyToMessageId = getReplyToMessageId(),
+ displayName = currentConversation?.displayName ?: ""
+ )
}
cursor?.close()
}
+ fun getReplyToMessageId(): Int {
+ var replyMessageId = messageInputViewModel.getReplyChatMessage.value?.id?.toInt()
+ if (replyMessageId == null || replyMessageId == 0) {
+ replyMessageId = conversationThreadInfo?.thread?.id ?: 0
+ }
+ return replyMessageId
+ }
+
@Throws(IllegalStateException::class)
private fun onPickCameraResult(intent: Intent?) {
try {
@@ -2447,35 +2540,27 @@ class ChatActivity :
private fun uploadFiles(files: MutableList, caption: String = "") {
for (i in 0 until files.size) {
if (i == files.size - 1) {
- uploadFile(files[i], false, caption)
+ uploadFile(
+ fileUri = files[i],
+ isVoiceMessage = false,
+ caption = caption,
+ roomToken = roomToken,
+ replyToMessageId = getReplyToMessageId(),
+ displayName = currentConversation?.displayName!!
+ )
} else {
- uploadFile(files[i], false)
+ uploadFile(
+ fileUri = files[i],
+ isVoiceMessage = false,
+ caption = "",
+ roomToken = roomToken,
+ replyToMessageId = getReplyToMessageId(),
+ displayName = currentConversation?.displayName!!
+ )
}
}
}
- private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "", token: String = "") {
- var metaData = ""
- var room = ""
-
- if (!participantPermissions.hasChatPermission()) {
- Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
- return
- }
-
- if (isVoiceMessage) {
- metaData = VOICE_MESSAGE_META_DATA
- }
-
- if (caption != "") {
- metaData = "{\"caption\":\"$caption\"}"
- }
-
- if (token == "") room = roomToken else room = token
-
- chatViewModel.uploadFile(fileUri, room, currentConversation?.displayName!!, metaData)
- }
-
fun showGalleryPicker() {
pickMultipleMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))
}
@@ -2516,10 +2601,67 @@ class ChatActivity :
fun showShareLocationScreen() {
Log.d(TAG, "showShareLocationScreen")
- val intent = Intent(this, LocationPickerActivity::class.java)
- intent.putExtra(KEY_ROOM_TOKEN, roomToken)
- intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
- startActivity(intent)
+ val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
+ val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
+
+ if (!isGpsEnabled) {
+ showLocationServicesDisabledDialog()
+ } else if (!permissionUtil.isLocationPermissionGranted()) {
+ showLocationPermissionDeniedDialog()
+ }
+
+ if (permissionUtil.isLocationPermissionGranted() && isGpsEnabled) {
+ val intent = Intent(this, LocationPickerActivity::class.java)
+ intent.putExtra(KEY_ROOM_TOKEN, roomToken)
+ intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
+ startActivity(intent)
+ }
+ }
+
+ private fun showLocationServicesDisabledDialog() {
+ val title = resources.getString(R.string.location_services_disabled)
+ val explanation = resources.getString(R.string.location_services_disabled_msg)
+ val positive = resources.getString(R.string.nc_permissions_settings)
+ val cancel = resources.getString(R.string.nc_cancel)
+ val dialogBuilder = MaterialAlertDialogBuilder(this)
+ .setTitle(title)
+ .setMessage(explanation)
+ .setPositiveButton(positive) { _, _ ->
+ val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
+ startActivity(intent)
+ }
+ .setNegativeButton(cancel, null)
+
+ viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+ val dialog = dialogBuilder.show()
+ viewThemeUtils.platform.colorTextButtons(
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+ )
+ }
+
+ private fun showLocationPermissionDeniedDialog() {
+ val title = resources.getString(R.string.location_permission_denied)
+ val explanation = resources.getString(R.string.location_permission_denied_msg)
+ val positive = resources.getString(R.string.nc_permissions_settings)
+ val cancel = resources.getString(R.string.nc_cancel)
+ val dialogBuilder = MaterialAlertDialogBuilder(this)
+ .setTitle(title)
+ .setMessage(explanation)
+ .setPositiveButton(positive) { _, _ ->
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", packageName, null)
+ }
+ startActivity(intent)
+ }
+ .setNegativeButton(cancel, null)
+
+ viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+ val dialog = dialogBuilder.show()
+ viewThemeUtils.platform.colorTextButtons(
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+ )
}
private fun showConversationInfoScreen() {
@@ -2587,6 +2729,7 @@ class ChatActivity :
if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
mentionAutocomplete?.dismissPopup()
}
+ adapter = null
}
private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations
@@ -2600,7 +2743,9 @@ class ChatActivity :
viewThemeUtils.platform.colorTextView(title, ColorRole.ON_SURFACE)
title.text =
- if (currentConversation?.displayName != null) {
+ if (isChatThread()) {
+ conversationThreadInfo?.thread?.title
+ } else if (currentConversation?.displayName != null) {
try {
EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
} catch (e: java.lang.IllegalStateException) {
@@ -2611,7 +2756,16 @@ class ChatActivity :
""
}
- if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
+ if (isChatThread()) {
+ val replyAmount = conversationThreadInfo?.thread?.numReplies ?: 0
+ val repliesAmountTitle = resources.getQuantityString(
+ R.plurals.thread_replies,
+ replyAmount,
+ replyAmount
+ )
+
+ statusMessageViewContents(repliesAmountTitle)
+ } else if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
var statusMessage = ""
if (currentConversation?.statusIcon != null) {
statusMessage += currentConversation?.statusIcon
@@ -3101,11 +3255,21 @@ class ChatActivity :
}
val searchItem = menu.findItem(R.id.conversation_search)
-
searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) &&
- currentConversation!!.remoteServer.isNullOrEmpty()
+ currentConversation!!.remoteServer.isNullOrEmpty() &&
+ !isChatThread()
- if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
+ val sharedItemsItem = menu.findItem(R.id.shared_items)
+ sharedItemsItem.isVisible = !isChatThread()
+
+ val conversationInfoItem = menu.findItem(R.id.conversation_info)
+ conversationInfoItem.isVisible = !isChatThread()
+
+ val showThreadsItem = menu.findItem(R.id.show_threads)
+ showThreadsItem.isVisible = !isChatThread() &&
+ hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
+
+ if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) && !isChatThread()) {
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
@@ -3136,10 +3300,24 @@ class ChatActivity :
menu.removeItem(R.id.conversation_video_call)
menu.removeItem(R.id.conversation_voice_call)
}
+
+ handleThreadNotificationIcon(menu.findItem(R.id.thread_notifications))
}
return true
}
+ private fun handleThreadNotificationIcon(threadNotificationItem: MenuItem) {
+ threadNotificationItem.isVisible = isChatThread() &&
+ hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)
+
+ val threadNotificationIcon = when (conversationThreadInfo?.attendee?.notificationLevel) {
+ 1 -> R.drawable.outline_notifications_active_24
+ 3 -> R.drawable.ic_baseline_notifications_off_24
+ else -> R.drawable.baseline_notifications_24
+ }
+ threadNotificationItem.icon = ContextCompat.getDrawable(context, threadNotificationIcon)
+ }
+
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.conversation_video_call -> {
@@ -3169,15 +3347,105 @@ class ChatActivity :
R.id.conversation_event -> {
val anchorView = findViewById(R.id.conversation_event)
- showPopupWindow(anchorView)
+ showConversationEventMenu(anchorView)
+ true
+ }
+
+ R.id.show_threads -> {
+ openThreadsOverview()
+ true
+ }
+
+ R.id.thread_notifications -> {
+ showThreadNotificationMenu()
true
}
else -> super.onOptionsItemSelected(item)
}
+ @Suppress("Detekt.LongMethod")
+ private fun showThreadNotificationMenu() {
+ fun setThreadNotificationLevel(level: Int) {
+ val threadNotificationUrl = ApiUtils.getUrlForThreadNotificationLevel(
+ version = 1,
+ baseUrl = conversationUser!!.baseUrl,
+ token = roomToken,
+ threadId = conversationThreadId!!.toInt()
+ )
+ chatViewModel.setThreadNotificationLevel(credentials!!, threadNotificationUrl, level)
+ }
+
+ if (overflowMenuHostView == null) {
+ val threadNotificationsAnchor: View? = findViewById(R.id.thread_notifications)
+
+ val colorScheme = viewThemeUtils.getColorScheme(this)
+
+ overflowMenuHostView = ComposeView(this).apply {
+ setContent {
+ MaterialTheme(
+ colorScheme = colorScheme
+ ) {
+ val items = listOf(
+ MenuItemData(
+ title = context.resources.getString(R.string.notifications_default),
+ subtitle = context.resources.getString(
+ R.string.notifications_default_description
+ ),
+ icon = R.drawable.baseline_notifications_24,
+ onClick = {
+ setThreadNotificationLevel(0)
+ }
+ ),
+ MenuItemData(
+ title = context.resources.getString(R.string.notification_all_messages),
+ subtitle = null,
+ icon = R.drawable.outline_notifications_active_24,
+ onClick = {
+ setThreadNotificationLevel(1)
+ }
+ ),
+ MenuItemData(
+ title = context.resources.getString(R.string.notification_mention_only),
+ subtitle = null,
+ icon = R.drawable.baseline_notifications_24,
+ onClick = {
+ setThreadNotificationLevel(2)
+ }
+ ),
+ MenuItemData(
+ title = context.resources.getString(R.string.notification_off),
+ subtitle = null,
+ icon = R.drawable.ic_baseline_notifications_off_24,
+ onClick = {
+ setThreadNotificationLevel(3)
+ }
+ )
+ )
+
+ OverflowMenu(
+ anchor = threadNotificationsAnchor,
+ expanded = isThreadMenuExpanded,
+ items = items,
+ onDismiss = { isThreadMenuExpanded = false }
+ )
+ }
+ }
+ }
+
+ addContentView(
+ overflowMenuHostView,
+ CoordinatorLayout.LayoutParams(
+ CoordinatorLayout.LayoutParams.MATCH_PARENT,
+ CoordinatorLayout.LayoutParams.MATCH_PARENT
+ )
+ )
+ }
+ isThreadMenuExpanded = true
+ }
+
@SuppressLint("InflateParams")
- private fun showPopupWindow(anchorView: View) {
+ private fun showConversationEventMenu(anchorView: View) {
val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null)
val subtitleTextView = popupView.findViewById(R.id.meetingTime)
@@ -3366,7 +3634,7 @@ class ChatActivity :
}
private fun handleSystemMessages(chatMessageList: List): List {
- val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
+ val chatMessageMap = chatMessageList.associateBy { it.id }.toMutableMap()
val chatMessageIterator = chatMessageMap.iterator()
while (chatMessageIterator.hasNext()) {
@@ -3375,7 +3643,8 @@ class ChatActivity :
if (isInfoMessageAboutDeletion(currentMessage) ||
isReactionsMessage(currentMessage) ||
isPollVotedMessage(currentMessage) ||
- isEditMessage(currentMessage)
+ isEditMessage(currentMessage) ||
+ isThreadCreatedMessage(currentMessage)
) {
chatMessageIterator.remove()
}
@@ -3384,29 +3653,56 @@ class ChatActivity :
}
private fun handleExpandableSystemMessages(chatMessageList: List): List {
- val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
+ val chatMessageMap = chatMessageList.associateBy { it.id }.toMutableMap()
val chatMessageIterator = chatMessageMap.iterator()
+
while (chatMessageIterator.hasNext()) {
val currentMessage = chatMessageIterator.next()
-
- val previousMessage = chatMessageMap[currentMessage.value.previousMessageId.toString()]
- if (isSystemMessage(currentMessage.value) &&
- previousMessage?.systemMessageType == currentMessage.value.systemMessageType
- ) {
- previousMessage?.expandableParent = true
- currentMessage.value.expandableParent = false
-
- if (currentMessage.value.lastItemOfExpandableGroup == 0) {
- currentMessage.value.lastItemOfExpandableGroup = currentMessage.value.jsonMessageId
+ chatMessageMap[currentMessage.value.previousMessageId.toString()]?.let { previousMessage ->
+ if (isSystemMessage(currentMessage.value) &&
+ previousMessage.systemMessageType == currentMessage.value.systemMessageType &&
+ isSameDayMessages(previousMessage, currentMessage.value)
+ ) {
+ groupSystemMessages(previousMessage, currentMessage.value)
}
-
- previousMessage?.lastItemOfExpandableGroup = currentMessage.value.lastItemOfExpandableGroup
- previousMessage?.expandableChildrenAmount = currentMessage.value.expandableChildrenAmount + 1
}
}
return chatMessageMap.values.toList()
}
+ private fun groupSystemMessages(previousMessage: ChatMessage, currentMessage: ChatMessage) {
+ previousMessage.expandableParent = true
+ currentMessage.expandableParent = false
+
+ if (currentMessage.lastItemOfExpandableGroup == 0) {
+ currentMessage.lastItemOfExpandableGroup = currentMessage.jsonMessageId
+ }
+
+ previousMessage.lastItemOfExpandableGroup = currentMessage.lastItemOfExpandableGroup
+ previousMessage.expandableChildrenAmount = currentMessage.expandableChildrenAmount + 1
+ }
+
+ private fun handleThreadMessages(chatMessageList: List): List {
+ fun isThreadChildMessage(currentMessage: MutableMap.MutableEntry): Boolean =
+ currentMessage.value.isThread &&
+ currentMessage.value.threadId?.toInt() != currentMessage.value.jsonMessageId
+
+ val chatMessageMap = chatMessageList.associateBy { it.id }.toMutableMap()
+
+ if (conversationThreadId == null) {
+ val chatMessageIterator = chatMessageMap.iterator()
+ while (chatMessageIterator.hasNext()) {
+ val currentMessage = chatMessageIterator.next()
+
+ if (isThreadChildMessage(currentMessage)) {
+ chatMessageIterator.remove()
+ }
+ }
+ }
+
+ return chatMessageMap.values.toList()
+ }
+
private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry): Boolean =
currentMessage.value.parentMessageId != null &&
currentMessage.value.systemMessageType == ChatMessage
@@ -3417,6 +3713,9 @@ class ChatActivity :
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
+ private fun isThreadCreatedMessage(currentMessage: MutableMap.MutableEntry): Boolean =
+ currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.THREAD_CREATED
+
private fun isEditMessage(currentMessage: MutableMap.MutableEntry): Boolean =
currentMessage.value.parentMessageId != null &&
currentMessage.value.systemMessageType == ChatMessage
@@ -3489,6 +3788,10 @@ class ChatActivity :
}
}
+ override fun openThread(chatMessage: ChatMessage) {
+ openThread(chatMessage.jsonMessageId.toLong())
+ }
+
override fun onLongClickReactions(chatMessage: ChatMessage) {
ShowReactionsDialog(
this,
@@ -3761,7 +4064,14 @@ class ChatActivity :
val type = message.getCalculateMessageType()
when (type) {
ChatMessage.MessageType.VOICE_MESSAGE -> {
- uploadFile(shareUri.toString(), true, token = roomToken)
+ uploadFile(
+ shareUri.toString(),
+ true,
+ roomToken = roomToken,
+ caption = "",
+ replyToMessageId = getReplyToMessageId(),
+ displayName = currentConversation?.displayName ?: ""
+ )
showSnackBar(roomToken)
}
@@ -3770,12 +4080,26 @@ class ChatActivity :
if (null != shareUri) {
try {
context.contentResolver.openInputStream(shareUri)?.close()
- uploadFile(shareUri.toString(), false, caption!!, roomToken)
+ uploadFile(
+ fileUri = shareUri.toString(),
+ isVoiceMessage = false,
+ caption = caption!!,
+ roomToken = roomToken,
+ replyToMessageId = getReplyToMessageId(),
+ displayName = currentConversation?.displayName ?: ""
+ )
showSnackBar(roomToken)
- } catch (e: java.lang.Exception) {
- Log.w(TAG, "File corresponding to the uri does not exist $shareUri")
+ } catch (e: Exception) {
+ Log.w(TAG, "File corresponding to the uri does not exist $shareUri", e)
downloadFileToCache(message, false) {
- uploadFile(shareUri.toString(), false, caption!!, roomToken)
+ uploadFile(
+ fileUri = shareUri.toString(),
+ isVoiceMessage = false,
+ caption = caption!!,
+ roomToken = roomToken,
+ replyToMessageId = getReplyToMessageId(),
+ displayName = currentConversation?.displayName ?: ""
+ )
showSnackBar(roomToken)
}
}
@@ -4097,6 +4421,10 @@ class ChatActivity :
pollVoteDialog.show(supportFragmentManager, TAG)
}
+ fun createThread() {
+ messageInputViewModel.startThreadCreation()
+ }
+
fun jumpToQuotedMessage(parentMessage: ChatMessage) {
var foundMessage = false
for (position in 0 until (adapter!!.items.size)) {
@@ -4109,10 +4437,38 @@ class ChatActivity :
}
if (!foundMessage) {
Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
- startContextChatWindowForMessage(parentMessage.id)
+ startContextChatWindowForMessage(parentMessage.id, conversationThreadId.toString())
}
}
+ private fun isChatThread(): Boolean = conversationThreadId != null && conversationThreadId!! > 0
+
+ fun openThread(messageId: Long) {
+ val bundle = Bundle()
+ bundle.putString(KEY_ROOM_TOKEN, roomToken)
+ bundle.putLong(KEY_THREAD_ID, messageId)
+ val chatIntent = Intent(context, ChatActivity::class.java)
+ chatIntent.putExtras(bundle)
+ startActivity(chatIntent)
+ }
+
+ fun openThreadsOverview() {
+ val threadsUrl = ApiUtils.getUrlForRecentThreads(
+ version = 1,
+ baseUrl = conversationUser!!.baseUrl,
+ token = roomToken
+ )
+
+ val bundle = Bundle()
+ bundle.putString(KEY_ROOM_TOKEN, roomToken)
+ bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.recent_threads))
+ bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl)
+
+ val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java)
+ threadsOverviewIntent.putExtras(bundle)
+ startActivity(threadsOverviewIntent)
+ }
+
override fun joinAudioCall() {
startACall(true, false)
}
@@ -4158,6 +4514,37 @@ class ChatActivity :
)
}
+ fun uploadFile(
+ fileUri: String,
+ isVoiceMessage: Boolean,
+ caption: String = "",
+ roomToken: String = "",
+ replyToMessageId: Int? = null,
+ displayName: String
+ ) {
+ chatViewModel.uploadFile(
+ fileUri,
+ isVoiceMessage,
+ caption,
+ roomToken,
+ replyToMessageId,
+ displayName
+ )
+ cancelReply()
+ }
+
+ fun cancelReply() {
+ messageInputViewModel.reply(null)
+ chatViewModel.messageDraft.quotedMessageText = null
+ chatViewModel.messageDraft.quotedDisplayName = null
+ chatViewModel.messageDraft.quotedImageUrl = null
+ chatViewModel.messageDraft.quotedJsonId = null
+ }
+
+ fun cancelCreateThread() {
+ chatViewModel.clearThreadTitle()
+ }
+
companion object {
val TAG = ChatActivity::class.simpleName
private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
@@ -4177,7 +4564,6 @@ class ChatActivity :
private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
private const val REQUEST_READ_CONTACT_PERMISSION = 234
private const val REQUEST_CAMERA_PERMISSION = 223
- private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
private const val VIDEO_SUFFIX = ".mp4"
private const val FULLY_OPAQUE_INT: Int = 255
diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
index 6273696..3225d13 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
@@ -35,7 +35,6 @@ import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.SeekBar
-import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
@@ -61,6 +60,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
+import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.FragmentMessageInputBinding
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
@@ -75,45 +75,28 @@ import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.CharPolicy
+import com.nextcloud.talk.utils.EmojiTextInputEditText
import com.nextcloud.talk.utils.ImageEmojiEditText
import com.nextcloud.talk.utils.SpreedFeatures
+import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete
-import com.stfalcon.chatkit.commons.models.IMessage
import com.vanniktech.emoji.EmojiPopup
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.util.Objects
import javax.inject.Inject
-@Suppress("LongParameterList", "TooManyFunctions")
+@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "LongMethod")
@AutoInjector(NextcloudTalkApplication::class)
class MessageInputFragment : Fragment() {
- companion object {
- fun newInstance() = MessageInputFragment()
- private val TAG: String = MessageInputFragment::class.java.simpleName
- private const val TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE = 10000L
- private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L
- private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping"
- private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping"
- const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
- private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
- private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
- private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
- private const val ANIMATION_DURATION: Long = 750
- private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -150
- private const val VOICE_RECORD_LOCK_THRESHOLD: Float = 100f
- private const val INCREMENT = 8f
- private const val CURSOR_KEY = "_cursor"
- private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000
- private const val FULLY_OPAQUE: Float = 1.0f
- private const val FULLY_TRANSPARENT: Float = 0.0f
- }
-
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@@ -144,6 +127,12 @@ class MessageInputFragment : Fragment() {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
conversationInternalId = arguments?.getString(ChatActivity.CONVERSATION_INTERNAL_ID).orEmpty()
+ chatActivity = requireActivity() as ChatActivity
+ val sharedText = arguments?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
+ if (sharedText.isNotEmpty()) {
+ chatActivity.chatViewModel.messageDraft.messageText = sharedText
+ chatActivity.chatViewModel.saveMessageDraft()
+ }
if (conversationInternalId.isEmpty()) {
Log.e(TAG, "internalId for conversation passed to MessageInputFragment is empty")
}
@@ -151,45 +140,75 @@ class MessageInputFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentMessageInputBinding.inflate(inflater)
- chatActivity = requireActivity() as ChatActivity
themeMessageInputView()
initMessageInputView()
initSmileyKeyboardToggler()
setupMentionAutocomplete()
initVoiceRecordButton()
+ initThreadHandling()
restoreState()
return binding.root
}
- override fun onPause() {
- super.onPause()
- saveState()
- }
-
override fun onDestroyView() {
super.onDestroyView()
if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
mentionAutocomplete?.dismissPopup()
}
clearEditUI()
- cancelReply()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initObservers()
+
+ binding.fragmentCreateThreadView.createThreadView.findViewById(
+ R.id
+ .createThreadInput
+ ).doAfterTextChanged { text ->
+ val threadTitle = text.toString()
+ chatActivity.chatViewModel.messageDraft.threadTitle = threadTitle
+ }
}
private fun initObservers() {
Log.d(TAG, "LifeCyclerOwner is: ${viewLifecycleOwner.lifecycle}")
chatActivity.messageInputViewModel.getReplyChatMessage.observe(viewLifecycleOwner) { message ->
- message?.let { replyToMessage(message) }
+ message?.let {
+ chatActivity.chatViewModel.messageDraft.quotedMessageText = message.text
+ chatActivity.chatViewModel.messageDraft.quotedDisplayName = message.actorDisplayName
+ chatActivity.chatViewModel.messageDraft.quotedImageUrl = message.imageUrl
+ chatActivity.chatViewModel.messageDraft.quotedJsonId = message.jsonMessageId
+ replyToMessage(
+ message.text,
+ message.actorDisplayName,
+ message.imageUrl
+ )
+ } ?: clearReplyUi()
}
chatActivity.messageInputViewModel.getEditChatMessage.observe(viewLifecycleOwner) { message ->
message?.let { setEditUI(it as ChatMessage) }
}
+ chatActivity.messageInputViewModel.createThreadViewState.observe(viewLifecycleOwner) { state ->
+ when (state) {
+ is MessageInputViewModel.CreateThreadStartState ->
+ binding.fragmentCreateThreadView.createThreadView.visibility = View.GONE
+
+ is MessageInputViewModel.CreateThreadEditState -> {
+ binding.fragmentCreateThreadView.createThreadView.visibility = View.VISIBLE
+ binding.fragmentCreateThreadView.createThreadView
+ .findViewById(R.id.createThreadInput)?.setText(
+ chatActivity.chatViewModel.messageDraft.threadTitle
+ )
+ }
+
+ else -> {}
+ }
+ initVoiceRecordButton()
+ }
+
chatActivity.chatViewModel.leaveRoomViewState.observe(viewLifecycleOwner) { state ->
when (state) {
is ChatViewModel.LeaveRoomSuccessState -> sendStopTypingMessage()
@@ -299,33 +318,29 @@ class MessageInputFragment : Fragment() {
}
private fun restoreState() {
- if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) {
- requireContext().getSharedPreferences(chatActivity.localClassName, AppCompatActivity.MODE_PRIVATE).apply {
- val text = getString(chatActivity.roomToken, "")
- val cursor = getInt(chatActivity.roomToken + CURSOR_KEY, 0)
- binding.fragmentMessageInputView.messageInput.setText(text)
- binding.fragmentMessageInputView.messageInput.setSelection(cursor)
- }
- }
- }
+ CoroutineScope(Dispatchers.IO).launch {
+ chatActivity.chatViewModel.updateMessageDraft()
- private fun saveState() {
- val text = binding.fragmentMessageInputView.messageInput.text.toString()
- val cursor = binding.fragmentMessageInputView.messageInput.selectionStart
- val previous = requireContext().getSharedPreferences(
- chatActivity.localClassName,
- AppCompatActivity
- .MODE_PRIVATE
- ).getString(chatActivity.roomToken, "null")
+ withContext(Dispatchers.Main) {
+ val draft = chatActivity.chatViewModel.messageDraft
+ binding.fragmentMessageInputView.messageInput.setText(draft.messageText)
+ binding.fragmentMessageInputView.messageInput.setSelection(draft.messageCursor)
- if (text != previous) {
- requireContext().getSharedPreferences(
- chatActivity.localClassName,
- AppCompatActivity.MODE_PRIVATE
- ).edit().apply {
- putString(chatActivity.roomToken, text)
- putInt(chatActivity.roomToken + CURSOR_KEY, cursor)
- apply()
+ if (draft.threadTitle?.isNotEmpty() == true) {
+ chatActivity.messageInputViewModel.startThreadCreation()
+ }
+
+ if (draft.messageText != "") {
+ binding.fragmentMessageInputView.messageInput.requestFocus()
+ }
+
+ if (isInReplyState()) {
+ replyToMessage(
+ chatActivity.chatViewModel.messageDraft.quotedMessageText,
+ chatActivity.chatViewModel.messageDraft.quotedDisplayName,
+ chatActivity.chatViewModel.messageDraft.quotedImageUrl
+ )
+ }
}
}
}
@@ -388,7 +403,11 @@ class MessageInputFragment : Fragment() {
}
override fun afterTextChanged(s: Editable) {
- // unused atm
+ val cursor = binding.fragmentMessageInputView.messageInput.selectionStart
+ val text = binding.fragmentMessageInputView.messageInput.text.toString()
+ chatActivity.chatViewModel.messageDraft.messageCursor = cursor
+ chatActivity.chatViewModel.messageDraft.messageText = text
+ handleButtonsVisibility()
}
})
@@ -396,11 +415,14 @@ class MessageInputFragment : Fragment() {
// See: https://developer.android.com/guide/topics/text/image-keyboard
(binding.fragmentMessageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
- uploadFile(it.toString(), false)
- }
-
- if (chatActivity.sharedText.isNotEmpty()) {
- binding.fragmentMessageInputView.inputEditText?.setText(chatActivity.sharedText)
+ chatActivity.chatViewModel.uploadFile(
+ fileUri = it.toString(),
+ isVoiceMessage = false,
+ caption = "",
+ roomToken = chatActivity.roomToken,
+ replyToMessageId = chatActivity.getReplyToMessageId(),
+ displayName = chatActivity.currentConversation?.displayName!!
+ )
}
binding.fragmentMessageInputView.setAttachmentsListener {
@@ -439,6 +461,9 @@ class MessageInputFragment : Fragment() {
binding.fragmentEditView.clearEdit.setOnClickListener {
clearEditUI()
}
+ binding.fragmentCreateThreadView.abortCreateThread.setOnClickListener {
+ cancelCreateThread()
+ }
if (CapabilitiesUtil.hasSpreedFeatureCapability(chatActivity.spreedCapabilities, SpreedFeatures.SILENT_SEND)) {
binding.fragmentMessageInputView.button?.setOnLongClickListener {
@@ -466,6 +491,10 @@ class MessageInputFragment : Fragment() {
binding.fragmentCallStarted.callStartedSecondaryText.visibility = if (collapsed) View.VISIBLE else View.GONE
setDropDown(collapsed)
}
+
+ binding.fragmentMessageInputView.findViewById(R.id.cancelReplyButton)?.setOnClickListener {
+ cancelReply()
+ }
}
private fun setDropDown(collapsed: Boolean) {
@@ -480,32 +509,7 @@ class MessageInputFragment : Fragment() {
@Suppress("ClickableViewAccessibility", "CyclomaticComplexMethod", "LongMethod")
private fun initVoiceRecordButton() {
- if (binding.fragmentMessageInputView.messageInput.text.isNullOrBlank()) {
- binding.fragmentMessageInputView.messageSendButton.visibility = View.GONE
- binding.fragmentMessageInputView.recordAudioButton.visibility = View.VISIBLE
- } else {
- binding.fragmentMessageInputView.messageSendButton.visibility = View.VISIBLE
- binding.fragmentMessageInputView.recordAudioButton.visibility = View.GONE
- }
- binding.fragmentMessageInputView.inputEditText.doAfterTextChanged {
- binding.fragmentMessageInputView.recordAudioButton.visibility =
- if (binding.fragmentMessageInputView.inputEditText.text.isEmpty() &&
- chatActivity.messageInputViewModel.getEditChatMessage.value == null
- ) {
- View.VISIBLE
- } else {
- View.GONE
- }
-
- binding.fragmentMessageInputView.messageSendButton.visibility =
- if (binding.fragmentMessageInputView.inputEditText.text.isEmpty() ||
- binding.fragmentEditView.editMessageView.isVisible
- ) {
- View.GONE
- } else {
- View.VISIBLE
- }
- }
+ handleButtonsVisibility()
var prevDx = 0f
var voiceRecordStartTime = 0L
@@ -568,9 +572,9 @@ class MessageInputFragment : Fragment() {
return@setOnTouchListener false
} else {
chatActivity.chatViewModel.stopAndSendAudioRecording(
- chatActivity.roomToken,
- chatActivity.currentConversation!!.displayName,
- VOICE_MESSAGE_META_DATA
+ roomToken = chatActivity.roomToken,
+ replyToMessageId = chatActivity.getReplyToMessageId(),
+ displayName = chatActivity.currentConversation!!.displayName
)
}
resetSlider()
@@ -615,7 +619,67 @@ class MessageInputFragment : Fragment() {
}
}
}
- v?.onTouchEvent(event) ?: true
+ v?.onTouchEvent(event) != false
+ }
+ }
+
+ private fun initThreadHandling() {
+ binding.fragmentMessageInputView.submitThreadButton.setOnClickListener {
+ submitMessage(false)
+ }
+
+ binding.fragmentCreateThreadView.createThreadInput.doAfterTextChanged {
+ handleButtonsVisibility()
+ }
+ }
+
+ private fun handleButtonsVisibility() {
+ fun View.setVisible(isVisible: Boolean) {
+ visibility = if (isVisible) View.VISIBLE else View.GONE
+ }
+
+ val isEditModeActive = binding.fragmentEditView.editMessageView.isVisible
+ val isThreadCreateModeActive = binding.fragmentCreateThreadView.createThreadView.isVisible
+ val inputContainsText = binding.fragmentMessageInputView.messageInput.text.isNotEmpty()
+ val threadTitleContainsText = binding.fragmentCreateThreadView.createThreadInput.text?.isNotEmpty() ?: false
+
+ binding.fragmentMessageInputView.apply {
+ when {
+ isEditModeActive -> {
+ messageSendButton.setVisible(false)
+ recordAudioButton.setVisible(false)
+ submitThreadButton.setVisible(false)
+ attachmentButton.setVisible(true)
+ }
+
+ isThreadCreateModeActive -> {
+ messageSendButton.setVisible(false)
+ recordAudioButton.setVisible(false)
+ attachmentButton.setVisible(false)
+ submitThreadButton.setVisible(true)
+ if (inputContainsText && threadTitleContainsText) {
+ submitThreadButton.isEnabled = true
+ submitThreadButton.alpha = FULLY_OPAQUE
+ } else {
+ submitThreadButton.isEnabled = false
+ submitThreadButton.alpha = OPACITY_DISABLED
+ }
+ }
+
+ inputContainsText -> {
+ recordAudioButton.setVisible(false)
+ submitThreadButton.setVisible(false)
+ messageSendButton.setVisible(true)
+ attachmentButton.setVisible(true)
+ }
+
+ else -> {
+ messageSendButton.setVisible(false)
+ submitThreadButton.setVisible(false)
+ recordAudioButton.setVisible(true)
+ attachmentButton.setVisible(true)
+ }
+ }
}
}
@@ -717,52 +781,45 @@ class MessageInputFragment : Fragment() {
}
}
- private fun replyToMessage(message: IMessage?) {
+ private fun replyToMessage(quotedMessageText: String?, quotedActorDisplayName: String?, quotedImageUrl: String?) {
Log.d(TAG, "Reply")
- val chatMessage = message as ChatMessage?
- chatMessage?.let {
- val view = binding.fragmentMessageInputView
- view.findViewById(R.id.attachmentButton)?.visibility =
- View.GONE
- view.findViewById(R.id.cancelReplyButton)?.visibility =
- View.VISIBLE
+ val view = binding.fragmentMessageInputView
+ view.findViewById(R.id.cancelReplyButton)?.visibility =
+ View.VISIBLE
- val quotedMessage = view.findViewById(R.id.quotedMessage)
+ val quotedMessage = view.findViewById(R.id.quotedMessage)
- quotedMessage?.maxLines = 2
- quotedMessage?.ellipsize = TextUtils.TruncateAt.END
- quotedMessage?.text = it.text
- view.findViewById(R.id.quotedMessageAuthor)?.text =
- it.actorDisplayName ?: requireContext().getText(R.string.nc_nick_guest)
+ quotedMessage?.maxLines = 2
+ quotedMessage?.ellipsize = TextUtils.TruncateAt.END
+ quotedMessage?.text = quotedMessageText
+ view.findViewById(R.id.quotedMessageAuthor)?.text =
+ quotedActorDisplayName ?: requireContext().getText(R.string.nc_nick_guest)
- chatActivity.conversationUser?.let {
- val quotedMessageImage = view.findViewById(R.id.quotedMessageImage)
- chatMessage.imageUrl?.let { previewImageUrl ->
- quotedMessageImage?.visibility = View.VISIBLE
+ chatActivity.conversationUser?.let {
+ val quotedMessageImage = view.findViewById(R.id.quotedMessageImage)
+ quotedImageUrl?.let { previewImageUrl ->
+ quotedMessageImage?.visibility = View.VISIBLE
- val px = TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
- resources.displayMetrics
- )
+ val px = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
+ resources.displayMetrics
+ )
- quotedMessageImage?.maxHeight = px.toInt()
- val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
- layoutParams.flexGrow = 0f
- quotedMessageImage.layoutParams = layoutParams
- quotedMessageImage.load(previewImageUrl) {
- addHeader("Authorization", chatActivity.credentials!!)
- }
- } ?: run {
- view.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE
+ quotedMessageImage?.maxHeight = px.toInt()
+ val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
+ layoutParams.flexGrow = 0f
+ quotedMessageImage.layoutParams = layoutParams
+ quotedMessageImage.load(previewImageUrl) {
+ addHeader("Authorization", chatActivity.credentials!!)
}
+ } ?: run {
+ view.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE
}
-
- val quotedChatMessageView =
- view.findViewById(R.id.quotedChatMessageView)
- quotedChatMessageView?.tag = message?.jsonMessageId
- quotedChatMessageView?.visibility = View.VISIBLE
}
+
+ val quotedChatMessageView = view.findViewById(R.id.quotedChatMessageView)
+ quotedChatMessageView?.visibility = View.VISIBLE
}
fun updateOwnTypingStatus(typedText: CharSequence) {
@@ -829,58 +886,34 @@ class MessageInputFragment : Fragment() {
private fun isTypingStatusEnabled(): Boolean =
!CapabilitiesUtil.isTypingStatusPrivate(chatActivity.conversationUser!!)
- private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "", token: String = "") {
- var metaData = ""
- val room: String
-
- if (!chatActivity.participantPermissions.hasChatPermission()) {
- Log.w(ChatActivity.TAG, "uploading file(s) is forbidden because of missing attendee permissions")
- return
- }
-
- if (isVoiceMessage) {
- metaData = VOICE_MESSAGE_META_DATA
- }
-
- if (caption != "") {
- metaData = "{\"caption\":\"$caption\"}"
- }
-
- if (token == "") room = chatActivity.roomToken else room = token
-
- chatActivity.chatViewModel.uploadFile(fileUri, room, chatActivity.currentConversation!!.displayName, metaData)
- }
-
private fun submitMessage(sendWithoutNotification: Boolean) {
if (binding.fragmentMessageInputView.inputEditText != null) {
val editable = binding.fragmentMessageInputView.inputEditText!!.editableText
replaceMentionChipSpans(editable)
binding.fragmentMessageInputView.inputEditText?.setText("")
sendStopTypingMessage()
- val replyMessageId = binding.fragmentMessageInputView
- .findViewById(R.id.quotedChatMessageView)?.tag as Int? ?: 0
-
sendMessage(
editable.toString(),
- replyMessageId,
sendWithoutNotification
)
cancelReply()
+ cancelCreateThread()
}
}
- private fun sendMessage(message: String, replyTo: Int?, sendWithoutNotification: Boolean) {
+ private fun sendMessage(message: String, sendWithoutNotification: Boolean) {
chatActivity.messageInputViewModel.sendChatMessage(
- chatActivity.conversationUser!!.getCredentials(),
- ApiUtils.getUrlForChat(
+ credentials = chatActivity.conversationUser!!.getCredentials(),
+ url = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser!!.baseUrl!!,
chatActivity.roomToken
),
- message,
- chatActivity.conversationUser!!.displayName ?: "",
- replyTo ?: 0,
- sendWithoutNotification
+ message = message,
+ displayName = chatActivity.conversationUser!!.displayName ?: "",
+ replyTo = chatActivity.getReplyToMessageId(),
+ sendWithoutNotification = sendWithoutNotification,
+ threadTitle = chatActivity.chatViewModel.messageDraft.threadTitle
)
}
@@ -964,6 +997,7 @@ class MessageInputFragment : Fragment() {
binding.fragmentMessageInputView.inputEditText.setSelection(end)
binding.fragmentMessageInputView.messageSendButton.visibility = View.GONE
binding.fragmentMessageInputView.recordAudioButton.visibility = View.GONE
+ binding.fragmentMessageInputView.submitThreadButton.visibility = View.GONE
binding.fragmentMessageInputView.editMessageButton.visibility = View.VISIBLE
binding.fragmentEditView.editMessageView.visibility = View.VISIBLE
binding.fragmentMessageInputView.attachmentButton.visibility = View.GONE
@@ -975,15 +1009,12 @@ class MessageInputFragment : Fragment() {
binding.fragmentEditView.editMessageView.visibility = View.GONE
binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE
chatActivity.messageInputViewModel.edit(null)
+ handleButtonsVisibility()
}
private fun themeMessageInputView() {
binding.fragmentMessageInputView.button?.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
- binding.fragmentMessageInputView.findViewById(R.id.cancelReplyButton)?.setOnClickListener {
- cancelReply()
- }
-
binding.fragmentMessageInputView.findViewById(R.id.cancelReplyButton)?.let {
viewThemeUtils.platform
.themeImageButton(it)
@@ -1021,6 +1052,9 @@ class MessageInputFragment : Fragment() {
binding.fragmentEditView.clearEdit.let {
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
}
+ binding.fragmentCreateThreadView.abortCreateThread.let {
+ viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+ }
binding.fragmentCallStarted.callStartedBackground.apply {
viewThemeUtils.talk.themeOutgoingMessageBubble(this, grouped = true, false)
@@ -1037,14 +1071,56 @@ class MessageInputFragment : Fragment() {
binding.fragmentCallStarted.callStartedCloseBtn.apply {
viewThemeUtils.platform.colorImageView(this, ColorRole.PRIMARY)
}
+
+ binding.fragmentMessageInputView.submitThreadButton.apply {
+ viewThemeUtils.platform.colorImageView(this, ColorRole.SECONDARY)
+ }
+
+ binding.fragmentCreateThreadView.createThreadInput.apply {
+ viewThemeUtils.platform.colorEditText(this)
+ }
+ }
+
+ private fun cancelCreateThread() {
+ chatActivity.cancelCreateThread()
+ chatActivity.messageInputViewModel.stopThreadCreation()
+ binding.fragmentCreateThreadView.createThreadView.visibility = View.GONE
}
private fun cancelReply() {
- val quote = binding.fragmentMessageInputView
- .findViewById(R.id.quotedChatMessageView)
+ chatActivity.cancelReply()
+ clearReplyUi()
+ }
+
+ private fun clearReplyUi() {
+ val quote = binding.fragmentMessageInputView.findViewById(R.id.quotedChatMessageView)
quote.visibility = View.GONE
- quote.tag = null
binding.fragmentMessageInputView.findViewById(R.id.attachmentButton)?.visibility = View.VISIBLE
- chatActivity.messageInputViewModel.reply(null)
+ }
+
+ private fun isInReplyState(): Boolean {
+ val jsonId = chatActivity.chatViewModel.messageDraft.quotedJsonId
+ return jsonId != null
+ }
+
+ companion object {
+ fun newInstance() = MessageInputFragment()
+ private val TAG: String = MessageInputFragment::class.java.simpleName
+ private const val TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE = 10000L
+ private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L
+ private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping"
+ private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping"
+ private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
+ private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
+ private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
+ private const val ANIMATION_DURATION: Long = 750
+ private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -150
+ private const val VOICE_RECORD_LOCK_THRESHOLD: Float = 100f
+ private const val INCREMENT = 8f
+ private const val CURSOR_KEY = "_cursor"
+ private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000
+ private const val FULLY_OPAQUE: Float = 1.0f
+ private const val FULLY_TRANSPARENT: Float = 0.0f
+ private const val OPACITY_DISABLED = 0.7f
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
index 945622e..98f529f 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
@@ -114,9 +114,9 @@ class MessageInputVoiceRecordingFragment : Fragment() {
binding.sendVoiceRecording.setOnClickListener {
chatActivity.chatViewModel.stopAndSendAudioRecording(
- chatActivity.roomToken,
- chatActivity.currentConversation!!.displayName,
- MessageInputFragment.VOICE_MESSAGE_META_DATA
+ roomToken = chatActivity.roomToken,
+ replyToMessageId = chatActivity.getReplyToMessageId(),
+ displayName = chatActivity.currentConversation!!.displayName
)
clear()
}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt b/app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt
new file mode 100644
index 0000000..fc403f6
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt
@@ -0,0 +1,160 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Marcel Hibbe
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat
+
+import android.view.View
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import com.nextcloud.talk.R
+
+data class MenuItemData(val title: String, val subtitle: String? = null, val icon: Int? = null, val onClick: () -> Unit)
+
+@Composable
+fun OverflowMenu(anchor: View?, expanded: Boolean, items: List, onDismiss: () -> Unit) {
+ if (!expanded) return
+
+ val rect = anchor?.boundsInWindow()
+ val xOffset = rect?.left ?: 0
+ val yOffset = rect?.bottom ?: 0
+
+ Popup(
+ onDismissRequest = onDismiss,
+ offset = IntOffset(xOffset, yOffset)
+ ) {
+ Column(
+ modifier = Modifier
+ .width(IntrinsicSize.Max)
+ .background(
+ color = colorResource(id = R.color.bg_default),
+ shape = RoundedCornerShape(1.dp)
+ )
+ .shadow(
+ elevation = 1.dp,
+ shape = RoundedCornerShape(1.dp),
+ clip = false
+ )
+ ) {
+ items.forEach { item ->
+ DynamicMenuItem(
+ item.copy(
+ onClick = {
+ item.onClick()
+ onDismiss()
+ }
+ )
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun DynamicMenuItem(item: MenuItemData) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ onClick = item.onClick,
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ .padding(horizontal = 12.dp, vertical = 12.dp)
+ ) {
+ item.icon?.let { icon ->
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(Modifier.width(8.dp))
+ }
+
+ Column {
+ Text(item.title, color = MaterialTheme.colorScheme.onSurface)
+ item.subtitle?.let {
+ Text(
+ it,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+private fun View.boundsInWindow(): android.graphics.Rect {
+ val location = IntArray(2)
+ getLocationOnScreen(location)
+ return android.graphics.Rect(
+ location[0],
+ location[1],
+ location[0] + width,
+ location[1] + height
+ )
+}
+
+@Preview
+@Composable
+fun OverflowMenuPreview() {
+ val items = listOf(
+ MenuItemData(
+ title = "first item title",
+ subtitle = "first item subtitle",
+ icon = R.drawable.baseline_notifications_24,
+ onClick = {}
+ ),
+ MenuItemData(
+ title = "second item title",
+ subtitle = null,
+ icon = R.drawable.outline_notifications_active_24,
+ onClick = {}
+ ),
+ MenuItemData(
+ title = "third item title",
+ subtitle = null,
+ icon = R.drawable.baseline_notifications_24,
+ onClick = {}
+ ),
+ MenuItemData(
+ title = "fourth item title",
+ subtitle = null,
+ icon = R.drawable.baseline_notifications_24,
+ onClick = {}
+ )
+ )
+
+ OverflowMenu(
+ anchor = null,
+ expanded = true,
+ items = items,
+ onDismiss = { }
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt
index 0205985..2b4399f 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt
@@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
- * SPDX-FileCopyrightText: 2024 Your Name
+ * SPDX-FileCopyrightText: 2025 Marcel Hibbe
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -44,7 +44,7 @@ interface ChatMessageRepository : LifecycleAwareManager {
val removeMessageFlow: Flow
- fun initData(credentials: String, urlForChatting: String, roomToken: String)
+ fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?)
fun updateConversation(conversationModel: ConversationModel)
@@ -76,6 +76,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
*/
suspend fun getMessage(messageId: Long, bundle: Bundle): Flow
+ suspend fun getNumberOfThreadReplies(threadId: Long): Int
+
@Suppress("LongParameterList")
suspend fun sendChatMessage(
credentials: String,
@@ -84,7 +86,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
displayName: String,
replyTo: Int,
sendWithoutNotification: Boolean,
- referenceId: String
+ referenceId: String,
+ threadTitle: String?
): Flow>
@Suppress("LongParameterList")
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt
index a72657d..9fc6d36 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt
@@ -45,6 +45,14 @@ data class ChatMessage(
var token: String? = null,
+ var threadId: Long? = null,
+
+ var isThread: Boolean = false,
+
+ var threadTitle: String? = null,
+
+ var threadReplies: Int? = 0,
+
// guests or users
var actorType: String? = null,
@@ -424,7 +432,8 @@ data class ChatMessage(
AVATAR_REMOVED,
FEDERATED_USER_ADDED,
FEDERATED_USER_REMOVED,
- PHONE_ADDED
+ PHONE_ADDED,
+ THREAD_CREATED
}
companion object {
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt
index 7276f44..5dcdfe3 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt
@@ -59,7 +59,8 @@ interface ChatNetworkDataSource {
displayName: String,
replyTo: Int,
sendWithoutNotification: Boolean,
- referenceId: String
+ referenceId: String,
+ threadTitle: String?
): ChatOverallSingleMessage
fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap): Observable>
@@ -73,7 +74,8 @@ interface ChatNetworkDataSource {
baseUrl: String,
token: String,
messageId: String,
- limit: Int
+ limit: Int,
+ threadId: Int?
): List
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt
index f37ba12..c5bcd51 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt
@@ -119,11 +119,13 @@ class OfflineFirstChatRepository @Inject constructor(
private lateinit var conversationModel: ConversationModel
private lateinit var credentials: String
private lateinit var urlForChatting: String
+ private var threadId: Long? = null
- override fun initData(credentials: String, urlForChatting: String, roomToken: String) {
+ override fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) {
internalConversationId = currentUser.id.toString() + "@" + roomToken
this.credentials = credentials
this.urlForChatting = urlForChatting
+ this.threadId = threadId
}
override fun updateConversation(conversationModel: ConversationModel) {
@@ -143,7 +145,7 @@ class OfflineFirstChatRepository @Inject constructor(
Log.d(TAG, "conversationModel.internalId: " + conversationModel.internalId)
Log.d(TAG, "conversationModel.lastReadMessage:" + conversationModel.lastReadMessage)
- var newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId)
+ var newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId)
Log.d(TAG, "newestMessageIdFromDb: $newestMessageIdFromDb")
val weAlreadyHaveSomeOfflineMessages = newestMessageIdFromDb > 0
@@ -189,7 +191,7 @@ class OfflineFirstChatRepository @Inject constructor(
Log.e(TAG, "initial loading of messages failed")
}
- newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId)
+ newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId)
Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb")
}
@@ -203,9 +205,9 @@ class OfflineFirstChatRepository @Inject constructor(
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
val list = getMessagesBeforeAndEqual(
- newestMessageIdFromDb,
- internalConversationId,
- limit
+ messageId = newestMessageIdFromDb,
+ internalConversationId = internalConversationId,
+ messageLimit = limit
)
if (list.isNotEmpty()) {
handleNewAndTempMessages(
@@ -234,7 +236,8 @@ class OfflineFirstChatRepository @Inject constructor(
val amountBetween = chatDao.getCountBetweenMessageIds(
internalConversationId,
messageId,
- chatBlock.oldestMessageId
+ chatBlock.oldestMessageId,
+ threadId
)
Log.d(TAG, "amount of messages between newestMessageId and oldest message of same ChatBlock:$amountBetween")
@@ -284,7 +287,7 @@ class OfflineFirstChatRepository @Inject constructor(
)
withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap)
- val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId)
+ val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId, DEFAULT_MESSAGES_LIMIT)
if (loadFromServer) {
Log.d(TAG, "Starting online request for loadMoreMessages")
@@ -346,7 +349,10 @@ class OfflineFirstChatRepository @Inject constructor(
updateUiForLastCommonRead()
- val newestMessage = chatDao.getNewestMessageId(internalConversationId).toInt()
+ val newestMessage = chatBlocksDao.getNewestMessageIdFromChatBlocks(
+ internalConversationId,
+ threadId
+ ).toInt()
// update field map vars for next cycle
fieldMap = getFieldMap(
@@ -409,7 +415,7 @@ class OfflineFirstChatRepository @Inject constructor(
_messageFlow.emit(triple)
}
- private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long): Boolean {
+ private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long, amountToCheck: Int): Boolean {
val loadFromServer: Boolean
val blockForMessage = getBlockOfMessage(beforeMessageId.toInt())
@@ -421,26 +427,25 @@ class OfflineFirstChatRepository @Inject constructor(
Log.d(TAG, "The last chatBlock is reached so we won't request server for older messages")
loadFromServer = false
} else {
- // we know that beforeMessageId and blockForMessage.oldestMessageId are in the same block.
- // As we want the last DEFAULT_MESSAGES_LIMIT entries before beforeMessageId, we calculate if these
- // messages are DEFAULT_MESSAGES_LIMIT entries apart from each other
-
val amountBetween = chatDao.getCountBetweenMessageIds(
internalConversationId,
beforeMessageId,
- blockForMessage.oldestMessageId
+ blockForMessage.oldestMessageId,
+ threadId
)
- loadFromServer = amountBetween < DEFAULT_MESSAGES_LIMIT
+ loadFromServer = amountBetween < amountToCheck
Log.d(
TAG,
"Amount between messageId " + beforeMessageId + " and " + blockForMessage.oldestMessageId +
- " is: " + amountBetween + " so 'loadFromServer' is " + loadFromServer
+ " is: " + amountBetween + " and $amountToCheck were needed, so 'loadFromServer' is " +
+ loadFromServer
)
}
return loadFromServer
}
+ @Suppress("LongParameterList")
private fun getFieldMap(
lookIntoFuture: Boolean,
timeout: Int,
@@ -461,17 +466,23 @@ class OfflineFirstChatRepository @Inject constructor(
fieldMap["lastCommonReadId"] = it
}
+ threadId?.let { fieldMap["threadId"] = it.toInt() }
+
fieldMap["timeout"] = timeout
fieldMap["limit"] = limit
+
fieldMap["lookIntoFuture"] = if (lookIntoFuture) 1 else 0
fieldMap["setReadMarker"] = if (setReadMarker) 1 else 0
return fieldMap
}
+ override suspend fun getNumberOfThreadReplies(threadId: Long): Int =
+ chatDao.getNumberOfThreadReplies(internalConversationId, threadId)
+
override suspend fun getMessage(messageId: Long, bundle: Bundle): Flow {
Log.d(TAG, "Get message with id $messageId")
- val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId)
+ val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId, 1)
if (loadFromServer) {
val fieldMap = getFieldMap(
@@ -487,8 +498,10 @@ class OfflineFirstChatRepository @Inject constructor(
Log.d(TAG, "Starting online request for single message (e.g. a reply)")
sync(bundle)
}
- return chatDao.getChatMessageForConversation(internalConversationId, messageId)
- .map(ChatMessageEntity::asModel)
+ return chatDao.getChatMessageForConversation(
+ internalConversationId,
+ messageId
+ ).map(ChatMessageEntity::asModel)
}
@Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught")
@@ -652,11 +665,12 @@ class OfflineFirstChatRepository @Inject constructor(
internalConversationId = internalConversationId,
accountId = conversationModel.accountId,
token = conversationModel.token,
+ threadId = threadId,
oldestMessageId = oldestMessageIdForNewChatBlock,
newestMessageId = newestMessageIdForNewChatBlock,
hasHistory = hasHistory
)
- chatBlocksDao.upsertChatBlock(newChatBlock)
+ chatBlocksDao.upsertChatBlock(newChatBlock) // crash when no conversation thread exists!
updateBlocks(newChatBlock)
return chatMessagesFromSyncToProcess
@@ -713,7 +727,11 @@ class OfflineFirstChatRepository @Inject constructor(
var blockContainingQueriedMessage: ChatBlockEntity? = null
if (queriedMessageId != null) {
val blocksContainingQueriedMessage =
- chatBlocksDao.getChatBlocksContainingMessageId(internalConversationId, queriedMessageId.toLong())
+ chatBlocksDao.getChatBlocksContainingMessageId(
+ internalConversationId = internalConversationId,
+ threadId = threadId,
+ messageId = queriedMessageId.toLong()
+ )
val chatBlocks = blocksContainingQueriedMessage.first()
if (chatBlocks.size > 1) {
@@ -732,9 +750,10 @@ class OfflineFirstChatRepository @Inject constructor(
private suspend fun updateBlocks(chatBlock: ChatBlockEntity): ChatBlockEntity? {
val connectedChatBlocks =
chatBlocksDao.getConnectedChatBlocks(
- internalConversationId,
- chatBlock.oldestMessageId,
- chatBlock.newestMessageId
+ internalConversationId = internalConversationId,
+ threadId = threadId,
+ oldestMessageId = chatBlock.oldestMessageId,
+ newestMessageId = chatBlock.newestMessageId
).first()
return if (connectedChatBlocks.size == 1) {
@@ -761,6 +780,7 @@ class OfflineFirstChatRepository @Inject constructor(
internalConversationId = internalConversationId,
accountId = conversationModel.accountId,
token = conversationModel.token,
+ threadId = threadId,
oldestMessageId = oldestIdFromDbChatBlocks,
newestMessageId = newestIdFromDbChatBlocks,
hasHistory = hasHistory
@@ -784,7 +804,8 @@ class OfflineFirstChatRepository @Inject constructor(
chatDao.getMessagesForConversationBeforeAndEqual(
internalConversationId,
messageId,
- messageLimit
+ messageLimit,
+ threadId
).map {
it.map(ChatMessageEntity::asModel)
}.first()
@@ -798,7 +819,8 @@ class OfflineFirstChatRepository @Inject constructor(
chatDao.getMessagesForConversationBefore(
internalConversationId,
messageId,
- messageLimit
+ messageLimit,
+ threadId
).map {
it.map(ChatMessageEntity::asModel)
}.first()
@@ -838,7 +860,8 @@ class OfflineFirstChatRepository @Inject constructor(
displayName: String,
replyTo: Int,
sendWithoutNotification: Boolean,
- referenceId: String
+ referenceId: String,
+ threadTitle: String?
): Flow> {
if (!networkMonitor.isOnline.value) {
return flow {
@@ -854,14 +877,16 @@ class OfflineFirstChatRepository @Inject constructor(
displayName,
replyTo,
sendWithoutNotification,
- referenceId
+ referenceId,
+ threadTitle
)
val chatMessageModel = response.ocs?.data?.asModel()
val sentMessage = chatDao.getTempMessageForConversation(
internalConversationId,
- referenceId
+ referenceId,
+ threadId
).firstOrNull()
sentMessage?.let {
@@ -877,7 +902,8 @@ class OfflineFirstChatRepository @Inject constructor(
val failedMessage = chatDao.getTempMessageForConversation(
internalConversationId,
- referenceId
+ referenceId,
+ threadId
).firstOrNull()
failedMessage?.let {
it.sendStatus = SendStatus.FAILED
@@ -900,7 +926,11 @@ class OfflineFirstChatRepository @Inject constructor(
sendWithoutNotification: Boolean,
referenceId: String
): Flow> {
- val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).firstOrNull()
+ val messageToResend = chatDao.getTempMessageForConversation(
+ internalConversationId,
+ referenceId,
+ threadId
+ ).firstOrNull()
return if (messageToResend != null) {
messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend)
@@ -909,13 +939,14 @@ class OfflineFirstChatRepository @Inject constructor(
_updateMessageFlow.emit(messageToResendModel)
sendChatMessage(
- credentials,
- url,
- message,
- displayName,
- replyTo,
- sendWithoutNotification,
- referenceId
+ credentials = credentials,
+ url = url,
+ message = message,
+ displayName = displayName,
+ replyTo = replyTo,
+ sendWithoutNotification = sendWithoutNotification,
+ referenceId = referenceId,
+ threadTitle = null
)
} else {
flow {
@@ -949,8 +980,7 @@ class OfflineFirstChatRepository @Inject constructor(
try {
val messageToEdit = chatDao.getChatMessageForConversation(
internalConversationId,
- message.jsonMessageId
- .toLong()
+ message.jsonMessageId.toLong()
).first()
messageToEdit.message = editedMessageText
chatDao.upsertChatMessage(messageToEdit)
@@ -964,7 +994,7 @@ class OfflineFirstChatRepository @Inject constructor(
}
override suspend fun sendUnsentChatMessages(credentials: String, url: String) {
- val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first()
+ val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId, threadId).first()
tempMessages.sortedBy { it.internalId }.onEach {
sendChatMessage(
credentials,
@@ -973,7 +1003,8 @@ class OfflineFirstChatRepository @Inject constructor(
it.actorDisplayName,
it.parentMessageId?.toIntOrZero() ?: 0,
it.silent,
- it.referenceId.orEmpty()
+ it.referenceId.orEmpty(),
+ null
).collect { result ->
if (result.isSuccess) {
Log.d(TAG, "Sent temp message")
@@ -1042,6 +1073,7 @@ class OfflineFirstChatRepository @Inject constructor(
internalId = "$internalConversationId@_temp_$currentTimeMillies",
internalConversationId = internalConversationId,
id = currentTimeWithoutYear.toLong(),
+ threadId = threadId,
message = message,
deleted = false,
token = conversationModel.token,
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt
index e5edaee..6bb6836 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt
@@ -144,7 +144,8 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
displayName: String,
replyTo: Int,
sendWithoutNotification: Boolean,
- referenceId: String
+ referenceId: String,
+ threadTitle: String?
): ChatOverallSingleMessage =
ncApiCoroutines.sendChatMessage(
credentials,
@@ -153,7 +154,8 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
displayName,
replyTo,
sendWithoutNotification,
- referenceId
+ referenceId,
+ threadTitle
)
override fun pullChatMessages(
@@ -196,10 +198,11 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
baseUrl: String,
token: String,
messageId: String,
- limit: Int
+ limit: Int,
+ threadId: Int?
): List {
val url = ApiUtils.getUrlForChatMessageContext(baseUrl, token, messageId)
- return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit).ocs?.data ?: listOf()
+ return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit, threadId).ocs?.data ?: listOf()
}
override suspend fun getOpenGraph(
diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
index 67fd96b..5d05189 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
@@ -17,6 +17,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.google.gson.Gson
+import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.MediaPlayerManager
@@ -24,22 +26,27 @@ import com.nextcloud.talk.chat.data.io.MediaRecorderManager
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
+import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel.Companion.FOLLOWED_THREADS_EXIST
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
+import com.nextcloud.talk.models.MessageDraft
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
-import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.models.json.reminder.Reminder
+import com.nextcloud.talk.models.json.threads.ThreadInfo
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceData
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
+import com.nextcloud.talk.threadsoverview.data.ThreadsRepository
import com.nextcloud.talk.ui.PlaybackSpeed
+import com.nextcloud.talk.utils.ParticipantPermissions
+import com.nextcloud.talk.utils.UserIdUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -51,6 +58,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
@@ -65,6 +74,7 @@ class ChatViewModel @Inject constructor(
private val appPreferences: AppPreferences,
private val chatNetworkDataSource: ChatNetworkDataSource,
private val chatRepository: ChatMessageRepository,
+ private val threadsRepository: ThreadsRepository,
private val conversationRepository: OfflineConversationsRepository,
private val reactionsRepository: ReactionsRepository,
private val mediaRecorderManager: MediaRecorderManager,
@@ -73,6 +83,9 @@ class ChatViewModel @Inject constructor(
) : ViewModel(),
DefaultLifecycleObserver {
+ @Inject
+ lateinit var arbitraryStorageManager: ArbitraryStorageManager
+
enum class LifeCycleFlag {
PAUSED,
RESUMED,
@@ -84,6 +97,9 @@ class ChatViewModel @Inject constructor(
val disposableSet = mutableSetOf()
var mediaPlayerDuration = mediaPlayerManager.mediaPlayerDuration
val mediaPlayerPosition = mediaPlayerManager.mediaPlayerPosition
+ var chatRoomToken: String = ""
+ var messageDraft: MessageDraft = MessageDraft()
+ lateinit var participantPermissions: ParticipantPermissions
fun getChatRepository(): ChatMessageRepository = chatRepository
@@ -103,6 +119,8 @@ class ChatViewModel @Inject constructor(
mediaRecorderManager.handleOnPause()
chatRepository.handleOnPause()
mediaPlayerManager.handleOnPause()
+
+ saveMessageDraft()
}
override fun onStop(owner: LifecycleOwner) {
@@ -152,9 +170,8 @@ class ChatViewModel @Inject constructor(
val voiceMessagePlaybackSpeedPreferences: LiveData
diff --git a/app/src/main/res/layout/view_message_input.xml b/app/src/main/res/layout/view_message_input.xml
index 3f021f6..6bca893 100644
--- a/app/src/main/res/layout/view_message_input.xml
+++ b/app/src/main/res/layout/view_message_input.xml
@@ -225,6 +225,16 @@
android:src="@drawable/ic_baseline_mic_24"
android:contentDescription="@string/nc_description_record_voice" />
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index edfaf2c..b4aa47e 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -5,6 +5,7 @@
إضافة إلى الملاحظات
إضافة المحادثة %1$sإلى المُفضّلة
بحث في %s
+ الحالة غير متصل
أرشفة المحادثة
رد أرشفة محادثة، سيتم إخفاؤها افتراضياً. حدِّد الفلتر \"مُؤرشَفة\" لعرض المحادثات المؤرشفة. الإشارات المباشرة ستظل قابلة ليُشار إليها حتى بعد الأرشفة.
مؤرشفة
@@ -21,6 +22,7 @@
حظر
حظر مشارك
قائمة المحظورين
+ مشغول
التقويم Calendar
خيارات الاتصال المتقدمة
المكالمة ما زالت مستمرة منذ حوالي الساعة.
@@ -56,6 +58,7 @@
حدثت مشكلة أثناء رفع دردشاتك
حدث خطأ عند محاولة رفع الحظر عن مشارك
تعذّر حفظ %1$s
+ 15 دقيقة
مُجلّد
التحميل جارٍ …
%1$s (%2$d)
@@ -89,6 +92,7 @@
أكتب هنا للبحث …
بحث …
الرسائل
+ عدم إظهار جميع التنبيهات
تم استيراد الحساب المحدد وهو متاح الآن
عن
مستخدِم نشط
@@ -172,7 +176,7 @@
حذف الكل
احذف المحادثة
إذا قمت بحذف المحادثة، سوف يتم حذفها أيضًا لدى المشاركين الآخرين.
- حذف
+ إحْذِفِ الرسالة
تم حذف الرسالة بنجاح، لكن من الممكن أن تم تسريبها لخدمات أخرى
تمّ حذف المستخدِم %1$s
تخفيض رتبة مشرف
@@ -509,6 +513,9 @@
الأسبوع القادم
لم يتم حفظ أي رسائل غير متصلة بالإنترنت
لا ارتباط لرقم الهاتف بسبب أذونات ناقصة
+ \@-إشارة فقط
+ معطل
+ التلقائي
1 ساعة
مُتّصلٌ
حالة الاتصال
@@ -611,6 +618,7 @@
هذا الأسبوع
هذه رسالة اختبار
نهاية هذا الأسبوع
+ الرَّدّ
اليوم
غدا
ترجِم
diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml
index 289e624..792b9a7 100644
--- a/app/src/main/res/values-ast/strings.xml
+++ b/app/src/main/res/values-ast/strings.xml
@@ -5,6 +5,7 @@
Amestar a Notes
La conversación «%1$s» metióse en Favoritos
Buscar en: %s
+ Apaecer desconectáu
Archivóse
Bluetooth
Salida d\'audiu
@@ -14,6 +15,7 @@
L\'estáu afitóse automáticamente
Avatar
Ausente
+ Ocupáu
Calendariu
Concedióse\'l permisu de la cámara. Volvi escoyer la cámara.
Escoyer un avatar de la nube
@@ -37,6 +39,7 @@
Finar la llamada
Hebo un problema al cargar les charres
Nun se pue guardar «%1$s»
+ 15 minutos
carpeta
Cargando…
%1$s (%2$d)
@@ -61,6 +64,7 @@
Nun hai nengún resultáu de la busca
Buscar…
Mensaxes
+ Desactivar tolos avisos
Tocante a
Usuariu activu
Amestar una cuenta
@@ -114,7 +118,7 @@
Desanciar too
Desaniciar la conversación
Si desanicies la conversación, tamién la desanicies pa los demás participantes.
- Desaniciar
+ Desaniciar el mensaxe
Unviar el mensaxe
Cuenta actual
Srividor
@@ -272,6 +276,9 @@
Webinariu
Sí
La selmana que vien
+ Tolos mensaxes
+ Non
+ Por defeutu
1 hora
En llinia
Estáu en llinia
@@ -328,6 +335,7 @@
Esta selmana
Esto ye un mensaxe de prueba
Esta fin de selmana
+ Responder
Güei
Mañana
Traducir
diff --git a/app/src/main/res/values-b+en+001/strings.xml b/app/src/main/res/values-b+en+001/strings.xml
index aa8d0b9..dc6524f 100644
--- a/app/src/main/res/values-b+en+001/strings.xml
+++ b/app/src/main/res/values-b+en+001/strings.xml
@@ -5,6 +5,7 @@
Add to Notes
Added conversation %1$s to favourites
Search in %s
+ Appear offline
Archive conversation
Once a conversation is archived, it will be hidden by default. Select the filter \"Archived\" to view archived conversations. Direct mentions will still be received.
Archived
@@ -22,6 +23,7 @@
Ban
Ban participant
Bans list
+ Busy
Calendar
Advanced call options
Note: the call has been going on for an hour already.
@@ -61,6 +63,7 @@
There was a problem loading your chats
Error occurred when unbanning participant
Failed to save %1$s
+ 15 minutes
folder
Loading …
%1$s (%2$d)
@@ -76,6 +79,10 @@
You left the conversation %1$s
Load more results
Local time: %1$s
+ Location permission denied
+ Please enable it in the app settings
+ Location services disabled
+ Please enable location services (GPS) to use this feature
Lock conversation
Lock symbol
Lower hand
@@ -89,6 +96,7 @@
Biggest first
Smallest first
Message copied
+ Are you sure you want to delete this message?
Message deleted by you
Edited by %1$s
Tap to open poll
@@ -96,7 +104,7 @@
Start typing to search …
Search …
Messages
- Messages could not be loaded
+ Mute all notifications
Selected account is now imported and available
About
Active user
@@ -181,7 +189,7 @@
Delete all
Delete conversation
If you delete the conversation, it will also be deleted for all other participants.
- Delete
+ Delete message
Message deleted successfully, but it might have been leaked to other services
Delete now
User %1$s was removed
@@ -426,7 +434,6 @@
%1$s sent a deck card
Test server connection
Please upgrade your %1$s database
- Unable to connect to server
Failed to import selected account
The link to your %1$s web interface when you open it in the browser.
Import account from the %1$s app
@@ -546,6 +553,11 @@
No archived conversations
No offline messages saved
No phone number integration due to missing permissions
+ All messages
+ \@-mentions only
+ Off
+ Default
+ Follow conversation settings
1 hour
Online
Online status
@@ -555,7 +567,6 @@
Go to thread
Play/pause voice message
Playback speed control
- Please continue the login process in the browser
Add option
Edit vote
End poll
@@ -633,10 +644,9 @@
Voice
Show ban reason
Show banned participants
- Show threads
Favourite
You are not allowed to start a call
- Start a thread
+ Create a thread
started a call
Status message
Status Reverted
@@ -656,8 +666,11 @@
This is a test message
This weekend
Cancel thread creation
- %1$d replies
+ Thread notifications
+ Reply
Thread title
+ Threads
+ No threads found
Today
Tomorrow
Translate
@@ -703,6 +716,10 @@
- This conversation will be automatically deleted for everyone in %1$d day of no activity
- This conversation will be automatically deleted for everyone in %1$d days of no activity
+
+ - %d reply
+ - %d replies
+
- %d vote
- %d votes
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000..b752fd6
--- /dev/null
+++ b/app/src/main/res/values-be/strings.xml
@@ -0,0 +1,729 @@
+
+
+ Рэдагаваць
+ Дадаць
+ Дадаць да Нататак
+ Размова %1$s дададзена ў абранае
+ Пошук у %s
+ Паказваць \"Па-за сеткай\"
+ Архіваваць размову
+ Пасля архівавання размова будзе прадвызначана схавана. Выберыце фільтр \"Архіваванае\", каб праглядзець архіваваныя размовы. Прамыя згадкі ўсё роўна будуць атрыманыя.
+ Архіваванае
+ Архівавана %1$s
+ Аўдыявыклік
+ Bluetooth
+ Аўдыявыхад
+ Тэлефон
+ Дынамік
+ Правадная гарнітура
+ Ваш статус будзе зададзены аўтаматычна
+ Аватар
+ Адышоў
+ Кнопка назад
+ Заблакіраваць
+ Заблакіраваць удзельніка
+ Спіс блакіроўвак
+ Заняты
+ Каляндар
+ Дадатковыя опцыі выкліку
+ Выклік доўжыцца ўжо гадзіну
+ Выклік без апавяшчэння
+ Дазвол на камеру атрыманы. Выберыце камеру яшчэ раз.
+ Скасаваць уваход
+ Выбраць аватар з воблака
+ Ачысціць
+ Ачысціць паведамленне статусу праз
+ Закрыць
+ Значок Закрыць
+ Злучэнне ўсталявана
+ Няма злучэння з серверам
+ Злучэнне страчана — адпраўленыя паведамленні пастаўлены ў чаргу
+ Блакіроўка запісу для бесперапыннага запісу галасавога паведамлення
+ Размова архівавана
+ Размова у рэжыме \"толькі для чытання\"
+ Не атрымалася перавесці размову ў рэжым \"толькі для чытання\"
+ Размовы
+ Стварыць размову
+ Стварыць справаздачу
+ Уласны
+ Небяспечная зона
+ %1$s у %2$s
+ Выдаліць аватар
+ Выдаліць галасавы запіс
+ Выдалена размова %1$s
+ Не турбаваць
+ Не ачышчаць
+ Рэдагаваць
+ Паведамленні, старэйшыя за 24 гадзіны, нельга рэдагаваць
+ Рэдагаваць паведамленне
+ Нядаўнія
+ Зашыфравана
+ Завяршыць выклік
+ Завяршыць выклік для ўсіх
+ Узнікла праблема з загрузкай вашых чатаў
+ Адбылася памылка пры разблакіраванні ўдзельніка
+ Не ўдалося захаваць %1$s
+ 15 хвілін
+ папка
+ Загрузка …
+ %1$s (%2$d)
+ 4 гадзіны
+ Не ўдалося атрымаць запрашэнні, якія чакаюць разгляду
+ (адрэдагавана)
+ Унутраная заўвага
+ Нябачны
+ Не ўдалося атрымаць мовы
+ Памылка атрымання
+ Пазней сёння
+ Выйсці з выкліку
+ Вы выйшлі з размовы %1$s
+ Загрузіць больш вынікаў
+ Мясцовы час: %1$s
+ Няма доступу да геалакацыі
+ Уключыце яго ў наладах праграмы
+ Службы геалакацыі адключаны
+ Каб карыстацца гэтай функцыяй, уключыце службы геалакацыі (GPS)
+ Заблакіраваць размову
+ Сімвал блакіроўкі
+ Апусціць руку
+ Размова %1$s пазначана як прачытаная
+ Размова %1$s пазначана як непрачытаная
+ Згаданы
+ Спачатку новыя
+ Спачатку старыя
+ А-Я
+ Я-А
+ Спачатку вялікія
+ Спачатку малыя
+ Паведамленне скапіявана
+ Вы ўпэўнены, што хочаце выдаліць гэта паведамленне?
+ Вы выдалілі паведамленне
+ Адрэдагаваў(-ла) %1$s
+ Націсніце, каб адкрыць апытанне
+ Няма вынікаў пошуку
+ Пачніце ўводзіць тэкст для пошуку …
+ Пошук ...
+ Паведамленні
+ Адключыць усе апавяшчэнні
+ Выбраны ўліковы запіс імпартаваны і даступны
+ Аб праграме
+ Актыўны карыстальнік
+ Дадаць уліковы запіс
+ Запланавана выдаленне ўліковага запісу і таму ён не можа быць зменены
+ Адкрыць галоўнае меню
+ Дадаць далучэнне
+ Дадаць эмодзі
+ Дадаць да размовы
+ Дадаць удзельнікаў
+ У абранае
+ ОК, усё гатова!
+ Разблакіраваць %1$s
+ Каб уключыць Bluetooth дынамікі, дайце доступ да \"Прылад паблізу\".
+ Прыняць як відэавыклік
+ Прыняць як галасавы выклік
+ Змяніць аўдыявыхад
+ Укл./выкл. камеру
+ Пакласці слухаўку
+ Укл./выкл. мікрафон
+ Адкрыць рэжым «Picture-in-Picture»
+ Пераключыцца на відэа з сабой
+ УВАХОДНЫ
+ Назва размовы
+ Апавяшчэнні аб выкліках
+ %1$s падняў(-ла) руку
+ Перазлучэнне ...
+ ВЫКЛІК
+ %1$s у выкліку
+ %1$s з тэлефонам
+ %1$s з відэа
+ Няма адказу на працягу 45 секунд, націсніце, каб паспрабаваць яшчэ раз
+ %s, выклік
+ %s, відэавыклік
+ %s, галасавы выклік
+ Каб уключыць відэасувязь, дайце дазвол на доступ да камеры.
+ Скасаваць
+ Не ўдалося атрымаць магчымасці, скасаванне
+ Подпіс
+ Ці давяраеце вы дагэтуль невядомаму SSL-сертыфікату, выдадзенаму %1$s для %2$s, які дзейнічае з %3$s да %4$s?
+ Праверка сертыфіката
+ Ваша канфігурацыя SSL перашкодзіла злучэнню
+ Змяніць сертыфікат аўтэнтыфікацыі
+ Змяніць пароль
+ Скасаваць рэдагаванне
+ Скасаваць рэдагаванне
+ Выдаліць усе паведамленні
+ Усе паведамленні былі выдалены
+ Вы сапраўды хочаце выдаліць усе паведамленні ў гэтай размове?
+ Змяніць сертыфікат кліента
+ Наладзіць сертыфікат кліента
+ і
+ Капіяваць
+ Скапіявана ў буфер абмену
+ Стварыць
+ Адключана
+ Адхіліць
+ Нешта пайшло не так!
+ Больш параметраў
+ Задаць
+ Прапусціць
+ Невядомы
+ Выберыце сертыфікат аўтэнтыфікацыі
+ Злучэнне …
+ Гатова
+ Апісанне размовы
+ Інфармацыя пра размову
+ Відэавыклік
+ Галасавы выклік
+ Размова не знойдзена
+ Налады размовы
+ Далучайцеся да размовы або пачніце новую
+ Прывітайцеся з сябрамі і калегамі!
+ Капіяваць
+ Стварыць новую размову
+ Стварыць апытанне
+ Вы:
+ Сёння
+ Учора
+ Выдаліць
+ Выдаліць усе
+ Выдаліць размову
+ Калі вы выдаліце размову, яна будзе выдалена і для ўсіх астатніх удзельнікаў.
+ Выдаліць паведамленне
+ Паведамленне паспяхова выдалена, але яно магло трапіць у іншыя сэрвісы
+ Выдаліць зараз
+ Карыстальнік %1$s быў выдалены
+ Пазбавіць правоў мадэратара
+ Запісаць галасавое паведамленне
+ Адправіць паведамленне
+ Бягучы ўліковы запіс
+ Сервер
+ Ці ўсталявана на серверы праграма для апавяшчэнняў?
+ Карыстальнік
+ Статус карыстальніка ўключаны?
+ Версія Android
+ Праграма
+ Назва праграмы
+ Зарэгістраваныя карыстальнікі
+ Версія праграмы
+ Аптымізацыя батарэі ігнаруецца, усё добра
+ Аптымізацыя батарэі ўключана, што можа выклікаць праблемы. Вам варта адключыць аптымізацыю батарэі!
+ Налады батарэі
+ Прылада
+ Адкрыць кантрольны спіс вырашэння праблем
+ Адкрыць экран дыягностыкі
+ Адкрыць dontkillmyapp.com
+ Атрыманне апошняга push-токена firebase
+ Генерацыя апошняга push-токена firebase
+ Не зададзены push-токен firebase. Стварыце справаздачу пра памылку.
+ Push-токен firebase
+ Сэрвісы Google Play недаступны. Апавяшчэнні не падтрымліваюцца
+ Сэрвісы Google Play
+ Сэрвісы Google Play даступны
+ Апошняя рэгістрацыя push на push-проксі
+ Пакуль не зарэгістраваны на push-проксі
+ Апошняя рэгістрацыя push на серверы
+ Пакуль не зарэгістраваны на серверы
+ Метаінфармацыя
+ Стварэнне сістэмнай справаздачы
+ Канал апавяшчэнняў аб выкліках уключаны?
+ Ці ўключаны канал апавяшчэнняў пра паведамленні?
+ Дазволы на апавяшчэнні
+ Тэлефон
+ Версія сервера Talk
+ Версія сервера
+ Знешні
+ Унутраны
+ Рэжым сігналізацыі
+ Няправільны пароль
+ Сервер знаходзіцца ў рэжыме тэхнічнага абслугоўвання.
+ Праграма састарэла
+ Праграма занадта старая і больш не падтрымліваецца гэтым серверам. Абнавіце яе.
+ Абнавіць
+ Вы хочаце паўторна аўтарызавацца ці выдаліць гэты ўліковы запіс?
+ Захаванне гэтага медыяфайла ў сховішчы дазволіць любым іншым праграмам на вашай прыладзе атрымаць да яго доступ.
+ Працягнуць?
+ Не
+ Захаваць у сховішча?
+ Так
+ Не ўдалося атрымаць імя для паказу, скасаванне
+ Не ўдалося захаваць імя для паказу, скасаванне
+ Рэдагаваць
+ Рэдагаваць
+ Рэдагаваць паведамленне
+ Адрэдагавана адміністратарам
+ Меню размовы пра падзею
+ Расклад
+ 8 гадзін
+ 4 тыдні
+ Выкл.
+ 1 дзень
+ 1 гадзіна
+ 1 тыдзень
+ Тэрмін дзеяння паведамленняў
+ Тэрмін дзеяння паведамленняў у чаце можа скончыцца праз пэўны час. Заўвага: файлы, абагуленыя ў чаце, не будуць выдалены для ўладальніка, але больш не будуць абагулены ў размове.
+ Прыняць
+ Адхіліць
+ ад %1$s у %2$s
+ Няма запрашэнняў у чаканні
+ У вас ёсць запрашэнні ў чаканні
+ Назад
+ Патрабуецца дазвол на доступ да файлаў
+ Фільтраваць размовы
+ Вы: %1$s
+ Пераслаць
+ Пераслаць …
+ Галерэя
+ У вас яшчэ няма сервера?\nНацісніце тут, каб атрымаць яго ў пастаўшчыка
+ Зыходны код
+ Група
+ Госць
+ Гасцявы доступ
+ Немагчыма ўключыць/выключыць гасцявы доступ.
+ Дазволіць гасцей, каб абагульваць публічную спасылку для далучэння да гэтай размовы.
+ Дазволіць гасцей
+ Увядзіце пароль
+ Пароль гасцявога доступу
+ Памылка падчас задання/адключэння пароля.
+ Задайце пароль, каб абмежаваць, хто можа карыстацца публічнай спасылкай.
+ Абарона паролем
+ Адправіць запрашэнні паўторна
+ Запрашэнні не былі адпраўлены з-за памылкі.
+ Запрашэнні былі адпраўлены зноў.
+ Абагуліць спасылку на размову
+ Увядзіце паведамленне …
+ Аптымізацыя батарэі не ігнаруецца. Гэта трэба змяніць, каб апавяшчэнні працавалі ў фонавым рэжыме! Націсніце \"ОК\" і выберыце \"Усе праграмы\" -> %1$s -> \"Не аптымізаваць\"
+ Ігнараваць аптымізацыю батарэі
+ Важная размова
+ Статус карыстальніка \"Не турбаваць\" ігнаруецца падчас важных размоў
+ Памылковы час
+ Запрашэнні
+ Далучыцца да адкрытых размоў
+ Пакінуць
+ Перш чым пакінуць размову, вам трэба прызначыць новага мадэратара
+ %1$s | Апошняе змяненне: %2$s
+ Выйсці з размовы
+ Выхад з выкліку …
+ Агульная публічная ліцэнзія GNU, версія 3
+ Ліцэнзія
+ Дасягнуты ліміт у %s сімвалы(-аў)
+ Лобі
+ Гэта сустрэча запланавана на %1$s
+ Сустрэча неўзабаве пачнецца
+ Вы зараз чакаеце ў лобі.
+ Ваша бягучае месцазнаходжанне
+ патрэбны доступ да геалакацыі
+ Месцазнаходжанне невядома
+ Заблакіравана
+ Націсніце, каб разблакіраваць
+ Не зададзена
+ Пазначыць як прачытанае
+ Пазначыць як непрачытанае
+ Размова пазначана як важная
+ З размовы знята пазнака сакрэтнасці
+ Размова пазначана як сакрэтная
+ Пазнака важнасці размовы скасавана
+ Сустрэча завершана
+ Паведамленне дададзена ў нататкі
+ Не ўдалося
+ Не атрымалася адправіць паведамленне:
+ Па-за сеткай
+ Скасаваць адказ
+ Паведамленне прачытана
+ Адпраўка
+ Паведамленне адпраўлена
+ Мікрафон уключаны, і гук запісваецца
+ Каб уключыць галасавую сувязь, дайце дазвол на доступ да мікрафона.
+ Вы прапусцілі выклік ад %s
+ Мадэратар
+ Новая размова
+ Бачнасць
+ Непрачытаныя згадкі
+ Непрачытаныя паведамленні
+ %1$s недаступна (не ўсталявана або абмежавана адміністратарам)
+ Госць
+ Не
+ Няма адкрытых размоў
+ Няма адкрытых размоў, да якіх вы можаце далучыцца.\nАбо адкрытых размоў няма, або вы ўжо далучыліся да ўсіх з іх.
+ Няма проксі
+ Вам не дазволена ўключаць гук!
+ Вам не дазволена ўключаць відэа!
+ Не цяпер
+ %1$s на канале апавяшчэнняў %2$s
+ Выклікі
+ Апавяшчаць пры ўваходных выкліках
+ Паведамленні
+ Апавяшчаць пры ўваходных паведамленнях
+ Запампоўванні
+ Апавяшчаць пра ход выканання запампоўвання
+ Налады апавяшчэнняў
+ Апавяшчэнні наладжаны няправільна
+ Дазвол на апавяшчэнні і налады батарэі зададзены правільна, каб атрымліваць апавяшчэнні. Калі ў вас усё адно ўзнікаюць праблемы з атрыманнем апавяшчэнняў, праверце, ці ўключаны каналы апавяшчэнняў для выклікаў і паведамленняў. Дадатковую дапамогу можна знайсці на сайце DontKillMyApp.com або ў спісе пошуку і вырашэння праблем. Калі гэта не дапаможа, перайдзіце на экран дыягностыкі і адпраўце справаздачу пра памылку.
+ Вырашэнне праблем з апавяшчэннямі
+ Заўсёды апавяшчаць
+ Апавяшчаць пры згадванні
+ Ніколі не апавяшчаць
+ Вы па-за сеткай, праверце падключэнне
+ OK
+ Бягучая сустрэча
+ Адкрыць размову для зарэгістраваных карыстальнікаў
+ Таксама адкрыта для гасцей
+ Уладальнік
+ Удзельнікі
+ Дадаць удзельнікаў
+ Пароль
+ Задаць дазволы
+ Некаторыя дазволы былі адхілены.
+ Дайце дазволы
+ Адкрыць налады
+ Дайце дазволы ў раздзеле \"Налады\" > \"Дазволы\"
+ Уліковы запіс не знойдзены
+ Чат праз %s
+ Выключыць мікрафон
+ Уключыць мікрафон
+ Паведамленні
+ Прыватнасць
+ Асабістыя звесткі
+ Прызначыць мадэратарам
+ Публічная размова
+ Push-апавяшчэнні адключаны
+ Нешта пайшло не так, памылка: %1$s
+ Нешта пайшло не так, не ўдалося атрымаць тэставае push-паведамленне
+ Push-апавяшчэнне паспяхова адпраўлена. Цяпер вы павінны атрымацьна гэтай прыладзе апавяшчэнне з назвай \"Тэставае push-апавяшчэнне\".
+ Націсні і гавары
+ Калі мікрафон адключаны, націсніце і ўтрымлівайце, каб выкарыстоўваць функцыю «Націсні і гавары»
+ Нагадаць пазней
+ Выдаліць з абранага
+ Выдаліць групу і ўдзельнікаў
+ Выдаліць удзельніка
+ Выдаліць пароль
+ Выдаліць каманду і членаў
+ Перайменаваць размову
+ Перайменаваць
+ Адказаць
+ Адказаць прыватна
+ Пакой паспяхова захаваны
+ Захаваць
+ Паспяхова захавана
+ 30 секунд
+ 5 хвілін
+ 1 хвіліна
+ 10 хвілін
+ Неадкладна
+ 600
+ 60
+ 30
+ 300
+ Пошук
+ Ачысціць пошук
+ Выберыце ўліковы запіс
+ Абнавіць паведамленне
+ Адправіць галасавы запіс
+ Сакрэтная размова
+ Перадпрагляд паведамленняў будзе адключаны ў спісе размоў і апавяшчэннях
+ %1$s адправіў(-ла) GIF.
+ Вы адправілі GIF.
+ %1$s адправіў(-ла) відэа.
+ Вы адправілі відэа.
+ %1$s адправіў(-ла) аўдыя.
+ Вы адправілі аўдыя.
+ %1$s адправіў(-ла) відарыс.
+ Вы адправілі відарыс.
+ Праверыць злучэнне з серверам
+ Абнавіце базу даных %1$s
+ Не ўдалося імпартаваць выбраны ўліковы запіс
+ Спасылка на вэб-інтэрфейс %1$s пры адкрыцці ў браўзеры.
+ Імпартаваць уліковы запіс з праграмы %1$s
+ Імпартаваць уліковы запіс
+ Імпартаваць уліковыя запісы з праграмы %1$s
+ Імпартаваць уліковыя запісы
+ Выведзіце %1$s з рэжыму тэхнічнага абслугоўвання
+ Звяршыце ўсталяванне %1$s
+ Праверка злучэння
+ На серверы не ўсталявана праграма Talk, якая падтрымліваецца
+ Адрас сервера https://…
+ %1$s працуе толькі з %2$s версіі 13 і вышэй
+ Задаць новы пароль
+ Задаць пароль
+ Налады
+ Быў абноўлены ваш існуючы ўліковы запіс замест дадавання новага
+ Пашыраныя
+ Знешні выгляд
+ Выклікі
+ Звярніцеся да адміністратара
+ Адкрыйце экран дыягностыкі, каб праверыць налады або стварыць справаздачу пра памылку
+ Дыягностыка
+ Дае каманду клавіятуры адключыць персаналізаванае навучанне (без гарантый)
+ Клавіятура інкогніта
+ Няма гуку
+ Праграма Talk не ўсталявана на серверы, на якім вы спрабавалі прайсці аўтэнтыфікацыю
+ Апавяшчэнні
+ Апавяшчэнні адхілены
+ Апавяшчэнні дазволены
+ Паведамленні
+ Супастаўленне кантактаў па нумары тэлефона для інтэграцыі хуткага доступу да Talk у сістэмную праграму кантактаў
+ Памылка 429: занадта шмат запытаў
+ Вы можаце задаць свой нумар тэлефона, каб іншыя карыстальнікі маглі вас знайсці
+ Увядзіце нумар тэлефона
+ Памылковы нумар тэлефона
+ Нумар тэлефона паспяхова зададзены
+ Нумар тэлефона
+ Інтэграцыя нумара тэлефона
+ Прыватнасць
+ Хост проксі
+ Пароль проксі
+ Порт проксі
+ Тып проксі
+ Імя карыстальніка проксі
+ Абагульваць мой статус прачытання і паказваць статус прачытання іншых
+ Статус прачытання
+ Паўторна аўтарызаваць уліковы запіс
+ Выдаліць
+ Выдаліць уліковы запіс
+ Пацвердзіце свой намер выдаліць бягучы ўліковы запіс.
+ Блакіраваць %1$s з дапамогай блакіроўкі экрана Android або біяметрычнага метаду, які падтрымліваецца
+ Час чакання блакіроўкі пры бяздзейнасці
+ Блакіроўка экрана
+ Забараняе рабіць здымкі экрана ў спісе нядаўняга і ўнутры праграмы
+ Бяспека экрана
+ Версія сервера вельмі старая і не будзе падтрымлівацца ў наступным выпуску!
+ Версія сервера занадта старая і не падтрымліваецца гэтай версіяй праграмы для Android.
+ Сервер не падтрымліваецца
+ Праграма для апавяшчэнняў не ўсталявана на серверы
+ Зададзена рэжымам эканоміі зараду батарэі
+ Цёмная
+ Сістэмная
+ тэма
+ Светлая
+ Тэма
+ Абагуліць мой статус набору тэксту і паказаць статус набору тэксту іншых
+ Статус набору тэксту даступны толькі пры выкарыстанні высокапрадукцыйнага бэкенда (HPB)
+ Статус набору тэксту
+ Проксі-сервер патрабуе ўліковых даных
+ Папярэджанне
+ Толькі бягучы ўліковы запіс можа быць перааўтарызаваны
+ Абагуліць кантакт
+ Патрабуецца дазвол на чытанне кантактаў
+ Абагуліць бягучае месцазнаходжанне
+ Абагуліць спасылку
+ Абагуліць месцазнаходжанне
+ Абагуліць гэта месцазнаходжанне
+ Выберыце ўліковы запіс
+ Абагуленыя элементы
+ Відарысы, файлы, галасавыя паведамленні…
+ Няма абагуленых элементаў
+ Месцазнаходжанне
+ Абагуленае месцазнаходжанне
+ Калі апавяшчэнні наладжаны няправільна, паказваць звычайнае папярэджанне
+ Паказваць звычайнае папярэджанне аб апавяшчэннях
+ Сартаваць па
+ Пачаць супольны чат
+ Час пачатку
+ Змяніць уліковы запіс
+ Каманда
+ Тэставае push-апавяшчэнне
+ Вынікі тэста
+ Сёння ў %1$s
+ Заўтра ў %1$s
+ Выберыце файлы
+ Адправіць гэтыя файлы карыстальніку %1$s?
+ Адправіць гэты файл карыстальніку %1$s?
+ Не ўдалося запампаваць
+ Не ўдалося запампаваць %1$s
+ Няўдача
+ Абагульванне з %1$s
+ Запампаваць з прылады
+ Запампоўванне
+ %1$s у %2$s - %3$s\%%
+ Зрабіць фота
+ Зняць відэа
+ Карыстальнік
+ Відэазапіс ад %1$s
+ Запіс размовы ад %1$s (%2$s)
+ Утрымлівайце для запісу, адпусціце для адпраўкі.
+ Патрабуецца дазвол на аўдыязапіс
+ « Правядзіце пальцам, каб скасаваць
+ Вэбінар
+ Так
+ На наступным тыдні
+ Няма архіваваных размоў
+ Няма захаваных пазасеткавых паведамленняў
+ Няма інтэграцыі нумара тэлефона з-за адсутнасці дазволаў
+ Усе паведамленні
+ Толькі згадкі з @
+ Выкл.
+ Прадвызначаныя
+ Прытрымлівацца налад размовы
+ 1 гадзіну
+ У сетцы
+ Статус у сетцы
+ Адкрытыя размовы
+ Адкрыць у Файлах
+ Адкрыць Нататкі
+ Перайсці да гутаркі
+ Прайграць/прыпыніць галасавое паведамленне
+ Кіраванне хуткасцю прайгравання
+ Дадаць варыянт
+ Рэдагаваць голас
+ Завяршыць апытанне
+ Вы сапраўды хочаце завяршыць гэта апытанне? Гэта нельга адрабіць.
+ Вы не можаце прагаласаваць, выбраўшы больш варыянтаў для гэтага апытання.
+ Некалькі адказаў
+ Выдаліць варыянт %1$d
+ Варыянт %1$d
+ Варыянты
+ Прыватнае апытанне
+ Пытанне
+ Ваша пытанне
+ Вынікі
+ Налады
+ Галасаваць
+ Голас адпраўлены
+ Зададзены раней
+ Не ўдалося прачытаць QR-код
+ Падняць руку
+ Усе
+ Абагульванне файлаў са сховішча немагчыма без дазволаў
+ Нядаўнія гутаркі
+ Выклік запісваецца
+ Скасаваць пачатак запісу
+ Не ўдалося зрабіць запіс. Звярніцеся да адміністратара.
+ Пачаць запіс
+ Вы сапраўды хочаце спыніць запіс?
+ Спыніць запіс выкліку
+ Спыніць запіс
+ Спыненне запісу …
+ Для ўсіх выклікаў патрабуецца згода на запіс
+ Запіс можа ўключаць ваш голас, відэа з камеры і абагульванне экрана. Перад далучэннем да размовы патрэбна ваша згода. Вы даяце згоду?
+ Патрабаваць згоду на запіс перад далучэннем да выкліку ў гэтай размове
+ Згода на запіс
+ Выклік можа быць запісаны.
+ Запіс
+ Размова %1$s выдалена з абранага
+ Размова %1$s перайменавана
+ Адправіць паўторна
+ Скінуць статус
+ Немагчыма далучыцца да іншых пакояў падчас выкліку
+ Захаваць
+ Сканіраваць QR-код
+ Сінхранізацыя толькі з даверанымі серверамі
+ Федэратыўны
+ Бачна толькі карыстальнікам гэтага сервера і гасцям
+ Лакальна
+ Бачна толькі людзям, якія супалі праз інтэграцыю нумара тэлефона з дапамогай Talk на мабільным тэлефоне
+ Прыватна
+ Сінхранізацыя з даверанымі серверамі і глабальнай і публічнай адраснай кнігай
+ Апублікавана
+ Пераключальнік бачнасці
+ Змяніць узровень прыватнасці %1$s
+ Прагартаць уніз
+ Значок пошуку
+ с таму
+ Выбрана
+ Адправіць электронны ліст
+ Адправіць
+ Вам забаронена абагульваць змесціва ў гэтым чаце
+ Адправіць …
+ Адправіць без апавяшчэння
+ Задаць
+ Задаць аватар з камеры
+ Задаць статус
+ Задаць
+ Абагуліць
+ Далучайцеся да размовы %1$s у %2$s
+ Аўдыя
+ Файл
+ Медыя
+ Іншае
+ Апытанне
+ Запіс выкліку
+ Голас
+ Паказаць прычыну блакіроўкі
+ Паказаць заблакіраваных удзельнікаў
+ Абранае
+ Вам не дазволена пачынаць выклік
+ Стварыць гутарку
+ пачаў(-ла) выклік
+ Паведамленне статусу
+ Статус вернуты
+ Пераключыцца на галоўны пакой
+ Зрабіць фота
+ Памылка пры здымку
+ Немагчыма зрабіць фота без дазволаў
+ Зрабіць фота паўторна
+ Адправіць
+ Пераключыць камеру
+ Абрэзаць фота
+ Зменшыць памер відарыса
+ Уключыць/выключыць ліхтарык
+ 30 хвілін
+ На гэтым тыдні
+ Гэта тэставае паведамленне
+ У гэты ўік-энд
+ Скасаваць стварэнне гутаркі
+ Апавяшчэнні гутаркі
+ Адказаць
+ Загаловак гутаркі
+ Гутаркі
+ Гутарак не знойдзена
+ Сёння
+ Заўтра
+ Перакласці
+ Пераклад
+ Скапіяваць перакладзены тэкст
+ Вызначыць мову
+ Налады прылады
+ Не ўдалося вызначыць мову
+ Не ўдалося перакласці
+ З
+ На
+ і яшчэ 1 пішуць …
+ пішуць …
+ піша …
+ і яшчэ %1$s пішуць …
+ Разархіваваць размову
+ Пасля таго, як размова будзе разархівавана, яна зноў будзе прадвызначана паказвацца.
+ Разархівавана %1$s
+ Разблакіраваць
+ Непрачытанае
+ Запампаваць новы аватар з прылады
+ %1$s не на працы і можа не адказаць
+ %1$s сёння не на працы
+ Замена:
+ Аватар карыстальніка
+ Адрас
+ Поўнае імя
+ Электронная пошта
+ Нумар тэлефона
+ Twitter
+ Вэб-сайт
+ Статус
+ Не ўдалося атрымаць асабістую інфармацыю карыстальніка.
+ Асабістая інфармацыя не зададзена
+ Дадайце імя, аватар і кантактную інфармацыю на старонку вашага профілю.
+ Відэавыклік
+ Які ў вас статус?
+
+ - Паглядзець %d падобнае паведамленне
+ - Паглядзець %d падобныя паведамленні
+ - Паглядзець %d падобных паведамленняў
+ - Паглядзець %d падобных паведамленняў
+
+
+ - Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дзень бяздзейнасці.
+ - Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дні бяздзейнасці.
+ - Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дзён бяздзейнасці.
+ - Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дзён бяздзейнасці.
+
+
+ - %d адказ
+ - %d адказы
+ - %d адказаў
+ - %d адказаў
+
+
+ - %d голас
+ - %d галасы
+ - %d галасоў
+ - %d галасоў
+
+
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
index a5183c2..9245474 100644
--- a/app/src/main/res/values-bg-rBG/strings.xml
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -12,6 +12,7 @@
Състоянието ви беше зададено автоматично
Аватар
Винаги
+ Зает
Kалендар
Разширени опции за повикване
Обаждане без известие
@@ -35,6 +36,7 @@
Прекратяване на разговора за всички
Имаше проблем със зареждането на вашите чатове
Неуспешно записване %1$s
+ 15 минути
папка
Зареждане …
%1$s (%2$d)
@@ -135,7 +137,7 @@
Изтриване на всички
Изтриване на разговора
Ако изтриете разговора, той ще бъде изтрит и за всички останали участници.
- Изтрий
+ Изтриване на съобщението
Съобщението е изтрито успешно, но може да е изтекло към други услуги
Понижаване от модератор
Запис на гласово съобщение
@@ -394,6 +396,10 @@
Да
Следваща седмица
Няма интеграция на телефонен номер поради липсващи права
+ Всички съобщения
+ \@-само споменавания
+ Изключен
+ По подразбиране
1 час
На линия
Състояние
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index f743377..244c33e 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -5,6 +5,7 @@
Afegeix-ho a les notes
S\'ha afegit la conversa %1$s als preferits
Cerca a %s
+ Apareixeu fora de línia
Arxivar la conversa
Un cop arxivada una conversa, s\'ocultarà per defecte. Seleccioneu el filtre \"Arxivada\" per a veure les converses arxivades. Es continuaran rebent les mencions directes.
Arxivat
@@ -22,6 +23,7 @@
Prohibeix
Prohibeix el participant
Llista de prohibicions
+ Ocupat
Calendari
Opcions avançades de trucada
La trucada porta una hora en marxa.
@@ -59,6 +61,7 @@
S\'ha produït un problema carregant els xats
S\'ha produït un error en anul·lar la prohibició del participant
No s\'ha pogut desar %1$s
+ 15 minuts
carpeta
Carregant …
%1$s (%2$d)
@@ -92,6 +95,7 @@
Comença a escriure per cercar …
Cerca …
Missatges
+ Silencieu totes les notificacions
El compte que heu seleccionat s\'ha importat i ja és disponible
Quant a
Usuari actiu
@@ -175,7 +179,7 @@
Suprimir-ho tot
Suprimir la conversa
Al suprimir la conversa, també serà suprimit per a la resta de participants.
- Suprimir
+ Suprimeix el missatge
El missatge s\'ha suprimit correctament, però podria haver-se filtrat a altres serveis
S\'ha suprimit l\'usuari %1$s
Deposat des del moderador
@@ -501,6 +505,8 @@
Setmana següent
No s\'han desat els missatges fora de línia
No hi ha integració amb el número de telèfon perquè falten permisos
+ Apagat
+ Per defecte
1 hora
En línia
Estat en línia
@@ -600,6 +606,7 @@
Aquesta setmana
Això és un missatge de prova
Aquest cap de setmana
+ Resposta
Avui
Demà
Traducció
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index ef23a23..9eea8be 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -5,6 +5,7 @@
Přidat do Poznámek
Konverzace %1$s přidána do oblíbených
Hledat v %s
+ Jevit se offline
Archivovat konverzaci
Jakmile je konverzace archivována, bude ve výchozím stavu skrytá. Pokud si chcete zobrazit archivované konverzace, vyberte filtr „Archivováno“. Přímá zmínění budou chodit i nadále.
Archivováno
@@ -22,6 +23,7 @@
Vyloučit
Vyloučit účastníka
Seznam vyloučení
+ Zaneprázdněn(a)
Kalendář
Pokročilé předvolby pro hovor
Hovor trvá už hodinu.
@@ -61,6 +63,7 @@
Došlo k problému při načítání vašich chatů
Došlo k chybě při rušení vyloučení účastníka
Nepodařilo se uložit %1$s
+ 15 minut
složka
Načítání…
%1$s (%2$d)
@@ -76,6 +79,10 @@
Opustili jste konverzaci %1$s
Načíst další výsledky
Místní čas: %1$s
+ Oprávnění k přístupu k poloze odepřeno
+ Povolte to v nastavení aplikace
+ Služby určování polohy vypnuty
+ Abyste mohli používat tuto funkci zapněte polohové služby (GPS)
Uzamknout konverzaci
Symbol zámku
Přestat se hlásit
@@ -89,6 +96,7 @@
Největší jako první
Nejmenší jako první
Zpráva zkopírována
+ Opravdu chcete tuto zprávu smazat?
Zprávu jste smazali
Upraveno %1$s
Klepnutím anketu otevřete
@@ -96,6 +104,7 @@
Hledejte psaním…
Hledat…
Zprávy
+ Ztlumit veškerá upozornění
Zvolený účet byl naimportován a je k dispozici
O aplikaci
Aktivní uživatel
@@ -180,7 +189,7 @@
Smazat vše
Smazat konverzaci
Pokud konverzaci smažete, bude smazána také pro všechny její ostatní účastníky.
- Smazat
+ Smazat zprávu
Zpráva úspěšně smazána, ale možná unikla do jiných služeb
Smazat nyní
Uživatel %1$s byl odebrán
@@ -544,6 +553,11 @@
Žádné archivované konverzace
Neuloženy žádné zprávy pro režim bez připojení
Není možné propojit telefonní čísla, protože chybí potřebná oprávnění
+ Všechny zprávy
+ Pouze zmínky ve stylu @jméno
+ Vypnuto
+ Výchozí
+ Nastavení následování konverzace
1 hodina
Online
Stav online
@@ -553,7 +567,6 @@
Přejít na vlákno
Přehrát/pozastavit hlasovou zprávu
Ovládání rychlosti přehrávání
- Pokračujte v procesu přihlášení ve webovém prohlížeči
Přidat volbu
Upravit hlas
Ukončit anketu
@@ -631,10 +644,9 @@
Hlas
Zobrazit důvod vyloučení
Zobrazit vyloučené účasníky
- Zobrazit vlákna
Oblíbené
Nemáte oprávnění pro zahájení hovoru
- Začít vlákno
+ Vytvořit vlákno
zahájen hovor
Stavová zpráva
Stav vrácen na původní
@@ -653,8 +665,12 @@
Tento týden
Toto je zkušební zpráva
Tento víkend
- %1$d odpovědí
+ Zrušit vytváření vlákna
+ Notifikace ohledně vlákna
+ Odpověď
Název vlákna
+ Vláken
+ Nenalezeny žádné hrozby
Dnes
Zítra
Překládání
@@ -704,6 +720,12 @@
- Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity.
- Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity.
+
+ - %d odpověď
+ - %d odpovědi
+ - %d odpovědí
+ - %d odpovědi
+
- %d hlas
- %d hlasy
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 387d434..85ceb6f 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -1,14 +1,16 @@
- Rediger
+ Redigér
Tilføj
Tilføj til Noter
Tilføjede samtalen %1$s til favoritter
Søg i %s
+ Er offline
Arkivér samtale
Når en samtale er arkiveret, vil den blive skjult som standard. Vælg filteret \"Arkiveret\" for at se arkiverede samtaler. Direkte omtaler vil stadig blive modtaget.
Arkiveret
Arkiverede %1$s
+ Lydopkald
Bluetooth
Lydudgang
Telefon
@@ -21,20 +23,23 @@
Bloker
Bloker deltager
Liste over blokerede
+ Optaget
Kalender
Avancerede opkaldsmuligheder
Opkaldet har været igang i en time.
Ring uden notifikation
Kameratilladelse er givet. Vælg kamera igen.
- Annuller log på
+ Annullér log på
Vælg avatar fra skyen
Ryd status notifikation
Ryd status notifikationer efter
Luk
Luk ikon
Forbindelse oprettet
+ Ingen forbindelse til server
Forbindelse mistet - Sendte beskeder er i kø
Lås optagelse for kontinuerlig optagelse af talebeskeden
+ Samtale er arkiveret
Samtalen er skrivebeskyttet
Samtalen kunne ikke indstilles som skrivebeskyttet
Samtaler
@@ -44,16 +49,21 @@
Farezone
%1$s i %2$s
Slet avatar
+ Slet stemmeoptagelse
Slettede samtale %1$s
Forstyr ikke
Ryd ikke
- Rediger
+ Redigér
+ Beskeder der er ældre end 24 timer kan ikke redigeres
+ Rediger besked
Nylige
Krypteret
+ Afslut opkald
Afslut opkald for alle
Der opstod et problem med at indlæse dine chats
En fejl opstod under fjernelse af blokering af deltager
Kunne ikke gemme %1$s
+ 15 minutter
mappe
Loading …
%1$s (%2$d)
@@ -68,18 +78,25 @@
Forlad opkald
Du forlod samtalen %1$s
Indlæs flere resultater
+ Lokal tid: %1$s
+ Lokationstilladelse nægtet
+ Aktiver det venligst under app indstillingerne
+ Lokationsservice deaktiveret
+ Aktiver venligst lokationsservice (GPS) for at bruge denne funktion
Lås samtale
Låsesymbol
Sænk hånden
Marker samtalen %1$s som læst
Marker samtalen %1$s som ulæst
- Nævnt
+ Omtalt
Nyeste først
Ældste først
A - Å
Å - A
Største først
Mindste først
+ Besked kopieret
+ Er du sikker på at du ønsker at slette denne besked?
Besked slettet af dig
Redigeret af %1$s
Rør for at åbne afstemning
@@ -87,6 +104,7 @@
Start indtastning for at søge ...
Søg ...
Beskeder
+ Vis ikke notifikationer
Den valgte konto blev importeret og kan bruges nu
Om
Aktiv bruger
@@ -132,6 +150,8 @@
Din SSL indstilling forhindrede forbindelse
Skift godkendelsescertifikat
Skift adgangskode
+ Annuller redigering
+ Annuller redigering
Slet alle beskeder
Alle beskeder blev slettet
Ønsker du virkelig at slette alle beskeder i denne samtale?
@@ -162,14 +182,16 @@
Kopier
Opret ny samtale
Opret afstemning
+ Dig:
I dag
I går
Slet
Slet alt
Slet samtale
Hvis du sletter konversationen, vil den også blive slettet for alle andre deltagere.
- Slet
+ Slet besked
Besked slettet, men kan kan være lækket til andre services
+ Slet nu
Brugeren %1$s blev fjernet
Degrader fra moderator
Optag stemmebesked
@@ -217,7 +239,7 @@
Server er aktuelt i vedligeholdelsestilstand.
App\'en er forældet is outdated
Denne app er for gammel og understøttes ikke længere af denne server. Opdater venligst.
- Opdater
+ Opdatér
Ønsker du at genautorisere eller slette denne konto?
Hvis du gemmer dette medie i dit lager, så vil enhver anden app på dit apparat kunne tilgå det.
Fortsæt?
@@ -227,8 +249,11 @@
Display navn kunne ikke hentes, afbrydes
Kunne ikke opbevare skærmnavn, afbryder
Redigér
- Rediger
+ Redigér
+ Rediger besked
Redigeret af admin
+ Begivenheds samtale menu
+ Planlæg
8 timer
4 uger
Slået fra
@@ -245,6 +270,7 @@
Du har afventende invitationer
Tilbage
Tilladelse til filadgang er krævet
+ Filtrer samtaler
Bruger følger et offentligt link
Dig: %1$s
Videresend
@@ -271,8 +297,11 @@
Batterioptimering bliver ikke ignoreret. Dette bør ændres for at være sikker på at notifikationer virker i baggrunden! Klik venligst på OK og vælg \"Alle apps\" -> %1$s -> Optimer ikke
Ignorer batterioptimering
Vigtig samtale
+ Brugerstatus \"Forstyr ikke\" ignoreres ved vigtige samtaler
+ Ugyldigt tidspunkt
Invitationer
Deltag i åbne samtaler
+ Behold
Du skal udnævne en ny moderator inden du kan forlade samtalen.
%1$s| Sidst ændret: %2$s
Forlad samtale
@@ -292,6 +321,12 @@
Ikke indstillet
Marker som læst
Marker som ulæst
+ Samtale markeret som vigtig
+ Samtal ikke markeret som følsom
+ Samtale markeret som følsom
+ Samtale ikke markeret som vigtig
+ Mødet sluttede
+ Besked til føjet til noter
Mislykkede
Kunne ikke sende besked:
Offline
@@ -299,6 +334,7 @@
Besked læst
Sender
Beskeden blev sendt
+ Mikrofon er aktiveret og lyden optages
For at aktivere kommunikation så tillad venligst \"Mikrofon\"
Du missede et opkald fra %s
Moderator
@@ -331,6 +367,7 @@
Giv aldrig besked
For nuværende offline, venligst kontroller din forbindelse
OK
+ Igangværende møde
Begynd en samtale til registrerede brugere
Også åben for gæste app brugere
Ejer
@@ -352,6 +389,9 @@
Forfrem til moderator
Offentlig samtale
Pushbeskeder er slået fra
+ Desværre, noget gik galt, fejlen er %1$s
+ Desværre, noget gik galt, kan ikke hente test push besked
+ Push notifikation afsendt. Du bør nu modtage en notifikation på apparatet med titlen \'Test push notifikationer\'
Tryk-for-at-tale
Med mikrofonen deaktiveret, hold&nede for at bruge Tryk-for-at-tale
Påmind mig senere
@@ -364,6 +404,7 @@
Omdøb
Besvar
Svar privat
+ Rummet er bevaret
Gem
Gemt
30 sekunder
@@ -378,6 +419,10 @@
Søg
Ryd søgning
Vælg konto
+ Opdater besked
+ Send stemmeoptagelse
+ Følsom samtale
+ Besked forhåndsvisning vil blive deaktiveret i samtalelisten og notifikationer
%1$s sendte en GIF.
Du sendte en GIF.
%1$s sendte en video.
@@ -448,6 +493,7 @@
Serverversionen er for gammel og er ikke understøttet af denne version af Android app\'en
Ikke understøttet server
Server notifikations app ikke installeret
+ Sat af batterisparer
Mørk
Brug system default
tema
@@ -479,6 +525,10 @@
Start tid
Skift konto
Team
+ Test push notifikationer
+ Testresultater
+ I dag kl. %1$s
+ I morgen kl. %1$s
Vælg filer
Send disse filer til %1$s?
Send denne fil til %1$s?
@@ -500,17 +550,25 @@
Webinar
Ja
Næste uge
+ Ingen arkiverede samtaler
Ingen offline beskeder gemt
Ingen telefonnummerintegration på grund af manglende rettigheder
+ Alle beskeder
+ Kun omtalt med @
+ Deaktivér
+ Standard
+ Følg samtaleindstillinger
1 time
Online
Online status
Åbne samtaler
Åben i appe\'en filer
+ Åben noter
+ Gå til tråd
Afspil/pauser stemmebesked
Afspilningshastighedskontrol
Tilføj valg
- Rediger stemme
+ Redigér stemme
Afslut afstemning
Ønsker du virkelig at afslutte denne afstemning? Dette kan ikke fortrydes.
Du kan ikke stemme med flere muligheder i denne afstemning.
@@ -526,9 +584,11 @@
Stemme
Stemme indsendt
Tidligere sat
+ QR koden kunne ikke læses
Løft hånden
Alle
Deling af filer fra lager er ikke muligt uden rettigheder
+ Seneste tråde
Opkaldet optages
Annuller opstagelsesstart
Optagelsen fejlede. Kontakt venligst din administrator
@@ -586,6 +646,7 @@
Vis blokerede deltagere
Favorit
Du har ikke tilladelse til at starte et opkald
+ Opret en tråd
startede et opkald
Statusbesked
Status omvendt
@@ -604,6 +665,12 @@
Denne uge
Dette er en testbesked
Denne weekend
+ Annuller trådoprettelse
+ Trådnotifikationer
+ Svar
+ Trådtitel
+ Tråde
+ Ingen tråde fundet
I dag
I morgen
Oversæt
@@ -645,6 +712,14 @@
- Se %d lignende besked
- Se %d lignende beskeder
+
+ - Denne samtale vil automatisk blive slettet for alle efter%1$d dage med inaktivitet
+ - Denne samtale vil automatisk blive slettet for alle efter %1$d dage med inaktivitet
+
+
+ - %d svar
+ - %d svar
+
- %d stemme
- %d stemmer
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 889e32c..3805405 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -5,6 +5,7 @@
Zu Notizen hinzufügen
Unterhaltung %1$s zu Favoriten hinzugefügt
Suche in %s
+ Offline erscheinen
Unterhaltung archivieren
Sobald eine Unterhaltung archiviert ist, wird sie standardmäßig ausgeblendet. Wählen Sie den Filter \"Archiviert\", um archivierte Unterhaltungen anzuzeigen. Direkte Erwähnungen werden weiterhin empfangen.
Archiviert
@@ -22,6 +23,7 @@
Sperren
Teilnehmer sperren
Sperrliste
+ Beschäftigt
Kalender
Erweiterte Anrufoptionen
Der Anruf läuft seit einer Stunde.
@@ -61,6 +63,7 @@
Beim Laden Ihrer Chats ist ein Problem aufgetreten
Fehler beim Entsperren des Teilnehmers
%1$s konnte nicht gespeichert werden
+ 15 Minuten
Ordner
Lade …
%1$s (%2$d)
@@ -76,6 +79,10 @@
Sie haben die Unterhaltung %1$s verlassen
Weitere Ergebnisse laden
Ortszeit: %1$s
+ Standort-Berechtigung abgelehnt
+ Dies bitte in in App-Einstellungen aktivieren
+ Standortdienste deaktiviert
+ Bitte die Standortdienste (GPS) aktivieren, um diese Funktion zu nutzen
Unterhaltung sperren
Schloss-Symbol
Hand herunternehmen
@@ -89,6 +96,7 @@
Größste zuerst
Kleinste zuerst
Nachricht kopiert
+ Möchten Sie diese Nachricht löschen?
Nachricht von Ihnen gelöscht
Bearbeitet von %1$s
Tippen, um die Umfrage zu öffnen
@@ -96,7 +104,7 @@
Beginnen Sie mit der Eingabe, um zu suchen …
Suche …
Nachrichten
- Nachrichten konnten nicht geladen werden.
+ Alle Benachrichtigungen stummschalten
Das ausgewählte Konto ist nun importiert und verfügbar
Über
Aktiver Benutzer
@@ -181,7 +189,7 @@
Alle löschen
Unterhaltung löschen
Wenn Sie diese Unterhaltung löschen, dann wird diese auch für alle anderen Teilnehmer gelöscht.
- Löschen
+ Nachricht löschen
Nachricht gelöscht, sie wurde aber möglicherweise an andere Dienste weitergegeben.
Jetzt löschen
Benutzer %1$s wurde entfernt
@@ -426,7 +434,6 @@
%1$s hat eine Deck-Karte gesendet
Prüfe Verbindung zum Server
Bitte aktualisieren Sie Ihre %1$s Datenbank
- Verbindung zum Server kann nicht hergestellt werden
Das ausgewählte Konto konnte nicht importiert werden
Der Link zu Ihrer %1$s Webseite, wenn Sie diese im Browser öffnen.
Ein Konto aus der App %1$s importieren
@@ -546,6 +553,11 @@
Keine archivierten Unterhaltungen
Keine Offlinenachrichten gespeichert
Keine Rufnummernintegration aufgrund fehlender Berechtigungen
+ Alle Nachrichten
+ Nur @-Erwähnungen
+ Aus
+ Standard
+ Unterhaltungseinstellungen folgen
1 Stunde
Online
Online-Status
@@ -555,7 +567,6 @@
Zu Thema gehen
Sprachnachricht wiedergeben/pausieren
Steuerung der Wiedergabegeschwindigkeit
- Bitte den Anmeldeprozess im Browser fortsetzen
Option hinzufügen
Abstimmung bearbeiten
Umfrage beenden
@@ -633,10 +644,9 @@
Sprachnachrichten
Sperrgrund anzeigen
Gesperrte Teilnehmer anzeigen
- Themen anzeigen
Favorit
Sie dürfen keinen Anruf zu tätigen
- Ein Thema starten
+ Ein Thema erstellen
hat einen Anruf begonnen
Statusnachricht
Status zurückgesetzt
@@ -656,8 +666,11 @@
Dies ist eine Testnachricht
Dieses Wochenende
Themenerstellung abbrechen
- %1$d Antworten
+ Themen-Benachrichtigungen
+ Antwort
Thementitel
+ Themen
+ Keine Themen gefunden
Heute
Morgen
Übersetzen
@@ -703,6 +716,10 @@
- Diese Unterhaltung wird für alle bei Inaktivität in %1$d Tag gelöscht
- Diese Unterhaltung wird für alle bei Inaktivität in %1$d Tagen gelöscht
+
+ - %d Antwort
+ - %d Antworten
+
- %d Stimme
- %d Stimmen
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 6a25dc2..c97cbd3 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -3,13 +3,18 @@
Επεξεργασία
Προσθήκη
Αναζήτηση στο %s
+ Αρχειοθέτηση συνομιλίας
Αρχειοθετήθηκε
Έξοδος ήχου
Τηλέφωνο
Μεγάφωνο
+ Η κατάστασή σας ορίστηκε αυτόματα
Εικόνα προφίλ
Λείπω
+ Απασχολημένος
Ημερολόγιο
+ Η κλήση εκτελείται για μία ώρα.
+ Κλήση χωρίς ειδοποίηση
Επιλογή εικόνας προφίλ από το cloud
Εκκαθάριση μηνύματος κατάστασης
Εκκαθάριση μηνύματος κατάστασης μετά από
@@ -27,15 +32,19 @@
Πρόσφατα
Κρυπτογραφημένο
Τερματισμός κλήσης
+ Τερματισμός κλήσης για όλους
Αποτυχία αποθήκευσης %1$s
+ 15 λεπτά
φάκελος
Φόρτωση …
%1$s (%2$d)
4 ώρες
+ (επεξεργασμένο)
Αόρατο
Αργότερα σήμερα
Αποχώρηση από την κλήση
Φόρτωση περισσοτέρων αποτελεσμάτων
+ Τοπική ώρα: %1$s
Κλειδώστε τη συνομιλία
Κατεβάστε το χέρι
Νεότερο πρώτα
@@ -45,7 +54,9 @@
Μεγαλύτερο πρώτα
Μικρότερο πρώτα
Το μήνυμα διαγράφηκε από εσάς
+ Επεξεργάστηκε από %1$s
Κανένα αποτέλεσμα
+ Αναζήτηση …
Μηνύματα
Ο επιλεγμένος λογαριασμός έχει εισαχθεί και είναι τώρα διαθέσιμος
Περί
@@ -110,14 +121,16 @@
Αντιγραφή
Δημιουργία νέας συνομιλίας
Δημιουργία ψηφοφορίας
+ Εσείς:
Σήμερα
Χθές
Διαγραφή
Διαγραφή όλων
Διαγραφή συνομιλίας
Αν διαγράψετε την συνομιλία, θα διαγραφεί επίσης για όλους τους υπόλοιπους συμμετέχοντες.
- Διαγραφή
+ Διαγραφή μηνύματος
Το μήνυμα διαγράφηκε με επιτυχία, αλλά ενδέχεται να έχει διαρρεύσει σε άλλες υπηρεσίες
+ Διαγραφή τώρα
Υποβάθμιση από συντονιστή
Εγγραφή φωνητικού μηνύματος
Αποστολή μηνύματος
@@ -143,13 +156,16 @@
Επεξεργασία
Επεξεργασία μηνύματος
8 ώρες
+ 4 εβδομάδες
Απενεργοποίηση
1 μέρα
1 ώρα
1 εβδομάδα
+ Τα μηνύματα συνομιλίας μπορούν να λήξουν μετά από συγκεκριμένο χρόνο. Σημείωση: Τα αρχεία που κοινοποιούνται στη συνομιλία δεν θα διαγραφούν για τον κάτοχο, αλλά δεν θα κοινοποιούνται πλέον στη συνομιλία.
Αποτυχία λήψης ρυθμίσεων σήματος
Αποδοχή
Απόρριψη
+ Δεν υπάρχουν εκκρεμείς προσκλήσεις
Πίσω
Χρήστης από δημόσιο σύνδεσμο
Εσείς: %1$s
@@ -160,6 +176,7 @@
Λήψη πηγαίου κώδικα
Ομάδα
Επισκέπτης
+ Πρόσβαση επισκεπτών
Επιτρέψτε τους επισκέπτες
Εισάγετε συνθηματικό
Ορίστε έναν κωδικό πρόσβασης για να περιορίσετε ποιος μπορεί να χρησιμοποιήσει τον δημόσιο σύνδεσμο.
@@ -168,8 +185,11 @@
Κοινή χρήση συνδέσμου συνομιλίας
Εισάγετε ένα μήνυμα ...
Σημαντική συνομιλία
+ Η κατάσταση χρήστη \"Μην ενοχλείτε\" αγνοείται για σημαντικές συνομιλίες
Προσκλήσεις
Δημιουργία νέας συνομιλίας
+ Διατήρηση
+ Πρέπει να προβιβάσετε έναν νέο συντονιστή πριν μπορέσετε να εγκαταλείψετε τη συνομιλία
%1$s Τελευταία τροποποίηση %2$s
Εγκατάλειψη συνομιλίας
Αποχώρηση από την κλήση ...
@@ -177,6 +197,8 @@
Άδεια χρήσης
Το όριο %s χαρακτήρων έχει συμπληρωθεί
Αναμονή
+ Αυτή η συνάντηση είναι προγραμματισμένη για %1$s
+ Η συνάντηση θα ξεκινήσει σύντομα
Αυτή τη στιγμή είστε σε αναμονή
Η τρέχουσα τοποθεσία σας
απαιτείται δικαιώματα τοποθεσίας
@@ -187,12 +209,14 @@
Σήμανση ως αναγνωσμένο
επισήμανση ως μή-αναγνωσμένο
Απέτυχε
+ Εκτός σύνδεσης
Ακύρωση απάντησης
Το μήνυμα διαβάστηκε
Το μήνυμα στάλθηκε
Συντονιστής
Νέα συνομιλία
Ορατότητα
+ Αδιάβαστες αναφορές
Μη αναγνωσμένα μηνύματα
Το %1$s δεν είναι διαθέσιμο (δεν έχει εγκατασταθεί ή έχει απαγορευθεί από τον διαχειριστή)
Επισκέπτης
@@ -223,6 +247,7 @@
Ιδιωτικότητα
Προσωπικές πληροφορίες
Προαγωγή από συντονιστή
+ Δημόσια συνομιλία
Οι ειδοποιήσεις push απενεργοποιήθηκαν
Push-to-talk
Με απενεργοποιημένο το μικρόφωνο, πιέστε και κρατήστε το & για χρήση του Push-to-talk
@@ -230,6 +255,7 @@
Αφαίρεση από τα αγαπημένα
Αφαίρεση ομάδων και μελών
Αφαίρεση συμμετέχοντα
+ Αφαίρεση ομάδας και μελών
Μετονομασία συνομιλίας
Μετονομασία
Απάντηση
@@ -247,6 +273,8 @@
Αναζήτηση
Εκκαθάριση αναζήτησης
Επιλογή λογαριασμού
+ Ευαίσθητη συνομιλία
+ Η προεπισκόπηση μηνυμάτων θα απενεργοποιηθεί στη λίστα συνομιλιών και στις ειδοποιήσεις
%1$s έστειλε GIF.
Στείλατε εικόνα GIF.
%1$s έστειλε βίντεο.
@@ -313,6 +341,7 @@
θέμα
Φωτεινό
Θέμα
+ Κοινή χρήση της κατάστασης πληκτρολόγησής μου και εμφάνιση της κατάστασης πληκτρολόγησης των άλλων
Ο διαμεσολαβητής απαιτεί διαπιστευτήρια
Προειδοποίηση
Μόνο ο παρόν λογαριασμός μπορεί να επαναεγκριθεί
@@ -322,12 +351,15 @@
Διαμοιρασμός τοποθεσίας
Διαμοιρασμός αυτής της τοποθεσίας
Επιλογή λογαριασμού
+ Κοινόχρηστα αντικείμενα
Κάρτα του Deck
+ Δεν υπάρχουν κοινόχρηστα αντικείμενα
Τοποθεσία
Διαμοιρασμένες τοποθεσίες
Ταξινόμηση κατά
Ώρα έναρξης
Αλλαγή λογαριασμού
+ Ομάδα
Επιλογή αρχείων
Να σταλούν αυτά τα αρχεία στον %1$s;
Να σταλεί αυτό το αρχείο στον %1$s;
@@ -337,6 +369,7 @@
Γίνεται μεταφόρτωση
Βγάλε φωτογραφία
Χρήστης
+ Εγγραφή ομιλίας από %1$s (%2$s)
Κρατήστε για εγγραφή, αφήστε για αποστολή.
Απαιτούνται δικαιώματα για ηχογράφηση
« Σύρετε για ακύρωση
@@ -344,23 +377,40 @@
Ναι
Επόμενη εβδομάδα
Δεν υπάρχει ενσωμάτωση αριθμού τηλεφώνου λόγω έλλειψης δικαιωμάτων
+ Απενεργοποιημένο
+ Προεπιλογή
+ Ακολούθηση ρυθμίσεων συνομιλίας
1 ώρα
Σε σύνδεση
Κατάσταση σε σύνδεση
Άνοιγμα συνομιλιών
Άνοιγμα την εφαρμογή Αρχεία
+ Μετάβαση στη συζήτηση
Αναπαραγωγή/παύση ηχητικού μηνύματος
Προσθήκη επιλογής
+ Τερματισμός δημοσκόπησης
+ Πολλαπλές απαντήσεις
Επιλογές
Ιδιωτική δημοσκόπηση
+ Ερώτηση
Αποτελέσματα
Ρυθμίσεις
Ψήφος
+ Προηγουμένως ορισμένη
Σηκώστε το χέρι
\'Ολα
Η κοινή χρήση αρχείων από τον χώρο αποθήκευσης δεν είναι δυνατή χωρίς δικαιώματα
+ Πρόσφατες συζητήσεις
+ Ακύρωση έναρξης εγγραφής
+ Η εγγραφή απέτυχε. Παρακαλούμε επικοινωνήστε με τον διαχειριστή σας.
Έναρξη εγγραφής
+ Διακοπή εγγραφής
+ Η συγκατάθεση εγγραφής απαιτείται για όλες τις κλήσεις
+ Απαιτείται συγκατάθεση εγγραφής πριν από τη συμμετοχή σε κλήση σε αυτή τη συνομιλία
+ Συγκατάθεση εγγραφής
+ Η κλήση μπορεί να καταγράφεται.
Καταγραφή
+ Επαναφορά κατάστασης
Αποθήκευση
Scan QR Code
Συγχρονισμός μόνο με έμπιστους διακομιστές.
@@ -378,6 +428,7 @@
Αποστολή email
Αποστολή σε
Αποστολή σε …
+ Αποστολή χωρίς ειδοποίηση
Ορισμός
Ορισμός κατάστασης
Ορισμός μηνύματος κατάστασης
@@ -386,8 +437,10 @@
Αρχείο
Μέσα ενημέρωσης
Άλλο
+ Δημοσκόπηση
Ομιλία
Αγαπημένο
+ Δημιουργία συζήτησης
Μήνυμα κατάστασης
Βγάλε μια φωτογραφία
Αποστολή
@@ -396,14 +449,21 @@
30 λεπτά
Αυτή την εβδομάδα
Αυτό το Σαββατοκύριακο
+ Ειδοποιήσεις συζήτησης
+ Τίτλος συζήτησης
+ Συζητήσεις
Σήμερα
Αύριο
Μετάφραση
+ Αντιγραφή μεταφρασμένου κειμένου
Ανίχνευση γλώσσας
Ρυθμίσεις συσκευής
Δεν ήταν δυνατός ο εντοπισμός της γλώσσας
+ Η μετάφραση απέτυχε
Από
Έως
+ Απο-αρχειοθέτηση συνομιλίας
+ Απο-αποκλεισμός
Μη αναγνωσμένο
Μεταφόρτωση νέας εικόνας προφίλ από την συσκευή
Άβαταρ χρήστη
@@ -418,4 +478,8 @@
Δεν ορίστηκαν προσωπικές πληροφορίες
Προσθέστε όνομα, εικόνα και λεπτομέρειες επικοινωνίας στο προφίλ σας.
Ποια είναι η κατάστασή σας;
+
+ - %d απάντηση
+ - %d απαντήσεις
+
diff --git a/app/src/main/res/values-es-rEC/strings.xml b/app/src/main/res/values-es-rEC/strings.xml
index 0a872b7..23c2e45 100644
--- a/app/src/main/res/values-es-rEC/strings.xml
+++ b/app/src/main/res/values-es-rEC/strings.xml
@@ -3,6 +3,7 @@
Editar
Guardar
Compartir en %s
+ Aparecer como desconectado
Archivado
Bluetooth
Salida de audio
@@ -12,6 +13,7 @@
Tu estado se estableció automáticamente
Avatar
Ausente
+ Ocupado
Calendario
Opciones avanzadas de llamada
Llamar sin notificación
@@ -36,6 +38,7 @@
Finalizar llamada para todos
Hubo un problema al cargar tus chats
Error al guardar %1$s
+ 15 minutos
carpeta
Loading …
%1$s (%2$d)
@@ -59,6 +62,7 @@
Comienza a escribir para buscar...
Buscar...
Mensajes
+ Silenciar todas las notificaciones
La cuenta seleccionada ha sido importada y está disponible
Acerca
Usuario activo
@@ -138,7 +142,7 @@
Borrar todo
Borrar conversación
Si borras la conversación, también se borrará para todos los demás participantes.
- Borrar
+ Eliminar mensaje
El mensaje se borró correctamente, pero podría haber sido filtrado a otros servicios
Degradar de moderador
Grabar mensaje de voz
@@ -398,6 +402,9 @@
Sí
Semana siguiente
No hay integración de número de teléfono debido a permisos faltantes
+ Todos los mensajes
+ Solo @-menciones
+ Apagado
1 hora
En línea
Estado en línea
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index a52e468..103b2c5 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -5,6 +5,7 @@
Añadir a Notas
Se añadió la conversación %1$s a los favoritos
Buscar en %s
+ Aparecer como desconectado
Archivar conversación
Una vez que una conversación es archivada, estará escondida por defecto. Seleccione el filtro \"Archivadas\" para ver las conversaciones archivadas. Cualquier mención directa seguirá siendo recibida.
Archivada
@@ -22,6 +23,7 @@
Bloquear
Bloquear participante
Lista de bloqueados
+ Ocupado
Calendario
Opciones avanzadas de llamada
La llamada ha estado activa por una hora.
@@ -61,6 +63,7 @@
Hubo un problema cargando sus chats
Ocurrió un error al desbloquear al participante
Fallo al guardar %1$s
+ 15 minutos
carpeta
Cargando …
%1$s (%2$d)
@@ -76,6 +79,10 @@
Ud. Abandonó la conversación %1$s
Cargar más resultados
Hora local: %1$s
+ Se denegó el permiso de ubicación
+ Por favor, habilítelo en los ajustes de aplicación
+ Servicios de ubicación desactivados
+ Por favor, habilite los servicios de ubicación (GPS) para utilizar esta característica
Bloquear conversación
Símbolo de bloqueo
Bajar la mano
@@ -89,6 +96,7 @@
Más grandes primero
Más pequeñas primero
Mensaje copiado
+ ¿Está seguro que desea eliminar este mensaje?
Has eliminado este mensaje
Editado por %1$s
Pulse para abrir la encuesta
@@ -96,6 +104,7 @@
Empiece a escribir para buscar…
Buscar …
Mensajes
+ Silenciar todas las notificaciones
La cuenta seleccionada ha sido importada y está disponible
Acerca de
Activar usuario
@@ -180,7 +189,7 @@
Eliminar todos
Eliminar conversación
Si borras la conversación, también se borrará para los demás participantes.
- Eliminar
+ Eliminar mensaje
Mensaje borrado con éxito, pero puede haber sido filtrado a otros servicios
Eliminar ahora
El Usuario %1$s fue eliminado
@@ -545,6 +554,11 @@
No hay conversaciones archivadas
No hay mensajes fuera de línea guardados
No hubo integración de la agenda telefónica debido a la falta de permisos
+ Todos los mensajes
+ Solo las menciones con @
+ Apagado
+ Predeterminado
+ Seguir los ajustes de la conversación
1 hora
En línea
Estado en línea
@@ -554,7 +568,6 @@
Ir al hilo
Reproducir/pausar mensaje de voz
Control de velocidad de reproducción
- Por favor, continúe el proceso de inicio de sesión en el navegador
Añadir opción
Editar voto
Cerrar la encuesta
@@ -632,10 +645,9 @@
Voz
Mostrar razón de bloqueo
Mostrar participantes bloqueados
- Mostrar hilos
Favorito
No está autorizado a iniciar una llamada
- Iniciar un hilo
+ Crear un hilo
inició una llamada
Mensaje de estado
El estado ha sido revertido
@@ -654,8 +666,12 @@
Esta semana
Esto es un mensaje de prueba
Este fin de semana
- %1$d respuestas
+ Cancelar la creación del hilo
+ Notificaciones para hilos
+ Responder
Título del hilo
+ Hilos
+ No se encontraron hilos
Hoy
Mañana
Traducir
@@ -703,6 +719,11 @@
- Esta conversación será eliminada automáticamente para todos tras %1$d días de inactividad
- Esta conversación será eliminada automáticamente para todos tras %1$d días de inactividad
+
+ - %d respuesta
+ - %d respuestas
+ - %d respuestas
+
- %d voto
- %d votos
diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml
index 25b7ec4..5db65f2 100644
--- a/app/src/main/res/values-et-rEE/strings.xml
+++ b/app/src/main/res/values-et-rEE/strings.xml
@@ -5,6 +5,7 @@
Lisa Märkmetesse
„%1$s“ vestlus on märgitud lemmikuks
Otsi siin: %s
+ Sellega paistad olema võrgust väljas
Arhiveeri vestlus
Kui vestlus on arhiveeritud, siis on ta vaikimisi peidetud. Saad neid leida, kui kasutad filtrivalikut „Arhiveeritud“. Otsemainimiste teave liigub sellele vaatamata.
Arhiveeritud
@@ -22,6 +23,7 @@
Sea suhtluskeeld
Sea osalejale suhtluskeeld
Suhtluskeelu saanute loend
+ Hõivatud
Kalender
Kõne lisavalikud
Kõne on kestnud üle tunni.
@@ -61,6 +63,7 @@
Sinu vestluste laadimisel tekkis viga
Osaleja suhtluskeelu eemaldamisel tekkis viga
„%1$s“ salvestamine ei õnnestunud
+ 15 minutit
kaust
Laadimisel...
%1$s (%2$d)
@@ -76,6 +79,10 @@
Sa lahkusid %1$s vestlusest
Laadi veel tulemusi
Kohalik aeg: %1$s
+ Õigused asukoha tuvastamiseks on keelatud
+ Palun luba see rakenduse seadistustest
+ Asukohateenused pole sisse lülitatud
+ Selle funktsionaalsuses kasutamiseks palun luba asukohateenused (GPS)
Lukusta vestlus
Lukuikoon
Lase käsi alla
@@ -89,6 +96,7 @@
Suuremad esimesena
Väiksemad esimesenaa
Sõnum on kopeeritud
+ Kas oled kindel, et tahad selle sõnumi kustutada?
Sina kustutasid sõnumi
%1$s muutis sõnumit
Küsitluse avamiseks klõpsi
@@ -96,7 +104,7 @@
Otsimiseks alusta kirjutamist…
Otsi…
Sõnumid
- Sõnumite laadimine ei õnnestunud
+ Sellega summutad teavitused
Valitud kasutajakonto on nüüd imporditud ja saadaval
Info
Aktiivne kasutaja
@@ -181,7 +189,7 @@
Kustuta kõik
Kustuta vestlus
Kui kustutad selle vestluse, siis kustub see ka kõikide osalejate jaoks.
- Kustuta
+ Kustuta sõnum
Sõnumi kustutamine õnnestus, kuid võis juhtuda, et ta oli juba teistesse sõnumiteenustesse edastatud
Kustuta kohe
Kasutaja „%1$s“ on eemaldatud
@@ -426,7 +434,6 @@
%1$s saatis kanbani kaardi
Testi ühendust serveriga
Palun uuenda oma %1$si andmebaasi
- Ei õnnestu luua ühendust serveriga
Valitud kasutajakonto importimine ei õnnestunud
See on sinu %1$s kasutajaliidese veebiaadress, kui sa avad ta veebibrauseris.
Impordi kasutajakonto rakendusest %1$s
@@ -546,6 +553,11 @@
Arhiveeritud vestlusi pole
Ühtegi vallasrežiimis salvestatud sõnumit ei leidu
Telefoninumbrite lõiming ei toimi puuduvate õiguste tõttu
+ Kõik sõnumid
+ Vaid @-mainimised
+ Pole kasutusel
+ Vaikimisi
+ Järgi vestluse seadistusi
1 tund
Online
Võrgus staatus
@@ -555,7 +567,6 @@
Ava jutulõng
Esita häälsõnumit või peata esitus
Taasesituse kiiruse juhtimine
- Palun jätka sisselogimisprotsessi veebibrauseris
Lisa valik
Muuda oma häält
Lõpeta küsitlus
@@ -633,10 +644,9 @@
Hääl
Näita suhtluskeelu seadmise põhjus
Näita suhtluskeelu saanud osalejaid
- Näita jutulõngu
Lemmik
Sul pole luba kõne algatamiseks
- Koosta jutulõng
+ Alusta jutulõnga
helistas
Staatuse teade
Olek on tagasi pööratud
@@ -656,8 +666,11 @@
See on testsõnum
See nädalavahetus
Katkesta jutulõnga koostamine
- %1$d vastust
+ Jutulõngade teavitused
+ Vasta
Jutulõnga pealkiri
+ Jutulõngad
+ Ühtegi jutulõnga ei leidu
Täna
Homme
Tõlgi
@@ -703,6 +716,10 @@
- See vestlus kustub automaatselt kõigi osalejate jaoks, kui siin pole olnud tegevust %1$d päeva jooksul.
- See vestlus kustub automaatselt kõigi osalejate jaoks, kui siin pole olnud tegevust %1$d päeva jooksul.
+
+ - %d vastus
+ - %d vastust
+
- %d hääl
- %d häält
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index cddc8db..f8aa767 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -17,6 +17,7 @@
Kanpoan
Debekatu
Debekatu parte-hartzailea
+ Lanpetua
Egutegia
Dei-ezarpen aurreratuak
Deia ordubetez egon da martxan.
@@ -46,6 +47,7 @@
Amaitu deia denontzat
Arazo bat gertatu da zure txatak kargatzean
%1$s gordetzeak huts egin du
+ 15 minutu
karpeta
Kargatzen …
%1$s(%2$d)
@@ -160,7 +162,7 @@
Ezabatu denak
Ezabatu elkarrizketa
Elkarrizketa hau ezabatzen baduzu beste kideentzat ere ezabatuko da.
- Ezabatu
+ Mezua ezabatzen
Mezua ondo ezabatu da, baina baliteke beste zerbitzu batzuetara filtratu izana
%1$s erabiltzailea kendu da
Moderatzailetik degradatua
@@ -452,6 +454,8 @@
Bai
Hurrengo astea
Ezin izan da telefono zenbakia integratu, baimen falta dela eta
+ Off
+ Lehenetsia
Ordu 1
Linean
Lineako egoera
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index ea35e0b..e7aabaa 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -1,9 +1,11 @@
ویرایش
+ افزودن
افزودن به یادداشتها
گفتگوی %1$sبه علاقهمندیها افزوده شد
جستجو در %s
+ نمایش آفلاین
Archived
بلوتوث
خروجی صدا
@@ -17,6 +19,7 @@
انسداد
مسدود کردن شرکتکننده
فهرست مسدودیها
+ مشغول
تقویم
گزینههای پیشرفتهٔ تماس
پیام وضعیت را پاک کن
@@ -35,6 +38,7 @@
اخیر
Encrypted
End call for everyone
+ ۱۵ دقیقه
پوشه
بارگذاری …
%1$s (%2$d)
@@ -56,6 +60,7 @@
جستجو نتیجهای نداشت
جستجو ...
پیام ها
+ خاموش کردن همه اعلانات
حساب انتخابشده اکنون وارد شده و در دسترس است .
درباره
کاربر فعال
@@ -125,7 +130,7 @@
همه را حذف کنید
مکالمه را حذف کنید
اگر این مکالمه را حذف کنید ، برای سایر شرکت کنندگان نیز حذف خواهد شد.
- حذف
+ Delete message
تنزل مقام از مدیر
ضبط پیام صوتی
فرستادن پیام
@@ -347,6 +352,9 @@
وبینار
بله
هفتهٔ بعد
+ All messages
+ \@-mentions only
+ پیشفرض
۱ ساعت
برخط
وضعیت برخط
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index ed7d075..baab44e 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -3,6 +3,7 @@
Muokkaa
Lisää
Etsi kohteesta %s
+ Näytä olevan poissa
Arkistoitu
Bluetooth
Äänen ulostulo
@@ -12,6 +13,7 @@
Tilatietosi asetettiin automaattisesti
Profiilikuva
Poissa
+ Varattu
Kalenteri
Tämä puhelu on kestänyt yhden tunnin.
Puhelu ilman ilmoitusta
@@ -34,6 +36,7 @@
Lopeta puhelu
Päätä puhelu kaikkien osalta
Ei voitu tallentaa %1$s
+ 15 minuuttia
kansio
Ladataan…
%1$s (%2$d)
@@ -55,6 +58,7 @@
Aloita kirjoittaminen hakeaksesi…
Hae…
Viestit
+ Mykistä kaikki ilmoitukset
Valittu tili on nyt tuotu ja käytettävissä
Tietoja
Aktiivinen käyttäjä
@@ -117,7 +121,7 @@
Poista kaikki
Poista keskustelu
Jos poistat keskustelun, keskustelu poistetaan myös muilta osapuolilta.
- Poista
+ Poista viesti
Alenna moderaattorista
Äänitä ääniviesti
Lähetä viesti
@@ -341,6 +345,7 @@
Webinaari
Kyllä
Seuraava viikko
+ Oletus
1 tunti
Paikalla
Online-tila
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index b60002e..06cfe12 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -5,6 +5,7 @@
Ajouter à Notes
La conversation %1$s a été ajoutée aux favoris
Rechercher dans %s
+ Apparaître hors-ligne
Archiver la conversation
Lorsqu\'une conversation est archivée, elle est cachée par défaut. Choisissez le filtre \"Archivés\" pour voir les conversations archivées. Les mentions directes seront toujours bien reçues.
Archivé
@@ -15,13 +16,14 @@
Téléphone
Haut-parleur
Écouteurs filaires
- Votre état a été automatiquement défini
+ Votre statut a été automatiquement défini
Avatar
Absent(e)
Bouton précédent
Bannir
Bannir le participant
Liste des bannis
+ Occupé
Agenda
Options d\'appel avancées
L\'appel est en cours depuis une heure.
@@ -47,6 +49,7 @@
Zone de danger
%1$s dans %2$s
Supprimer l\'avatar
+ Supprimer l\'enregistrement vocal
La conversation %1$s a été supprimée
Ne pas déranger
Ne pas effacer
@@ -60,6 +63,7 @@
Un problème est survenu lors du chargement de vos discussions
Une erreur est survenue à la réintégration du participant
Échec de la sauvegarde de %1$s
+ 15 minutes
Dossier
Chargement…
%1$s (%2$d)
@@ -75,6 +79,8 @@
Vous avez quitté la conversation %1$s
Charger plus de résultats
Heure locale : %1$s
+ Autorisation de localisation refusée
+ Services de localisation désactivés
Verrouiller la conversation
Symbole de verrouillage
Baisser la main
@@ -88,6 +94,7 @@
Les plus gros d\'abord
Le plus petit en premier
Message copié
+ Êtes-vous sûr de vouloir effacer ce message ?
Message supprimé par vous
Modifié par %1$s
Toucher pour ouvrir le sondage
@@ -95,6 +102,7 @@
Commencer à taper pour lancer la recherche…
Recherche…
Messages
+ Désactiver les notifications
Le compte sélectionné est maintenant importé et disponible
À propos
Utilisateur actif
@@ -179,7 +187,7 @@
Supprimer tout
Supprimer la conversation
Si vous supprimez la conversation, elle sera supprimée pour tous les participants.
- Supprimer
+ Supprimer le message
Message supprimé avec succès, mais il pourrait avoir été divulgué à d’autres services
Supprimer maintenant
L\'utilisateur %1$s a été supprimé
@@ -287,7 +295,7 @@
L\'optimisation de la batterie n\'est pas ignorée. Ceci devrait être modifié pour vous assurer que les notifications fonctionnent en arrière-plan. Merci de cliquer OK et sélectionner \"Toutes les applications\" -> %1$s -> Ne pas optimiser
Ignorer l\'optimisation de batterie
Conversation importante
- Le statut utilisateur \"Ne pas déranger\" est ignoré pour les conversations importantes.
+ Le statut utilisateur \"Ne pas déranger\" est ignoré pour les conversations importantes
Heure invalide
Invitations
Rejoindre des conversations ouvertes
@@ -324,6 +332,7 @@
Message lu
Envoi en cours
Message envoyé
+ Le micro est activé et le son est enregistré
Pour autoriser la communication audio, merci de donner l\'autorisation « Microphone ».
Vous avez manqué un appel de %s
Modérateur
@@ -393,7 +402,7 @@
Renommer
Répondre
Répondre en privé
- Salle retenue avec succès
+ Salle réservée avec succès
Enregistrer
Enregistré avec succès
30 secondes
@@ -409,6 +418,7 @@
Effacer la recherche
Choisissez un compte
Message de mise à jour
+ Envoyer l\'enregistrement vocal
Conversation sensible
La prévisualisation des messages sera désactivée dans la liste des conversations et les notifiactions
%1$s a envoyé un GIF.
@@ -488,7 +498,7 @@
Clair
Thème
Partager mon statut de saisie et montrer le statut de saisie des autres
- L\'état de saisie n\'est disponible que lors de l\'utilisation d\'un backend haute performance (HPB).
+ Le statut de saisie n\'est disponible que lors de l\'utilisation d\'un backend haute performance (HPB).
État de saisie
Le proxy requiert les informations d\'identification
Attention
@@ -541,6 +551,10 @@
Aucune conversation archivée
Aucun message hors-ligne trouvé
Pas d\'intégration avec le carnet d\'adresses à cause d\'autorisations manquantes
+ Seulement les mentions @
+ Désactiver
+ Défaut
+ Utiliser les paramètres de la conversation
1 heure
En ligne
Statut de connexion
@@ -567,6 +581,7 @@
Vote
Vote soumis
Précédemment défini
+ Le code QR ne peut être lu
Lever la main
Tout
le partage de fichier n\'est pas possible sans les permissions
@@ -588,10 +603,10 @@
La conversation %1$s a été supprimée des favorites
La conversation %1$s a été renommée
Renvoyer
- Réinitialiser l\'état
+ Réinitialiser le statut
Il est impossible de rejoindre une autre salle pendant un appel
Sauvegarder
- Scan QR Code
+ Scanner le QR Code
Synchronisation avec les serveurs de confiance uniquement
Fédéré
Visible uniquement aux personnes dans l\'instance et aux invités
@@ -628,6 +643,7 @@
Afficher les participants bannis
Favori
Vous n\'êtes pas autorisé à lancer un appel
+ Créer une conversation
a lancé un appel
Message d\'état
Statut rétabli
@@ -646,8 +662,11 @@
Cette semaine
Ceci est un message de test
Ce week-end
- %1$d réponses
+ Annuler la création de la conversation
+ Notifications de conversation
+ Répondre
Titre de la conversation
+ Conversations
Aujourd\'hui
Demain
Traduire
@@ -684,7 +703,7 @@
Aucunes informations personnelles renseignées
Ajoutez vos nom, photo et coordonnées sur votre page de profil.
Appel vidéo
- Quel est votre état ?
+ Quel est votre statut ?
- Voir %d message similaire
- Voir %d messages similaires
@@ -695,6 +714,11 @@
- Cette conversation sera automatiquement supprimée pour tout le monde après %1$d jours d\'inactivité
- Cette conversation sera automatiquement supprimée pour tout le monde après %1$d jours d\'inactivité
+
+ - %d réponses
+ - %d réponses
+ - %d réponses
+
- %d vote
- %d votes
diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml
index 0579139..13c8425 100644
--- a/app/src/main/res/values-ga/strings.xml
+++ b/app/src/main/res/values-ga/strings.xml
@@ -5,6 +5,7 @@
Cuir le Nótaí
Cuireadh comhrá %1$s le ceanáin
Cuardaigh i %s
+ Le feiceáil as líne
Comhrá cartlainne
Nuair a bheidh comhrá curtha i gcartlann, cuirfear i bhfolach é de réir réamhshocraithe. Roghnaigh an scagaire \"Cartlannaithe\" chun féachaint ar chomhráite cartlainne. Gheofar tagairtí díreacha fós.
Cartlannaithe
@@ -22,6 +23,7 @@
Bac
Cosc ar rannpháirtí
Liosta toirmisc
+ Gnóthach
Féilire
Ardroghanna glaonna
Tá an glao ar siúl ar feadh uair an chloig.
@@ -61,6 +63,7 @@
Tharla fadhb agus do chomhráite á lódáil
Tharla earráid agus rannpháirtí á bhaint de
Theip ar shábháil %1$s
+ 15 nóiméad
fillteán
Á lódáil…
%1$s (%2$d)
@@ -76,6 +79,10 @@
D\'fhág tú an comhrá %1$s
Íoslódáil níos mó torthaí
Am áitiúil: %1$s
+ Cead suímh diúltaithe
+ Cumasaigh é i socruithe an aip le do thoil
+ Seirbhísí suímh díchumasaithe
+ Cumasaigh seirbhísí suímh (GPS) le go mbeidh tú in ann an ghné seo a úsáid
Cuir glas ar an gcomhrá
Siombail ghlais
Lámh íochtair
@@ -89,6 +96,7 @@
An ceann is mó ar dtús
Is lú ar dtús
Teachtaireacht cóipeáilte
+ An bhfuil tú cinnte gur mian leat an teachtaireacht seo a scriosadh?
Scrios tú an teachtaireacht
Curtha in eagar ag %1$s
Tapáil chun vótaíocht a oscailt
@@ -96,6 +104,7 @@
Tosaigh ag clóscríobh chun cuardach a dhéanamh…
Cuardaigh…
Teachtaireachtaí
+ Balbhaigh gach fógra
Tá cuntas roghnaithe iompórtáilte anois agus ar fáil
Faoi
Úsáideoir gníomhach
@@ -180,7 +189,7 @@
Scrios go léir
Scrios an comhrá
Má scriosann tú an comhrá, scriosfar é do gach rannpháirtí eile freisin.
- Scrios
+ Scrios teachtaireacht
D\'éirigh leis an teachtaireacht a scriosadh, ach seans gur sceitheadh chuig seirbhísí eile í
Scrios anois
Baineadh úsáideoir %1$s
@@ -544,6 +553,11 @@
Níl aon chomhráite sa chartlann
Níor sábháladh aon teachtaireacht as líne
Níl aon chomhtháthú uimhir theileafóin mar gheall ar cheadanna in easnamh
+ Gach teachtaireacht
+ \@-luaite amháin
+ as
+ Réamhshocrú
+ Lean socruithe comhrá
1 uair
Ar líne
Stádas ar líne
@@ -553,7 +567,6 @@
Téigh go dtí an snáithe
Seinn/cuir teachtaireacht gutha ar sos
Rialú luas athsheinm
- Lean ar aghaidh leis an bpróiseas logála isteach sa bhrabhsálaí le do thoil.
Cuir rogha leis
Cuir vóta in eagar
Deireadh vótaíocht
@@ -631,10 +644,9 @@
Guth
Taispeáin Cúis Toirmeasc
Taispeáin rannpháirtithe toirmiscthe
- Taispeáin snáitheanna
is fearr leat
Níl cead agat glao a thosú
- Tosaigh snáithe
+ Cruthaigh snáithe
thosaigh glao
Teachtaireacht stádais
Stádas ar ais
@@ -653,8 +665,12 @@
An tseachtain seo
Is teachtaireacht tástála é seo
An deireadh seachtaine seo
- %1$d freagraí
+ Cealaigh cruthú snáithe
+ Fógraí snáithe
+ Freagra
Teideal an snáithe
+ Snáitheanna
+ Níor aimsíodh aon snáitheanna
Inniu
Amárach
Aistrigh
@@ -706,6 +722,13 @@
- Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht
- Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht
+
+ - %d freagra
+ - %d freagraí
+ - %d freagraí
+ - %d freagraí
+ - %d freagraí
+
- %d vóta
- %d vótaí
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index b1b10b4..3d7e3a7 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -5,10 +5,12 @@
Engadir a Notas
Engadiuse a conversa %1$s aos favoritos
Buscar en %s
+ Aparecer como sen conexión
Arquivar a conversa
Unha vez arquivada unha conversa, de xeito predeterminado vai ser agochada. Seleccione o filtro «Arquivado» para ver as conversas arquivadas. Seguirá a recibir as mencións directas.
Arquivada
%1$s foi arquivada
+ Chamada de voz
Bluetooth
Saída de son
Teléfono
@@ -21,6 +23,7 @@
Expulsión
Expulsar o participante
Lista de expulsións
+ Ocupado
Calendario
Opcións avanzadas de chamada
A chamada leva unha hora en curso.
@@ -33,8 +36,10 @@
Pechar
Icona «Pechar»
Estabeleceuse a conexión
- Perdeuse a conexión – As mensaxes enviadas póñense en cola
+ Non hai conexión co servidor
+ Perdeuse a conexión — As mensaxes enviadas póñense en cola
Bloquear a gravación para gravar continuamente a mensaxe de voz
+ A conversa está arquivada
A conversa é só de lectura
Produciuse un fallo ao definir a conversa como de só lectura
Conversas
@@ -44,10 +49,12 @@
Zona de perigo
%1$s en %2$s
Eliminar avatar
+ Eliminar a gravación de voz
A conversa %1$s foi eliminada
Non molestar
Non limpar
Editar
+ Non é posíbel editar as mensaxes con máis de 24 horas
Editar a mensaxe
Recente
Cifrado
@@ -56,6 +63,7 @@
Produciuse un problema ao cargar as súas parolas
Produciuse un erro ao retirar a expulsión dun participante
Produciuse un fallo ao gardar %1$s
+ 15 minutos
cartafol
Cargando…
%1$s (%2$d)
@@ -67,9 +75,14 @@
Non foi posíbel recuperar os idiomas
Produciuse un fallo na recuperación
Hoxe máis tarde
- Deixar a chamada
+ Abandonar a chamada
Vde. deixou a conversa %1$s
Cargando máis resultados
+ Hora local: %1$s
+ Permiso de localización denegado
+ Actíveo nos axustes da aplicación
+ Servizos de localización desactivados
+ Active os servizos de localización (GPS) para usar esta función
Bloquear a conversa
Símbolo de bloqueo
Baixar a man
@@ -82,6 +95,8 @@
Z - A
Primeiro o máis grande
Primeiro o máis pequeno
+ Copiouse a mensaxe
+ Confirma que quere eliminar esta mensaxe?
Mensaxe eliminada por Vde.
Editado por %1$s
Toque para abrir a enquisa
@@ -89,6 +104,7 @@
Comece a escribir para buscar…
Buscar…
Mensaxes
+ Silenciar todas as notificacións
A conta seleccionada foi importada e xa está dispoñíbel
Sobre
Usuario activo
@@ -166,14 +182,16 @@
Copiar
Crear unha nova conversa
Crear enquisa
+ Vde.:
Hoxe
Onte
Eliminar
Eliminar todo
Eliminar a conversa
Se elimina a conversa, tamén se eliminará para todos os demais participantes.
- Eliminar
+ Eliminar a mensaxe
A mensaxe foi eliminada correctamente, pero é posíbel que se filtrase a outros servizos
+ Eliminar agora
O usuario %1$s foi retirado
Relegar de moderador
Gravar mensaxe de voz
@@ -234,7 +252,8 @@
Editar
Editar a mensaxe
Editado por alguén de administración
- Programa
+ Menú do evento de conversa
+ Programación
8 horas
4 semanas
Apagado
@@ -251,6 +270,7 @@
Vde. ten convites pendentes
Atrás
Precísase de permiso para acceder ao ficheiro
+ Filtrar conversas
Usuario seguindo unha ligazón pública
Vde.: %1$s
Reenviar
@@ -277,12 +297,14 @@
Non se ignora a optimización da batería. Isto debería cambiarse para asegurarse de que as notificacións funcionan en segundo plano. Prema en Aceptar e seleccione «Todas as aplicacións» → %1$s → Non optimizar
Ignorar a optimización da batería
Conversa importante
+ Nas conversas importantes, ignorarase o estado «Non molestar»
+ Hora incorrecta
Convites
Unirse a conversas abertas
- Manter
- Debe promover un novo moderador antes de poder deixar a conversa
+ Conservar
+ Debe promover un novo moderador antes de poder abandonar a conversa
%1$s | Última modificación: %2$s
- Deixar a conversa
+ Abandonar a conversa
Abandonando a chamada…
Licenza Pública Xeral GNU, versión 3
licenza
@@ -299,6 +321,12 @@
Sen definir
Marcar como lido
Marcar como sen ler
+ Conversa marcada como importante
+ Conversa sen marcar como sensíbel
+ Conversa marcada como sensíbel
+ Conversa sen marcar como importante
+ Rematou a xuntanza
+ Mensaxe engadida ás notas
Fallado
Produciuse un fallo ao enviar a mensaxe:
Sen conexión
@@ -306,6 +334,7 @@
Mensaxe lida
Enviando
Mensaxe enviada
+ O micrófono está activado e estase a gravar o son
Para activar a comunicación por voz, conceda o permiso de «Micrófono».
Perdeu unha chamada de %s
Moderador
@@ -319,8 +348,8 @@
Non hai conversas abertas
Non hai conversas abertas ás que poida unirse.\nNon hai conversas abertas ou xa se uníu a todas.
Sen proxy
- Non ten permiso para activar o son!
- Non ten permiso para activar o vídeo!
+ Vde. non ten permiso para activar o son!
+ Vde. non ten permiso para activar o vídeo!
Agora non
%1$s na canle de notificación %2$s
Chamadas
@@ -338,20 +367,21 @@
Non notificar nunca
Non ten conexión, verifique a súa conectividade
Aceptar
+ Xuntanza en curso
Abrir a conversa a usuarios rexistrados
Aberta tamén aos usuarios da aplicación convidados
Propietario
Participantes
Engadir participantes
Contrasinal
- Establecer os permisos
+ Estabelecer os permisos
Algúns permisos foron denegados.
Autorice os permisos
Abrir os axustes
Conceda os permisos en Axustes > Permisos
Non se atopou a conta
Parolar a través de %s
- Enmudecer o micrófono
+ Silenciar o micrófono
Activar o micrófono
Mensaxes
Privacidade
@@ -359,6 +389,9 @@
Promover a moderador
Conversa pública
Desactivadas as notificacións emerxentes
+ Desculpe, algo foi mal, o erro é %1$s
+ Desculpe, algo foi mal,, non foi posíbel obter a mensaxe emerxente de proba
+ A notificación emerxente foi enviada satisfactoriamente. Agora ten que recibir unha notificación sobre este dispositivo co título «Probas de notificacións emerxentes»
Prema para falar
Co microfono desactivado, prema e manteña para usar a función «Prema para falar»
Lembrarmo más adiante
@@ -371,6 +404,7 @@
Cambiar o nome
Responder
Responder en privado
+ A sala foi reservada satisfactoriamente
Gardar
Gardadao satisfactoriamente
30 segundos
@@ -385,6 +419,10 @@
Buscar
Limpar a busca
Seleccione unha conta
+ Actualizar a mensaxe
+ Enviar a gravación de voz
+ Conversa sensíbel
+ A vista previa da mensaxe estará desactivada na lista de conversas e nas notificacións
%1$s enviou un GIF.
Vde. enviou un GIF.
%1$s enviou un vídeo.
@@ -455,6 +493,7 @@
A versión do servidor é demasiado antiga e non é compatíbel con esta versión da aplicación para Android
Servidor non admitido
A aplicación de notificacións do servidor non está instalada
+ Definido polo aforrador de batería
Escuro
Usar o predeterminado do sistema
tema
@@ -482,9 +521,14 @@
Cando as notificacións non estean configuradas correctamente, amosa unha advertencia periódica
Amosar a advertencia de notificación periódica
Ordenar por
+ Iniciar unha parola en grupo
Hora de comezo
Cambiar de conta
Equipo
+ Proba das notificacións emerxentes
+ Resultados da proba
+ Hoxe ás %1$s
+ Mañá ás %1$s
Escoller os ficheiros
Quere enviar estes ficheiros a %1$s?
Quere enviar este ficheiro a %1$s?
@@ -506,13 +550,21 @@
Seminario web
Si
Semana seguinte
+ Non hai conversas arquivadas
Non se gardou ningunha mensaxe sen conexión
Non hai integración do número de teléfono por mor da falta de permisos
+ Todas as mensaxes
+ Só as mencións con \@
+ Apagado
+ Predeterminado
+ Seguir os axustes da conversa
1 hora
En liña
Estado en liña
Conversas abertas
Abrir na aplicación de Ficheiros
+ Abrir Notas
+ Ir ao fío
Reproducir/poñer en pausa a mensaxe de voz
Control da velocidade de reprodución
Engadir unha opción
@@ -532,9 +584,11 @@
Votar
Voto enviado
Estabelecido previamente
+ Non foi posíbel ler o código QR.
Erguer a man
Todo
- Non é posible compartir ficheiros desde o almacenamento sen permisos
+ Non é posíbel compartir ficheiros desde o almacenamento sen permisos
+ Fíos recentes
Estase a gravar a chamada
Cancelar o inicio da gravación
Produciuse un fallo na gravación. Póñase en contacto coa administración desta instancia.
@@ -553,9 +607,9 @@
Cambióuselle o nome a conversa %1$s
Volver enviar
Restabelecer o estado
- Non é posible unirse a outras salas mentres está nunha chamada
+ Non é posíbel unirse a outras salas mentres está nunha chamada
Gardar
- Scan QR Code
+ Escanear o código QR
Sincronizar só con servidores de confianza
Federado
Visíbel só para as persoas desta instancia e os convidados
@@ -572,7 +626,7 @@
Seleccionado
Enviar o correo
Enviar a
- Non ten permiso para compartir contido nesta parola
+ Vde. non ten permiso para compartir contido nesta parola
Enviar a…
Enviar sen notificación
Definir
@@ -591,7 +645,8 @@
Amosar o motivo da expulsión
Amosar os participantes expulsados
Favorito
- Non ten permiso para iniciar unha chamada
+ Vde. non ten permiso para iniciar unha chamada
+ Crear un fío
iniciar unha chamada
Mensaxe de estado
Estado revertido
@@ -599,7 +654,7 @@
Cambiar á sala principal
Tirar unha foto
Produciuse un erro ao tirar a foto
- Non é posible tirar unha foto sen permisos
+ Non é posíbel tirar unha foto sen permisos
Volver tirar a foto
Enviar
Cambiar de cámara
@@ -610,6 +665,12 @@
Esta semana
Esta é unha mensaxe de proba
Este fin de semana
+ Cancelar a creación do fío
+ Notificacións de fíos
+ Responder
+ Título do fío
+ Fíos
+ Non se atopou ningún fío
Hoxe
Mañá
Traducir
@@ -633,7 +694,7 @@
Enviar un novo avatar desde o dispositivo
%1$s está fóra da oficina e é posíbel que non responda
%1$s hoxe está fóra da oficina
- Substitución:
+ Substitución:
Avatar do usuario
Enderezo
Nome completo
@@ -651,6 +712,14 @@
- Ver %d mensaxe semellante
- Ver %d mensaxes semellantes
+
+ - Esta conversa eliminarase automaticamente para todos con %1$d día sen actividade.
+ - Esta conversa eliminarase automaticamente para todos aos %1$d días sen actividade.
+
+
+ - %d resposta
+ - %d respostas
+
- %d voto
- %d votos
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 0ff9f88..e6ea335 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -8,6 +8,7 @@
Zvučnik
Avatar
Odsutan
+ Zauzeto
Kalendar
Odaberi avatar iz oblaka
Izbriši poruku statusa
@@ -25,6 +26,7 @@
Nedavni
Šifrirano
Spremanje %1$s nije uspjelo
+ 15 minuta
mapa
Učitavanje…
%1$s (%2$d)
@@ -110,7 +112,7 @@
Izbriši sve
Izbriši razgovor
Ako izbrišete razgovor, također će biti izbrisan za sve ostale sudionike.
- Izbriši
+ Izbriši poruku
Poruka je uspješno izbrisana, ali je možda prenesena u druge usluge
Ukloni moderatora
Snimi glasovnu poruku
@@ -333,6 +335,7 @@
Da
Sljedeći tjedan
Integracija broja telefona nije moguća jer nedostaje dopuštenje
+ Zadani
1 sat
Na mreži
Status na mreži
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 5add234..809e40d 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -5,6 +5,7 @@
Hozzáadás a jegyzetekhez
A(z) %1$s beszélgetés hozzáadva a kedvencekhez
Keresés itt: %s
+ Megjelenés nem kapcsolódottként
Beszélgetés archiválása
Ha archivál egy beszélgetést, akkor alapértelmezetten el lesz rejtve. Válassza az „Archiválva” szűrőt az archivált beszélgetések megtekintéséhez. A közvetlen említéseket továbbra is meg fogja kapni.
Archiválva
@@ -22,6 +23,7 @@
Tiltás
Résztvevő letiltása
Tiltólista
+ Foglalt
Naptár
Speciális hívásbeállítások
A hívás egy órája tart.
@@ -47,6 +49,7 @@
Veszélyes területet
%1$s itt: %2$s
Profilkép törlése
+ Hangfelvétel törlése
%1$s beszélgetés törlése
Ne zavarjanak
Ne törölje
@@ -60,6 +63,7 @@
Hiba történt a csevegések betöltése során
Hiba történt a résztvevő tiltásának visszavonása során
Sikertelen mentés: %1$s
+ 15 perc
mappa
Betöltés…
%1$s (%2$d)
@@ -75,6 +79,10 @@
Elhagyta a következő beszélgetést: %1$s
További találatok betöltése
Helyi idő: %1$s
+ Hely engedély szükséges
+ Engedélyezze az alkalmazásbeállításokban
+ Helyszolgáltatások letiltva
+ A funkció használatához engedélyezze a helyszolgáltatásokat (GPS)
Beszélgetés zárolása
Zár szimbólum
Kéz letétele
@@ -88,6 +96,7 @@
Legnagyobb elöl
Legkisebb elöl
Üzenet másolva
+ Biztos, hogy törli ezt az üzenetet?
Törölte az üzenetet
Szerkesztette: %1$s
Koppintson a szavazás megnyitásához
@@ -95,6 +104,7 @@
Kezdjen el gépelni a kereséshez…
Keresés…
Üzenetek
+ Összes értesítés némítása
A kiválasztott fiók importálva lett és elérhető
Leírás
Aktív felhasználó
@@ -179,7 +189,7 @@
Összes törlése
Beszélgetés törlése
Ha törli a beszélgetést, akkor az összes többi résztvevő számára is törölve lesz.
- Törlés
+ Üzenet törlése
Az üzenet törlése sikeresen megtörtént, de lehet, hogy az már megjelent más szolgáltatásokon
Törlés most
%1$s felhasználó el lett távolítva
@@ -324,6 +334,7 @@
Üzenet elolvasva
Küldés
Üzenet elküldve
+ A mikrofon engedélyezve van, és a hang felvételre kerül
A hanghívás engedélyezéséhez meg kell adnia a „Mikrofon” engedélyt.
Nem fogadott hívás a következőtől: %s
Moderátor
@@ -409,6 +420,7 @@
Keresés törlése
Fiók kiválasztása
Üzenet frissítése
+ Hangfelvétel küldése
Érzékeny beszélgetés
Az üzenet-előnézet le lesz tiltva a beszélgetési listában és az értesítésekben
%1$s GIF képet küldött.
@@ -541,12 +553,18 @@
Nincs archivált beszélgetés
Nincs mentett offline üzenet
A hiányzó engedélyek miatt nincs telefonszám-integráció
+ Összes üzenet
+ csak @-megemlítések
+ Ki
+ Alapértelmezett
+ Beszélgetésbeállítások követése
1 óra
Elérhető
Elérhető állapot
Beszélgetések megnyitása
Megnyitás a Fájlok alkalmazásban
Jegyzetek megnyitása
+ Ugrás a szálhoz
Hangüzenet lejátszása/szüneteltetése
Lejátszási sebesség vezérlése
Lehetőség hozzáadása
@@ -566,9 +584,11 @@
Szavazat leadása
Szavazat leadva
Előzőleg beállított
+ A QR-kód nem olvasható el
Kéz felemelése
Összes
A fájlok megosztása a tárhelyről engedély nélkül nem lehetséges
+ Legutóbbi szálak
A hívásról felvétel készül
Felvétel indításának megszakítása
A felvétel sikertelen. Lépjen kapcsolatba a rendszergazdával.
@@ -589,7 +609,7 @@
Állapot visszaállítása
Hívás közben nem lehet más szobákhoz csatlakozni
Mentés
- Scan QR Code
+ QR-kód leolvasása
Szinkronizálás csak a megbízható kiszolgálókkal
Föderált
Csak az ezen a példányon lévő személyek és a vendégek láthatják
@@ -626,6 +646,7 @@
Kitiltott résztvevők megjelenítése
Kedvenc
Nincs jogosultsága hívást indítani
+ Szál létrehozása
hívás indítás
Állapotüzenet
Üzenet visszaállítva
@@ -644,6 +665,12 @@
Ez a hét
Ez egy tesztüzenet
Ezen a hétvégén
+ Szál létrehozásának megszakítása
+ Szálértesítések
+ Válasz
+ Szál címe
+ Szálak
+ Nem találhatók szálak
Ma
Holnap
Lefordítás
@@ -689,6 +716,10 @@
- Ez a beszélgetés %1$d nap tétlenség után mindenkinél törölve lesz
- Ez a beszélgetés %1$d nap tétlenség után mindenkinél törölve lesz
+
+ - %d válasz
+ - %d válasz
+
- %d szavazat
- %d szavazat
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 1e829f5..6e01104 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -2,9 +2,15 @@
Modifica
Aggiungi
+ Aggiungi alle Note
+ Aggiunta conversazione %1$s ai preferiti
Cerca in %s
+ Appari non in linea
+ Archivia conversazione
+ Una volta archiviata, una conversazione verrà nascosta per impostazione predefinita. Seleziona il filtro “Archiviate” per visualizzare le conversazioni archiviate. Le menzioni dirette continueranno a essere ricevute.
Archiviati
Archiviato %1$s
+ Chiamata audio
Bluetooth
Uscita audio
Telefono
@@ -13,8 +19,14 @@
Stato impostato automaticamente
Avatar
Assente
+ Tasto indietro
+ Bandisci
+ Bandisci partecipante
+ Lista dei bandi
+ Occupato
Calendario
Opzioni avanzate per le chiamate
+ La chiamata è in corso da un\'ora.
Chiama senza notifica
Autorizzazione fotocamera concessa. Scegli di nuovo la fotocamera.
Annulla l\'accesso
@@ -22,42 +34,77 @@
Cancella il messaggio di stato
Cancella il messaggio di stato dopo
Chiudi
+ Chiudi Icona
Connessione stabilita
+ Nessuna connessione al server
+ Connessione persa - I messaggi inviati sono in coda
+ Blocca registrazione per registrare in modo continuo il messaggio vocale
+ La conversazione è archiviata
+ La conversazione è in sola lettura
+ Impossibile impostare conversazione in Sola-lettura
Conversazioni
Crea conversazione
+ Crea problema
Personalizzato
Zona pericolosa
+ %1$s in %2$s
Elimina avatar
+ Cancella registrazione vocale
+ Cancellata conversazione %1$s
Non disturbare
Non cancellare
Modifica
+ Messaggi più vecchi di 24 ore non possono essere modificati
Modifica messaggio
Recenti
Cifrato
+ Termina chiamata
+ Termina chiamata per tutti
Si è verificato un problema durante il caricamento delle tue chat
+ Si è verificato un errore durante la rimozione del ban dal partecipante
Salvataggio di %1$s fallito
+ 15 minuti
cartella
Caricamento …
%1$s (%2$d)
4 ore
+ Impossibile recuperare gli inviti in sospeso
+ (modificato)
+ Note interne
Invisibile
+ Impossibile recuperare le lingue
+ Recupero non riuscito
Più tardi oggi
Lascia la chiamata
+ Hai lasciato la conversazione %1$s
Mostra altri risultati
+ Ora locale: %1$s
+ Autorizzazione alla localizzazione negata
+ Abilitala nelle impostazioni dell\'app.
+ Servizi di localizzazione disattivati
+ Abilita i servizi di localizzazione (GPS) per utilizzare questa funzione.
Blocca conversazione
Simbolo lucchetto
Abbassa la mano
+ Contrassegna la conversazione %1$s come letta
+ Contrassegna la conversazione %1$s come non letta
+ Citato
Prima i più recenti
Prima i più datati
A - Z
Z - A
Prima i più grandi
Prima i più piccoli
+ Messaggio copiato
+ Vuoi davvero eliminare questo messaggio?
Messaggio eliminato da te
+ Modificato da %1$s
+ Premi per aprire il sondaggio
Nessun risultato di ricerca
Inizia a digitare per cercare …
Cerca …
Messaggi
+ Muta tutte le notifiche
L\'account selezionato è ora importato e disponibile
Informazioni
Utente attivo
@@ -72,13 +119,19 @@
OK, tutto fatto!
Appunta: %1$s
Sblocca %1$s
+ Per abilitare gli altoparlanti Bluetooth, concedi l\'autorizzazione “Dispositivi vicini”.
Rispondi come videochiamata
Rispondi solo come chiamata vocale
Cambia l\'uscita audio
+ Attiva/disattiva fotocamera
Riaggancia
+ Attiva/disattiva microfono
+ Apri la modalità picture-in-picture
+ Passa al video personale
IN ARRIVO
Nome della conversazione
Notifiche di chiamata
+ %1$s ha alazato la mano
Riconnessione in corso …
STA SQUILLANDO
%1$s in chiamata
@@ -88,13 +141,17 @@
%s chiamata
%s videochiamata
%s chiamata vocale
+ Per abilitare la comunicazione video, concedi l\'autorizzazione “Fotocamera”.
Annulla
Recupero delle capacità non riuscito, interruzione in corso
+ Sottotitolo
Ti fidi del certificato SSL fino ad ora sconosciuto, rilasciato da %1$s per %2$s, valido da %3$s a %4$s?
Controlla il certificato
La tua configurazione SSL ha impedito la connessione
Cambia certificato di autenticazione
Cambia password
+ Cancella modifica
+ Cancella modifica
Elimina tutti i messaggi
Tutti i messaggi sono stati eliminati
Vuoi davvero eliminare tutti i messaggi in questa conversazione?
@@ -114,48 +171,89 @@
Seleziona certificato di autenticazione
Connessione in corso…
Fine
+ Descrizione della conversazione
Informazioni di conversazione
Chiamata video
Chiamata vocale
+ Conversazione non trovata
Impostazioni conversazione
Unisciti a una conversazione o iniziane una nuova
Saluta i tuoi amici e i tuoi colleghi!
Copia
Crea una nuova conversazione
Crea sondaggio
+ Tu:
Oggi
Ieri
Elimina
Elimina tutto
Elimina conversazione
Se elimini la conversazione, sarà eliminata anche per tutti i partecipanti.
- Elimina
+ Elimina messaggio
Messaggio eliminato correttamente, ma potrebbe essere stato distribuito ad altri servizi
+ Cancella ora
+ L\'utente %1$s è stato rimosso
Declassa da moderatore
Registra messaggio vocale
Invia messaggio
Account attuale
Server
+ App di notifica server installata?
Utente
+ Stato utente abilitato?
Versione Android
Applicazione
Nome applicazione
+ Utenti registrati
+ Versione App
+ L\'ottimizzazione della batteria viene ignorata, tutto bene
+ L\'ottimizzazione della batteria è abilitata e potrebbe causare problemi. È necessario disabilitare l\'ottimizzazione della batteria!
Impostazioni batteria
Dispositivo
+ Apri la lista di controllo per la risoluzione dei problemi
+ Apri schermata diagnosi
+ Apri dontkillmyapp.com
+ Ultimo recupero token push Firebase
+ Generazione dell\'ultimo token push Firebase
+ Nessun token push Firebase impostato. Si prega di creare una segnalazione di bug.
+ Firebase push token
+ I servizi Google Play non sono disponibili. Le notifiche non sono supportate.
+ Servizi Google Play
+ Servizi Google Play non disponibili
+ Ultima registrazione push presso il proxy push
+ Non ancora registrato su push proxy
+ Ultima registrazione push sul server
+ Non ancora registrato sul server
+ Meta informazioni
+ Generazione del rapporto di sistema
+ Canale di notifica delle chiamate abilitato?
+ Canale di notifica dei messaggi abilitato?
+ Permessi di notifica
Telefono
Versione del server Talk
+ Versione server
Esterno
Interni
+ Modalità di segnalazione
Password non valida
+ Il server è attualmente in modalità di manutenzione.
+ L\'app non è aggiornata
+ L\'app è troppo vecchia e non è più supportata da questo server. Si prega di aggiornare.
Aggiorna
Vuoi autorizzare nuovamente o eliminare questo account?
+ Salvare questo file multimediale nella memoria consentirà a tutte le altre app presenti sul dispositivo di accedere a questo.
+ Continuare?
No
+ Salvare nella memoria?
Sì
Il nome visualizzato non può essere recuperato, interruzione in corso
Nome visualizzato non memorizzato, interruzione in corso
Modifica
Modifica
Modifica messaggio
+ Modificato da admin
+ Menu conversazione evento
+ Programma
8 ore
4 settimane
Spenta
@@ -167,7 +265,12 @@
Recupero delle impostazioni di segnalazione non riuscito
Accetta
Rifiuta
+ da %1$s a %2$s
+ Nessun invito in attesa
+ Hai inviti in attesa
Indietro
+ È richiesta l\'autorizzazione per l\'accesso al file
+ Filtra conversazioni
Utente che segue un collegamento pubblico
Tu: %1$s
Inoltra
@@ -178,15 +281,28 @@
Gruppo
Ospite
Accesso ospiti
+ Impossibile abilitare/disabilitare l\'accesso ospite.
+ Consenti agli ospiti di condividere un link pubblico per partecipare a questa conversazione.
Consenti ospiti
Digita una password
+ Password per accesso ospiti
+ Errore durante l\'impostazione/disattivazione della password.
Imposta una password per limitare chi può utilizzare il collegamento pubblico.
Protezione password
Rispedisci inviti
+ Gli inviti non sono stati inviati a causa di un errore.
+ Gli inviti sono stati inviati nuovamente.
Condividi collegamento della conversazione
Digita un messaggio …
+ L\'ottimizzazione della batteria non viene ignorata. Questo dovrebbe essere modificato per garantire che le notifiche funzionino in background! Fare clic su OK e selezionare \"Tutte le app\" -> %1$s -> Non ottimizzare
+ Ignora ottimizzazione batteria
Conversazione importante
+ Lo stato utente “Non disturbare” viene ignorato per le conversazioni importanti.
+ Orario non valido
Inviti
+ Entra nelle conversazioni aperte
+ Mantieni
+ Devi promuovere un nuovo moderatore prima di poter lasciare la conversazione.
%1$s | Ultima modifica: %2$s
Lascia la conversazione
Chiusura della chiamata in corso …
@@ -205,12 +321,22 @@
Non impostato
Segna come letto
Segna come non letto
+ Conversazione contrassegnata come importante
+ Conversazione non contrassegnata come sensibile
+ Conversazione contrassegnata come sensibile
+ Conversazione non contrassegnata come importante
+ Meeting terminato
+ Messaggio aggiunto alle note
Non riuscito
Invio del messaggio non riuscito:
Offline
Annulla risposta
Messaggio letto
+ Inviando
Messaggio inviato
+ Il microfono è attivato e l\'audio è in registrazione.
+ Per abilitare la comunicazione vocale, concedi l\'autorizzazione “Microfono”.
+ Hai perso una chiamata da %s
Moderatore
Nuova conversazione
Visibilità
@@ -219,7 +345,11 @@
%1$s non disponibile (non installato o limitato dall\'amministratore)
Ospite
No
+ Nessuna conversazione aperta
+ Non ci sono conversazioni aperte a cui puoi partecipare.\nO non ci sono conversazioni aperte o hai già partecipato a tutte.
Nessun proxy
+ Non è consentito attivare l\'audio!
+ Non è consentito attivare il video!
Non ora
%1$s sul canale di notifica %2$s
Chiamate
@@ -227,19 +357,28 @@
Messaggi
Notifica sui messaggi in arrivo
Caricamenti
+ Notifica sullo stato di avanzamento del caricamento
Impostazioni di notifica
+ Le notifiche non sono configurate correttamente
+ Le impostazioni relative alle notifiche e alla batteria sono configurate correttamente per ricevere le notifiche. Se riscontri comunque problemi nella ricezione delle notifiche, verifica che i canali di notifica per chiamate e messaggi siano abilitati. Ulteriori informazioni sono disponibili su DontKillMyApp.com o nella lista di controllo per la risoluzione dei problemi. Se ciò non fosse d\'aiuto, vai alla schermata di diagnosi e invia una segnalazione di bug.
+ Risoluzione dei problemi relativi alle notifiche
Notifica sempre
Notifica su menzione
Non notificare mai
Attualmente non in linea, controlla la tua connettività
OK
+ Meeting in corso
Apri la conversazione agli utenti registrati
Apri anche agli utenti dell\'applicazione ospite
Proprietario
Partecipanti
Aggiungi partecipanti
Password
+ Imposta permessi
+ Alcuni permessi sono stati negati.
+ Per favore, concedi i permessi
Apri impostazioni
+ Concedi i permessi da Impostazioni > Permessi
Account non trovato
Chat tramite %s
Spegni microfono
@@ -248,23 +387,31 @@
Riservatezza
Informazioni personali
Promuovi a moderatore
+ Conversazione pubblica
Notifiche push disabilitate
+ Qualcosa è andato storto, l\'errore è %1$s
+ Ci dispiace, si è verificato un errore, impossibile recuperare il messaggio di prova.
+ La notifica push è stata inviata con successo. Ora dovresti ricevere una notifica su questo dispositivo con il titolo “Test delle notifiche push”.
Premi per parlare
Con il microfono disabilitato, fai clic e mantieni per utilizzare Premi per parlare
Ricordamelo più tardi
Rimuovi dai preferiti
Rimuovi gruppo e membri
Rimuovi partecipante
+ Rimuovi Password
+ Rimuovi team e membri
Rinomina conversazione
Rinomina
Rispondi
Rispondi in privato
+ La camera è stata prenotata con successo.
Salva
Salvato correttamente
30 secondi
5 minuti
1 minuto
10 minuti
+ Immediato
600
60
30
@@ -272,6 +419,10 @@
Cerca
Svuota ricerca
Seleziona account
+ Aggiorna messaggio
+ Invia registrazione vocale
+ Conversazione delicata
+ L\'anteprima dei messaggi sarà disabilitata nell\'elenco delle conversazioni e nelle notifiche.
%1$s ha inviato una GIF.
Hai inviato una GIF.
%1$s ha inviato un video.
@@ -280,6 +431,7 @@
Hai inviato un audio.
%1$s ha inviato un\'immagine.
Hai inviato un\'immagine.
+ %1$s ha mandato una scheda Deck
Prova di connessione al server
Aggiorna il tuo database %1$s
Importazione dell\'account selezionato non riuscita
@@ -295,18 +447,25 @@
Indirizzo server https://…
%1$s funziona solo con %2$s 13 e successivi
Imposta una nuova password
+ Imposta Password
Impostazioni
Il tuo account preesistente è stato aggiornato, invece di aggiungerne un nuovo
Avanzate
Aspetto
Chiamate
+ Per favore, contatta l\'amministratore di
+ Apri la schermata di diagnosi per controllare le impostazioni o creare un rapporto sui bug
+ Diagonsi
Ordina alla tastiera di disattivare l\'apprendimento personalizzato (senza garanzie)
Tastiera incognito
Nessun suono
L\'applicazione Talk non è installata sul server sul quale hai provato ad autenticarti
Notifiche
+ Le notifiche sono state rifiutate
+ Le notifiche sono state concesse
Messaggi
Verifica i contatti in base al numero di telefono per integrare il collegamento di Talk nell\'applicazione dei contatti di sistema
+ Errore 429 Troppe Richieste
Puoi impostare il tuo numero di telefono in modo che gli altri utenti ti trovino
Digita numero di telefono
Numero di telefono non valido
@@ -333,12 +492,16 @@
La versione del server è molto datata e non sarà più supportata nella prossima versione!
La versione del server è troppo datata e non supportata da questa versione dell\'applicazione Android
Server non supportato
+ App di notifiche server non installata
+ Impostato dal risparmio batteria
Scuro
Usa valori predefiniti di sistema
tema
Chiaro
Tema
Condividi il mio stato di digitazione e mostra lo stato di digitazione degli altri.
+ Lo stato di digitazione è disponibile solo quando si utilizza un backend ad alte prestazioni (HPB).
+ Stato di digitazione
Il proxy richiede credenziali
Avviso
Può essere autorizzato nuovamente solo l\'account attuale
@@ -351,25 +514,35 @@
Scegli account
Oggetti condivisi
Scheda di Deck
+ Immagini, file, messaggi vocali …
Nessun elemento condiviso
Posizione
Posizione condivisa
Quando le notifiche non sono impostate correttamente, mostra un avviso regolare
Mostra avviso di notifica regolare
Ordina per
+ Inizia chat di gruppo
Ora di inizio
Cambia account
Team
+ Prova notifiche push
+ Risultati test
+ Oggi alle %1$s
+ Domani alle %1$s
Scegli i file
Inviare questi file a %1$s?
Inviare questo file a %1$s?
Spiacenti, caricamento non riuscito
+ Impossibile caricare %1$s
Problema
Condividi da %1$s
Carica dal dispositivo
Caricamento
+ %1$s a %2$s - %3$s\%%
Scatta foto
+ Cattura video
Utente
+ Registrazione video da %1$s
Registrazione Talk da %1$s (%2$s)
Tieni premuto per registrare, rilascia per inviare.
Autorizzazione di registrazione audio richiesta
@@ -377,28 +550,64 @@
Webinar
Sì
Settimana successiva
+ Nessuna conversazione archiviato
+ Nessun messaggio offline salvato
Nessuna integrazione del numero di telefono a causa di autorizzazioni mancanti
+ Tutti i messaggi
+ \@-solo menzioni
+ Spento
+ Predefinito
+ Segui le impostazioni della conversazione
1 ora
In linea
Stato in linea
Apri conversazioni
Apri nell\'applicazione File
+ Apri note
+ Vai all\'argomento
Riproduci/ferma messaggio vocale
+ Velocità di riproduzione
Aggiungi opzione
+ Modifica voto
+ Termina sondaggio
+ Vuoi davvero terminare questo sondaggio? Non è possibile annullare l\'operazione.
+ Non puoi votare con più opzioni per questo sondaggio.
+ Risposta multipla
+ Cancella opzione %1$d
+ Opzione %1$d
Opzioni
Sondaggio privato
Domanda
+ La tua domanda
Risultati
Impostazioni
Votare
+ Voto inviato
Impostato in precedenza
+ Il QR code non può essere letto
Alza la mano
Tutti
La condivisione dei file dall\'archiviazione non è possibile senza permessi
+ Argomenti recenti
+ La chiamata viene registrata
+ Annulla avvio registrazione
+ La registrazione è fallita. Per favore, contatta un amministratore.
+ Avvia registrazione
+ Vuoi davvero interrompere la registrazione?
+ Interrompi registrazione chiamata
+ Termina registrazione
+ Interrompi registrazione …
Il permesso di registrazione è richiesto per tutte le call
+ La registrazione potrebbe includere la tua voce, il video dalla telecamera e la condivisione dello schermo. È necessario il tuo consenso prima di partecipare alla chiamata. Acconsenti?
+ Richiedi il consenso alla registrazione prima di partecipare alla chiamata in questa conversazione
Permesso di registrazione
+ La chiamata potrebbe essere registrata.
Registrazione
+ Conversazione %1$s rimossa dai preferiti
+ La conversazione %1$s è stata rinominata
+ Reinvia
Ripristina stato
+ Non è possibile entrare in altre stanze mentre si è impegnati in una chiamata.
Salva
Scan QR Code
Sincronizza solo con server fidati
@@ -412,24 +621,37 @@
Cambio di ambito
Cambia livello di privacy di %1$s
Scorri in fondo
+ Cerca Icona
secondi fa
Selezionato
Invia email
Invia a
+ Non è consentito condividere contenuti in questa chat.
Invia a…
+ Invia messaggio senza notifica
Imposta
+ Imposta avatar dalla fotocamera
Imposta stato
Imposta messaggio di stato
Condividi
+ Partecipa alla conversazione %1$s su %2$s
Audio
File
Media
Altro
Sondaggio
+ Registrazione chiamata
Voce
+ Mostra motivo del bando
+ Mostra partecipanti banditi
Preferito
Non ti è consentito avviare una chiamata
+ Crea un argomeno
+ ha iniziato una chiamata
Messaggio di stato
+ Stato Ripristinato
+ Passa a sessione secondaria
+ Passa a stanza principale
Scatta una foto
Errore acquisizione immagine
Non è possibile scattare una foto senza autorizzazioni
@@ -441,19 +663,38 @@
Accendi/spegni la torcia
30 minuti
Questa settimana
+ Questo è un messaggio di test
Questo fine settimana
+ Cancella creazione argomento
+ Notifiche dagli argomenti
+ Rispondi
+ Titolo dell\'argomento
+ Argomenti
+ Nessun argomento trovato
Oggi
Domani
Traduci
+ Traduzione
+ Copia testo tradotto
Rileva lingua
Impostazioni dei dispositivi
Impossibile rilevare la lingua
Traduzione fallita
Da
A
+ e 1 altro stanno scrivendo …
+ stanno scrivendo …
+ sta scrivendo …
+ e %1$s altri stanno scrivendo …
+ Disarchivia conversazione
+ Una volta che una conversazione viene rimossa dall\'archivio, verrà nuovamente visualizzata per impostazione predefinita.
Non archiviato %1$s
+ Rimuovi ban
Da leggere
Carica nuovo avatar dal dispositivo
+ %1$s è fuori ufficio e potrebbe non rispondere
+ %1$s è fuori ufficio oggi
+ Sostituzione:
Avatar dell\'utente
Indirizzo
Nome completo
@@ -465,5 +706,26 @@
Impossibile ottenere le informazioni personali dell\'utente.
Nessuna informazione personale impostata
Aggiungi nome, immagine e dettagli di contatto sulla tua pagina di profilo.
+ Chiamata video
Qual è il tuo stato?
-
+
+ - Vedi %d messaggio simile
+ - Vedi %d messaggi simili
+ - Vedi %d messaggi simili
+
+
+ - Questa conversazione verrà automaticamente cancellata per tutti dopo %1$d giorno di inattività.
+ - Questa conversazione verrà automaticamente cancellata per tutti dopo %1$d giorni di inattività.
+ - Questa conversazione verrà automaticamente cancellata per tutti dopo %1$d giorni di inattività.
+
+
+ - %d risposta
+ - %d risposte
+ - %d risposte
+
+
+ - %d voto
+ - %d voti
+ - %d voti
+
+
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 52a50a2..d0fba1d 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -5,6 +5,7 @@
メモに追加する
%1$s会話をお気に入りに追加した
%sを検索
+ オフライン
会話をアーカイブ
アーカイブ済み
Bluetooth
@@ -15,6 +16,7 @@
あなたのステータスは自動的に設定されました
アバター
離席中
+ ビジー
カレンダー
高度なコールオプション
通話が1 時間経過
@@ -45,6 +47,7 @@
全員の通話を終了する
あなたのチャットの読み込み中に問題が発生しました
%1$sの保存に失敗しました
+ 15分
フォルダー
読み込み中…
%1$s (%2$d)
@@ -77,6 +80,7 @@
入力して検索を開始 …
検索…
メッセージ
+ 全ての通知をミュートします
選択したアカウントがインポートされ、使用可能になりました
バージョン情報
アクティブなユーザー
@@ -160,8 +164,9 @@
全て削除
会話を削除
会話を削除すると、ほかの参加者でも一緒に解除されます。
- 削除
+ メッセージを削除
メッセージは削除されましたが、他サービスへは転送されている可能性があります。
+ 今すぐ削除
ユーザー%1$sは削除されました
モデレータから降格
ボイスメッセージを録音
@@ -221,6 +226,7 @@
編集
メッセージを編集
管理者に編集されました
+ スケジュール
8時間
4週間
オフ
@@ -264,6 +270,7 @@
重要な会議
招待
オープンな会話に参加する
+ 保持
会話から離れる前に、新しいモデレーターを昇格させる必要があります
%1$s最終更新:%2$s
会話を離れる
@@ -285,6 +292,7 @@
未読にする
失敗しました
メッセージの送信に失敗しました:
+ オフライン
返信をキャンセル
メッセージ既読
メッセージ送信済
@@ -362,6 +370,7 @@
検索
検索をクリア
アカウントを選択
+ プライベートな会話
%1$sがGIFを送信しました。
GIFを送信しました。
%1$sが動画ファイルを送信しました。
@@ -478,6 +487,8 @@
はい
来週
利用権限がないため、電話番号統合ができません。
+ \@で直接会話
+ デフォルト
1時間
オンライン
オンラインステータス
@@ -571,6 +582,8 @@
今週
これはテストメッセージです
この週末
+ 返信
+ スレッド
今日
明日
翻訳
@@ -600,6 +613,9 @@
個人情報はありません
プロフィールページに名前、写真、連絡先の詳細を追加します。
現在のオンラインステータスは?
+
+ - %d件の返信
+
- %d投票数
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index e0ab1cd..2b115ba 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -3,6 +3,7 @@
편집
추가
%s 검색
+ 접속 안함으로 표시
보관된
블루투스
오디오 출력
@@ -11,6 +12,7 @@
유선 헤드셋
아바타
자리비움
+ 바쁨
일정
통화가 한 시간동안 진행되었습니다.
알림 없이 전화하기
@@ -35,6 +37,7 @@
모든 이들에 대해 통화 끝내기
당신의 채팅을 불러들이는데 문제가 생겼습니다.
%1$s 저장 실패
+ 15분
폴더
불러오는 중 …
%1$s (%2$d)
@@ -59,6 +62,7 @@
...를 검색하기 위하여 입력을 시작합니다
...를 검색
메시지
+ 모든 알림을 음소거
선택한 계정을 가져왔고 사용할 수 있음
정보
활성 사용자
@@ -129,7 +133,7 @@
모두 삭제
대화 삭제
대화를 삭제하면 다른 모든 참가자에게서도 삭제됩니다.
- 삭제
+ 메시지 삭제
메시지가 성공적으로 삭제되었지만, 다른 서비스에 노출되었을 수 있습니다.
중재자 권한 제거
음성 메시지 녹음
@@ -386,6 +390,8 @@
예
다음주
권한 없음으로 인하여 전화 번호를 통합하지 않습니다.
+ \@-언급만
+ 디폴트
1시간
접속 중
접속 상태
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
index 740d407..28a9a9f 100644
--- a/app/src/main/res/values-land/dimens.xml
+++ b/app/src/main/res/values-land/dimens.xml
@@ -2,10 +2,12 @@
24dp
+ 110dp
diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml
index bc4ac81..36191bd 100644
--- a/app/src/main/res/values-lt-rLT/strings.xml
+++ b/app/src/main/res/values-lt-rLT/strings.xml
@@ -3,6 +3,7 @@
Taisyti
Pridėti
Ieškoti %s
+ Atrodyti atsijungusiu
Archyvuota
Bluetooth
Garso išvestis
@@ -10,6 +11,7 @@
Jūsų būsena buvo nustatyta automatiškai
Avataras
Atsitraukęs
+ Užimtas laikas
Kalendorius
Išplėstinės skambučio parinktys
Atsisakyti prisijungimo
@@ -28,6 +30,7 @@
Taisyti laišką
Paskiausi
Nepavyko įrašyti %1$s
+ 15 minučių
aplankas
Įkeliama…
%1$s (%2$d)
@@ -49,6 +52,7 @@
Rašykite norėdami atlikti paiešką…
Ieškoti…
Žinutės
+ Išjungti visus pranešimus
Dabar, pasirinkta paskyra yra importuota ir prieinama
Apie
Aktyvus naudotojas
@@ -112,7 +116,7 @@
Ištrinti visus
Ištrinti pokalbį
Jei ištrinsite pokalbį, jis taip pat bus ištrintas visiems kitiems dalyviams.
- Ištrinti
+ Ištrinti laišką
Pažeminti iš moderatorių
Siųsti žinutę
Dabartinė paskyra
@@ -310,6 +314,7 @@
Internetinis seminaras
Taip
Kita savaitė
+ Numatytasis
1 valanda
Prisijungęs
Prisijungimo būsena
@@ -361,6 +366,7 @@
Šią savaitę
Tai yra bandomoji žinutė
Šį savaitgalį
+ Atsakyt
Šiandiena
Rytoj
Verskite
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index a597d66..1f756ec 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -5,6 +5,7 @@
Legg til notater
La til samtale %1$s i favoritter
Søk i %s
+ Vis som frakoblet
Arkivert
Bluetooth
Lydutgang
@@ -18,6 +19,7 @@
Utesteng
Utesteng deltaker
Liste over utestengelser
+ Opptatt
Kalender
Avanserte samtalealternativer
Samtalen har pågått i én time.
@@ -51,6 +53,7 @@
Det oppstod et problem med å laste inn chattene dine
Det oppstod feil ved oppheving av utestengelse av deltaker
Kunne ikke lagre %1$s
+ 15 minutter
mappe
Laster ...
%1$s (%2$d)
@@ -84,6 +87,7 @@
Begynn å skrive for å søke...
Søk...
Meldinger
+ Demp alle varslinger
Valgt konto er nå importert og tilgjengelig
Om
Aktiv bruker
@@ -167,7 +171,7 @@
Slett alle
Slett samtale
Hvis du sletter samtalen, blir den også slettet for alle andre deltakere.
- Slett
+ Slett melding
Meldingen ble slettet, men den kan ha blitt lekket til andre tjenester
Bruker %1$s ble fjernet
Fjern moderatorstatus
@@ -490,6 +494,10 @@
Ja
Neste uke
Ingen telefonnummerintegrasjon på grunn av manglende rettigheter
+ Alle meldinger
+ kun @-nevner
+ Av
+ Forvalg
1 time
Pålogget
Online-status
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 05d9f10..3623f75 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -13,7 +13,6 @@
@color/colorPrimary
#ff6F6F6F
-
#1E1E1E
#FFFFFF
@@ -40,6 +39,9 @@
#484848
+
+ #33000000
+
#121212
#2A2A2A
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index c698e66..c2a3dbb 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -5,6 +5,7 @@
Toevoegen aan Notities
Gesprek %1$s toegevoegd aan favorieten
Zoeken in %s
+ Toon afwezig
Archiveer gesprek
Wanneer een gesprek wordt gearchiveerd is het standaard verborgen. Selecteer de filter \"Gearchiveerd\" om gearchiveerdde gesprekken te zien. Directe vermeldingen worden nog steeds ontvangen.
Gearchiveerd
@@ -22,6 +23,7 @@
Blokkeer
Blokkeer deelnemer
Blokkeerlijst
+ Bezet
Agenda
Geavanceerde oproepopties
Dit gesprek is actief gedurende een uur
@@ -59,6 +61,7 @@
Het laden van uw gesprekken was problematisch
Fout opgetreden bij deblokkeren deelnemer
Kon %1$s niet opslaan
+ 15 minuten
map
Laden …
%1$s (%2$d)
@@ -94,6 +97,7 @@
Begin met typen om te zoeken
Zoeken ...
Berichten
+ Onderdruk alle meldingen
Het geselecteerde account is nu geïmporteerd en beschikbaar
Over
Actieve gebruiker
@@ -178,7 +182,7 @@
Alles verwijderen
Verwijder gesprek
Als je het gesprek verwijdert, wordt het ook verwijderd voor alle andere deelnemers.
- verwijderen
+ Bericht verwijderen
Het bericht is verwijderd, maar het is mogelijk gelekt naar andere services
Verwijder nu
Gebruiker %1$s is verwijderd
@@ -542,6 +546,7 @@ Kies er eentje van een provider.
Geen gearchiveerde gesprekken
Geen offline berichten bewaard
Geen telefoonnummer-integratie wegens ontbrekende machtigingen
+ Standaard
1 uur
Online
Online status
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 2529ee1..78ed36c 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -5,6 +5,7 @@
Dodaj do Notatek
Dodano rozmowę %1$s do ulubionych
Szukaj w %s
+ Wyglądaj jako offline
Archiwizuj rozmowę
Po zarchiwizowaniu rozmowa zostanie domyślnie ukryta. Wybierz filtr „Zarchiwizowane”, aby wyświetlić zarchiwizowane rozmowy. Bezpośrednie wzmianki będą nadal otrzymywane.
Zarchiwizowane
@@ -22,6 +23,7 @@
Blokowanie
Zablokuj uczestnika
Lista zablokowanych
+ Brak dostępności
Kalendarz
Zaawansowane opcje połączeń
Połączenie trwa od godziny.
@@ -61,6 +63,7 @@
Wystąpił problem z wczytaniem Twoich czatów
Wystąpił błąd podczas odblokowywania uczestnika
Nie udało się zapisać %1$s
+ 15 minut
katalog
Wczytywanie…
%1$s (%2$d)
@@ -76,6 +79,10 @@
Opuściłeś rozmowę %1$s
Wczytaj więcej wyników
Czas lokalny: %1$s
+ Odmowa dostępu do lokalizacji
+ Proszę włączyć w ustawieniach aplikacji
+ Usługi lokalizacji wyłączone
+ Proszę włączyć usługi lokalizacji (GPS), aby użyć tej funkcji
Zablokuj rozmowę
Symbol zamknięcia
Opuścić rękę
@@ -89,6 +96,7 @@
Od największych
Od najmniejszych
Wiadomość skopiowana
+ Czy na pewno chcesz usunąć tę wiadomość?
Wiadomość usunięta przez Ciebie
Edytowany przez %1$s
Dotknij, aby otworzyć sondę
@@ -96,6 +104,7 @@
Zacznij pisać, aby wyszukać…
Szukaj…
Wiadomości
+ Wycisz wszystkie powiadomienia
Wybrane konto jest teraz zaimportowane i dostępne
O aplikacji
Aktywny użytkownik
@@ -180,7 +189,7 @@
Usuń wszystko
Usuń rozmowę
Jeśli usuniesz rozmowę, zostanie ona również usunięta dla wszystkich pozostałych uczestników.
- Usuń
+ Usuń wiadomość
Wiadomość została pomyślnie usunięta, ale mogła przedostać się do innych usług
Usuń teraz
Użytkownik %1$s został usunięty
@@ -544,6 +553,11 @@
Brak zarchiwizowanych rozmów
Nie zapisano żadnych wiadomości offline
Brak integracji numeru telefonu z powodu braku uprawnień
+ Wszystkie wiadomości
+ Tylko, gdy @-wspomniano
+ Wyłączone
+ Domyślny
+ Śledź ustawienia rozmowy
1 godzina
Online
Status online
@@ -553,7 +567,6 @@
Przejdź do wątku
Odtwórz/wstrzymaj wiadomość głosową
Kontrola prędkości odtwarzania
- Proszę kontynuować proces logowania w przeglądarce
Dodaj opcję
Edytuj głos
Zakończ sondę
@@ -631,10 +644,9 @@
Połączenie głosowe
Pokaż powód zablokowania
Pokaż zablokowanych uczestników
- Pokaż wątki
Ulubione
Nie możesz rozpocząć połączenia
- Rozpocznij wątek
+ Utwórz wątek
rozpoczął połączenie
Komunikat statusu
Status przywrócone
@@ -653,8 +665,12 @@
W tym tygodniu
To jest wiadomość testowa
W ten weekend
- %1$d odpowiedzi
+ Anuluj tworzenie wątku
+ Powiadomienia wątków
+ Odpowiedź
Tytuł wątku
+ Wątki
+ Nie znaleziono wątków
Dzisiaj
Jutro
Tłumaczenie
@@ -704,6 +720,12 @@
- Ta konwersacja zostanie automatycznie usunięta dla wszystkich po %1$d dniach braku aktywności.
- Ta konwersacja zostanie automatycznie usunięta dla wszystkich po %1$d dniach braku aktywności.
+
+ - %d odpowiedź
+ - %d odpowiedzi
+ - %d odpowiedzi
+ - %d odpowiedzi
+
- %d głos
- %d głosy
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index f4e78c4..cfba471 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -5,6 +5,7 @@
Adicionar a Notas
Conversa %1$s adicionada para favoritos
Pesquisar em %s
+ Aparecer off-line
Arquivar conversa
Quando uma conversa for arquivada, ela ficará oculta por padrão. Selecione o filtro \"Arquivada\" para visualizar as conversas arquivadas. As menções diretas ainda serão recebidas.
Arquivada
@@ -22,6 +23,7 @@
Banir
Banir participante
Lista de banimentos
+ Ocupado
Calendário
Opções avançadas de chamada
A chamada está em andamento há uma hora.
@@ -61,6 +63,7 @@
Ocorreu um problema ao carregar seus bate-papos
Ocorreu um erro ao cancelar o banimento do participante
Falha ao salvar %1$s
+ 15 minutos
pasta
Carregando …
%1$s (%2$d)
@@ -76,6 +79,10 @@
Você saiu da conversa %1$s
Carregar mais resultados
Horário local: %1$s
+ Permissão de localização negada
+ Por favor, ative-a nas configurações do aplicativo
+ Serviços de localização desativados
+ Ative os serviços de localização (GPS) para usar este recurso
Bloquear conversa
Símbolo de cadeado
Baixar mão
@@ -89,6 +96,7 @@
Maior primeiro
Menor primeiro
Mensagem copiada
+ Tem certeza de que deseja excluir esta mensagem?
Mensagem excluída por você
Editado por %1$s
Toque para abrir enquete
@@ -96,6 +104,7 @@
Comece a digitar para pesquisar …
Pesquisar …
Mensagens
+ Silenciar todas as notificações
A conta selecionada agora está importada e disponível
Sobre
Usuário ativo
@@ -180,7 +189,7 @@
Excluir todos
Excluir conversa
Se você excluir a conversa, ela também será excluída para todos os outros participantes.
- Excluir
+ Excluir mensagem
Mensagem excluída com sucesso, mas pode ter sido vazada para outros serviços
Excluir agora
Usuário %1$s foi removido
@@ -544,6 +553,11 @@
Nenhuma conversa arquivada
Nenhuma mensagem off-line foi salva
Sem integração de número de telefone devido à falta de permissões
+ Todas as mensagens
+ Apenas menções de @
+ Desativado
+ Padrão
+ Seguir as configurações da conversa
1 hora
On-line
Status on-line
@@ -553,7 +567,6 @@
Ir para fio
Reproduzir/pausar mensagem de voz
Controle de velocidade de reprodução
- Por favor, continue o processo de login no navegador
Adicionar opção
Editar voto
Encerrar enquete
@@ -631,10 +644,9 @@
Voz
Mostrar motivo do banimento
Mostrar participantes banidos
- Mostrar fios
Favorito
Você não tem permissão para iniciar uma chamada
- Iniciar um fio
+ Criar um fio
iniciou uma chamada
Mensagem de status
Status Revertido
@@ -653,8 +665,12 @@
Esta semana
Esta é uma mensagem de teste
Este fim de semana
- %1$d respostas
+ Cancelar a criação do fio
+ Notificações de fios
+ Responder
Título do fio
+ Fios
+ Nenhum fio encontrado
Hoje
Amanhã
Traduzir
@@ -702,6 +718,11 @@
- Esta conversa será excluída automaticamente para todos em %1$d de dias sem atividade
- Esta conversa será excluída automaticamente para todos em %1$d dias sem atividade
+
+ - %d resposta
+ - %d de respostas
+ - %d respostas
+
- %d voto
- %d votos
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 8e6dc5d..4b9eea0 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -3,9 +3,14 @@
Редактирование
Добавить
Добавить в заметки
+ Обсуждение добавлено %1$s в избранное
Искать в %s
+ \"Не в сети\" для остальных
Архивировать обсуждение
+ После архивации обсуждение будет скрыто по-умолчанию. Выберите фильтр \"Архивировано\", чтобы увидеть архивированные обсуждения. Прямые упоминания все еще будут получены.
В архиве
+ Архивировано %1$s
+ Голосовой звонок
Bluetooth
Аудиовыход
Телефон
@@ -14,6 +19,11 @@
Ваш статус был установлен автоматически
Аватар
Отошёл
+ Кнопка Назад
+ Заблокировать
+ Заблокировать участника
+ Список заблокированных
+ Занят
Календарь
Дополнительные настройки звонка
Звонок длится уже час
@@ -24,19 +34,27 @@
Очистить сообщение о статусе
Очищать статус после
Закрыть
+ Значок Закрыть
Соединение установлено
+ Нет подключения к серверу
Соединение потеряно - отправленные сообщения ставятся в очередь
Блокировка записи для непрерывной записи голосового сообщения
+ Обсуждение архивировано
Разговор доступен только для чтения
+ Ошибка установки обсуждения только на чтение
Беседы
Создать беседу
+ Создать запрос
Задать
Опасная зона
+ %1$s в %2$s
Удалить аватар
+ Удалить голосовую запись
Удалённый чат %1$s
Не беспокоить
Не очищать
Редактировать
+ Сообщения старше 24 часов нельзя редактировать
Редактировать сообщение
Недавние
Зашифровано
@@ -45,6 +63,7 @@
Возникла проблема при загрузке ваших чатов
Произошла ошибка при разблокировке участника
Не удалось сохранить %1$s
+ 15 минут
каталог
Загрузка …
%1$s (%2$d)
@@ -53,6 +72,8 @@
(изменено)
Внутреннее примечание
Невидимый
+ Языки не могут быть извлечены
+ Не удалось получить
Позже сегодня
Покинуть вызов
Вы покинули чат %1$s
@@ -70,6 +91,8 @@
Я - А
Самый большой первый
Самый маленький первый
+ Сообщение скопировано
+ Вы уверены, что хотите удалить это сообщение?
Сообщение удалено вами
Изменено %1$s
Нажмите для открытия опроса
@@ -77,6 +100,7 @@
Начните печатать для поиска …
Поиск …
Сообщения
+ Без уведомлений
Выбранная учётная запись теперь доступна
О программе
Активный пользователь
@@ -161,7 +185,7 @@
Удалить все
Удалить беседу
При удалении беседы, она будет также удалена у других участников
- Удалить
+ Удалить сообщение
Сообщение удалено, но его содержимое могло быть прочитано другими службами
Удалить сейчас
Пользователь %1$s был удален
@@ -179,13 +203,30 @@
Зарегистрированные пользователи
Версия приложения
Оптимизация батареи игнорируется, все в порядке
+ Включена оптимизация батареи, что может вызвать проблемы. Вам нужно отключить оптимизацию батареи!
Настройки батареи
Устройство
Открыть контрольный список устранения неполадок
Открыть экран диагностики
Открыть dontkillmyapp.com
+ Получение последнего пуш-токена firebaseпоследнийпоследний
+ Генерация последнего пуш-токена firebase
+ Не установлен пуш-токен firebase. Создайте сообщение о проблеме.
+ Пуш-токен firebase
+ Сервисы Google Play недоступны. Уведомления не поддерживаются
+ Сервисы Google Play
+ Сервисы Google Play доступны
+ Последняя пуш-регистрация на пуш-прокси
+ Пока не зарегистрирован на пуш-прокси
+ Последняя пуш-регистрация на сервере
+ Пока не зарегистрирован на сервере
+ Общее уведомление
+ Генерация отчета системы
+ Включен ли канал голосовых звонков?
+ Включен ли канал уведомлений?
Разрешения на уведомления
Телефон
+ Версия Talk на сервере
Версия сервера
Внешний
Внутренние
@@ -207,6 +248,8 @@
Редактирование
Редактировать сообщение
Изменено администратором
+ Меню обсуждения события
+ Расписание
8 часов
4 недели
Отключить
@@ -218,10 +261,12 @@
Не удалось получить параметры сигнализации
Принять
Отклонить
+ из %1$s на %2$s
Нет ожидающих приглашений
У вас есть ожидающие приглашения
Назад
Требуется разрешение на доступ к файлу
+ Фильтровать обсуждения
Пользователь, вошедший по ссылке
Вы: %1$s
Переслать
@@ -245,9 +290,11 @@
Приглашения были разосланы повторно.
Поделиться ссылкой на беседу
Напишите сообщение …
+ Оптимизация батареи не игнорируется. Это необходимо изменить, чтобы уведомления работали в фоне. Нажмите OK и выберите \"Все приложения\" -> %1$s -> Не оптимизировать
Игнорировать оптимизацию батареи
Важное обсуждение
Статус «Не беспокоить» игнорируется для важных обсуждений
+ Некорректное время
Приглашения
Присоединиться к открытым обсуждениям
Сохранить
@@ -270,6 +317,12 @@
Не задано
Отметить прочитанным
Отметить непрочитанным
+ Обсуждение помечено как важное
+ Обсуждение не помечено как конфиденциальное
+ Обсуждение помечено как конфиденциальное
+ Обсуждение не помечено как важное
+ Встреча завершена
+ Сообщение добавлено в записки
Не удалось
Не удалось отправить сообщение:
Не в сети
@@ -277,6 +330,7 @@
Сообщение прочитано
Отправка
Сообщение отправлено
+ Микрофон включен, и беседа записывается
Для включения голосовой связи предоставьте разрешение «Микрофон».
Вы пропустили звонок от %s
Модератор
@@ -288,6 +342,7 @@
Гость
Нет
Нет открытых чатов
+ Нет открытых обсуждений, к которым вы могли бы присоединиться. Либо открытых обсуждений нет, либо вы уже подключились ко всем.
Не использовать прокси-сервер
Вам запрещено активировать аудио!
Вам запрещено активировать видео!
@@ -300,6 +355,7 @@
Загрузки
Уведомление о ходе загрузки
Параметры уведомлений
+ Уведомления некорректно настроены
Разрешение на уведомления и настройки батареи настроены правильно для получения уведомлений. Если у вас всё равно возникли проблемы с получением уведомлений, проверьте, включены ли каналы уведомлений для звонков и сообщений. Дополнительную помощь можно найти на сайте DontKillMyApp.com или в контрольном списке устранения неполадок. Если это не помогло, перейдите на экран диагностики и отправьте отчёт об ошибке.
Устранение неполадок с уведомлениями
Всегда уведомлять
@@ -307,6 +363,7 @@
Не уведомлять
Нет подключения к сети, пожалуйста, проверьте ваше соединение
ОК
+ Идущая беседа
Открыть обсуждение для зарегистрированных пользователей
Открыть для гостей
Владелец
@@ -315,6 +372,7 @@
Пароль
Установить разрешения
В некоторых разрешениях было отказано.
+ Пожалуйста, разрешить права доступа
Открыть настройки
Пожалуйста, предоставьте разрешения в разделе «Настройки» > «Разрешения».
Учётная запись не найдена
@@ -327,23 +385,29 @@
Назначить модератором
Открытое обсуждение
Всплывающие уведомления отключены
+ Что-то пошло не так, ошибка %1$s
+ Что-то пошло не так, не могу получить тестовое пуш-сообщение
+ Пуш-сообщение отправлено успешно. Вы теперь должны получить сообщение на этом устройстве с заголовком \"Тестирование пуш-сообщений\"
PTT (нажми чтобы говорить)
Нажмите и удерживайте для использования PTT при отключённом микрофоне
Напомнить позже
Удалить из избранного
Удалить группу и её участников
Удалить участника
+ Удалить пароль
Исключить команду и её участников
Переименовать разговор
Переименовать
Ответить
Ответить личным сообщением
+ Комната сохранена успешно
Сохранить
Сохранено успешно
30 секунд
5 минут
1 минута
10 минут
+ Немедленно
600
60
30
@@ -351,6 +415,8 @@
Поиск
Очистить поиск
Выберите учётную запись
+ Обновить сообщение
+ Послать голосовую запись
Конфиденциальное обсуждение
Предварительный просмотр сообщений будет отключен в списке обсуждений и уведомлениях
%1$s отправил(а) GIF.
@@ -361,6 +427,7 @@
Вы отправили аудиофайл.
%1$s отправил(а) изображение.
Вы отправили изображение.
+ %1$s отправил карточку
Проверить соединение с сервером
Обновите базу данных %1$s
Не удалось импортировать выбранную учётную запись
@@ -382,7 +449,9 @@
Дополнительно
Внешний вид
Вызовы
+ Свяжитесь с администратором
Откройте экран диагностики, чтобы проверить настройки или создать отчёт об ошибке.
+ Диагностика
Указывает клавиатуре отключить персонализированное обучение (без гарантий)
Клавиатура инкогнито
Без звука
@@ -419,6 +488,8 @@
Версия сервера очень старая и не будет поддерживаться в следующем релизе!
Версия сервера слишком старая и не поддерживается данной версией приложения Android
Неподдерживаемый сервер
+ Приложение сообщений от сервера не установлно
+ Установлено охранителем батареи
Тёмное
Как в системе
стиль оформления
@@ -443,10 +514,17 @@
Нет общих элементов
Местоположение
Местоположением поделились
+ Если уведомления настроены неверно, показывать регулярное предупреждение
+ Показывать регулярное предупреждение
Сортировать по
+ Начать групповой чат
Время начала
Сменить аккаунт
Команда
+ Проверить пуш-уведомления
+ Результаты теста
+ Сегодня в %1$s
+ Завтра в %1$s
Выберите файлы
Отправить эти файлы %1$s?
Отправить этот файл %1$s?
@@ -468,7 +546,12 @@
Вебинар
Да
Следующая неделя
+ Отсутствуют архивированные обсуждения
+ Нет сохраненных оффлайн сообщений
Отсутствуют разрешения на интеграцию номера телефона
+ Отключить
+ По умолчанию
+ Следовать настройкам обсуждения
1 час
В сети
Онлайн статус
@@ -477,12 +560,15 @@
Открыть примечания
Перейти в тему
Воспроизведение/пауза голосового сообщения
+ Управление скоростью вопроизведения
Добавить вариант
Изменить голос
Завершить опрос
Вы действительно хотите закончить этот опрос? Это нельзя отменить.
Вы не можете голосовать с несколькими вариантами для этого опроса.
Множество ответов
+ Удалите опцию %1$d
+ Опция %1$d
Варианты
Закрытый опрос
Вопрос
@@ -506,10 +592,13 @@
Остановить запись
Запись останавливается...
Согласие на запись вызова требуется для всех вызовов
+ В записи могут оказаться ваш голос, видео с камеры и изображения экрана. Ваше согласие необходимо перед присоединением ко звонку. Вы согласны?
Требовать согласие на запись вызова перед присоединением к вызову в этом обсуждении
Согласие на запись вызова
Звонок может быть записан.
Запись
+ Удалено обсуждение %1$s из избранного
+ Обсуждение %1$s переименовано
Отправить заново
Сбросить статус
Невозможно присоединиться к другим комнатам находясь в звонке
@@ -539,6 +628,7 @@
Установить статус
Установить статус
Поделиться
+ Вступить в обсуждение %1$s в %2$s
Звук
Файл
Медиа
@@ -546,11 +636,14 @@
Опрос
Запись звонка
Голосовая почта
- Показать темы
+ Показать причину блокировки
+ Показать заблокированных участников
Избранное
Вам не разрешено звонить
- Начать тему
+ Создать тему
+ начал(-а) звонок
Описание статуса
+ Статус возвращен
Перейти в комнат отдыха
Перейти в основную комнату
Сфотографировать
@@ -564,9 +657,13 @@
Переключатель фонарика
30 минут
Эта неделя
+ Это тестовое сообщение
Эти выходные
- %1$d ответов
+ Отменить создание темы
+ Уведомления для темы
+ Ответ
Заголовок темы
+ Темы
Сегодня
Завтра
Помочь с переводом
@@ -583,9 +680,13 @@
печатает...
и ещё %1$s собеседника печатают...
Разархивировать обсуждение
+ После разархивирования обсуждения оно будет видимо по-умолчанию
+ Разархивировано %1$s
Разбанить
Непрочитанное
Загрузить новый аватар с устройства
+ %1$s находится вне офиса и может не ответить
+ %1$s вне офиса сегодня
Замена:
Изображение профиля
Адрес
@@ -598,11 +699,24 @@
Не удалось получить личную информации о пользователе
Личная информация не указана
На странице профиля укажите своё имя, добавьте изображение и подробные сведения
+ Видеозвонок
Какой у вас статус?
+
+ - Посмотреть %d похожее сообщение
+ - Посмотреть %d похожих сообщения
+ - Посмотреть %d похожих сообщений
+ - Посмотреть %d похожих сообщений
+
+
+ - Данное обсуждение будет автоматически удалено для всех через %1$d день отсутствия активности.
+ - Данное обсуждение будет автоматически удалено для всех через %1$d дня отсутствия активности.
+ - Данное обсуждение будет автоматически удалено для всех через %1$d дней отсутствия активности.
+ - Данное обсуждение будет автоматически удалено для всех через %1$d дней отсутствия активности.
+
- %d голос
- %d голоса
- %d голосов
- - %d голоса
+ - %d голосов
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index e56a0fc..04701b3 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -3,11 +3,13 @@
Modìfica
Agiunghe
Chirca in %s
+ Mustra•ti foras de lìnia
Archiviadu
Telèfonu
Altoparlante
Avatar
Ausente
+ Impinnadu
Calendàriu
Sèbera s\'avatar dae sa nue virtuale
Lìmpia su messàgiu de istadu
@@ -24,6 +26,7 @@
Reghente
Tzifradu
No at fatu a sarvare %1$s
+ 15 minutos
cartella
Carrigamentu …
%1$s (%2$d)
@@ -43,6 +46,7 @@
Messàgiu cantzelladu dae tue
Perunu resurtadu de chirca
Messàgios
+ Istuda totu is notìficas
Su contu seletzionadu est importadu e a disponimentu
In contu de
Utèntzia ativa
@@ -108,7 +112,7 @@
Cantzella totu
Cantzella resonada
Si cantzellas sa resonada, s\'at a cantzellare puru pro su restu de partetzipantes.
- Cantzella
+ Cantzella messàgiu
Messàgiu cantzelladu, ma diat pòdere èssere istadu dispensadu a àteros servìtzios
Lea·ssi su permissu de moderare
Registra messàgiu de boghe
@@ -323,6 +327,7 @@
Eja
Sa chida chi benit
Peruna integratzione de nùmeru de telèfonu pro farta de permissos
+ Predefinidu
1 ora
In lìnia
Istadu in lìnia
@@ -372,6 +377,7 @@
Ativa sa tortza
30 minutos
Custa chida
+ Risponde
Oe
Cras
Borta
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index 8bbd289..e8b6f60 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -21,6 +21,7 @@
Ban
Udeliť účastníkovy ban
Zoznam banov
+ Zaneprázdnený
Kalendár
Pokročilé možnosti hovoru
Hovor už trvá viac ako jednu hodinu.
@@ -56,6 +57,7 @@
Nastal problém s načítaním vašich správ
Pri rušení banu pre účastníka sa vyskytla chyba
Nepodarilo sa uložiť %1$s
+ 15 minút
priečinok
Načítavam …
%1$s (%2$d)
@@ -172,7 +174,7 @@
Vymazať všetko
Zmazať konverzáciu
Ak zmažete konverzáciu, bude takisto zmazaná pre všetkých ostatných zúčastnených
- Zmazať
+ Zmazať správu
Správa úspešne odstránená, ale mohla uniknúť na iné serveri.
Užívateľ %1$s bol vymazaný
Odobrať moderovanie
@@ -507,6 +509,9 @@
Nasledujúci týždeň
Žiadne správy pri odpojení neboli uložené
Registrácia telefónneho čísla nie je povolená pre chýbajúce oprávnenia
+ Iba zmienky v tvare @-meno
+ Vypnúť
+ Predvolené
1 hodina
Pripojený
Stav pripojenia
@@ -609,6 +614,7 @@
Tento týždeň
Toto je skúšobná správa
Tento víkend
+ Odpovedať
Dnes
Zajtra
Preložiť
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index ab0bd86..6fc359d 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -3,6 +3,7 @@
Uredi
Dodaj
Poišči v %s
+ Pokaže kot brez povezave
Arhivirano
Bluetooth
Odvod zvoka
@@ -12,6 +13,7 @@
Stanje je določeno samodejno
Podoba
Ne spremljam
+ Zasedeno
Koledar
Napredne možnosti klica
Pogovor traja že eno uro.
@@ -37,6 +39,7 @@
Končaj klic za vse
Prišlo je do napake nalaganja pogovorov.
Shranjevanje %1$s je spodletelo.
+ 15 minut
mapa
Poteka nalaganje …
%1$s (%2$d)
@@ -61,6 +64,7 @@
Vpišite niz za iskanje …
Poišči …
Sporočila
+ Utiša vsa obvestila
Izbran račun je uvožen in na voljo za uporabo
O programu
Dejavni uporabnik
@@ -140,7 +144,7 @@
Izbriši vse
Izbriši pogovor
Če izbrišete pogovor, bo ta izbrisan za vse udeležence.
- Izbriši
+ Izbriši sporočilo
Sporočilo je uspešno izbrisano, a je lahko že poslano na druge storitve.
Izbriši
Ponižaj iz moderatorja
@@ -408,6 +412,7 @@
Da
Naslednji teden
Povezava s telefonskim imenikom ni na voljo zaradi neustreznih dovoljenj.
+ Privzeto
1 uri
Trenutno na spletu
Povezano stanje
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 0a3b034..a478797 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -5,6 +5,7 @@
Додај у Белешке
Разговор %1$s је додат у омиљене
Тражи у %s
+ Прикажи као ван мреже
Архивирај разговор
Када се разговор архивира, подразумевано ће бити сакривен. Да бисте видели архивиране разговоре, изаберите филтер „Архивирано”. И даље ће се примати директна помињања.
Архивирано
@@ -22,6 +23,7 @@
Забрана
Забрани учесника
Листа забрањених
+ Заузет
Календар
Напредне опције позива
Позив траје један сат.
@@ -61,6 +63,7 @@
Дошло је до проблема приликом учитавања ваших четова
Дошло је до грешке приликом укидања забране за учесника
Није успело чување %1$s
+ 15 минута
фасцикли
Учитавање…
%1$s (%2$d)
@@ -76,6 +79,10 @@
Напустили сте разговор %1$s
Учитај још резултата
Локално време: %1$s
+ Одбијена је дозвола за локацију
+ Молимо вас да је укључите у подешавањима апликације
+ Искључени су сервиси локације
+ Да бисте користили ову функционалност, молимо вас да укључите сервисе локације (GPS)
Закључај разговор
Симбол катанца
Спуштена рука
@@ -89,6 +96,7 @@
Прво највеће
Прво најмање
Порука је копирана
+ Да ли сте сигурни да желите да обришете ову поруку?
Обрисали сте поруку
Уредио је %1$s
Тапните да отворите гласање
@@ -96,6 +104,7 @@
Крените да куцате и претрага почиње ...
Претрага ...
Поруке
+ Искључи сва обавештења
Одабрани налог је сада увезен и доступан
О програму
Активни корисник
@@ -180,7 +189,7 @@
Обриши све
Обриши разговор
Ако обришете разговор, он ће бити обрисан и за све друге учеснике.
- Избриши
+ Обриши поруку
Порука је успешно обрисана, али је можда процурела на друге сервисе
Обриши одмах
Корисник %1$s је уклоњен
@@ -544,6 +553,11 @@
Нема архивираних разговора
Није сачувана ниједна порука за ван мреже
Нема интеграције броја телефона јер недостају дозволе
+ Све поруке
+ Само @-помињања
+ Искључено
+ Подразумевано
+ Прати подешавања разговора
1 сат
На мрежи
Мрежни статус
@@ -553,7 +567,6 @@
Иди на нит
Пусти/паузирај гласовну поруку
Контрола брзине репродукције
- Молимо вас да процес пријаве наставите у интернет прегледачу
Додај ставку
Уреди гласање
Заврши гласање
@@ -633,6 +646,7 @@
Прикажи забрањене кориснике
Омиљени
Није вам дозвољене да започнете позив
+ Креирај нит
је започео позив
Порука стања
Враћено је старо стање статуса
@@ -651,6 +665,12 @@
Ове недеље
Ово је тест порука
Овог викенда
+ Откажи креирање нити
+ Обавештења низова
+ Одговори
+ Наслов нити
+ Нити
+ Није пронађен ниједан низ
Данас
Сутра
Превођење
@@ -698,6 +718,11 @@
- Овај разговор ће се аутоматски обрисати за све након %1$d дана неактивности
- Овај разговор ће се аутоматски обрисати за све након %1$d дана неактивности
+
+ - %d одговор
+ - %d одговора
+ - %d одговора
+
- %d глас
- %d гласа
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index f0bc51c..3465dc5 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -5,6 +5,7 @@
Lägg till i anteckningar
Lade till konversationen %1$s till favoriter
Sök i %s
+ Visa som frånkopplad
Arkivera konversation
När en konversation väl har arkiverats döljs den som standard. Välj filtret \"Arkiverad\" för att se arkiverade konversationer. Direkta omnämnanden kommer fortfarande att tas emot.
Arkiverad
@@ -22,6 +23,7 @@
Blockera
Blockera deltagare
Blockeringslista
+ Upptagen
Kalender
Avancerade samtalsalternativ
Samtalet har pågått i en timme.
@@ -60,6 +62,7 @@
Det gick inte att ladda dina chattar
Fel uppstod när deltagare skulle avblockeras
Misslyckades att spara %1$s
+ 15 minuter
mapp
Läser in …
%1$s (%2$d)
@@ -75,6 +78,10 @@
Du lämnade konversationen %1$s
Visa fler resultat
Lokal tid: %1$s
+ Behörighet för plats nekad
+ Aktivera det i appens inställningar
+ Platstjänster inaktiverat
+ Aktivera platstjänster (GPS) för att använda denna funktion
Lås konversation
Låssymbol
Ta ner handen
@@ -88,6 +95,7 @@
Störst först
Minst först
Meddelandet kopierat
+ Är du säker på att du vill ta bort detta meddelande?
Meddelandet togs bort av dig
Redigerad av %1$s
Tryck för att öppna omröstningen
@@ -95,6 +103,7 @@
Börja skriva för att söka ...
Sök ...
Meddelanden
+ Stäng av alla aviseringar
Valt konto är nu importerat och tillgängligt
Om
Aktiv användare
@@ -179,7 +188,7 @@
Ta bort alla
Ta bort konversation
Om du raderar konversationen, kommer den även att raderas för alla andra deltagare.
- Radera
+ Radera meddelande
Meddelandet har raderats, men kan ha läckt till andra tjänster
Ta bort nu
Användare %1$s togs bort
@@ -543,6 +552,11 @@
Inga arkiverade konversationer
Inga offlinemeddelanden sparade
Ingen telefonnummerintegrering på grund av saknade behörigheter
+ Alla meddelanden
+ Endast @-omnämnanden
+ Av
+ Förvald
+ Följ konversationsinställningar
1 timme
Online
Online-status
@@ -552,7 +566,6 @@
Gå till tråd
Spela/pausa röstmeddelande
Kontroll av uppspelningshastighet
- Fortsätt inloggningen i webbläsaren
Lägg till alternativ
Redigera röst
Avsluta omröstning
@@ -629,10 +642,9 @@
Röst
Visa orsak till blockering
Visa blockerade deltagare
- Visa trådar
Favorit
Du får inte starta ett samtal
- Starta en tråd
+ Skapa en tråd
startade ett samtal
Statusmeddelande
Status återställd
@@ -651,7 +663,12 @@
Denna vecka
Detta är ett testmeddelande
Denna helgen
- %1$d svar
+ Avbryt skapande av tråd
+ Trådaviseringar
+ Svara
+ Trådtitel
+ Trådar
+ Inga trådar hittades
Idag
I morgon
Översätt
@@ -697,6 +714,10 @@
- Den här konversationen kommer automatiskt att tas bort för alla om %1$d dag utan aktivitet
- Den här konversationen kommer automatiskt att tas bort för alla om %1$d dagar utan aktivitet
+
+ - %d svar
+ - %d svar
+
- %d röst
- %d röster
diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml
index 9d99542..a91fc6b 100644
--- a/app/src/main/res/values-sw/strings.xml
+++ b/app/src/main/res/values-sw/strings.xml
@@ -5,6 +5,7 @@
Ongeza katika kumbukumbu
Mazungumzo yaliyoongezwa %1$skatika vipendwa
Tafuta katika %s
+ Tokea nje ya mtandao
Hifadhi mazungumzo
Mara tu mazungumzo yanapowekwa kwenye kumbukumbu, yatafichwa kwa chaguo-msingi. Chagua kichujio \"Kilichohifadhiwa\" ili kutazama mazungumzo yaliyohifadhiwa. Matangazo ya moja kwa moja bado yatapokelewa.
Imewekwa katika kumbukumbu
@@ -22,6 +23,7 @@
Piga marufuku
Mshiriki aliyepigwa marufuku
Orodha iliyopigwa marufuku
+ Bize
Kalenda
Chaguzi za simu za hali ya juu
Simu imekuwa ikifanya kazi kwa saa moja.
@@ -61,6 +63,7 @@
Kulikuwa na tatizo kupakia chati zako
Hitilafu ilitokea wakati wa kubatilisha mshiriki
Imeshindwa kuhifadhi %1$s
+ Dakika 15
folda
Inapakia
%1$s (%2$d)
@@ -76,6 +79,10 @@
Umeacha mazungumzo %1$s
Pakia matokeo zaidi
Muda wa kawaida: %1$s
+ Ruhusa ya eneo imekataliwa
+ Tafadhali iwashe katika mipangilio ya programu
+ Huduma za eneo zimezimwa
+ Tafadhali wezesha huduma za eneo (GPS) ili kutumia kipengele hiki
Funga mazungumzo
Funga ishara
Mkono wa chini
@@ -89,6 +96,7 @@
Kubwa zaidi kwanza
Ndogo zaidi kwanza
Ujumbe umenakiliwa
+ Je, una uhakika unataka kufuta ujumbe huu?
Ujumbe umefutwa na wewe
Imehaririwa na %1$s
Bonyeza kufungua poll
@@ -96,12 +104,12 @@
Anza kuchapa ili kutafuta
Tafuta...
Jumbe
+ Zima arifu zote
Akaunti iliyochaguliwa sasa imeingizwa na inapatikana
Kuhusu
Mtumiaji anayetumia
Ongeza akaunti
- Akaunti imeratibiwa kufutwa, na haiwezi kubadilishwa
-
+ Akaunti imeratibiwa kufutwa, na haiwezi kubadilishwa
Fungua mwongozo mkuu
Ongeza kiambatisho
Ongeza emoji
@@ -181,7 +189,7 @@
Futa zote
Futa mazungumzo
Ukifuta mazungumzo, yatafutwa pia kwa washiriki wengine wote.
- Futa
+ Futa ujumbe
Ujumbe umefutwa kikamilifu, lakini huenda umevuja kwa huduma zingine
Futa sasa
Mtumiaji %1$s aliondolewa
@@ -190,8 +198,7 @@
Tuma ujumbe
Akaunti ya sasa
Seva
- Programu ya arifa ya seva imesakinishwa?
-
+ Programu ya arifa ya seva imesakinishwa?
Mtumiaji
Je, hali ya mtumiaji imewezeshwa?
Toleo la Android
@@ -548,6 +555,11 @@
Hakuna mazungumzo yaliyohifadhiwa kwenye kumbukumbu
Hakuna jumbe za nje ya mtandao zilizohifadhiwa
Hakuna muunganisho wa nambari ya simu kwa sababu ya kukosa ruhusa
+ Jumbe zote
+ \@-mitajo pekee
+ Imezimwa
+ Chaguo msingi
+ Fuata mipangilio ya mazungumzo
Saa 1
Mtandaoni
Hadhi ya mtandaoni
@@ -557,7 +569,6 @@
Nenda kwenye mjadala
Cheza/simamisha ujumbe wa sauti
Udhibiti wa kasi ya mshindonyuma
- Tafadhali endeleza mchakato wa kuingia katika kivinjari
Ongeza mbadala
Hariri kura
Maliza kura ya maoni
@@ -600,7 +611,7 @@
Pangilia hali
Haiwezekani kujiunga na vyumba vingine ukiwa kwenye simu
Hifadhi
- Scan QR Code
+ Changanua Msimbo wa QR
Sawazisha kwa seva zinazoaminika pekee
Shirikisho
Inaonekana kwa watu katika tukio hili na wageni pekee
@@ -635,10 +646,9 @@
Sauti
Onesha sababu ya zuio
Onesha washiriki waliozuiliwa
- Onesha mijadala
Kipendwa
Huruhusiwi kuanzisha simu
- Anza mjadala
+ Unda mjadala
Umeanzisha simu
Ujumbe wa hadhi
Hali Imerejeshwa
@@ -652,13 +662,17 @@
Badili kamera
Dondosha picha
Punguza ukubwa wa picha
- Toggle torch
+ Washa tochi
Dakika 30
Wiki hii
Huu ni ujumbe wa jaribio
Wikendi hii
- %1$d majibu
+ Ghairi uundaji wa mjadala
+ Arifa za mjadala
+ Jibu
Kichwa cha mjadala
+ Mijadala
+ Hakuna nyuzi zilizopatikana
Leo
Kesho
Tafsiri
@@ -702,8 +716,11 @@
- This conversation will be automatically deleted for everyone in %1$d day of no activity
- - Mazungumzo haya yatafutwa kiotomatiki kwa kila mtu baada ya siku %1$d bila shughuli yoyote
-
+ - Mazungumzo haya yatafutwa kiotomatiki kwa kila mtu baada ya siku %1$d bila shughuli yoyote
+
+
+ - %d reply
+ - %dmajibu
- %d vote
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 5018bf0..74e83e1 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -5,6 +5,7 @@
Notlar uygulamasına ekle
%1$s görüşmesi sık kullanılara eklendi
%s içinde ara
+ Çevrim dışı görün
Görüşmeyi arşivle
Bir görüşme arşivlendiğinde, varsayılan olarak gizlenir. Arşivlenmiş görüşmeleri görüntülemek için \"Arşivlenmiş\" süzgecini seçin. Doğrudan anmalar yine de alınır.
Arşivlenmiş
@@ -22,6 +23,7 @@
Yasakla
Katılımcıyı yasakla
Yasaklama listesi
+ Meşgul
Takvim
Gelişmiş çağrı seçenekleri
Çağrı bir saattir sürüyor.
@@ -61,6 +63,7 @@
Çizelgeleriniz yüklenirken bir sorun çıktı
Katılımcının yasaklaması kaldırılırken sorun çıktı
%1$s kaydedilemedi
+ 15 dakika
klasör
Yükleniyor…
%1$s (%2$d)
@@ -76,6 +79,10 @@
%1$s görüşmesinden çıktınız
Diğer sonuçları yükle
Yerel zaman: %1$s
+ Konum izni reddedildi
+ Lütfen Ayarlar içinden izin verin
+ Konum hizmetleri kapalı
+ Bu özelliği kullanabilmek için konum hizmetlerini (GPS) açın
Görüşmeyi kilitle
Kilit simgesi
Eli indir
@@ -89,6 +96,7 @@
Büyükten küçüğe
Küçükten büyüğe
İleti kopyalandı
+ Bu iletiyi silmek istediğinize emin misiniz?
İleti sizin tarafınızdan silindi
%1$s tarafından düzenlendi
Anketi açmak için dokunun
@@ -96,7 +104,7 @@
Aramak için yazmaya başlayın…
Arama…
İletiler
- İletiler yüklenemedi
+ Tüm bildirimleri kapat
Seçilmiş hesap içe aktarıldı ve kullanılabilir
Hakkında
Etkin kullanıcı
@@ -181,7 +189,7 @@
Tümünü sil
Görüşmeyi sil
Görüşmeyi silerseniz tüm diğer katılımcılar için de silinecek.
- Sil
+ İletiyi sil
İleti silindi ancak başka hizmetlere aktarılmış olabilir
Şimdi sil
%1$s kullanıcısı kaldırıldı
@@ -426,7 +434,6 @@
%1$s bir tahta kartı gönderdi
Sunucu bağlantısını sına
Lütfen %1$s veri tabanınızı güncelleyin
- Sunucu ile bağlantı kurulamadı
Seçilmiş hesap içe aktarılamadı
%1$s site arayüzü için tarayıcıda açacağınız bağlantı.
%1$s uygulamasındaki hesabı içe aktar
@@ -546,6 +553,11 @@
Arşivlenmiş bir görüşme yok
Herhangi bir çevrim dışı iletisi kaydedilmemiş
İzinler eksik olduğundan telefon numarası bütünleştirmesi yok
+ Tüm iletiler
+ Yalnızca @-anmaları
+ Kapalı
+ Varsayılan
+ Görüşme ayarları kullanılsın
1 saat
Çevrim içi
Çevrim içi durumu
@@ -555,7 +567,6 @@
Yazışmaya git
Ses iletisini oynat/duraklat
Oynatma hızı denetimi
- Lütfen oturum açma işlemini tarayıcınızdan sürdürün
Seçenek ekle
Oyu düzenle
Anleti sonlandır
@@ -633,10 +644,9 @@
Ses
Yasaklama nedeni görüntülensin
Yasaklanmış katılımcıları görüntüle
- Yazışmaları görüntüle
Sık kullanılanlara ekle
Bir çağrı başlatma izniniz yok
- Yazışma başlat
+ Bir yazışma oluştur
bir çağrı başlattı
Durum iletisi
Durum geri alındı
@@ -656,13 +666,16 @@
Bu bir deneme iletisidir
Bu hafta sonu
Yazışma başlatmaktan vazgeç
- %1$d yanıt
+ Yazışma bildirimleri
+ Yanıtla
Yazışma başlığı
+ Yazışmalar
+ Herhangi bir yazışma bulunamadı
Bugün
Yarın
Çevir
Çeviri
- Çevrilmiş metni kopyala
+ Çevrilmiş yazıyı kopyala
Dili algıla
Aygıt ayarları
Dil algılanamadı
@@ -703,6 +716,10 @@
- Bu görüşme, %1$d gün boyunca etkileşim olmazsa herkesten otomatik olarak silinecek.
- Bu görüşme, %1$d gün boyunca etkileşim olmazsa herkesten otomatik olarak silinecek.
+
+ - %d yanıt
+ - %d yanıt
+
- %d oy
- %d oy
diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml
index 562c894..d367fed 100644
--- a/app/src/main/res/values-ug/strings.xml
+++ b/app/src/main/res/values-ug/strings.xml
@@ -5,6 +5,7 @@
ئىزاھاتقا قوشۇڭ
ياقتۇرىدىغانلارغا %1$s پاراڭ قوشۇلدى
%s دىن ئىزدەڭ
+ تورسىز كۆرۈنۈش
ئارخىپ سۆھبىتى
ئارخىپلاشتۇرۇلغان
كۆك چىش
@@ -19,6 +20,7 @@
Ban
قاتناشقۇچىنى چەكلەش
چەكلەش تىزىملىكى
+ ئالدىراش
كالېندار
ئىلغار چاقىرىش تاللانمىلىرى
تېلېفون بىر سائەت داۋاملاشتى.
@@ -53,6 +55,7 @@
پاراڭلىرىڭىزنى يۈكلەشتە مەسىلە كۆرۈلدى
قاتناشقۇچىنى چەكلىگەندە خاتالىق كۆرۈلدى
%1$s نى تېجەلمىدى
+ 15 مىنۇت
ھۆججەت قىسقۇچ
Loading…
%1$s (%2$d)
@@ -86,6 +89,7 @@
ئىزدەشنى باشلاڭ…
ئىزدەش…
ئۇچۇرلار
+ بارلىق ئۇقتۇرۇشلارنى ئاۋازسىز قىلىڭ
تاللانغان ھېسابات ھازىر ئىمپورت قىلىندى ۋە ئىشلەتكىلى بولىدۇ
ھەققىدە
ئاكتىپ ئىشلەتكۈچى
@@ -169,7 +173,7 @@
ھەممىنى ئۆچۈرۈڭ
سۆھبەتنى ئۆچۈرۈڭ
ئەگەر سۆھبەتنى ئۆچۈرسىڭىز ، ئۇ باشقا بارلىق قاتناشقۇچىلار ئۈچۈن ئۆچۈرۈلىدۇ.
- ئۆچۈرۈش
+ ئۇچۇرنى ئۆچۈرۈڭ
ئۇچۇر مۇۋەپپەقىيەتلىك ئۆچۈرۈلدى ، ئەمما ئۇ باشقا مۇلازىمەتلەرگە ئاشكارىلانغان بولۇشى مۇمكىن
ئىشلەتكۈچى%1$s چىقىرىۋېتىلدى
رىياسەتچىدىن تۆۋەنلەش
@@ -495,6 +499,10 @@
ھەئە
كېلەر ھەپتە
ئىجازەت يوقالغانلىقتىن تېلېفون نومۇرىنى بىرلەشتۈرۈش يوق
+ بارلىق ئۇچۇرلار
+ \@ -mentions only
+ Off
+ كۆڭۈلدىكى
1 سائەت
توردا
توردىكى ئورنى
@@ -592,6 +600,7 @@
بۇ ھەپتە
بۇ بىر سىناق ئۇچۇرى
بۇ ھەپتە ئاخىرى
+ جاۋاب
بۈگۈن
ئەتە
تەرجىمە
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 5a40f20..289e889 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -5,6 +5,7 @@
Додати до нотаток
Додано розмову %1$s до обраного
Пошук у %s
+ Перебуваю поза мережею
Архівна розмова
Після архівування розмова буде прихована за замовчуванням. Виберіть фільтр «Архів» для перегляду архівних розмов. Прямі згадки все одно будуть надходити.
Заархівовані
@@ -22,6 +23,7 @@
Заборона
Забанити учасника
Список заборон
+ Зайнято
Календар
Розширені опції дзвінка
Дзвінок триває вже годину.
@@ -61,6 +63,7 @@
Виникла проблема під час завантаження ваших чатів
Виникла помилка при знятті бану з учасника
Не вдалося зберегти %1$s
+ 15 хвилин
каталог
Завантаження …
%1$s (%2$d)
@@ -96,7 +99,7 @@
Що шукаємо?
Пошук …
Повідомлення
- Не вдалося отримати повідомлення
+ Вимкнути всі сповіщення
Обраний обліковий запис імпортовано і він доступний
Про застосунок
Активний користувач
@@ -181,7 +184,7 @@
Вилучити все
Вилучити розмову
Якщо ви вилучите розмову, її буде вилучено у всіх інших учасників цієї розмови.
- Вилучити
+ Вилучити повідомлення
Повідомлення вилучено, але воно могло потрапити до інших сервісів
Видалити зараз
Користувача %1$s було видалено
@@ -426,7 +429,6 @@
%1$s надіслав карту колоди
Перевірка з\'єднання з сервером
Будь ласка, оновіть вашу %1$s базу даних
- Не вдалося з\'єднатися з сервером
Не вдалося імпортувати обраний акаунт.
Посилання на ваш веб-інтерфейс %1$s, коли ви відкриєте його у веб-переглядачі.
Імпортувати обліковий запис із застосунку %1$s
@@ -546,6 +548,9 @@
Розмови не архівуються
Не збережено жодного офлайн-повідомлення
Відсутні дозволи на інтеграцію номера телефону
+ Вимкнено
+ Типово
+ Слідкувати за налаштуваннями розмови
1 година
Онлайн
Статус онлайну
@@ -555,7 +560,6 @@
Перейти до теми
Відтворення/пауза голосового повідомлення
Регулювання швидкості відтворення
- Будь ласка, продовжте процес входу в браузері
Додати варіант
Змінити голос
Завершити опитування
@@ -633,10 +637,9 @@
Голос
Показати причину бану
Показати заборонених учасників
- Показати теми
Із зірочкою
Ви не маєте права починати розмову
- Почніть тему
+ Створіть тему
почала дзвонити.
Повідомлення про статус
Статус скасовано
@@ -656,8 +659,8 @@
Це тестове повідомлення
Цими вихідними
Відхилити створення гілки
- %1$d відповідей
Назва гілки
+ Нитки
Сьогодні
Завтра
Перекласти
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 9c5adde..7fa6c41 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -4,6 +4,7 @@
添加
对话%1$s已添加至收藏
搜索位置 %s
+ 显示为离线
存档对话
已存档
蓝牙
@@ -14,6 +15,7 @@
您的状态已自动设定
头像
离开
+ 忙碌
日历
高级呼叫选项
通话已持续一小时
@@ -42,6 +44,7 @@
为所有人结束通话
加载聊天记录时出错
保存%1$s失败
+ 15 分钟
文件夹
正在加载 …
%1$s (%2$d)
@@ -71,6 +74,7 @@
开始输入以搜索…
搜索...
消息
+ 静音所有通知
所选帐户现已导入并可用
关于
活跃用户
@@ -154,7 +158,7 @@
删除全部
删除对话
如果你删除了该对话,它也会被从其他所有与会者那里删除。
- 删除
+ 删除邮件
消息已成功删除,但可能已泄漏给其他服务
立即删除
取消主持人资格
@@ -281,7 +285,7 @@
当被提及时提醒
从不提醒
当前离线,请检查您的连接
- OK
+ 确定
向注册用户开放对话
同样对访客用户开放
所有者
@@ -441,6 +445,10 @@
是的
下周
由于缺少权限,没有电话集成
+ 仅被@提及的消息
+ 关
+ 默认
+ 遵循对话设置
1小时
在线
在线状态
@@ -519,6 +527,7 @@
语音
收藏
您没有开始通话的权限。
+ 创建帖子
发起了通话
状态消息
切换到分组讨论
@@ -536,8 +545,9 @@
本周
这是一个测试消息
本周末
- %1$d 个回复
+ 帖子通知
帖子标题
+ 帖子
今天
明天
翻译
@@ -569,7 +579,7 @@
检索个人用户信息失败
未设置个人信息
在你的个人资料页上添加姓名、图片和联系方式。
- 你什么状态?
+ 您的状态如何?
- %d 票
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 563201a..aeaf70f 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -5,6 +5,7 @@
添加到筆記
添加了對話 %1$s 至最愛
%s內搜尋
+ 顯示為離線
封存對話
對話封存後,預設會隱藏。選取篩選條件「已封存」可檢視已封存的對話。仍會收到直接提及。
已封存
@@ -22,6 +23,7 @@
封禁
封禁參與者
封禁清單
+ 忙碌
日曆
進階通話選項
請注意,通話已經持續一個小時了。
@@ -61,6 +63,7 @@
載入您的聊天記錄時時發生問題
取消封禁參與者時發生錯誤
儲存 %1$s 失敗
+ 15 分鐘
資料夾
加載中 …
%1$s(%2$d)
@@ -76,6 +79,10 @@
你離開了對話 %1$s
正在載入更多結果
本地時間︰%1$s
+ 位置權限被拒絕
+ 請在「應用程式設定」中啟用
+ 已停用位置服務
+ 您必須啟用位置服務 (GPS) 以使用此功能
鎖定對話
鎖符號
放下手
@@ -89,6 +96,7 @@
最大先
最小先
已複製訊息
+ 您確定要刪除此訊息?
訊息被您刪除
由 %1$s 編輯
點按即可打開民意調查
@@ -96,7 +104,7 @@
輸入文字以搜尋 …
搜尋 …
訊息
- 載入訊息失敗
+ 所有通知靜音
所選帳戶現已匯入並可用
關於
活躍用戶
@@ -181,7 +189,7 @@
全部刪除
刪除對話
如果您刪除了此對話,它也將會從所有其他參與者處刪除。
- 刪除
+ 刪除訊息
消息已成功刪除,但可能已分發到其他服務
立刻刪除
用户 %1$s 已被移除
@@ -426,7 +434,6 @@
%1$s 傳送了一張卡片
測試伺服器連線
請升級你的%1$s數據庫
- 無法連接至伺服器
無法匯入所選的帳戶
在瀏覽器中打開 %1$s 網絡界面的連結。
從%1$s應用程式匯入帳戶
@@ -546,6 +553,11 @@
沒有已封存的對話
未儲存離線訊息
由於缺少權限而無法集成電話號碼
+ 全部消息
+ 僅被 @提及的消息
+ 關閉
+ 默認
+ 跟隨對話設定
1 小時
在線
線上狀態
@@ -555,7 +567,6 @@
前往討論串
播放﹨暫停話音短訊
播放速度控制
- 請在瀏覽器中完成登入流程
添加選項
編輯選票
結束民意調查
@@ -633,10 +644,9 @@
音頻
顯示封禁原因
顯示被封禁的參與者
- 顯示討論串
我的最愛
你無權開始通話
- 開始討論串
+ 創建討論串
發起了通話
狀態訊息
狀態已還原
@@ -656,8 +666,11 @@
此乃測試訊息
本週末
取消創建討論串
- %1$d 個回覆
+ 討論串通知
+ 回覆
討論串標題
+ 討論串
+ 找不到討論串
今日
明日
翻譯
@@ -701,6 +714,9 @@
- 此對話如無人於%1$d天內互動,將會自動為所有人刪除。
+
+ - %d 個回覆
+
- %d 選票
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index d070475..a9d7885 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -5,6 +5,7 @@
新增至筆記
已新增對話 %1$s 至最愛
在 %s內搜尋
+ 顯示為離線
封存對話
對話封存後,預設會隱藏。選取篩選條件「已封存」可檢視已封存的對話。仍會收到直接提及。
已封存
@@ -22,6 +23,7 @@
封鎖
封鎖參與者
封鎖清單
+ 忙碌
日曆
進階通話選項
通話已經持續一個小時了。
@@ -61,6 +63,7 @@
在載入您的對話時出現錯誤
解除封鎖參與者時遇到錯誤
儲存 %1$s 失敗
+ 15分鐘
資料夾
正在載入……
%1$s (%2$d)
@@ -76,6 +79,10 @@
您離開了對話 %1$s
載入更多結果
當地時間:%1$s
+ 位置權限被拒絕
+ 請在應用程式設定中啟用
+ 已停用位置服務
+ 請啟用位置服務 (GPS) 以使用此功能
鎖定對話
上鎖符號
放手
@@ -89,6 +96,7 @@
最大優先
最小優先
已複製訊息
+ 您確定要刪除此訊息?
訊息已被您刪除
由 %1$s 編輯
點按開啟投票
@@ -96,7 +104,7 @@
輸入文字以搜尋 …
搜尋 …
訊息
- 無法載入訊息
+ 靜音所有通知
選擇已匯入且可用的帳號
關於
活躍使用者
@@ -181,7 +189,7 @@
全部刪除
刪除對話
如果您刪除了此對話,它也將會從所有其他參與者處刪除。
- 刪除
+ 刪除訊息
訊息已成功刪除,但可能已分發到其他服務
立刻刪除
使用 %1$s 已移除
@@ -426,7 +434,6 @@
%1$s 傳送了一張卡片
測試伺服器連線
請升級你的%1$s資料庫
- 無法連線至伺服器
無法匯入所選的帳戶
在瀏覽器中開啟您 %1$s 網頁介面的連結。
從%1$s應用程式匯入帳戶
@@ -546,6 +553,11 @@
無封存的對話
未儲存離線訊息
因為缺少權限而無法整合電話號碼
+ 所有訊息
+ 僅被 @ - 提及 的訊息
+ 關閉
+ 預設
+ 遵循對話設定
1小時
線上
線上狀態
@@ -555,7 +567,6 @@
到討論串
播放/暫停語音訊息
播放速度控制
- 請在瀏覽器中繼續登入流程
新增選項
編輯投票
結束投票
@@ -633,10 +644,9 @@
語音
顯示封鎖理由
顯示已封鎖的參與者
- 顯示討論串
收藏
您不被允許開始通話
- 開始討論串
+ 建立討論串
開始通話
狀態訊息
狀態已還原
@@ -656,8 +666,11 @@
這是測試訊息
本週末
取消建立討論串
- %1$d 個回覆
+ 討論串通知
+ 回覆
討論串標題
+ 討論串
+ 找不到討論串
今天
明天
翻譯
@@ -701,6 +714,9 @@
- 此對話若於%1$d天內沒有活動,將會自動為所有人刪除
+
+ - %d 個回覆
+
- %d 票
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 661c429..a39e44a 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -14,8 +14,6 @@
#ffffff
#B3FFFFFF
-
-
@android:color/white
#666666
@@ -39,6 +37,8 @@
#FFFFFF
@color/high_emphasis_text
+
+ #55FFFFFF
@color/high_emphasis_text
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index f9ccd0c..9f309e0 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,6 +2,7 @@
Add to conversation
Take photo
@@ -869,4 +894,12 @@ How to translate with transifex:
Conversation is archived
Local time: %1$s
Open Notes
+ Cancel Login
+ Scan QR Code
+ QR code could not be read
+ Are you sure you want to delete this message?
+ Location permission denied
+ Please enable it in the app settings
+ Location services disabled
+ Please enable location services (GPS) to use this feature
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 83ca98d..927e7b2 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -89,10 +89,12 @@
+
+
+
+
diff --git a/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt
new file mode 100644
index 0000000..b6bda98
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt
@@ -0,0 +1,596 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.login.data
+
+import android.os.Bundle
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
+import com.nextcloud.talk.account.data.LoginRepository
+import com.nextcloud.talk.account.data.io.LocalLoginDataSource
+import com.nextcloud.talk.account.data.model.LoginCompletion
+import com.nextcloud.talk.account.data.model.LoginResponse
+import com.nextcloud.talk.account.data.network.NetworkLoginDataSource
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@Suppress("TooManyFunctions", "TooGenericExceptionCaught")
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner.Silent::class)
+class LoginRepositoryTest {
+
+ @get:Rule
+ val rule = InstantTaskExecutorRule()
+
+ // Repository dependencies
+ @Mock
+ lateinit var networkLoginDataSource: NetworkLoginDataSource
+
+ @Mock
+ lateinit var localLoginDataSource: LocalLoginDataSource
+
+ // Additional mocks for LocalLoginDataSource dependencies
+ @Mock
+ lateinit var liveData: LiveData
+
+ lateinit var repo: LoginRepository
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ repo = LoginRepository(networkLoginDataSource, localLoginDataSource)
+ }
+
+ // ========== pollLogin() Tests ==========
+
+ @Test
+ fun `pollLogin returns successful LoginCompletion when network returns HTTP 200`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val successfulLoginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(successfulLoginData)
+
+ // Act
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals(200, result?.status)
+ assertEquals("https://server.com", result?.server)
+ assertEquals("testuser", result?.loginName)
+ assertEquals("apppass123", result?.appPassword)
+ }
+
+ @Test
+ fun `pollLogin returns null when network returns null`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(null)
+
+ // Act
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `pollLogin continues polling when status is not HTTP 200 then returns successful result`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val pendingLoginData = LoginCompletion(202, "https://server.com", "testuser", "apppass123")
+ val successfulLoginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(pendingLoginData)
+ .thenReturn(successfulLoginData)
+
+ // Act
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals(200, result?.status)
+ verify(networkLoginDataSource, times(2)).performLoginFlowV2(mockResponse)
+ }
+
+ @Test
+ fun `pollLogin handles slow connection by continuing to poll with delays`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val slowResponse1 = LoginCompletion(202, "https://server.com", "testuser", "apppass123")
+ val slowResponse2 = LoginCompletion(404, "https://server.com", "testuser", "apppass123")
+ val slowResponse3 = LoginCompletion(500, "https://server.com", "testuser", "apppass123")
+ val successResponse = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(slowResponse1)
+ .thenReturn(slowResponse2)
+ .thenReturn(slowResponse3)
+ .thenReturn(successResponse)
+
+ // Act
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals(200, result?.status)
+ verify(networkLoginDataSource, times(4)).performLoginFlowV2(mockResponse)
+ }
+
+ @Test
+ fun `pollLogin handles network timeouts during slow connection gracefully`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val timeoutResponse = LoginCompletion(408, "https://server.com", "testuser", "apppass123")
+ val successResponse = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(timeoutResponse)
+ .thenReturn(timeoutResponse)
+ .thenReturn(successResponse)
+
+ // Act
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals(200, result?.status)
+ verify(networkLoginDataSource, times(3)).performLoginFlowV2(mockResponse)
+ }
+
+ @Test
+ fun `pollLogin stops when cancelLoginFlow is called`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val pendingLoginData = LoginCompletion(
+ 202,
+ "https://server.com",
+ "testuser",
+ "apppass123"
+ )
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(pendingLoginData)
+
+ // Act - cancel before polling
+ repo.cancelLoginFlow()
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNull(result)
+ verify(networkLoginDataSource, never()).performLoginFlowV2(any())
+ }
+
+ // ========== startLoginFlowFromQR() Tests ==========
+
+ @Test
+ fun `startLoginFlowFromQR returns LoginCompletion for valid QR data with all parameters`() {
+ // Arrange
+ val qrData = "nc://login/user:testuser&server:https%3A//example.com&password:testpass"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals(200, result?.status)
+ assertEquals("https://example.com", result?.server)
+ assertEquals("testuser", result?.loginName)
+ assertEquals("testpass", result?.appPassword)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR returns LoginCompletion for minimal valid QR data`() {
+ // Arrange
+ val qrData = "nc://login/server:https%3A//example.com"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR returns null for invalid prefix`() {
+ // Arrange
+ val qrData = "invalid://login/user:testuser&server:https://example.com"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR returns null when too many arguments provided`() {
+ // Arrange
+ val qrData = "nc://login/user:test&server:https://example.com&password:pass&extra:value"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR returns null for empty data`() {
+ // Arrange
+ val qrData = "nc://login/"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR sets reAuth flag correctly`() {
+ // Arrange
+ val qrData = "nc://login/server:https%3A//example.com"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData, reAuth = true)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR handles URL encoding correctly`() {
+ // Arrange
+ val qrData = "nc://login/user:test%40user.com&server:https%3A//example.com%3A8080&password:test%26pass"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals("test@user.com", result?.loginName)
+ assertEquals("https://example.com:8080", result?.server)
+ assertEquals("test&pass", result?.appPassword)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR handles mixed parameter order`() {
+ // Arrange
+ val qrData = "nc://login/password:testpass&user:testuser&server:https%3A//example.com"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals("https://example.com", result?.server)
+ assertEquals("testuser", result?.loginName)
+ assertEquals("testpass", result?.appPassword)
+ }
+
+ // ========== startLoginFlow() Tests ==========
+
+ @Test
+ fun `startLoginFlow returns LoginResponse from network`() =
+ runTest {
+ // Arrange
+ val baseUrl = "https://example.com"
+ val mockResponse = LoginResponse("token123", "https://example.com/poll", "https://example.com/login")
+ whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl))
+ .thenReturn(mockResponse)
+
+ // Act
+ val result = repo.startLoginFlow(baseUrl)
+
+ // Assert
+ assertEquals(mockResponse, result)
+ verify(networkLoginDataSource).anonymouslyPostLoginRequest(baseUrl)
+ }
+
+ @Test
+ fun `startLoginFlow returns null when network returns null`() =
+ runTest {
+ // Arrange
+ val baseUrl = "https://example.com"
+ whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl))
+ .thenReturn(null)
+
+ // Act
+ val result = repo.startLoginFlow(baseUrl)
+
+ // Assert
+ assertNull(result)
+ }
+
+ @Test
+ fun `startLoginFlow sets reAuth flag correctly`() =
+ runTest {
+ // Arrange
+ val baseUrl = "https://example.com"
+ val mockResponse = LoginResponse("token123", "https://example.com/poll", "https://example.com/login")
+ whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl))
+ .thenReturn(mockResponse)
+
+ // Act
+ val result = repo.startLoginFlow(baseUrl, reAuth = true)
+
+ // Assert
+ assertEquals(mockResponse, result)
+ }
+
+ @Test
+ fun `startLoginFlow handles network SSL exceptions`() =
+ runTest {
+ // Arrange
+ val baseUrl = "https://example.com"
+ whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl))
+ .thenReturn(null) // NetworkLoginDataSource catches SSL exceptions and returns null
+
+ // Act
+ val result = repo.startLoginFlow(baseUrl)
+
+ // Assert
+ assertNull(result)
+ verify(networkLoginDataSource).anonymouslyPostLoginRequest(baseUrl)
+ }
+
+ // ========== cancelLoginFlow() Tests ==========
+
+ @Test
+ fun `cancelLoginFlow stops polling loop`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val pendingLoginData = LoginCompletion(202, "https://server.com", "testuser", "apppass123")
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(pendingLoginData)
+
+ // Act
+ repo.cancelLoginFlow()
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNull(result)
+ }
+
+ // ========== parseAndLogin() Tests ==========
+
+ @Test
+ fun `parseAndLogin returns null when user is scheduled for deletion`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(true)
+ whenever(localLoginDataSource.startAccountRemovalWorker())
+ .thenReturn(liveData)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertNull(result)
+ verify(localLoginDataSource).startAccountRemovalWorker()
+ }
+
+ @Test
+ fun `parseAndLogin returns null when user exists and reAuth is false`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(true)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertNull(result)
+ verify(localLoginDataSource, never()).updateUser(any())
+ }
+
+ @Test
+ fun `parseAndLogin updates user when user exists and reAuth is true`() {
+ // Arrange - First set reAuth to true via QR flow
+ val qrData = "nc://login/server:https%3A//example.com"
+ repo.startLoginFlowFromQR(qrData, reAuth = true)
+
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(true)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertNull(result)
+ verify(localLoginDataSource).updateUser(loginData)
+ }
+
+ @Test
+ fun `parseAndLogin returns Bundle for new user with https protocol`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(false)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertNotNull(result)
+ assertTrue(result is Bundle)
+ }
+
+ @Test
+ fun `parseAndLogin returns Bundle for new user with http protocol`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "http://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(false)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertNotNull(result)
+ assertTrue(result is Bundle)
+ }
+
+ @Test
+ fun `parseAndLogin returns Bundle for new user without protocol prefix`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(false)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertNotNull(result)
+ assertTrue(result is Bundle)
+ }
+
+ // ========== LocalLoginDataSource Integration Tests ==========
+
+ @Test
+ fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserIsScheduledForDeletion`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(true)
+ whenever(localLoginDataSource.startAccountRemovalWorker())
+ .thenReturn(liveData)
+
+ // Act
+ repo.parseAndLogin(loginData)
+
+ // Assert
+ verify(localLoginDataSource).checkIfUserIsScheduledForDeletion(loginData)
+ verify(localLoginDataSource).startAccountRemovalWorker()
+ }
+
+ @Test
+ fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserExists`() {
+ // Arrange
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(true)
+
+ // Act
+ repo.parseAndLogin(loginData)
+
+ // Assert
+ verify(localLoginDataSource).checkIfUserExists(loginData)
+ verify(localLoginDataSource, never()).updateUser(any())
+ }
+
+ @Test
+ fun `parseAndLogin calls updateUser with correct LoginCompletion data`() {
+ // Arrange - Set reAuth flag first
+ val qrData = "nc://login/server:https%3A//example.com"
+ repo.startLoginFlowFromQR(qrData, reAuth = true)
+
+ val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(true)
+
+ // Act
+ repo.parseAndLogin(loginData)
+
+ // Assert
+ verify(localLoginDataSource).updateUser(loginData)
+ }
+
+ // ========== Edge Cases and Error Handling ==========
+
+ @Test
+ fun `pollLogin handles performLoginFlowV2 returning error status codes`() =
+ runTest {
+ // Arrange
+ val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login")
+ val errorResponse = LoginCompletion(404, "", "", "")
+ val successResponse = LoginCompletion(200, "https://server.com", "testuser", "apppass123")
+
+ whenever(networkLoginDataSource.performLoginFlowV2(mockResponse))
+ .thenReturn(errorResponse)
+ .thenReturn(successResponse)
+
+ // Act
+ val result = repo.pollLogin(mockResponse)
+
+ // Assert
+ assertNotNull(result)
+ assertEquals(200, result?.status)
+ }
+
+ @Test
+ fun `startLoginFlowFromQR handles malformed URL gracefully`() {
+ // Arrange
+ val qrData = "nc://login/malformed&data&without&proper&key:value"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNull(result) // Should still create LoginCompletion with empty values
+ }
+
+ @Test
+ fun `startLoginFlowFromQR handles partial parameter data`() {
+ // Arrange
+ val qrData = "nc://login/user:testuser&server:"
+
+ // Act
+ val result = repo.startLoginFlowFromQR(qrData)
+
+ // Assert
+ assertNull(result)
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt b/app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt
new file mode 100644
index 0000000..e83156f
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Julius Linus
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.login.data.network
+
+import com.nextcloud.talk.account.data.model.LoginResponse
+import com.nextcloud.talk.account.data.network.NetworkLoginDataSource
+import junit.framework.TestCase.assertNotNull
+import junit.framework.TestCase.assertNull
+import okhttp3.OkHttpClient
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Before
+import org.junit.Test
+import org.mockito.MockitoAnnotations
+
+@Suppress("ktlint:standard:max-line-length", "MaxLineLength")
+class NetworkLoginDataSourceTest {
+
+ lateinit var network: NetworkLoginDataSource
+ private val okHttpClient: OkHttpClient = OkHttpClient.Builder().build()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ network = NetworkLoginDataSource(okHttpClient)
+ }
+
+ @Test
+ fun `testing anonymouslyPostLoginRequest correct path`() {
+ val server = MockWebServer()
+ server.start(0)
+ val httpUrl = server.url("index.php/login/v2")
+ val validResponse = """
+ {
+ "poll":{
+ "token":"mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1",
+ "endpoint":"https:\/\/cloud.example.com\/login\/v2\/poll"
+ },
+ "login":"https:\/\/cloud.example.com\/login\/v2\/flow\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg"
+ }
+ """.trimIndent()
+ val mockResponse = MockResponse().setBody(validResponse)
+ server.enqueue(mockResponse)
+
+ val loginResponse = network.anonymouslyPostLoginRequest(httpUrl.toString())
+ assertNotNull(loginResponse)
+ }
+
+ @Test
+ fun `testing anonymouslyPostLoginRequest error path`() {
+ val server = MockWebServer()
+ val invalidResponse = MockResponse()
+ .addHeader("Content-Type", "application/json; charset=utf-8")
+ .addHeader("Cache-Control", "no-cache")
+ .setResponseCode(404)
+ .setBody("{}")
+
+ server.start()
+ server.enqueue(invalidResponse)
+ val httpUrl = server.url("index.php/login/v2")
+
+ val loginResponse = network.anonymouslyPostLoginRequest(httpUrl.toString())
+ assertNull(loginResponse)
+ }
+
+ @Test
+ fun `testing anonymouslyPostLoginRequest malformed response`() {
+ val server = MockWebServer()
+ val validResponse = """
+ {
+ "poll":{
+ "token":"mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1"
+ },
+ "login":"https:\/\/cloud.example.com\/login\/v2\/flow\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg"
+ }
+ """.trimIndent()
+
+ val mockResponse = MockResponse().setBody(validResponse)
+ server.enqueue(mockResponse)
+ server.start()
+ val httpUrl = server.url("index.php/login/v2")
+
+ val loginResponse = network.anonymouslyPostLoginRequest(httpUrl.toString())
+ assertNull(loginResponse)
+ }
+
+ @Test
+ fun `testing performLoginFlowV2 correct path`() {
+ val server = MockWebServer()
+ val validBody = """
+ {
+ "server":"https:\/\/cloud.example.com",
+ "loginName":"username",
+ "appPassword":"yKTVA4zgxjfivy52WqD8kW3M2pKGQr6srmUXMipRdunxjPFripJn0GMfmtNOqOolYSuJ6sCN"
+ }
+ """.trimIndent()
+
+ val validResponse = MockResponse()
+ .setBody(validBody)
+
+ server.enqueue(validResponse)
+ server.start()
+ val httpUrl = server.url("login/v2/poll")
+ val loginResponse = LoginResponse(
+ token = "mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1",
+ pollUrl = httpUrl.toString(),
+ loginUrl = "https:\\/\\/cloud.example.com\\/login\\/v2\\/flow\\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg"
+ )
+
+ val loginCompletion = network.performLoginFlowV2(loginResponse)
+ assertNotNull(loginCompletion)
+ }
+
+ @Test
+ fun `testing performLoginFlowV2 error path`() {
+ val server = MockWebServer()
+
+ val invalidResponse = MockResponse()
+ .addHeader("Content-Type", "application/json; charset=utf-8")
+ .addHeader("Cache-Control", "no-cache")
+ .setResponseCode(404)
+ .setBody("{}")
+
+ server.enqueue(invalidResponse)
+ server.start()
+ val httpUrl = server.url("login/v2/poll")
+ val loginResponse = LoginResponse(
+ token = "mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1",
+ pollUrl = httpUrl.toString(),
+ loginUrl = "https:\\/\\/cloud.example.com\\/login\\/v2\\/flow\\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg"
+ )
+
+ val loginCompletion = network.performLoginFlowV2(loginResponse)
+ assert(loginCompletion?.status == 404)
+ }
+
+ @Test
+ fun `testing performLoginFlowV2 malformed response`() {
+ val server = MockWebServer()
+ val validBody = """
+ {
+ "server":"https:\/\/cloud.example.com",
+ "loginName":"username"
+ }
+ """.trimIndent()
+
+ val validResponse = MockResponse()
+ .setBody(validBody)
+
+ server.enqueue(validResponse)
+ server.start()
+ val httpUrl = server.url("login/v2/poll")
+ val loginResponse = LoginResponse(
+ token = "mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1",
+ pollUrl = httpUrl.toString(),
+ loginUrl = "https:\\/\\/cloud.example.com\\/login\\/v2\\/flow\\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg"
+ )
+
+ val loginCompletion = network.performLoginFlowV2(loginResponse)
+ assertNull(loginCompletion)
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
index 814c70b..9e51c4b 100644
--- a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
@@ -27,8 +27,9 @@ class MessageSearchHelperTest {
title: String = "foo",
messageExcerpt: String = "foo",
conversationToken: String = "foo",
- messageId: String? = "foo"
- ) = SearchMessageEntry(searchTerm, thumbnailURL, title, messageExcerpt, conversationToken, messageId)
+ messageId: String? = "foo",
+ threadId: String? = "foo"
+ ) = SearchMessageEntry(searchTerm, thumbnailURL, title, messageExcerpt, conversationToken, threadId, messageId)
@Before
fun setUp() {
diff --git a/build.gradle b/build.gradle
index d6b39e3..180a439 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,7 +11,7 @@
buildscript {
ext {
- kotlinVersion = '2.2.0'
+ kotlinVersion = '2.2.20'
}
repositories {
@@ -21,12 +21,12 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.11.1'
+ classpath 'com.android.tools.build:gradle:8.13.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
classpath "org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}"
- classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.2.2'
+ classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.2.7'
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8"
- classpath "org.jlleitschuh.gradle:ktlint-gradle:13.0.0"
+ classpath "org.jlleitschuh.gradle:ktlint-gradle:13.1.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@@ -42,10 +42,10 @@ allprojects {
repositories {
google()
mavenCentral()
- maven { url 'https://jitpack.io' }
+ maven { url = 'https://jitpack.io' }
}
}
tasks.register('clean', Delete) {
- delete rootProject.buildDir
+ delete rootProject.layout.buildDirectory
}
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 663e42c..ce65ac9 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -307,6 +307,50 @@ CqZ5vSc=
=B+Kr
-----END PGP PUBLIC KEY BLOCK-----
+pub 893A028475557671
+uid Gradle Inc.
+
+sub 5E9AEEBA28836032
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBGUVRogBEAChVh0t3YAJIdreb6SP/lf4x097IRpOiJ7Ww+DDtXFUhKJBwgfC
+4T10TBGP835tV6TfkEeCPGWABoxaD88zUlSHs7k7v/SfedwfOKbOE3c+oR43JL7P
+Gi2++Z+ZYiEJwPuEgoKITj76Pn/x7yyoRUI2VEX4U6UzZSi9QQ6EltQFTxHPB8Gp
+XBpRf9j1e6K4INGga4wyAXqrUl84PAahoQnspc16suc5ouJYINpf6/bbZqELHvcx
++x3uACrQq0ZoU/2V3N/E7dF4BJP2Bt93HV8xGrRz/rG7xu6ki2+PtZzxp+hBpgZL
+VOQKwfm/jLmO7xK8XjcOzQu7vEetWdrYv7a2TA4MBZCcSS/C+u02XlacYqh7bTYC
+Fy0nZO6p0qej1OiQI+dfsbYCSqooUPGhIC0aOAJjPGsmtkxlFVTcg2nqFABw65Uj
+nENeBAvCMz8155UqLEFcgF/KrMjIFN8j8QGC9vAQ3Jegi0EBvyEOBydw93zziCE2
+POhaGABn2P6tx+7BmXrwwtycrPrTFNhb/4/ofQVZA0dA98zXHNOP8dYwbLVCtnYH
+QEt0uorqoj+bEI1Q0WKKzyocaS5nnw1rYjs4tih1rhJqL1ThUiFFeFSU54v/D8CO
+5KSm2Toqf0qzv0zj3Q4ICXLTdGG6iQtGonNynPc5a76waUjGdhtW2+of0QARAQAB
+tB1HcmFkbGUgSW5jLiA8aW5mb0BncmFkbGUuY29tPrkCDQRlFUaIARAAx7Jeb988
+XoHevPyfazUgd7O+0mPafYsH8+pPmVu3jXoOA7BLRMdQpX9ckc045A+Zmx/VJbLK
+gFcHubGLWvay8KOBxVbexvckZbwIpsXqynOyCKscre5yK9rIIslYtceo3faLTKVh
+JHJdg7EDwdjbwiMtMLj/YbvPIrNRggQ43asg1S6vVdqIhsaCWHZ/81MYm4VgOMxZ
+vPQHIladKZFqjIMmoQ57knduClIh0ML52tXxt3czmgeZ798as5QD6hv9RWeB3JgP
+9bgXfX7s5MjOKTaPu1zRSdOkLvDZ1CUbsvh5XiIxpwEtjzLFJOCA1blRTuhmc5eg
+Fp5V6669SppnTPezX24nSM3zBZ72em3JXl7R3aNBAuJIIvikN0d511dg/LSmoSUU
+LQnF2CQU9ZR9dLGM0KR15m05EbD01jxtPdHLPcWDG058At6ZcHRQHWnysEBdg7cX
+mqXPUDUqjpojIY5KD6HixxeY2oFVMnpNDtJ1e8PNwv7RaKglE3i/XOXlaY3RHQy+
+q9ER0iEI2bGPWBONO778hR4zyX9VUSNDtvzrbeTVlfyLC8yWbsA+GbpOt28MhaWD
+de6/WtIl+O3wKO1O7F6cLTqXe/nc6smZco41tiII2DnUG6eFMn5zCfuohcoUY2Gp
+5zHCJiZZh2jZ8/oZPNAJ/mtjHN+GWhMLv7cAEQEAAYkCNgQYAQgAIBYhBHt5rdEf
+inef6Q/T0Ik6AoR1VXZxBQJlFUaIAhsMAAoJEIk6AoR1VXZxgwwP/1bH9XxxzyVE
+TexhKm7Yc/RlgrIdE+TGUV0W0b+233jHN01l0cOIU35dn5Ohi/7+PH4Tq0I8rGnW
+dUaHLHkmF/tJC+y3etnsqsLVxiZH0reBoq+EnjwOCRdpU2IrOeLTaDjkvpy8nmNj
+aA1tsEooT4iKyU1OxUk5GzH5z18HTTxuQ7EYPUFxBCkhx33EvRe1XTxflBd1AMZM
+/+tc/2r3LBZPZLMKSz6fhwdx+kN2dIGoyuN6UuG95BwADu7ePFD/BlSJXE8RKkSN
+wjuV1ZUsyJdX9h99ljYaknE9i8AyBb3AF9Nc8k/Cd3m6b+nUuA/ZWmMWHOXEyVlc
+Oih1/jf0DL6ZiaHEeHi5K5lDN5WGCljDrrfR4b0Z5Xz1BbE6ZYy+ZzKjs/yJc/YH
+3g7/7NuxyK+k+wIpgyUMYe0s7Djy2yx+6eNuHsv6AGi3Z253mATH5G7mpatPxWKZ
+uBaF/k2v38BBsvD0dLHFZGLABOWIKXJE0VcYyT1zR5CGviYlykG8SD8qtBj6Aynp
+4cZtKf/Oe8MlAZAvB1w/KGrZQIBpTN5E9ybEVkxFEiF8oqXuN7TPXJPL+3oAVU6s
+qSGbP5W6LdZKGCYM+FivMHDvAyRJhHK/lKDxIqIEwtAmUO66SkBPyFvQUTAeT9LR
+WzZKkqBVoahM3qqyoKOy7mfpt1hB4gEq
+=E5AV
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 8CD7D660AE857DAD
uid Emarc Magtanong
@@ -996,6 +1040,24 @@ EEIhZlI/ojefaZkRseFrtl3X
=pJaU
-----END PGP PUBLIC KEY BLOCK-----
+pub A6EA2E2BF22E0543
+uid Tobias Warneke (for development purposes)
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6
+xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ
+N5N5gUj/dqVI2rIvypIuxUApl88BYMsxYpn2+8FKeMd8oBJLqFRJ3WNjB4Op2tRO
+XRWoxs1ypubS/IV1zkphHHpi6VSABlTyTWu4kXEj/1/GpsdtHRa9kvdWw7yKQbnM
+XuwOxtzZFJcyu0P2jYVfHHvxcjxuklc9edmCGdNxgKIoo0LXZOeFIi6OWtwzD0pn
+O6ovJ+PL9QscMdnQlPwsiCwjNUNue20GBv3aUIYc+Z8Gq0SqSan5V0IiKRHMJkzd
+FAhnpkSFBvHhPJn07BCcb1kctqL+xnLxIdi7arq3WNA/6bJjsojc/x3FdIvORIeP
+sqejhtL8mCBvbMAMHSBrFxclMp+HSz2ouHEEPIQam0KeN8t1yEqIy3/aYKMzHj9c
+C3s8XOaBCbJbKpMAEQEAAbQ9VG9iaWFzIFdhcm5la2UgKGZvciBkZXZlbG9wbWVu
+dCBwdXJwb3NlcykgPHQud2FybmVrZUBnbXgubmV0Pg==
+=q1C6
+-----END PGP PUBLIC KEY BLOCK-----
+
pub A730529CA355A63E
uid Jukka Zitting
@@ -2119,6 +2181,18 @@ ikmfPIGVw73RF3HXjJ8GVqTkqbo4ZpgTw/7Z3+fAYE/vxquhnpl2HvE=
=5tlI
-----END PGP PUBLIC KEY BLOCK-----
+pub D46C5610D06E7001
+sub 00E25FE3776F21F2
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEZsO4fRYJKwYBBAHaRw8BAQdAcxaiyqFbaECpSz7mhsXzopzN9Cxwv80WlWGN
+gM3qpOi4OARmw7h9EgorBgEEAZdVAQUBAQdA45tqjMbTvozvroU+Z0nApYG0r3Zl
+gI27fUSEIetPuEIDAQgHiH4EGBYKACYWIQTlLweHelgF+a9KsKzUbFYQ0G5wAQUC
+ZsO4fQIbDAUJBaOagAAKCRDUbFYQ0G5wAZG1AP4m+dkpV5g3AT7Lws/lUDrKrdTr
+5noqEjUmwUNCiuTOugEArO87llEEIabZngdpe7D+dvJ7Bb+BSX2fzHKmrsE6uwY=
+=9gxs
+-----END PGP PUBLIC KEY BLOCK-----
+
pub D4DA5EAB3CD7E958
uid Jiaxiang Chen
@@ -2224,6 +2298,42 @@ zU7kkPUpKBx6hHg/zJnwTVAY/g4+Iw6CHwBhw+2/KoMpjQ63VqjwQZ6+VIwdsSCh
=Uwqi
-----END PGP PUBLIC KEY BLOCK-----
+pub E08F28E25BD93E9F
+uid Jens Nyman
+
+sub D7B501D37A3AE550
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBGQdnjEBDADm35hD/kMiNXVocImrrdOkA4xvNQ8OLirzETb5DSSocIW+OBLZ
+V3k5OiKTqYxlZIa9JT8XLLVc8pi9Is/x/xjoodEYSsj0LL0BODMeyt6ECmfi0brN
+CqKv1J46cuWun0iSog3KaGrMx1tcNAVvYHdDuBlZ8X1BbaFwSqdNv1Hq9LkfOUwB
+kafs8vXc+5THSkB2XAq9v4B+P9Rl4fwMPsjgkS0X/3JNMq0dqlUXuxNTj9h/dNAv
+l7sYEBF5gY1wqmVbesHWurl8Bqbq6wvDualZ0ccGyisaaz2FxIKuQHBK63ffxXVF
+/AABfDBsS1nx83qK3T7LFemSwQ+hMdKjck4Vng7RbB/FlUNZlbTiyrwZP9492YG7
+k5EcbB1K9tIPJDccYN3zkmYbaEAHL8j7PnTq7XyRPrNYFLKWBBa3sYz21+d8Ym2+
+ExuFgqvtnyToeKVfGX+mNbftrVK4j2OL6fZMmkDL/6niG34UJvHdsKvqVddTMPXM
+1jlZYh35WbM1eIEAEQEAAbQeSmVucyBOeW1hbiA8am55bWFuQGdvb2dsZS5jb20+
+uQGNBGQdnjEBDADQUQl5FP5g6/KTI0aAe05jJk8rkLK7nMQK7ayHdij5RmuRPJO6
+snlEPmSpd80BL9sCuCq8Fo87Al6NcD8Fd39IZl/n7ZbKZHfcSQNkyKlGoNMprAtA
+nh4rFdhG8Fn8Z+HRP5VcwG9BUIzvScyCLF4f4gGaaOAMd57I4q1+PsgvUZbGee74
+wqfLBspnkut0CEblr//nYf+Vkoo6EfSwi9QbJLYacy/3IrfByUIyUyrwEGFWMpul
+RXFzTRExpnX2j9g+XZO12LHMxJQ2ptOI70m4JDGd8MnPnvMofLsy65DBz/5qZKtV
+8haE73KKSWjJi9d81kwAzZR9WjltDDhyYOYYpDFcK4x0Py/XB5Unj1jf49PrgF16
+NVlfRh7vS6VeqUoNHYAR5qagftBiDZQ317GpRpm3LUidqyiFexlLhYHGdUk/vpAO
+SCK6azGBmW+dXx/BCC9fX6wVVRv0LNeAWgnyf13SG0d0jFJjzxqRzXclsffo/oEd
+JV7MQqo+ZHcdI8sAEQEAAYkBvAQYAQoAJhYhBC7okAcK6TGMOtNSDOCPKOJb2T6f
+BQJkHZ4xAhsMBQkDwmcAAAoJEOCPKOJb2T6fNkoMAIVtequTvVXw9Ax92uPbmCDg
+iNB2aiF4U23e5ZTPvSMWZlMx8hgI2vMCgRGcNOcEQcIyvFy1dZ5qxqzd/QjFvaY5
+2JutEUbeXvNfKmzjn2VzDiW/SHAc8KnWlAmJfqY+SPSIJNJ72Qc0EkxuGbjbWW+y
+eazFuDOafej5Cw/nRTMvAVm5hElNKtdRztQVJOOBPEFefbb9YG9gcVVo0Jxs2WY4
+1rh+GjK8rB/PnK4NicQcbh1hk21enla6j2jYKNhCJkifw0HXsa185cnISSv6fVsG
+H8tCCDoaZ8mXhU0W49n+NyWhRdHM9Gr9USkYVsHzJTZgTk429CI+ThEMjsxeIpJH
+Qu/m64DNlDu0rrzHpgjyUedNB05QEwEtxv3nMKryLcYifLhbrk4ktLIMtYLdDgJe
+ddn3I+ytL779X+SOCm292NQdg9+M6hkvgITfIoPe/seHjc0hP5COIbrfDeIdciyJ
+1c3wN0nS/z6/50wBTdmj11Kx3Phb0uKtekcn3BVXug==
+=36gl
+-----END PGP PUBLIC KEY BLOCK-----
+
pub E16AB52D79FD224F
sub 5A34A5E06B936F93
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -2780,6 +2890,52 @@ IQkQ9C6H+WZQFckWIQTzGEvNVfTQFuMNTJv0Lof5ZlAVyX7PAJ9ztvyEP04cy6zP
=dtK1
-----END PGP PUBLIC KEY BLOCK-----
+pub F5C81DE10A0B8ECC
+uid Andrey Somov (SnakeYAML)
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBGY/odABDADzlZ1BXT0zN3rL+z4HP8r/2xM6zN950fwRimBTOiT6uE8aQSxq
+283R/gIgM+yQBGjStLP3k/TsFJ2FCz7sug+7s1RP70ymkshalTRg+9QHBr2MU1Cx
+Xoh3fiH8BLOb3FIxH1wdAtOOoNxbz64Ftcptu3L0I1y2qEwOGNOyvqbntdCuwNbX
+/zwZUyb3tOVVrrZ5bp+6jMoBKEEWS7effqhGqXLlO4yTMBXR4pwzhch2IGCe+4M3
+a5C2SIJbR70PMk6aJ2+no2LycYRYJx/t2umAbxuCtwT6t/xh8v5ekbXAu5G3h5y+
+T2bF8rjMhVe6DBgJ08uFge3Oom5a5uZx8sQASdLCng8nKjGO4Q8jWmsEj+OiHnnX
+g0oKkirnWbAVrWysgNKAXfwGfDBG95K1F67kVhNjXTx0MDcxpsT9TPxz/nDuzRpQ
+ey5M11+Bl/fEM5UuzRpPgPd//bU+L3FgEUguB4kzsiYlhsUQRyCq1x868AchLWey
+vaVIq2DY101GIP0AEQEAAbQxQW5kcmV5IFNvbW92IChTbmFrZVlBTUwpIDxwdWJs
+aWMuc29tb3ZAZ21haWwuY29tPg==
+=9D46
+-----END PGP PUBLIC KEY BLOCK-----
+
+pub F6CE9695C9318406
+uid Sean Owen (ZXing)
+
+sub 811B3B85BC31841F
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFHW7ksBCADGzo3LGVyWpBWqxRQlGhpQ9YNav7jiR5WSLnatr8chZx+ldy5j
+KquO7GHO0qaHGXyc/CKeKH9Eey0mH4EcvOvEhBOE27Fnuw2NppxQjxXyhYTfvr6q
+CHGN+lTORVC3zD6UkMm2R92zNI+ZGWqK2zND5/RZQMW4JNH/Y4ZA6t+fm+dHm/Q5
+Mn/2XEnMnDiuJGnIwb6+sgH4GHdXkzGl+/grayerAp52HWGmKo3TWWtxdZQcdZe7
+spaLlVJEfw5K7uwpR0JDwSHtak7gfs613n1VuQeT9ZA/CnBk0L1JkkSezPO0NFxL
+ONjrQA6zBA6apJsdDQYgg55xtFaAWUfBAV37ABEBAAG0JFNlYW4gT3dlbiAoWlhp
+bmcpIDxzcm93ZW5AZ21haWwuY29tPrkBDQRR1u5LAQgArA+334bZKR9IOvArfF6T
+Yo1gx1wQjiFrbl3rcNrkADzu9/h5PbvkLma1zTSYUo5VZPIn0HbX+GctInY9AkjG
+sc3OrBmPi2FI/KOUXnMCmd1ShyphdB5CJjG2VpR4ejG/I1YyMQ2ABWGes1IQJNPs
+Hf1PXkJ2NA1gCxD+oAT9RgdXZBolln+TL3sYV4Z0EWhEL+yPjxInvFpabZErssim
+tRRrfSuT/wczrLt46zTgmtEKJ7udp6kzC3Nmut6IozlBr5qcEOTdiH6+BxgvW4hH
+uqANx4PzVWCCqwTxuiME/Q5kr45tgawSSoIsMAZaPGqeNluXap9qEsXPd3SsZUfg
+IQARAQABiQEfBBgBCgAJBQJR1u5LAhsMAAoJEPbOlpXJMYQGP2AH/jkwFM70jQCz
+uyMJNX4uHlmP37TNq8n2WxCNb5rQrXJ7UQ/3FSOiF86PRhOYAJHz0aEKWjQG+gr2
+aXc1HZr9g3AB5dLVxJ27SNgrV7Bvw1fI8NvYp+XyDodbQzyjavuslkf6BrQ9CSer
+R3WahwNtscMXYCi08f9dB1hooKmjkqgHGE+WHvs5zxtVnmdQ9Oaeu7IYYkhSAFA1
+Pdb2T90L+0xno4kCXaN7Mlw2ffxV53eLDq3fCoO4wkmVdjHjNc6Cq2qa7ntPo1wS
+BEhqoEjsNLqVXYq/cm7h+X5AwuLgkYCqA/TOjClEe8C/rVLVj9++Qw2lgbiUC6ry
+lkXgEU5D+DY=
+=6M1h
+-----END PGP PUBLIC KEY BLOCK-----
+
pub F6D4A1D411E9D1AE
uid Christopher Povirk
@@ -2836,6 +2992,42 @@ B7NlACz0MCH3fYo=
=PlCZ
-----END PGP PUBLIC KEY BLOCK-----
+pub FEFE78456EDDC34A
+uid Mattia Tommasone
+
+sub C3720DDC2E713B7C
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBF+y5lsBDAC5h0qk+OBAscHc/ac3A9C8ZPohXcTVpsOjds73soUAH+QCKO0y
+gAUuG/hUUU9xkm9PgTwWOEl2qDDcOFXY+9ykeYNUUcCWfs+JmVRfRod4W5pntaT4
+g9Z+T6LbXKNAfZgPvTv4rr7UjD05N4XS4vckrS4taYLtBRJAmqT3pt43KxlyoTbh
+f4xcO2rWeXsPqgzTYIHH7M5mYPeqA2gc9NBAhkHjesFuYfWXqUfOVcOLvzULxrra
+pZDOyrINr83WikC8DkuDrAav4mIWjIhYmfBWzuNeYJsusVnFENeOxpEHV8RT+8uE
+v1gPjbjAKUPfZoa7egvz3EmDkshpNIIym0XxNGTj5ntJWR2SLT7mDrSYPeHrZKW6
+/aKuAcOxpGLpdVOMM+y4N5mTQfdlL81G9kbGQarMmwGaJb2a82PaF20wRwVgiVfO
+p/GWgwXr0XdJNLqx13LdM8BMM5vmLomOQOjnpQBOlJWRgrYUJQOReKAEAQqNMsxS
+IW9laXkrewJtblEAEQEAAbQtTWF0dGlhIFRvbW1hc29uZSA8bWF0dGlhLnRvbW1h
+c29uZUBnbWFpbC5jb20+uQGNBF+y5lsBDACv+jA4LF5tkxOOn1yhSwOMVpsmjBkf
+QX76+6HvdRj+/bP6+rC6Dz0AGOs9QhxwT3+3l1HISMG3QQPYoUzeaLr3ZCHJgTy5
+FpQpbPSRhow+7DRbtFNuWGFcSsGuivWbTSJs0MZJ/d8iv0Vnu7l6n9FMUMINpmCq
+1ZAZUP64ueoDkQd3BTKJ5YNKB5OFF10zeEpcHV6V3gkok/NDRfBcuC/wyZs5z1bm
+nFvVQsPjizXtIoOOUG1G+tJF2ATGB+kpTrccfQPqaf6Qk/TrqSdh126c3DsewqC/
+aY+51NUhBdgZvzEMuE/pRgmjB28kRszy+nW3938KAIxfJVUk6VNnB0loQITSfiAw
+naKENnypsOXzU5CFOJa+Cgo4UctPBmCbKEqm3fpG+ReSjLCqUo0ZplU2L0K3VfF+
+muPiyjMDkxx/wEBrFdX2xB9ksofs2EasXNz+vW/ZHftFhZ+Zgvv10mPXGytwQ3zb
+pxKjIuLr35c7G9t2jgp/KilHEQJN13o6qT8AEQEAAYkBvAQYAQgAJhYhBFmwYiT9
+iRLjZgO+ef7+eEVu3cNKBQJfsuZbAhsMBQkDwmcAAAoJEP7+eEVu3cNKJfwMAJOe
+VNi01GBqtS3gz85GUGBngKt04cvVL1JgG3ov6YCJ9fw4a4WSQIhQU6Q8w4EJ+Nr1
+64IIpv7N5G2mhLJSJVcUzlA8G8ZFXPiW6opYwQFh9HhrqMA92yrwdXYzxGKyhaXZ
+TMXZfGryjuHLs8YMQfSXX8giGZLtvJSMkfJsM5TWi4bjlMqpoJ3P8qgmTm9dni74
+tdAWWP0VpFiDl6nD7mz7IP4S1ntcsl8IGCD8bKmnYoL9IGIhXlBatKqPra+hyNrI
+iGZQciVA8vxmVFBGnuuR5oCCYDiwuH9/BMWjyj1sGlN+dk7HI7phL+kwwxVpyTYW
+Oyt20j/MShit+/cGsDEXkgHgnKi7sxKIgenBz/ECSGEjYiLioZcxnIdBYSfdfDG/
+LvFJeNoNu0g3HZeNNcvKRB2/mlJ/HeJssV41ctc0sL3F3ShOK9NlzY4nNn8yKtv7
+Shma5nGIB6R1N366OBlUvjTs0ggfYypbVA+6WqpzParu/r7S8VozcUwcNZt4Cw==
+=Ru9R
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 012579464D01C06A
uid Herve Boutemy
@@ -3716,6 +3908,50 @@ pIXjPlQ0i6kV0h8KapE1Uo005JYgeg==
=ASmD
-----END PGP PUBLIC KEY BLOCK-----
+pub 1DE461528F1F1B2A
+uid Julian Reschke (CODE SIGNING KEY)
+
+sub D4569BDF799A59AB
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBFd7wYcBEAC1jmtowY8q/BXHFr4bOvA4WtniUcECC36dHmQzd3LrG8zdDPK4
+DgO/5w8xdilEe7BRD9etCV/uKXVM3KsKjFDHgh2puge4JElbePQL5l1oMmDUIGpK
+cj+O7REa8fSAh8MOKRYTBQ6C8z3S4iEJuiiO73gvoe8XvAdoM9tN6G8lh8HBcpIZ
+OT552jofRcStDw5WRKWj/MFYnqacReFo4ni6i+A733P+vtU5ZzWqtvhza6YNy4YA
+dLTqc2AUzCGFSEaFLTxQNuUGPaykvTUqnN+6sg2EY+3aTLoR2FuXJLN+iwYekWS0
+GBwS/mkK5uvj3+PlIxuuYWu79Aa4W/g4bLzlRZBEigh5yHYHR8qcsBoINiZIq/Ak
+7Az1QQRZd9WJFXQFusFTsCcrMt/5FOaBZml92uiLrVQzn8azt9G6aVK6m8yIlLE0
+Ya1Qvt4oGijY+BKNnoiKBBJnQuay4y0dgfolpdmFZf7BR2wmh4Svzttwt00v2OEB
+6Qs+caUz4uoa6mzDpuFaZaIpdP4kTSwsGdwqemHsjLaAYcdWssTOi9oS+ioUNjLa
+sIPGtI8pZ0WuN1X/IRSGSA6d/S+efqDGGyRligMtxuUicCk7+ew0fQ4dNxettN0B
+/wKRJ0ZP9TClw1jdLHDcuonI+8gxdxiT6dNOzCjMssQ1D4qfV56SenTNAQARAQAB
+tDZKdWxpYW4gUmVzY2hrZSAoQ09ERSBTSUdOSU5HIEtFWSkgPHJlc2Noa2VAYXBh
+Y2hlLm9yZz65Ag0EV3vBhwEQAJfPmNzuUFCB3grJBPq+XTxA25hFAUJJyguOLcnv
+MSXKqjT6O3IVetA3C5PEp1uFwqtPV6KqWjv9kFjMMwH9hDBsn+wrrODKst2jz57U
+bVIBaOW+hBGCCY0gwfLrtGNkLeUHZ2TIvBbBpc0y7LI4ZcmuGcIa++kwVscAW60u
+zkOHip8D5ch1WB070kM6ZmIx6aW2DgxHcp3S+Q0MH8HzUkZPKRniNtaJmNJgeXeF
+jEJZe72EGJECAcnuyenS87Bsuf/6lhIN8ThuuKkwGnc0SaxQSwI9qiCHvgI6s63z
++eT21PRJE4P73+4zzSgwmEyLYeQU+q1R9DqIoOGzUAiEXqETMOwDPXFp5WV/K+Ep
+cCvzPWkn8ojje9F5JjGRzxFFv7HgtILurmLrHtEEMaO9hN7MVyh8cnbgHZpVk5YJ
+9Y3Szm3DtMYljhVa9+j/K/0887yT4XvktUX1jQLj1ItglA3vLUeWN/8ifnmbEqfT
+NnMXDpvUGNxbUasHcGjV81kExbVBmM9PH02g6wb8K9x1dqOF1owD7zbzssbkP1VS
+FX9RnqwRCLZdtUyQmePj2ES/F+nm4Gns4kI+O0VqQqGCkyZoYbaSpnL/Jz86USKs
+xV1dDbVMKzFDkslbxX1X5z7ZeKA4HRvqfcAfMzl67zlcG8af6Nu5uTnnFDE4mF8A
+LXx7ABEBAAGJAh8EGAECAAkFAld7wYcCGwwACgkQHeRhUo8fGyrJ+BAAh3L+kebn
+F33N19iwVI2dEddDigZutE7Lq2KRr2BQtn53eINQ7FvEsnXy35fZPu/sjaEsYT43
+zfK6uXCx4+DP0ySPMl3rdLuOjJ8hrWYWX1R9yqQIAdL4AOqMX6eXWTCS2lwxoFpz
+0dCIonadn8xwftaHgcHebjAtAChd80ckKk9wMbBRxdi3i6+8aeqARf/SZt5toxbm
+zXg+oDFDnCigta7xkR2olc72xWhimjBUMMuoQuicpz46huQRCqc5fh79KbKHvaUo
+2eEOhN9mGFAfmCJJipe9EDxLfYlnnmlZpAYPSkQLffcEPK9p1Q++voSkb/48GpB7
+lW+Sf7yyVdiqhbUMpy7SuX5c0cOpv1hucaPs0ripKjvRSMvhOH5kPoQtqXGq4AcR
+OElystZcX7VEuNZZFymlVJWlAo9M0HFVWcuAZRLGAZIWM2uPhhebqeElDOJORU3B
+3sKQjtdKY1zidyPsjsKuTeq2qkq6bJTbJkZmu/7SX/qAP1Q0fEvsFhGf0ou6RFxe
+Rlu5Z4N59K6lkXo9+GLAWuc99maXWuYioln5OcoP7xTaFZwqDlF+Z9/Pxzu1foQW
+B8KrqPXwhp7/Z3JmW/7jP4EeiXKgA5r4tbBjZnugOhkeRv58z0IKlLLo0c4raiuJ
+ZAstgdvEqFLHh1yX4I+if5VODD2nEzXFDzE=
+=d5S6
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 1F7A8F87B9D8F501
uid Download
@@ -4954,6 +5190,35 @@ xXZy2unHAPYyfxDLPkbTR7Mm4k8Cva8PCcXotDow4bDLm9rhwVkJ
=Hgs4
-----END PGP PUBLIC KEY BLOCK-----
+pub 44CE7BF2825EA2CD
+uid ICU Project @IBM (International Components for Unicode Development at IBM)
+
+sub E01173141D06B1BF
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBEzQQMUBCACbwbw7tuTWgwPsDAdQTWGO355jP75oBLHwGgEwV+OCKtxkNXNw
+wrJqXst83vmD1dEJyHflQww+d+Olj90IefQGfR+K7O005C2nky7eNGIomxaP52Y/
+90+tmw8qtsI4nsPWPuVj4WdFvlFgUwIZ0SmX4CauVzg0Ris8f0taxg7PH9zEvICs
+G/WAXdB9em08WDD6ruhMAvDF4W8Yy7mpGmdWiFD+B9OC006tv+GzYAvUHRFeCnnT
+SoKRiBeLejW+t4kpdMnEfC9ILAYBEEjNYvBIyPdPKBwNfy0yjRebsUf0eNmjGTpk
+VPlfofjVaUaOZytUOQvntYpocMX+377DGQIdABEBAAG0X0lDVSBQcm9qZWN0IEBJ
+Qk0gKEludGVybmF0aW9uYWwgQ29tcG9uZW50cyBmb3IgVW5pY29kZSBEZXZlbG9w
+bWVudCBhdCBJQk0pIDxpY3VpbnRsQHVzLmlibS5jb20+uQENBEzQQMUBCADad+Zs
+ICC1lkCN+hdkslhUxRfM7qY86pXBeFcdTI7Nq0GBK79arNPUyXzvrsT0QL2tHyop
+PTxZmdLyzO1AL3mYGpJ8400L3jmgCbk+To7naMqbRvNYpRzuuvnvUZ7sleoEiDZ7
+esJ6uJ/CkT6BJiP58TIb5gQj8PmJsRt2XFOWZKOBrYywOCdNZ2oO42Pix9PKhXz3
+CrAA5sexLQkbgXF4ENtNpBd6OwY1C3C1d0fL54t46eZ5Yx9LyRuVEkmNmUdWxYi7
+UwjDvHJvoyLmstuYlopeQ2nowEiF82807UmIPUpMXrdvQFo5a7dfsymbDEiz2yie
+qoXVuVzVOEKPcnMdABEBAAGJAR8EGAECAAkFAkzQQMUCGwwACgkQRM578oJeos2c
+IAf+IDqhh3NAnNeuT2YIMG39Z8iWfmn9EIilmrIKFM6OGuzkiCEpgWDxp1v1T+BD
+LGjRIZ6lit07vnLonVus+zFh0bKi3L2FCnomRNiIZzD/OWndkteFDhxPHG5C8HyB
+nvqyLvs2Kc8nOW3EWKni73eQ5NR/3ltP4bGnVvaqUlkERsVjxLDKmgIYq3rdJ7ZR
+h9ooLMGe0mDWCzbSu/MKaRaqhR3xb51btVzZokww7pNiKJDyBkY3ljufXkkf5KLY
+NM6MeEvgiW0XUHT6tqHwzaigiBLYOsuMjPnXO3O59U8ZOd3zATM13xpyxn20xS6N
+YaWHQubHqQGQNV1i0YGd/ufrww==
+=/t0B
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 4604091C01C3086A
uid Dave Brosius
@@ -5465,6 +5730,37 @@ cDvA
=Sagc
-----END PGP PUBLIC KEY BLOCK-----
+pub 5ED22F661BBF0ACC
+uid Igor Sereda
+
+sub 31ADCD8BFCB760B4
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBExyNhsRBAC/W5cMapoP7NUn8S22iWG5bPw0bconApJHP4kQdT17gT2JgNJz
+BmuGWV59ZOGQkc6woeFKc1s6twlsgIL51jMeVOtgLJRGTS4So2hthNqDcgO4j8Lm
+yXpqbTkbD7/ZlRzL2hhedrMz4NQOZCvsZpQ1RaCDrr2hxDq/HhD2omGdlwCg/9Mt
+JNc7897LgfCMmtPOvAFt+rsD/0K87nvW37nlRqHdEtzvwUlyLJmYxdW9hDr8tm4Q
+Y/8rDvNFlhKV/yXmxQuhtgQ1qpBo75dwD86aJmzIMIWM0iei9Ecfu2DsWiWvArq1
+heDjMYSeQl6k37cmD59afo6e/jQmg2/ALC6mRf3912SfmqV5spw0k+NYdFxAnbot
+9jOfA/41shIdZloZ0aDcJDTNe22wFFh2sW8RwWtJJO8rmOCgh3MmkPn7LHPI9idJ
+bSdD1dRcR7UTyeigEeDTu0PAKfKZutc91lfcIGSZdk39SEEhUkL2JdPKVRBotiZZ
+Jsi+NxDdsprF/yQtr00XSGJYzh2TW/Srnb5nZQm2Iyokod3M1rQeSWdvciBTZXJl
+ZGEgPHNlcmVkYUBnbWFpbC5jb20+uQINBExyNhsQCACTTDNmrbllfvcMiXHDfS7x
+CbLQczZxWZF7jk+PznsWVJCw7zZg084zFKQAIKYcABPekAK4wJ/lnQc7mItsevF6
+AU8ZYuQ0TcInnKBrXMiIWDM1z2o2pa09AhIkhCMAGK3hKx4xjyL7e3oDz02NZz8A
+4rUGWgd2MCa29Pnihi2oVgIyU0y0ItpHqKhJQffyotWNe0W+ZNjg6qiQ1rPolfHN
+PKbKjyAl9/0lGlybZLZeSyvR0s1iblDAkQ3spfgTyIfxHPxNbF8CGt/TOfjkPtuA
+c6TckU8RfMoZXWX3pNvmH/LL3C43gl4/JcMZ+KC5v7WCtXkMEpDeyuQTDLqLABjD
+AAMFB/4tehSPbgS+Fx744n1kJo8SmmZXrEn4tpT/0PWHAyRDoJdzQgfeh40YBts4
+eFczYbKZF+DyebNsgdW55M/whJ47sNz6AyvuXuXmQHVbsiy0yVLXEjdV2D0Pwdo8
+lrmAHeoQasg90M/UpFHAl+XD4OZBcHmM6Pi9bXEQKpfRsZj1c0UVpIyGSvldFVPf
+GBDx3TC4IH2i25RkL2nGbAWIfbD8W8rMxMoxHArghE6fkU+FI7ZUEcGeW8X+ZJsQ
+PbaOMHsOeBQLisXR8TUysDoMMkgAquwOWTRUjvH+0g5huQQ7LKkOAF/gkuvGCsjq
+OfeIJF2aLnWFLt21dfdkiSYNUms5iEkEGBECAAkFAkxyNhsCGwwACgkQXtIvZhu/
+CsymUgCg/kZJOCScWbc41n9bn2Ir6X5claEAn0Gu7stpIpkazuPCpeoY2tnRnPE3
+=ZFxP
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 5F69AD087600B22C
uid Eric Bruneton
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 1fab9b7..37c8365 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -3,6 +3,7 @@
true
true
+ armored
@@ -14,9 +15,7 @@
-
-
-
+
@@ -48,17 +47,20 @@
+
+
+
@@ -76,12 +78,17 @@
+
-
+
+
+
+
+
@@ -92,6 +99,7 @@
+
@@ -102,6 +110,8 @@
+
+
@@ -112,6 +122,7 @@
+
@@ -120,6 +131,7 @@
+
@@ -154,8 +166,10 @@
+
+
@@ -164,8 +178,10 @@
+
+
@@ -188,6 +204,7 @@
+
@@ -220,7 +237,10 @@
-
+
+
+
+
@@ -237,14 +257,17 @@
+
+
+
@@ -264,6 +287,9 @@
+
+
+
@@ -299,9 +325,11 @@
+
+
@@ -317,6 +345,8 @@
+
+
@@ -326,6 +356,7 @@
+
@@ -383,6 +414,14 @@
+
+
+
+
+
+
+
+
@@ -589,6 +628,9 @@
+
+
+
@@ -634,6 +676,9 @@
+
+
+
@@ -1072,6 +1117,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2363,6 +2423,14 @@
+
+
+
+
+
+
+
+
@@ -2371,6 +2439,14 @@
+
+
+
+
+
+
+
+
@@ -2636,6 +2712,14 @@
+
+
+
+
+
+
+
+
@@ -2740,6 +2824,14 @@
+
+
+
+
+
+
+
+
@@ -2844,6 +2936,14 @@
+
+
+
+
+
+
+
+
@@ -3670,6 +3770,14 @@
+
+
+
+
+
+
+
+
@@ -3686,6 +3794,14 @@
+
+
+
+
+
+
+
+
@@ -3702,6 +3818,14 @@
+
+
+
+
+
+
+
+
@@ -3766,6 +3890,11 @@
+
+
+
+
+
@@ -3774,6 +3903,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3811,6 +3953,14 @@
+
+
+
+
+
+
+
+
@@ -3827,6 +3977,14 @@
+
+
+
+
+
+
+
+
@@ -3843,6 +4001,14 @@
+
+
+
+
+
+
+
+
@@ -3859,6 +4025,14 @@
+
+
+
+
+
+
+
+
@@ -3884,6 +4058,9 @@
+
+
+
@@ -3899,6 +4076,9 @@
+
+
+
@@ -3914,6 +4094,14 @@
+
+
+
+
+
+
+
+
@@ -3930,6 +4118,14 @@
+
+
+
+
+
+
+
+
@@ -3946,6 +4142,14 @@
+
+
+
+
+
+
+
+
@@ -3962,6 +4166,14 @@
+
+
+
+
+
+
+
+
@@ -3978,6 +4190,14 @@
+
+
+
+
+
+
+
+
@@ -3994,6 +4214,14 @@
+
+
+
+
+
+
+
+
@@ -4020,6 +4248,14 @@
+
+
+
+
+
+
+
+
@@ -4045,6 +4281,9 @@
+
+
+
@@ -4086,6 +4325,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -4102,6 +4352,14 @@
+
+
+
+
+
+
+
+
@@ -4118,6 +4376,14 @@
+
+
+
+
+
+
+
+
@@ -4134,6 +4400,14 @@
+
+
+
+
+
+
+
+
@@ -4150,6 +4424,14 @@
+
+
+
+
+
+
+
+
@@ -4159,6 +4441,9 @@
+
+
+
@@ -4184,6 +4469,14 @@
+
+
+
+
+
+
+
+
@@ -4192,6 +4485,14 @@
+
+
+
+
+
+
+
+
@@ -4272,6 +4573,14 @@
+
+
+
+
+
+
+
+
@@ -4320,6 +4629,14 @@
+
+
+
+
+
+
+
+
@@ -4368,6 +4685,14 @@
+
+
+
+
+
+
+
+
@@ -4416,6 +4741,14 @@
+
+
+
+
+
+
+
+
@@ -4464,6 +4797,14 @@
+
+
+
+
+
+
+
+
@@ -4512,6 +4853,14 @@
+
+
+
+
+
+
+
+
@@ -4560,6 +4909,14 @@
+
+
+
+
+
+
+
+
@@ -4608,6 +4965,14 @@
+
+
+
+
+
+
+
+
@@ -5031,6 +5396,9 @@
+
+
+
@@ -5051,6 +5419,14 @@
+
+
+
+
+
+
+
+
@@ -5059,6 +5435,14 @@
+
+
+
+
+
+
+
+
@@ -5080,6 +5464,14 @@
+
+
+
+
+
+
+
+
@@ -5228,6 +5620,14 @@
+
+
+
+
+
+
+
+
@@ -5244,6 +5644,14 @@
+
+
+
+
+
+
+
+
@@ -5260,6 +5668,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -5284,6 +5708,14 @@
+
+
+
+
+
+
+
+
@@ -5316,6 +5748,14 @@
+
+
+
+
+
+
+
+
@@ -5332,6 +5772,14 @@
+
+
+
+
+
+
+
+
@@ -5348,6 +5796,14 @@
+
+
+
+
+
+
+
+
@@ -5364,6 +5820,14 @@
+
+
+
+
+
+
+
+
@@ -5380,6 +5844,14 @@
+
+
+
+
+
+
+
+
@@ -5396,6 +5868,14 @@
+
+
+
+
+
+
+
+
@@ -5420,6 +5900,14 @@
+
+
+
+
+
+
+
+
@@ -5436,6 +5924,14 @@
+
+
+
+
+
+
+
+
@@ -5452,6 +5948,14 @@
+
+
+
+
+
+
+
+
@@ -5468,6 +5972,14 @@
+
+
+
+
+
+
+
+
@@ -5484,6 +5996,14 @@
+
+
+
+
+
+
+
+
@@ -5641,6 +6161,14 @@
+
+
+
+
+
+
+
+
@@ -5657,6 +6185,14 @@
+
+
+
+
+
+
+
+
@@ -5673,6 +6209,14 @@
+
+
+
+
+
+
+
+
@@ -5811,6 +6355,14 @@
+
+
+
+
+
+
+
+
@@ -5915,6 +6467,14 @@
+
+
+
+
+
+
+
+
@@ -6019,6 +6579,14 @@
+
+
+
+
+
+
+
+
@@ -6123,6 +6691,14 @@
+
+
+
+
+
+
+
+
@@ -6227,6 +6803,14 @@
+
+
+
+
+
+
+
+
@@ -6331,6 +6915,14 @@
+
+
+
+
+
+
+
+
@@ -6475,6 +7067,14 @@
+
+
+
+
+
+
+
+
@@ -6579,6 +7179,14 @@
+
+
+
+
+
+
+
+
@@ -6683,6 +7291,14 @@
+
+
+
+
+
+
+
+
@@ -6787,6 +7403,14 @@
+
+
+
+
+
+
+
+
@@ -6891,6 +7515,14 @@
+
+
+
+
+
+
+
+
@@ -6995,6 +7627,14 @@
+
+
+
+
+
+
+
+
@@ -7099,6 +7739,14 @@
+
+
+
+
+
+
+
+
@@ -7203,6 +7851,14 @@
+
+
+
+
+
+
+
+
@@ -7283,6 +7939,14 @@
+
+
+
+
+
+
+
+
@@ -7387,6 +8051,14 @@
+
+
+
+
+
+
+
+
@@ -7491,6 +8163,14 @@
+
+
+
+
+
+
+
+
@@ -7595,6 +8275,14 @@
+
+
+
+
+
+
+
+
@@ -7699,6 +8387,14 @@
+
+
+
+
+
+
+
+
@@ -7803,6 +8499,14 @@
+
+
+
+
+
+
+
+
@@ -7907,6 +8611,14 @@
+
+
+
+
+
+
+
+
@@ -8067,6 +8779,14 @@
+
+
+
+
+
+
+
+
@@ -8187,6 +8907,14 @@
+
+
+
+
+
+
+
+
@@ -8307,6 +9035,14 @@
+
+
+
+
+
+
+
+
@@ -8427,6 +9163,14 @@
+
+
+
+
+
+
+
+
@@ -8571,6 +9315,14 @@
+
+
+
+
+
+
+
+
@@ -8675,6 +9427,14 @@
+
+
+
+
+
+
+
+
@@ -8899,6 +9659,14 @@
+
+
+
+
+
+
+
+
@@ -9163,6 +9931,14 @@
+
+
+
+
+
+
+
+
@@ -9267,6 +10043,14 @@
+
+
+
+
+
+
+
+
@@ -9371,6 +10155,14 @@
+
+
+
+
+
+
+
+
@@ -9475,6 +10267,14 @@
+
+
+
+
+
+
+
+
@@ -9579,6 +10379,14 @@
+
+
+
+
+
+
+
+
@@ -9683,6 +10491,14 @@
+
+
+
+
+
+
+
+
@@ -9787,6 +10603,14 @@
+
+
+
+
+
+
+
+
@@ -9859,6 +10683,14 @@
+
+
+
+
+
+
+
+
@@ -9931,6 +10763,14 @@
+
+
+
+
+
+
+
+
@@ -10035,6 +10875,14 @@
+
+
+
+
+
+
+
+
@@ -10139,6 +10987,14 @@
+
+
+
+
+
+
+
+
@@ -10243,6 +11099,14 @@
+
+
+
+
+
+
+
+
@@ -10347,6 +11211,14 @@
+
+
+
+
+
+
+
+
@@ -10451,6 +11323,14 @@
+
+
+
+
+
+
+
+
@@ -10555,6 +11435,14 @@
+
+
+
+
+
+
+
+
@@ -10659,6 +11547,14 @@
+
+
+
+
+
+
+
+
@@ -10763,6 +11659,14 @@
+
+
+
+
+
+
+
+
@@ -10867,6 +11771,14 @@
+
+
+
+
+
+
+
+
@@ -10971,6 +11883,14 @@
+
+
+
+
+
+
+
+
@@ -11075,6 +11995,14 @@
+
+
+
+
+
+
+
+
@@ -11323,6 +12251,14 @@
+
+
+
+
+
+
+
+
@@ -11427,6 +12363,14 @@
+
+
+
+
+
+
+
+
@@ -11531,6 +12475,14 @@
+
+
+
+
+
+
+
+
@@ -11691,6 +12643,11 @@
+
+
+
+
+
@@ -11716,6 +12673,11 @@
+
+
+
+
+
@@ -11736,6 +12698,11 @@
+
+
+
+
+
@@ -11757,6 +12724,14 @@
+
+
+
+
+
+
+
+
@@ -11807,6 +12782,14 @@
+
+
+
+
+
+
+
+
@@ -11956,6 +12939,14 @@
+
+
+
+
+
+
+
+
@@ -12100,6 +13091,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -12184,6 +13199,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -12264,6 +13303,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -12376,6 +13439,14 @@
+
+
+
+
+
+
+
+
@@ -12642,6 +13713,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -13885,6 +14972,11 @@
+
+
+
+
+
@@ -14226,6 +15318,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -14292,6 +15397,14 @@
+
+
+
+
+
+
+
+
@@ -16427,6 +17540,14 @@
+
+
+
+
+
+
+
+
@@ -16495,6 +17616,11 @@
+
+
+
+
+
@@ -16541,6 +17667,14 @@
+
+
+
+
+
+
+
+
@@ -16629,6 +17763,11 @@
+
+
+
+
+
@@ -17230,6 +18369,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -17366,6 +18515,11 @@
+
+
+
+
+
@@ -17502,6 +18656,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -17706,6 +18870,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -17821,6 +18995,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -18225,6 +19409,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -18313,6 +19507,14 @@
+
+
+
+
+
+
+
+
@@ -18856,6 +20058,11 @@
+
+
+
+
+
@@ -19423,6 +20630,11 @@
+
+
+
+
+
@@ -19579,6 +20791,11 @@
+
+
+
+
+
@@ -19640,6 +20857,14 @@
+
+
+
+
+
+
+
+
@@ -19764,6 +20989,11 @@
+
+
+
+
+
@@ -19983,6 +21213,14 @@
+
+
+
+
+
+
+
+
@@ -20084,6 +21322,11 @@
+
+
+
+
+
@@ -20174,6 +21417,14 @@
+
+
+
+
+
+
+
+
@@ -20238,6 +21489,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -20302,6 +21569,14 @@
+
+
+
+
+
+
+
+
@@ -20411,6 +21686,14 @@
+
+
+
+
+
+
+
+
@@ -20597,6 +21880,318 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -20735,6 +22330,11 @@
+
+
+
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 1b33c55..8bdaf60 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d4081da..2a84e18 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 23d15a9..ef07e01 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
diff --git a/scripts/QA_keystore.jks b/scripts/QA_keystore.jks
deleted file mode 100644
index 2b8fb9b..0000000
Binary files a/scripts/QA_keystore.jks and /dev/null differ
diff --git a/scripts/QA_keystore.jks.license b/scripts/QA_keystore.jks.license
deleted file mode 100644
index 2c6c5df..0000000
--- a/scripts/QA_keystore.jks.license
+++ /dev/null
@@ -1,2 +0,0 @@
-SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
-SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt
index a4c0bfd..9dc6fc1 100644
--- a/scripts/analysis/lint-results.txt
+++ b/scripts/analysis/lint-results.txt
@@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE
- Lint Report: 97 warnings
+ Lint Report: 99 warnings
diff --git a/scripts/repo b/scripts/repo
new file mode 100644
index 0000000..c1cc361
--- /dev/null
+++ b/scripts/repo
@@ -0,0 +1 @@
+talk-android
diff --git a/scripts/repo.license b/scripts/repo.license
new file mode 100644
index 0000000..23e2d6b
--- /dev/null
+++ b/scripts/repo.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/scripts/uploadArtifact.sh b/scripts/uploadArtifact.sh
deleted file mode 100755
index cc8da35..0000000
--- a/scripts/uploadArtifact.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env bash
-
-#
-# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
-# SPDX-FileCopyrightText: 2021 Andy Scherzinger
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-
-#1: LOG_USERNAME
-#2: LOG_PASSWORD
-#3: DRONE_BUILD_NUMBER
-#4: DRONE_PULL_REQUEST
-#5: GITHUB_TOKEN
-
-PUBLIC_URL=https://www.kaminsky.me/nc-dev/android-artifacts
-USER=$1
-PASS=$2
-BUILD=$3
-PR=$4
-GITHUB_TOKEN=$5
-DAV_URL=https://nextcloud.kaminsky.me/remote.php/dav/files/$USER/android-artifacts/
-
-if ! test -e app/build/outputs/apk/qa/debug/app-qa-*.apk ; then
- exit 1
-fi
-echo "Uploaded artifact to $DAV_URL/$BUILD-talk.apk"
-
-# delete all old comments, starting with "APK file:"
-oldComments=$(curl 2>/dev/null --header "authorization: Bearer $GITHUB_TOKEN" -X GET https://api.github.com/repos/nextcloud/talk-android/issues/$PR/comments | jq '.[] | (.id |tostring) + "|" + (.user.login | test("github-actions") | tostring) + "|" + (.body | test("APK file:.*") | tostring)' | grep "true|true" | tr -d "\"" | cut -f1 -d"|")
-
-echo $oldComments | while read comment ; do
- curl 2>/dev/null --header "authorization: Bearer $GITHUB_TOKEN" -X DELETE https://api.github.com/repos/nextcloud/talk-android/issues/comments/$comment
-done
-
-apt-get -y install qrencode
-
-qrencode -o $PR.png "$PUBLIC_URL/$BUILD-talk.apk"
-
-curl -u $USER:$PASS -X PUT $DAV_URL/$BUILD-talk.apk --upload-file app/build/outputs/apk/qa/debug/app-qa-*.apk
-curl -u $USER:$PASS -X PUT $DAV_URL/$BUILD-talk.png --upload-file $PR.png
-curl --header "authorization: Bearer $GITHUB_TOKEN" -X POST https://api.github.com/repos/nextcloud/talk-android/issues/$PR/comments -d "{ \"body\" : \"APK file: $PUBLIC_URL/$BUILD-talk.apk

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud Talk app. \" }"