diff --git a/COPYING b/COPYING deleted file mode 100644 index c3f5b52..0000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 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 General Public License is a free, copyleft license for -software and other kinds of works. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is 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. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -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. - -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - -Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - -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 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. Use with the GNU Affero General Public License. - -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 Affero 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 special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - -14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of -the GNU 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 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 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 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 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 General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) -This program 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, your program's commands -might be different; for a GUI interface, you would use an "about box". - -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 GPL, see -. - -The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1b883df --- /dev/null +++ b/Makefile @@ -0,0 +1,70 @@ +SHELL := /bin/bash + +APP_ID := com.github.avojak.iridium + +ELEMENTARY_FLATPAK_REMOTE_URL := https://flatpak.elementary.io/repo.flatpakrepo +ELEMENTARY_FLATPAK_REMOTE_NAME := appcenter +ELEMENTARY_PLATFORM_VERSION := 7 + +# FLATHUB_FLATPAK_REMOTE_URL := https://flathub.org/repo/flathub.flatpakrepo +# FLATHUB_FLATPAK_REMOTE_NAME := flathub +# FLATHUB_PLATFORM_VERSION := 42 + +BUILD_DIR := build +NINJA_BUILD_FILE := $(BUILD_DIR)/build.ninja + +FLATPAK_BUILDER_FLAGS := --user --install --force-clean +ifdef OFFLINE_BUILD +FLATPAK_BUILDER_FLAGS += --disable-download +endif + +# Check for executables which are assumed to already be present on the system +EXECUTABLES = flatpak flatpak-builder +K := $(foreach exec,$(EXECUTABLES),\ + $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH"))) + +.DEFAULT_GOAL := flatpak + +.PHONY: all +all: translations flatpak + +.PHONY: flatpak-init +flatpak-init: + flatpak remote-add --if-not-exists --system $(ELEMENTARY_FLATPAK_REMOTE_NAME) $(ELEMENTARY_FLATPAK_REMOTE_URL) + flatpak install -y --user $(ELEMENTARY_FLATPAK_REMOTE_NAME) io.elementary.Platform//$(ELEMENTARY_PLATFORM_VERSION) + flatpak install -y --user $(ELEMENTARY_FLATPAK_REMOTE_NAME) io.elementary.Sdk//$(ELEMENTARY_PLATFORM_VERSION) + +.PHONY: init +init: flatpak-init + +.PHONY: flatpak +flatpak: + flatpak-builder build $(APP_ID).yml $(FLATPAK_BUILDER_FLAGS) + +# .PHONY: flathub-init +# flathub-init: +# flatpak remote-add --if-not-exists --system $(FLATHUB_FLATPAK_REMOTE_NAME) $(FLATHUB_FLATPAK_REMOTE_URL) +# flatpak install -y --user $(FLATHUB_FLATPAK_REMOTE_NAME) org.gnome.Platform//$(FLATHUB_PLATFORM_VERSION) +# flatpak install -y --user $(FLATHUB_FLATPAK_REMOTE_NAME) org.gnome.Sdk//$(FLATHUB_PLATFORM_VERSION) + +# .PHONY: flathub +# flathub: +# flatpak-builder build flathub/$(APP_ID).yml --user --install --force-clean + +.PHONY: lint +lint: + io.elementary.vala-lint ./src + +$(NINJA_BUILD_FILE): + meson build --prefix=/user + +.PHONY: translations +translations: $(NINJA_BUILD_FILE) + ninja -C build $(APP_ID)-pot + ninja -C build $(APP_ID)-update-po + +.PHONY: clean +clean: + rm -rf ./.flatpak-builder/ + rm -rf ./build/ + rm -rf ./builddir/ \ No newline at end of file diff --git a/README.md b/README.md index a282028..4251f94 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.com/avojak/iridium.svg?branch=develop)](https://travis-ci.com/avojak/iridium) +![CI](https://github.com/avojak/iridium/workflows/CI/badge.svg) ![Lint](https://github.com/avojak/iridium/workflows/Lint/badge.svg) ![GitHub](https://img.shields.io/github/license/avojak/iridium.svg?color=blue) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/avojak/iridium?sort=semver) @@ -53,10 +53,12 @@ $ com.github.avojak.iridium To test the Flatpak build with Flatpak Builder: ```bash -$ flatpak-builder build com.github.avojak.iridium.yml --user --install --force-clean -$ flatpak run com.github.avojak.iridium +$ flatpak-builder build com.github.avojak.iridium.yml --user --install --force-clean +$ flatpak run --env=G_MESSAGES_DEBUG=all com.github.avojak.iridium ``` +Another helpful environment variable to set is `GTK_DEBUG=interactive` for investigating UI and styling issues. + ### Development Build You can also install a development build alongside a stable version by specifying the dev profile: @@ -64,7 +66,7 @@ You can also install a development build alongside a stable version by specifyin ```bash $ meson build --prefix=/usr -Dprofile=dev $ sudo ninja -C build install -$ com.github.avojak.iridium-dev +$ G_MESSAGES_DEBUG=all com.github.avojak.iridium-dev ``` ### Updating Translations @@ -102,6 +104,8 @@ You should now be able to connect to the server from Iridium using the Server `l ### Troubleshooting and Debugging +#### Logging + Log messages can be found using the `journalctl` command. For example, the following will show journal messages for the current boot of the OS: ```bash @@ -119,6 +123,10 @@ Jan 01 11:13:24 avojak-eOS plank.desktop[1992]: [INFO 11:13:24.850977] SQLClient This can also be useful to locate where the application started amidst all of the journal entries. +#### Config Files + +With Flatpak, application config files can be found in: `~/.var/app/com.github.avojak.iridium/` + ## Project Status This project is very much in-progress and has a lot of remaining work. Check out the [Projects](https://github.com/avojak/iridium/projects) page to track progress towards the next milestone. diff --git a/com.github.avojak.iridium.yml b/com.github.avojak.iridium.yml index 3eaeed5..31167aa 100644 --- a/com.github.avojak.iridium.yml +++ b/com.github.avojak.iridium.yml @@ -1,7 +1,7 @@ app-id: com.github.avojak.iridium runtime: io.elementary.Platform -runtime-version: '6' +runtime-version: '7' sdk: io.elementary.Sdk command: com.github.avojak.iridium @@ -11,7 +11,7 @@ finish-args: - '--share=network' - '--socket=fallback-x11' - '--socket=wayland' - - '--system-talk-name=org.freedesktop.Accounts' + - '--talk-name=org.freedesktop.secrets' modules: - name: gtksourceview @@ -24,6 +24,4 @@ modules: buildsystem: meson sources: - type: dir - path: . - # config-opts: - # - '-Dprofile=dev' \ No newline at end of file + path: . \ No newline at end of file diff --git a/data/assets/icons/16x16/com.github.avojak.iridium.image-loading-symbolic.svg b/data/assets/icons/16x16/com.github.avojak.iridium.image-loading-symbolic.svg deleted file mode 100644 index 488c108..0000000 --- a/data/assets/icons/16x16/com.github.avojak.iridium.image-loading-symbolic.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/data/assets/icons/16x16/com.github.avojak.iridium.network-server-connected.svg b/data/assets/icons/16x16/com.github.avojak.iridium.network-server-connected.svg new file mode 100644 index 0000000..8816051 --- /dev/null +++ b/data/assets/icons/16x16/com.github.avojak.iridium.network-server-connected.svg @@ -0,0 +1,357 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/16x16/com.github.avojak.iridium.network-server-disconnected.svg b/data/assets/icons/16x16/com.github.avojak.iridium.network-server-disconnected.svg new file mode 100644 index 0000000..65ac96d --- /dev/null +++ b/data/assets/icons/16x16/com.github.avojak.iridium.network-server-disconnected.svg @@ -0,0 +1,392 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/16x16/com.github.avojak.iridium.network-server-error.svg b/data/assets/icons/16x16/com.github.avojak.iridium.network-server-error.svg new file mode 100644 index 0000000..7df50b4 --- /dev/null +++ b/data/assets/icons/16x16/com.github.avojak.iridium.network-server-error.svg @@ -0,0 +1,380 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-1-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-1-symbolic.svg new file mode 100644 index 0000000..93d6451 --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-1-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-10-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-10-symbolic.svg new file mode 100644 index 0000000..3a0eedd --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-10-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-11-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-11-symbolic.svg new file mode 100644 index 0000000..9e88b4e --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-11-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-12-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-12-symbolic.svg new file mode 100644 index 0000000..4adf7d9 --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-12-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-2-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-2-symbolic.svg new file mode 100644 index 0000000..ae8b3ef --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-2-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-3-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-3-symbolic.svg new file mode 100644 index 0000000..d022eb5 --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-3-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-4-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-4-symbolic.svg new file mode 100644 index 0000000..4f23048 --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-4-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-5-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-5-symbolic.svg new file mode 100644 index 0000000..5258603 --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-5-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-6-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-6-symbolic.svg new file mode 100644 index 0000000..77bcb4b --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-6-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-7-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-7-symbolic.svg new file mode 100644 index 0000000..20ba36f --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-7-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-8-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-8-symbolic.svg new file mode 100644 index 0000000..5e09850 --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-8-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-9-symbolic.svg b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-9-symbolic.svg new file mode 100644 index 0000000..2bb412b --- /dev/null +++ b/data/assets/icons/24x24/spinner/com.github.avojak.iridium.process-working-9-symbolic.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/assets/screenshots/iridium-screenshot-01.png b/data/assets/screenshots/iridium-screenshot-01.png index 3a1b0cf..9aae4bd 100644 Binary files a/data/assets/screenshots/iridium-screenshot-01.png and b/data/assets/screenshots/iridium-screenshot-01.png differ diff --git a/data/assets/screenshots/iridium-screenshot-02.png b/data/assets/screenshots/iridium-screenshot-02.png index a891dba..96e3c62 100644 Binary files a/data/assets/screenshots/iridium-screenshot-02.png and b/data/assets/screenshots/iridium-screenshot-02.png differ diff --git a/data/assets/screenshots/iridium-screenshot-03.png b/data/assets/screenshots/iridium-screenshot-03.png index ffcbd95..6948c61 100644 Binary files a/data/assets/screenshots/iridium-screenshot-03.png and b/data/assets/screenshots/iridium-screenshot-03.png differ diff --git a/data/assets/screenshots/iridium-screenshot-04.png b/data/assets/screenshots/iridium-screenshot-04.png index 05d2c10..472b94a 100644 Binary files a/data/assets/screenshots/iridium-screenshot-04.png and b/data/assets/screenshots/iridium-screenshot-04.png differ diff --git a/data/com.github.avojak.iridium.appdata.xml.in.in b/data/com.github.avojak.iridium.appdata.xml.in.in index 6673dc2..6ef29f9 100644 --- a/data/com.github.avojak.iridium.appdata.xml.in.in +++ b/data/com.github.avojak.iridium.appdata.xml.in.in @@ -21,6 +21,103 @@ @appid@ + + +

1.9.0 Release

+
    +
  • Update to elementary OS 7 runtime
  • +
+
+
+ + +

1.8.1 Release

+
    +
  • Fix missing application window title (#222)
  • +
+
+
+ + +

1.8.0 Release

+
    +
  • Redesign of the headerbar!
  • +
  • Update elementary OS runtime to 6.1 (#207)
  • +
  • Remove unnecessary sandbox hole for accountsservice (#215)
  • +
  • Fix slow switching of channel views when there is a large number of users (#210)
  • +
  • Fix auto-scrolling to be more reliable and simpler (#211)
  • +
+
+
+ + +

1.7.0 Release

+
    +
  • Use non-symbolic icons in the headerbar (#123)
  • +
  • Re-open application to the last chat view (#187)
  • +
  • Use new icons for server items in the side panel (#199)
  • +
  • Fix parsing of ACTION messages causing empty private messages to appear (#153)
  • +
  • Fix inability to reconnect to a server if it dies without restarting the application (#203)
  • +
  • Fix app icon badge not clearing with private message chat view regains focus (#208)
  • +
+
+
+ + +

1.6.1 Release

+
    +
  • Updated Dutch translations by Vistaus
  • +
+
+
+ + +

1.6.0 Release

+
    +
  • Integrate with OS notifications to display a notification when mentioned in a channel or private message (#23)
  • +
  • Display the number of unread mentions as an application badge in the dock (#24)
  • +
  • Set initial default nickname and real name based on system user account (#171)
  • +
  • Fix marker line in chat views showing incorrectly when font scaling other than 1x is used (#182)
  • +
  • Fix incorrect handling of network availability state (#189)
  • +
  • Fix the "restoring connections" overlay not appearing on startup (#190)
  • +
  • Fix incorrect parsing of command-line arguments (#193)
  • +
  • Revert the overly-restrictive 8-character nickname limit in the server connection dialog (#197)
  • +
+
+
+ + +

1.5.0 Release

+
    +
  • Add ability to browse a curated list of IRC servers (#156)
  • +
  • Fix parsing of MODE messages which caused crashes when connecting to certain servers (#184)
  • +
+
+
+ + +

1.4.0 Release

+
    +
  • Respect the system light/dark style preference (#150)
  • +
  • Support SASL (plain and external) authentication (#16)
  • +
  • Fix autoscrolling when receving QUIT messages (#152)
  • +
  • Fix text not wrapping for errors on server connection (#167)
  • +
  • Fix window size and position not saving (#170)
  • +
  • Fix certificate warning dialog not appearing (#172)
  • +
  • Fix secrets not saving when using the Flatpak installation (#173)
  • +
  • Fix BrowseChannelsDialog not using Granite.Dialog (#176)
  • +
  • Fix new auth tokens not saving when editing a connection (#179)
  • +
+
+
+ + +

1.3.2 Release

+
    +
  • Fix Stripe public key (#164)
  • +
+
+

1.3.1 Release

@@ -129,6 +226,6 @@ #fff #333 5 - pk_live_51GeuzCC9Tk2ZlXfTbOnAI75yox9JaKnuePQyatCwWbZOARtcdE + pk_live_51GeuzCC9Tk2ZlXfTbOnAI75yox9JaKnuePQyatCwWbZOARtcdEJtkhwUjc9itmHZNfoLLoRLwpLgEvfWDGtZG6tu00IxyC21mz diff --git a/data/com.github.avojak.iridium.desktop.in.in b/data/com.github.avojak.iridium.desktop.in.in index bbc3c46..59645c9 100644 --- a/data/com.github.avojak.iridium.desktop.in.in +++ b/data/com.github.avojak.iridium.desktop.in.in @@ -8,4 +8,5 @@ Icon=@icon@ Terminal=false Type=Application Keywords=IRC;IM;Chat; -MimeType=x-scheme-handler/irc; \ No newline at end of file +MimeType=x-scheme-handler/irc; +X-GNOME-UsesNotifications=true \ No newline at end of file diff --git a/data/com.github.avojak.iridium.gresource.xml b/data/com.github.avojak.iridium.gresource.xml index 00bd7f6..dac2bcc 100644 --- a/data/com.github.avojak.iridium.gresource.xml +++ b/data/com.github.avojak.iridium.gresource.xml @@ -3,5 +3,6 @@ styles/CertificateWarningDialog.css styles/PreferencesDialog.css + styles/WelcomeView.css \ No newline at end of file diff --git a/data/com.github.avojak.iridium.gschema.xml.in b/data/com.github.avojak.iridium.gschema.xml.in index 80bcf08..99ffefb 100644 --- a/data/com.github.avojak.iridium.gschema.xml.in +++ b/data/com.github.avojak.iridium.gschema.xml.in @@ -36,6 +36,11 @@ If channel join and part messages should be suppressed If channel join and part messages should be suppressed + + false + If notifications for mentioning the user should be muted + If notifications for mentioning the user should be muted + 'Monospace Regular 9' Preferred Font @@ -61,5 +66,15 @@ The saved height of the window. The saved height of the window. + + "" + The last shown server. + The last shown server. + + + "" + The last shown channel. + The last shown channel. + diff --git a/data/meson.build b/data/meson.build index e34ffa5..9a97f75 100644 --- a/data/meson.build +++ b/data/meson.build @@ -72,10 +72,30 @@ install_data( ) install_data( - join_paths('assets', 'icons', '16x16', meson.project_name() + '.image-loading-symbolic.svg'), + join_paths('assets', 'icons', '16x16', meson.project_name() + '.network-server-connected.svg'), install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '16x16', 'status'), - rename: '@0@.image-loading-symbolic.svg'.format(application_id) + rename: '@0@.network-server-connected.svg'.format(application_id) ) +install_data( + join_paths('assets', 'icons', '16x16', meson.project_name() + '.network-server-disconnected.svg'), + install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '16x16', 'status'), + rename: '@0@.network-server-disconnected.svg'.format(application_id) +) +install_data( + join_paths('assets', 'icons', '16x16', meson.project_name() + '.network-server-error.svg'), + install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '16x16', 'status'), + rename: '@0@.network-server-error.svg'.format(application_id) +) + +# Install the spinner +spinner_image_indices = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] +foreach i : spinner_image_indices + install_data( + join_paths('assets', 'icons', '24x24', 'spinner', meson.project_name() + '.process-working-' + i + '-symbolic.svg'), + install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', '24x24', 'status'), + rename: '@0@.process-working-@1@-symbolic.svg'.format(application_id, i) + ) +endforeach # Install the settings schema schema_path = '/com/github/avojak/iridium/' diff --git a/data/styles/WelcomeView.css b/data/styles/WelcomeView.css new file mode 100644 index 0000000..e3dc92d --- /dev/null +++ b/data/styles/WelcomeView.css @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +.view.welcome { + /* + * Set the background color of Granite.Widgets.Welcome to fully transparent so + * that there isn't a noticable color difference between the header bar and the + * welcome view on first launch. + */ + background-color: rgba(255, 255, 255, 0); +} \ No newline at end of file diff --git a/meson.build b/meson.build index 2bd6148..996fe26 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ # Project name and programming language -project('com.github.avojak.iridium', 'vala', 'c', version: '1.3.1') +project('com.github.avojak.iridium', 'vala', 'c', version: '1.9.0') i18n = import('i18n') gnome = import('gnome') diff --git a/po/com.github.avojak.iridium.pot b/po/com.github.avojak.iridium.pot index cd22f1f..2e27576 100644 --- a/po/com.github.avojak.iridium.pot +++ b/po/com.github.avojak.iridium.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.avojak.iridium\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-16 17:19-0600\n" +"POT-Creation-Date: 2021-12-30 12:00-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,26 +17,34 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/Layouts/MainLayout.vala:399 +#: src/Layouts/MainLayout.vala:425 msgid "Restoring server connections…" msgstr "" -#: src/Layouts/MainLayout.vala:418 +#: src/Layouts/MainLayout.vala:446 msgid "Opening URI…" msgstr "" -#: src/Models/AuthenticationMethod.vala:31 +#: src/Models/AuthenticationMethod.vala:33 msgid "None" msgstr "" -#: src/Models/AuthenticationMethod.vala:33 +#: src/Models/AuthenticationMethod.vala:35 msgid "Server Password" msgstr "" -#: src/Models/AuthenticationMethod.vala:35 +#: src/Models/AuthenticationMethod.vala:37 msgid "NickServ" msgstr "" +#: src/Models/AuthenticationMethod.vala:39 +msgid "SASL (Plain)" +msgstr "" + +#: src/Models/AuthenticationMethod.vala:41 +msgid "SASL (External)" +msgstr "" + #: src/Models/CertificateErrorMapping.vala:27 msgid "An error has occurred processing the server's certificate" msgstr "" @@ -77,37 +85,45 @@ msgstr "" msgid "Allow" msgstr "" -#: src/Services/ServerConnection.vala:87 +#: src/Services/ServerConnection.vala:90 msgid "Error while connecting" msgstr "" -#: src/Services/ServerConnection.vala:183 +#: src/Services/ServerConnection.vala:160 +msgid "Certificate file not found" +msgstr "" + +#: src/Services/ServerConnection.vala:210 msgid "Certificate rejected:" msgstr "" -#: src/Services/ServerConnection.vala:183 +#: src/Services/ServerConnection.vala:210 msgid "" "See the application preferences to configure the certificate validation " "policy." msgstr "" -#: src/Services/ServerConnection.vala:200 +#: src/Services/ServerConnection.vala:233 msgid "Certificate was rejected by the user." msgstr "" +#: src/Services/ServerConnection.vala:323 +msgid "No stored secret found for this server." +msgstr "" + #: src/Views/ChannelChatView.vala:38 msgid "You must join this channel to begin chatting" msgstr "" -#: src/Views/ChatView.vala:74 +#: src/Views/ChatView.vala:75 msgid "You have unread messages!" msgstr "" -#: src/Views/ChatView.vala:75 +#: src/Views/ChatView.vala:76 msgid "Take me there" msgstr "" -#: src/Views/ChatView.vala:95 src/Widgets/Dialogs/BrowseChannelsDialog.vala:96 +#: src/Views/ChatView.vala:96 src/Widgets/Dialogs/BrowseChannelsDialog.vala:96 msgid "Clear" msgstr "" @@ -115,22 +131,30 @@ msgstr "" msgid "You are not connected to this server" msgstr "" -#: src/Views/Welcome.vala:29 +#: src/Views/Welcome.vala:31 msgid "Welcome to Iridium" msgstr "" -#: src/Views/Welcome.vala:30 +#: src/Views/Welcome.vala:32 msgid "Connect to Any IRC Server" msgstr "" -#: src/Views/Welcome.vala:42 +#: src/Views/Welcome.vala:51 msgid "Add a New Server" msgstr "" -#: src/Views/Welcome.vala:42 +#: src/Views/Welcome.vala:51 msgid "Connect to a server and save it in the server list" msgstr "" +#: src/Views/Welcome.vala:52 +msgid "Browse Servers" +msgstr "" + +#: src/Views/Welcome.vala:52 +msgid "Browse a curated list of popular IRC servers" +msgstr "" + #: src/Widgets/Dialogs/BrowseChannelsDialog.vala:53 #: src/Widgets/Dialogs/BrowseChannelsDialog.vala:73 msgid "Browse Channels" @@ -152,40 +176,40 @@ msgstr "" msgid "Topic" msgstr "" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:168 -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:112 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:169 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:113 #: src/Widgets/Dialogs/ForgetConnectionsWarningDialog.vala:38 -#: src/Widgets/Dialogs/NicknameEditDialog.vala:92 -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:124 +#: src/Widgets/Dialogs/NicknameEditDialog.vala:93 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:140 msgid "Cancel" msgstr "" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:173 -#: src/Widgets/Dialogs/ChannelJoinDialog.vala:154 src/Widgets/HeaderBar.vala:38 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:174 +#: src/Widgets/Dialogs/ChannelJoinDialog.vala:155 msgid "Join" msgstr "" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:286 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:287 msgid "Retrieving channels, this may take a minute…" msgstr "" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:51 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:53 msgid "Untrusted Connection" msgstr "" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:55 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:57 msgid "Don't Connect" msgstr "" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:56 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:58 msgid "Connect Anyway" msgstr "" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:99 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:101 msgid "View certificate" msgstr "" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:102 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:104 msgid "Remember my decision" msgstr "" @@ -199,10 +223,11 @@ msgid "Channel:" msgstr "" #: src/Widgets/Dialogs/ChannelJoinDialog.vala:121 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:206 msgid "Browse…" msgstr "" -#: src/Widgets/Dialogs/ChannelJoinDialog.vala:149 +#: src/Widgets/Dialogs/ChannelJoinDialog.vala:150 msgid "Not Now" msgstr "" @@ -211,13 +236,13 @@ msgstr "" msgid "Edit Channel Topic" msgstr "" -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:117 -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:134 -#: src/Widgets/Dialogs/NicknameEditDialog.vala:97 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:118 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:135 +#: src/Widgets/Dialogs/NicknameEditDialog.vala:98 msgid "Submit" msgstr "" -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:132 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:133 msgid "Clear topic" msgstr "" @@ -252,14 +277,14 @@ msgstr "" msgid "Real Name" msgstr "" -#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:126 -#: src/Widgets/Dialogs/PreferencesDialog.vala:245 -#: src/Widgets/SidePanel/ChannelRow.vala:121 -#: src/Widgets/SidePanel/PrivateMessageRow.vala:84 +#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:127 +#: src/Widgets/Dialogs/PreferencesDialog.vala:254 +#: src/Widgets/SidePanel/ChannelRow.vala:125 +#: src/Widgets/SidePanel/PrivateMessageRow.vala:90 msgid "Close" msgstr "" -#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:131 +#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:132 msgid "Save" msgstr "" @@ -290,216 +315,219 @@ msgid "Suppress join/part messages:" msgstr "" #: src/Widgets/Dialogs/PreferencesDialog.vala:109 +msgid "Mute mention notifications:" +msgstr "" + +#: src/Widgets/Dialogs/PreferencesDialog.vala:116 msgid "Security and Privacy" msgstr "" -#: src/Widgets/Dialogs/PreferencesDialog.vala:111 +#: src/Widgets/Dialogs/PreferencesDialog.vala:118 msgid "Unacceptable SSL/TLS Certificates:" msgstr "" -#: src/Widgets/Dialogs/PreferencesDialog.vala:157 +#: src/Widgets/Dialogs/PreferencesDialog.vala:164 msgid "" "If a server presents an unacceptable SSL/TLS certificate, no connection " "will be made. (Recommended)" msgstr "" -#: src/Widgets/Dialogs/PreferencesDialog.vala:174 +#: src/Widgets/Dialogs/PreferencesDialog.vala:181 msgid "" "If a server presents an unacceptable SSL/TLS certificate, the user will " "be warned and can choose whether or not to proceed." msgstr "" -#: src/Widgets/Dialogs/PreferencesDialog.vala:191 +#: src/Widgets/Dialogs/PreferencesDialog.vala:198 msgid "" "If a server presents an unacceptable SSL/TLS certificate, the connection " "will still be made. (Not recommended)" msgstr "" -#: src/Widgets/Dialogs/PreferencesDialog.vala:205 +#: src/Widgets/Dialogs/PreferencesDialog.vala:212 msgid "Remember connections between sessions:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:48 -msgid "Connect to a Server" -msgstr "" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:66 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:81 msgid "Connection secure" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:68 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:83 msgid "" "Connection secure, provided only trusted certificates are accepted when " "prompted" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:70 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:85 msgid "" "Connection may be insecure. Consider rejecting unacceptable certificates " "from the application preferences." msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:72 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:87 msgid "Connection insecure. Consider enabling SSL/TLS from the Advanced tab." msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:80 -msgid "New Connection" -msgstr "" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:104 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:117 msgid "Basic" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:105 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:118 msgid "Advanced" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:129 -#: src/Widgets/SidePanel/ServerRow.vala:163 -msgid "Connect" -msgstr "" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:166 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:199 msgid "Server:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:173 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:211 msgid "Nickname:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:187 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:218 msgid "Real Name:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:192 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:223 msgid "Iridium IRC Client" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:194 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:228 msgid "Authentication Method:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:222 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:260 +msgid "SASL External requires SSL/TLS" +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:260 +msgid "" +"To use SASL External authentication, you must enable SSL/TLS for this server " +"connection." +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:278 msgid "Password:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:263 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:300 +msgid "Identity File:" +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:303 +msgid "Select Your Identity File…" +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:356 msgid "Use SSL/TLS:" msgstr "" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:279 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:379 msgid "Port:" msgstr "" -#: src/Widgets/SidePanel/ChannelRow.vala:96 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:408 +msgid "Invalid identity file" +msgstr "" + +#: src/Widgets/SidePanel/ChannelRow.vala:100 msgid "Edit topic…" msgstr "" -#: src/Widgets/SidePanel/ChannelRow.vala:101 +#: src/Widgets/SidePanel/ChannelRow.vala:105 msgid "Add to favorites" msgstr "" -#: src/Widgets/SidePanel/ChannelRow.vala:106 +#: src/Widgets/SidePanel/ChannelRow.vala:110 msgid "Remove from favorites" msgstr "" -#: src/Widgets/SidePanel/ChannelRow.vala:111 +#: src/Widgets/SidePanel/ChannelRow.vala:115 msgid "Join channel" msgstr "" -#: src/Widgets/SidePanel/ChannelRow.vala:116 +#: src/Widgets/SidePanel/ChannelRow.vala:120 msgid "Leave channel" msgstr "" -#: src/Widgets/SidePanel/Panel.vala:58 +#: src/Widgets/SidePanel/Panel.vala:79 msgid "Favorite Channels" msgstr "" -#: src/Widgets/SidePanel/Panel.vala:73 +#: src/Widgets/SidePanel/Panel.vala:94 msgid "Servers" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:56 -msgid "Dismiss" -msgstr "" - -#: src/Widgets/SidePanel/ServerRow.vala:153 src/Widgets/StatusBar.vala:28 +#: src/Widgets/SidePanel/ServerRow.vala:119 msgid "Join a Channel…" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:158 +#: src/Widgets/SidePanel/ServerRow.vala:124 msgid "Edit Connection…" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:168 +#: src/Widgets/SidePanel/ServerRow.vala:129 +msgid "Connect" +msgstr "" + +#: src/Widgets/SidePanel/ServerRow.vala:134 msgid "Disconnect" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:173 +#: src/Widgets/SidePanel/ServerRow.vala:139 msgid "Remove" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:207 +#: src/Widgets/SidePanel/ServerRow.vala:173 msgid "Are you sure you want to proceed?" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:208 +#: src/Widgets/SidePanel/ServerRow.vala:174 msgid "" "By removing this connection you will be disconnected, and will not be able " "to recover the connection settings. If you wish to join this server again in " "the future, you will need to re-enter the connection settings." msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:213 +#: src/Widgets/SidePanel/ServerRow.vala:179 msgid "Yes, remove" msgstr "" -#: src/Widgets/SidePanel/ServerRow.vala:217 +#: src/Widgets/SidePanel/ServerRow.vala:183 msgid "Don't warn me again" msgstr "" -#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:38 -msgid "No users" +#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:98 +#, c-format +msgid "%d user" msgstr "" -#: src/Widgets/HeaderBar.vala:43 -msgid "New Server Connection…" -msgstr "" - -#: src/Widgets/HeaderBar.vala:53 -msgid "Join Channel…" +#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:100 +#, c-format +msgid "%d users" msgstr "" -#: src/Widgets/HeaderBar.vala:78 +#: src/Widgets/HeaderBar.vala:41 msgid "Channel users" msgstr "" -#: src/Widgets/HeaderBar.vala:88 +#: src/Widgets/HeaderBar.vala:53 msgid "Menu" msgstr "" -#: src/Widgets/HeaderBar.vala:93 -msgid "Light background" -msgstr "" - -#: src/Widgets/HeaderBar.vala:94 -msgid "Dark background" -msgstr "" - -#: src/Widgets/HeaderBar.vala:103 +#: src/Widgets/HeaderBar.vala:58 msgid "Toggle Sidebar" msgstr "" -#: src/Widgets/HeaderBar.vala:113 +#: src/Widgets/HeaderBar.vala:68 msgid "Reset Marker Line" msgstr "" -#: src/Widgets/HeaderBar.vala:123 +#: src/Widgets/HeaderBar.vala:78 msgid "Preferences…" msgstr "" -#: src/Widgets/HeaderBar.vala:133 +#: src/Widgets/HeaderBar.vala:88 msgid "Quit" msgstr "" @@ -515,94 +543,98 @@ msgstr "" msgid "Network Settings…" msgstr "" -#: src/Widgets/StatusBar.vala:27 -msgid "Connect to a Server…" +#: src/Widgets/StatusBar.vala:28 +msgid "New Server Connection…" msgstr "" #: src/Widgets/StatusBar.vala:38 -msgid "Join a Server or Channel" +msgid "Join Channel…" +msgstr "" + +#: src/Widgets/StatusBar.vala:60 +msgid "Join…" msgstr "" -#: src/Widgets/StatusBar.vala:43 -msgid "Manage connections…" +#: src/Widgets/StatusBar.vala:63 +msgid "Join a Server or Channel" msgstr "" -#: src/MainWindow.vala:410 src/MainWindow.vala:456 +#: src/MainWindow.vala:423 src/MainWindow.vala:474 msgid "Already connected to this server!" msgstr "" -#: src/MainWindow.vala:629 src/MainWindow.vala:631 +#: src/MainWindow.vala:674 src/MainWindow.vala:676 msgid "You've already joined this channel" msgstr "" -#: src/MainWindow.vala:640 +#: src/MainWindow.vala:685 msgid "Channel must begin with '#' or '&'" msgstr "" -#: src/MainWindow.vala:644 +#: src/MainWindow.vala:689 msgid "Enter a channel name" msgstr "" -#: src/MainWindow.vala:674 +#: src/MainWindow.vala:719 msgid "Start your message with a /" msgstr "" -#: src/MainWindow.vala:740 +#: src/MainWindow.vala:785 msgid "No recipient nickname specified (Usage: /msg )" msgstr "" -#: src/MainWindow.vala:744 +#: src/MainWindow.vala:789 msgid "No message specified (Usage: /msg )" msgstr "" -#: src/MainWindow.vala:755 +#: src/MainWindow.vala:800 msgid "No action specified (Usage: /me )" msgstr "" -#: src/MainWindow.vala:944 src/MainWindow.vala:958 +#: src/MainWindow.vala:986 src/MainWindow.vala:1003 msgid " has quit" msgstr "" -#: src/MainWindow.vala:993 +#: src/MainWindow.vala:1039 msgid " has cleared the topic" msgstr "" -#: src/MainWindow.vala:995 +#: src/MainWindow.vala:1041 msgid " has changed the topic to: " msgstr "" -#: src/MainWindow.vala:1010 +#: src/MainWindow.vala:1056 msgid "Topic for " msgstr "" -#: src/MainWindow.vala:1010 +#: src/MainWindow.vala:1056 msgid " is: " msgstr "" -#: src/MainWindow.vala:1012 +#: src/MainWindow.vala:1058 msgid "Topic set by " msgstr "" -#: src/MainWindow.vala:1024 src/MainWindow.vala:1027 +#: src/MainWindow.vala:1070 src/MainWindow.vala:1073 msgid "Nickname already in use." msgstr "" -#: src/MainWindow.vala:1033 +#: src/MainWindow.vala:1079 msgid "Nickname already in use" msgstr "" -#: src/MainWindow.vala:1033 +#: src/MainWindow.vala:1079 msgid "Choose a new nickname and retry the connection." msgstr "" -#: src/MainWindow.vala:1038 +#: src/MainWindow.vala:1084 msgid " is not a valid nickname." msgstr "" -#: src/MainWindow.vala:1099 +#: src/MainWindow.vala:1171 msgid " has joined" msgstr "" -#: src/MainWindow.vala:1112 +#: src/MainWindow.vala:1185 msgid " has left" msgstr "" diff --git a/po/es.po b/po/es.po index a2b474a..39c0c7e 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.avojak.iridium\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-16 17:19-0600\n" +"POT-Creation-Date: 2021-12-30 12:00-0700\n" "PO-Revision-Date: 2021-04-26 20:09-0600\n" "Last-Translator: Jeyson Antonio Flores Deras \n" "Language-Team: \n" @@ -18,26 +18,34 @@ msgstr "" "X-Generator: Poedit 2.4.3\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/Layouts/MainLayout.vala:399 +#: src/Layouts/MainLayout.vala:425 msgid "Restoring server connections…" msgstr "Restaurando conexiones a servidores..." -#: src/Layouts/MainLayout.vala:418 +#: src/Layouts/MainLayout.vala:446 msgid "Opening URI…" msgstr "" -#: src/Models/AuthenticationMethod.vala:31 +#: src/Models/AuthenticationMethod.vala:33 msgid "None" msgstr "Ninguno" -#: src/Models/AuthenticationMethod.vala:33 +#: src/Models/AuthenticationMethod.vala:35 msgid "Server Password" msgstr "Contraseña del Servidor" -#: src/Models/AuthenticationMethod.vala:35 +#: src/Models/AuthenticationMethod.vala:37 msgid "NickServ" msgstr "NickServ" +#: src/Models/AuthenticationMethod.vala:39 +msgid "SASL (Plain)" +msgstr "" + +#: src/Models/AuthenticationMethod.vala:41 +msgid "SASL (External)" +msgstr "" + #: src/Models/CertificateErrorMapping.vala:27 msgid "An error has occurred processing the server's certificate" msgstr "Se ha producido un error al procesar el certificado del servidor" @@ -79,15 +87,20 @@ msgstr "Advertir" msgid "Allow" msgstr "Permitir" -#: src/Services/ServerConnection.vala:87 +#: src/Services/ServerConnection.vala:90 msgid "Error while connecting" msgstr "Error al conectar" -#: src/Services/ServerConnection.vala:183 +#: src/Services/ServerConnection.vala:160 +#, fuzzy +msgid "Certificate file not found" +msgstr "Certificado rechazado:" + +#: src/Services/ServerConnection.vala:210 msgid "Certificate rejected:" msgstr "Certificado rechazado:" -#: src/Services/ServerConnection.vala:183 +#: src/Services/ServerConnection.vala:210 msgid "" "See the application preferences to configure the certificate validation " "policy." @@ -95,23 +108,28 @@ msgstr "" "Consulte las preferencias de la aplicación para configurar la política de " "validación de certificados." -#: src/Services/ServerConnection.vala:200 +#: src/Services/ServerConnection.vala:233 msgid "Certificate was rejected by the user." msgstr "El certificado fue rechazado por el usuario." +#: src/Services/ServerConnection.vala:323 +#, fuzzy +msgid "No stored secret found for this server." +msgstr "No estás conectado a este servidor" + #: src/Views/ChannelChatView.vala:38 msgid "You must join this channel to begin chatting" msgstr "Necesitar unirte a este canal para empezar a chatear" -#: src/Views/ChatView.vala:74 +#: src/Views/ChatView.vala:75 msgid "You have unread messages!" msgstr "Tienes mensajes no leídos!" -#: src/Views/ChatView.vala:75 +#: src/Views/ChatView.vala:76 msgid "Take me there" msgstr "Llévame allí" -#: src/Views/ChatView.vala:95 src/Widgets/Dialogs/BrowseChannelsDialog.vala:96 +#: src/Views/ChatView.vala:96 src/Widgets/Dialogs/BrowseChannelsDialog.vala:96 msgid "Clear" msgstr "Limpiar" @@ -119,22 +137,31 @@ msgstr "Limpiar" msgid "You are not connected to this server" msgstr "No estás conectado a este servidor" -#: src/Views/Welcome.vala:29 +#: src/Views/Welcome.vala:31 msgid "Welcome to Iridium" msgstr "Bienvenido a Iridium" -#: src/Views/Welcome.vala:30 +#: src/Views/Welcome.vala:32 msgid "Connect to Any IRC Server" msgstr "Conéctate a Cualquier Servidor IRC" -#: src/Views/Welcome.vala:42 +#: src/Views/Welcome.vala:51 msgid "Add a New Server" msgstr "Añadir Nuevo Servidor" -#: src/Views/Welcome.vala:42 +#: src/Views/Welcome.vala:51 msgid "Connect to a server and save it in the server list" msgstr "Conéctate a un servidor y añádelo en la lista de servidores" +#: src/Views/Welcome.vala:52 +#, fuzzy +msgid "Browse Servers" +msgstr "Servidores" + +#: src/Views/Welcome.vala:52 +msgid "Browse a curated list of popular IRC servers" +msgstr "" + #: src/Widgets/Dialogs/BrowseChannelsDialog.vala:53 #: src/Widgets/Dialogs/BrowseChannelsDialog.vala:73 #, fuzzy @@ -159,40 +186,40 @@ msgstr "Nombre de Usuario" msgid "Topic" msgstr "" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:168 -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:112 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:169 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:113 #: src/Widgets/Dialogs/ForgetConnectionsWarningDialog.vala:38 -#: src/Widgets/Dialogs/NicknameEditDialog.vala:92 -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:124 +#: src/Widgets/Dialogs/NicknameEditDialog.vala:93 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:140 msgid "Cancel" msgstr "Cancelar" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:173 -#: src/Widgets/Dialogs/ChannelJoinDialog.vala:154 src/Widgets/HeaderBar.vala:38 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:174 +#: src/Widgets/Dialogs/ChannelJoinDialog.vala:155 msgid "Join" msgstr "Unirte" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:286 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:287 msgid "Retrieving channels, this may take a minute…" msgstr "" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:51 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:53 msgid "Untrusted Connection" msgstr "Conexión no confiable" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:55 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:57 msgid "Don't Connect" msgstr "No Conectar" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:56 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:58 msgid "Connect Anyway" msgstr "Conectar Igualmente" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:99 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:101 msgid "View certificate" msgstr "Ver certificado" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:102 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:104 msgid "Remember my decision" msgstr "Recordar mi decisión" @@ -206,10 +233,11 @@ msgid "Channel:" msgstr "Canal:" #: src/Widgets/Dialogs/ChannelJoinDialog.vala:121 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:206 msgid "Browse…" msgstr "" -#: src/Widgets/Dialogs/ChannelJoinDialog.vala:149 +#: src/Widgets/Dialogs/ChannelJoinDialog.vala:150 msgid "Not Now" msgstr "No Ahora" @@ -218,13 +246,13 @@ msgstr "No Ahora" msgid "Edit Channel Topic" msgstr "Editar Asunto del Canal" -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:117 -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:134 -#: src/Widgets/Dialogs/NicknameEditDialog.vala:97 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:118 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:135 +#: src/Widgets/Dialogs/NicknameEditDialog.vala:98 msgid "Submit" msgstr "Subir" -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:132 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:133 msgid "Clear topic" msgstr "Limpiar Asunto" @@ -262,14 +290,14 @@ msgstr "Nombre de Usuario" msgid "Real Name" msgstr "Nombre Real" -#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:126 -#: src/Widgets/Dialogs/PreferencesDialog.vala:245 -#: src/Widgets/SidePanel/ChannelRow.vala:121 -#: src/Widgets/SidePanel/PrivateMessageRow.vala:84 +#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:127 +#: src/Widgets/Dialogs/PreferencesDialog.vala:254 +#: src/Widgets/SidePanel/ChannelRow.vala:125 +#: src/Widgets/SidePanel/PrivateMessageRow.vala:90 msgid "Close" msgstr "Cerrar" -#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:131 +#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:132 msgid "Save" msgstr "Guardar" @@ -300,14 +328,18 @@ msgid "Suppress join/part messages:" msgstr "" #: src/Widgets/Dialogs/PreferencesDialog.vala:109 +msgid "Mute mention notifications:" +msgstr "" + +#: src/Widgets/Dialogs/PreferencesDialog.vala:116 msgid "Security and Privacy" msgstr "Seguridad y Privacidad" -#: src/Widgets/Dialogs/PreferencesDialog.vala:111 +#: src/Widgets/Dialogs/PreferencesDialog.vala:118 msgid "Unacceptable SSL/TLS Certificates:" msgstr "Certificados SSL/TLS Inaceptables:" -#: src/Widgets/Dialogs/PreferencesDialog.vala:157 +#: src/Widgets/Dialogs/PreferencesDialog.vala:164 msgid "" "If a server presents an unacceptable SSL/TLS certificate, no connection " "will be made. (Recommended)" @@ -315,7 +347,7 @@ msgstr "" "Si un servidor presenta un certificado SSL/TLS inaceptable, la conexión " "no se efectuará. (Recomendado)" -#: src/Widgets/Dialogs/PreferencesDialog.vala:174 +#: src/Widgets/Dialogs/PreferencesDialog.vala:181 msgid "" "If a server presents an unacceptable SSL/TLS certificate, the user will " "be warned and can choose whether or not to proceed." @@ -323,7 +355,7 @@ msgstr "" "Si un servidor presenta un certificado SSL/TLS inaceptable, el usuario " "será advertido y podrá elegir si proceder o no." -#: src/Widgets/Dialogs/PreferencesDialog.vala:191 +#: src/Widgets/Dialogs/PreferencesDialog.vala:198 msgid "" "If a server presents an unacceptable SSL/TLS certificate, the connection " "will still be made. (Not recommended)" @@ -331,19 +363,15 @@ msgstr "" "Si un servidor presenta un certificado SSL/TLS inaceptable, la conexión " "se efectuará igualmente. (No recomendado)" -#: src/Widgets/Dialogs/PreferencesDialog.vala:205 +#: src/Widgets/Dialogs/PreferencesDialog.vala:212 msgid "Remember connections between sessions:" msgstr "Recordar conexiones entre sesiones:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:48 -msgid "Connect to a Server" -msgstr "Conectar a un Servidor" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:66 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:81 msgid "Connection secure" msgstr "Conexión segura" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:68 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:83 msgid "" "Connection secure, provided only trusted certificates are accepted when " "prompted" @@ -351,7 +379,7 @@ msgstr "" "Conexión segura, siempre que solo se acepten certificados de confianza " "cuando sea solicitado." -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:70 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:85 msgid "" "Connection may be insecure. Consider rejecting unacceptable certificates " "from the application preferences." @@ -359,113 +387,126 @@ msgstr "" "La conexión puede ser insegura. Considere rechazar certificados inaceptables " "desde las preferencias de la aplicación." -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:72 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:87 msgid "Connection insecure. Consider enabling SSL/TLS from the Advanced tab." msgstr "Conexión insegura. Considere activar SSL/TLS desde la pestaña Avazado." -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:80 -msgid "New Connection" -msgstr "Nueva Conexión" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:104 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:117 msgid "Basic" msgstr "Básico" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:105 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:118 msgid "Advanced" msgstr "Avanzado" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:129 -#: src/Widgets/SidePanel/ServerRow.vala:163 -msgid "Connect" -msgstr "Conectar" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:166 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:199 msgid "Server:" msgstr "Servidor:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:173 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:211 msgid "Nickname:" msgstr "Apodo:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:187 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:218 msgid "Real Name:" msgstr "Nombre Real:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:192 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:223 msgid "Iridium IRC Client" msgstr "Iridium Cliente IRC" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:194 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:228 msgid "Authentication Method:" msgstr "Método de Autenticación:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:222 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:260 +msgid "SASL External requires SSL/TLS" +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:260 +msgid "" +"To use SASL External authentication, you must enable SSL/TLS for this server " +"connection." +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:278 msgid "Password:" msgstr "Contraseña:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:263 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:300 +msgid "Identity File:" +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:303 +msgid "Select Your Identity File…" +msgstr "" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:356 msgid "Use SSL/TLS:" msgstr "Usar SSL/TLS:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:279 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:379 msgid "Port:" msgstr "Puerto:" -#: src/Widgets/SidePanel/ChannelRow.vala:96 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:408 +msgid "Invalid identity file" +msgstr "" + +#: src/Widgets/SidePanel/ChannelRow.vala:100 msgid "Edit topic…" msgstr "Editar asunto..." -#: src/Widgets/SidePanel/ChannelRow.vala:101 +#: src/Widgets/SidePanel/ChannelRow.vala:105 msgid "Add to favorites" msgstr "Añadir a favoritos" -#: src/Widgets/SidePanel/ChannelRow.vala:106 +#: src/Widgets/SidePanel/ChannelRow.vala:110 msgid "Remove from favorites" msgstr "Remover de favoritos" -#: src/Widgets/SidePanel/ChannelRow.vala:111 +#: src/Widgets/SidePanel/ChannelRow.vala:115 msgid "Join channel" msgstr "Unirte al canal" -#: src/Widgets/SidePanel/ChannelRow.vala:116 +#: src/Widgets/SidePanel/ChannelRow.vala:120 msgid "Leave channel" msgstr "Dejar el canal" -#: src/Widgets/SidePanel/Panel.vala:58 +#: src/Widgets/SidePanel/Panel.vala:79 msgid "Favorite Channels" msgstr "Canales Favoritos" -#: src/Widgets/SidePanel/Panel.vala:73 +#: src/Widgets/SidePanel/Panel.vala:94 msgid "Servers" msgstr "Servidores" -#: src/Widgets/SidePanel/ServerRow.vala:56 -msgid "Dismiss" -msgstr "Ocultar" - -#: src/Widgets/SidePanel/ServerRow.vala:153 src/Widgets/StatusBar.vala:28 +#: src/Widgets/SidePanel/ServerRow.vala:119 msgid "Join a Channel…" msgstr "Unirte a un Canal...." -#: src/Widgets/SidePanel/ServerRow.vala:158 +#: src/Widgets/SidePanel/ServerRow.vala:124 #, fuzzy msgid "Edit Connection…" msgstr "Nueva Conexión" -#: src/Widgets/SidePanel/ServerRow.vala:168 +#: src/Widgets/SidePanel/ServerRow.vala:129 +msgid "Connect" +msgstr "Conectar" + +#: src/Widgets/SidePanel/ServerRow.vala:134 msgid "Disconnect" msgstr "Desconectar" -#: src/Widgets/SidePanel/ServerRow.vala:173 +#: src/Widgets/SidePanel/ServerRow.vala:139 msgid "Remove" msgstr "Remover" -#: src/Widgets/SidePanel/ServerRow.vala:207 +#: src/Widgets/SidePanel/ServerRow.vala:173 msgid "Are you sure you want to proceed?" msgstr "¿Estás seguro que quieres proceder?" -#: src/Widgets/SidePanel/ServerRow.vala:208 +#: src/Widgets/SidePanel/ServerRow.vala:174 msgid "" "By removing this connection you will be disconnected, and will not be able " "to recover the connection settings. If you wish to join this server again in " @@ -475,58 +516,46 @@ msgstr "" "los ajustes de la conexión. Si quieres unirte a este servidor de nuevo en el " "futuro, tendrás que volver a ingresar los ajustes de la conexión." -#: src/Widgets/SidePanel/ServerRow.vala:213 +#: src/Widgets/SidePanel/ServerRow.vala:179 msgid "Yes, remove" msgstr "Si, remover" -#: src/Widgets/SidePanel/ServerRow.vala:217 +#: src/Widgets/SidePanel/ServerRow.vala:183 msgid "Don't warn me again" msgstr "No advertirme de nuevo" -#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:38 -msgid "No users" +#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:98 +#, fuzzy, c-format +msgid "%d user" msgstr "Sin usuarios" -#: src/Widgets/HeaderBar.vala:43 -#, fuzzy -msgid "New Server Connection…" -msgstr "Nueva Conexión de Servidor" - -#: src/Widgets/HeaderBar.vala:53 -#, fuzzy -msgid "Join Channel…" -msgstr "Unirte a un Canal...." +#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:100 +#, fuzzy, c-format +msgid "%d users" +msgstr "Sin usuarios" -#: src/Widgets/HeaderBar.vala:78 +#: src/Widgets/HeaderBar.vala:41 msgid "Channel users" msgstr "Usuarios del Canal" -#: src/Widgets/HeaderBar.vala:88 +#: src/Widgets/HeaderBar.vala:53 msgid "Menu" msgstr "Menú" -#: src/Widgets/HeaderBar.vala:93 -msgid "Light background" -msgstr "Tema claro" - -#: src/Widgets/HeaderBar.vala:94 -msgid "Dark background" -msgstr "Tema oscuro" - -#: src/Widgets/HeaderBar.vala:103 +#: src/Widgets/HeaderBar.vala:58 msgid "Toggle Sidebar" msgstr "Alternar Barra Lateral" -#: src/Widgets/HeaderBar.vala:113 +#: src/Widgets/HeaderBar.vala:68 msgid "Reset Marker Line" msgstr "Reiniciar Línea del Marcador" -#: src/Widgets/HeaderBar.vala:123 +#: src/Widgets/HeaderBar.vala:78 #, fuzzy msgid "Preferences…" msgstr "Preferencias" -#: src/Widgets/HeaderBar.vala:133 +#: src/Widgets/HeaderBar.vala:88 msgid "Quit" msgstr "Salir" @@ -542,100 +571,128 @@ msgstr "Conéctate a la internet para unirte a servidores." msgid "Network Settings…" msgstr "Ajustes de Red..." -#: src/Widgets/StatusBar.vala:27 -msgid "Connect to a Server…" -msgstr "Conectar a Servidor..." +#: src/Widgets/StatusBar.vala:28 +#, fuzzy +msgid "New Server Connection…" +msgstr "Nueva Conexión de Servidor" #: src/Widgets/StatusBar.vala:38 +#, fuzzy +msgid "Join Channel…" +msgstr "Unirte a un Canal...." + +#: src/Widgets/StatusBar.vala:60 +#, fuzzy +msgid "Join…" +msgstr "Unirte" + +#: src/Widgets/StatusBar.vala:63 msgid "Join a Server or Channel" msgstr "Únete a un Servidor o Canal" -#: src/Widgets/StatusBar.vala:43 -msgid "Manage connections…" -msgstr "Gestionar conexiones..." - -#: src/MainWindow.vala:410 src/MainWindow.vala:456 +#: src/MainWindow.vala:423 src/MainWindow.vala:474 msgid "Already connected to this server!" msgstr "¡Ya estás conectado a este servidor!" -#: src/MainWindow.vala:629 src/MainWindow.vala:631 +#: src/MainWindow.vala:674 src/MainWindow.vala:676 msgid "You've already joined this channel" msgstr "¡Ya te has unido a este canal!" -#: src/MainWindow.vala:640 +#: src/MainWindow.vala:685 msgid "Channel must begin with '#' or '&'" msgstr "El canal debe de empezar con '#' o '&'" -#: src/MainWindow.vala:644 +#: src/MainWindow.vala:689 msgid "Enter a channel name" msgstr "Ingresa un nombre de canal" -#: src/MainWindow.vala:674 +#: src/MainWindow.vala:719 msgid "Start your message with a /" msgstr "Empieza tu mensaje con un /" -#: src/MainWindow.vala:740 +#: src/MainWindow.vala:785 msgid "No recipient nickname specified (Usage: /msg )" msgstr "Apodo del destinatario no especificado (Uso: /msg )" -#: src/MainWindow.vala:744 +#: src/MainWindow.vala:789 msgid "No message specified (Usage: /msg )" msgstr "Mensaje no especificado (Uso: /msg )" -#: src/MainWindow.vala:755 +#: src/MainWindow.vala:800 #, fuzzy msgid "No action specified (Usage: /me )" msgstr "Mensaje no especificado (Uso: /msg )" -#: src/MainWindow.vala:944 src/MainWindow.vala:958 +#: src/MainWindow.vala:986 src/MainWindow.vala:1003 msgid " has quit" msgstr " ha salido" -#: src/MainWindow.vala:993 +#: src/MainWindow.vala:1039 #, fuzzy msgid " has cleared the topic" msgstr "Limpiar Asunto" -#: src/MainWindow.vala:995 +#: src/MainWindow.vala:1041 msgid " has changed the topic to: " msgstr "" -#: src/MainWindow.vala:1010 +#: src/MainWindow.vala:1056 msgid "Topic for " msgstr "" -#: src/MainWindow.vala:1010 +#: src/MainWindow.vala:1056 msgid " is: " msgstr "" -#: src/MainWindow.vala:1012 +#: src/MainWindow.vala:1058 msgid "Topic set by " msgstr "" -#: src/MainWindow.vala:1024 src/MainWindow.vala:1027 +#: src/MainWindow.vala:1070 src/MainWindow.vala:1073 msgid "Nickname already in use." msgstr "Apodo en uso." -#: src/MainWindow.vala:1033 +#: src/MainWindow.vala:1079 #, fuzzy msgid "Nickname already in use" msgstr "Apodo en uso." -#: src/MainWindow.vala:1033 +#: src/MainWindow.vala:1079 msgid "Choose a new nickname and retry the connection." msgstr "" -#: src/MainWindow.vala:1038 +#: src/MainWindow.vala:1084 msgid " is not a valid nickname." msgstr " no es un apodo válido." -#: src/MainWindow.vala:1099 +#: src/MainWindow.vala:1171 msgid " has joined" msgstr " se ha unido" -#: src/MainWindow.vala:1112 +#: src/MainWindow.vala:1185 msgid " has left" msgstr " se ha ido" +#~ msgid "Dismiss" +#~ msgstr "Ocultar" + +#~ msgid "Connect to a Server…" +#~ msgstr "Conectar a Servidor..." + +#~ msgid "Manage connections…" +#~ msgstr "Gestionar conexiones..." + +#~ msgid "Connect to a Server" +#~ msgstr "Conectar a un Servidor" + +#~ msgid "New Connection" +#~ msgstr "Nueva Conexión" + +#~ msgid "Light background" +#~ msgstr "Tema claro" + +#~ msgid "Dark background" +#~ msgstr "Tema oscuro" + #~ msgid "Join Channel" #~ msgstr "Unirte al Canal" diff --git a/po/nl.po b/po/nl.po index c6be9cf..4235d6a 100644 --- a/po/nl.po +++ b/po/nl.po @@ -7,37 +7,45 @@ msgid "" msgstr "" "Project-Id-Version: com.github.avojak.iridium\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-16 17:19-0600\n" -"PO-Revision-Date: 2021-05-17 13:52+0200\n" +"POT-Creation-Date: 2021-12-30 12:00-0700\n" +"PO-Revision-Date: 2021-12-04 14:18+0100\n" "Last-Translator: Heimen Stoffels \n" "Language-Team: \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.4.2\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0\n" -#: src/Layouts/MainLayout.vala:399 +#: src/Layouts/MainLayout.vala:425 msgid "Restoring server connections…" msgstr "Bezig met herstellen van serververbindingen…" -#: src/Layouts/MainLayout.vala:418 +#: src/Layouts/MainLayout.vala:446 msgid "Opening URI…" msgstr "Bezig met openen van uri…" -#: src/Models/AuthenticationMethod.vala:31 +#: src/Models/AuthenticationMethod.vala:33 msgid "None" msgstr "Geen" -#: src/Models/AuthenticationMethod.vala:33 +#: src/Models/AuthenticationMethod.vala:35 msgid "Server Password" msgstr "Serverwachtwoord" -#: src/Models/AuthenticationMethod.vala:35 +#: src/Models/AuthenticationMethod.vala:37 msgid "NickServ" msgstr "NickServ" +#: src/Models/AuthenticationMethod.vala:39 +msgid "SASL (Plain)" +msgstr "SASL (platte tekst)" + +#: src/Models/AuthenticationMethod.vala:41 +msgid "SASL (External)" +msgstr "SASL (extern)" + #: src/Models/CertificateErrorMapping.vala:27 msgid "An error has occurred processing the server's certificate" msgstr "" @@ -80,38 +88,46 @@ msgstr "Waarschuwen" msgid "Allow" msgstr "Toestaan" -#: src/Services/ServerConnection.vala:87 +#: src/Services/ServerConnection.vala:90 msgid "Error while connecting" msgstr "Fout bij verbinden" -#: src/Services/ServerConnection.vala:183 +#: src/Services/ServerConnection.vala:160 +msgid "Certificate file not found" +msgstr "Geen certificaatbestand aangetroffen" + +#: src/Services/ServerConnection.vala:210 msgid "Certificate rejected:" msgstr "Certificaat geweigerd:" -#: src/Services/ServerConnection.vala:183 +#: src/Services/ServerConnection.vala:210 msgid "" "See the application preferences to configure the certificate validation " "policy." msgstr "" "Ga naar de toepassingsvoorkeuren om het certificaatbeleid in te stellen." -#: src/Services/ServerConnection.vala:200 +#: src/Services/ServerConnection.vala:233 msgid "Certificate was rejected by the user." msgstr "Het certificaat is geweigerd door de gebruiker." +#: src/Services/ServerConnection.vala:323 +msgid "No stored secret found for this server." +msgstr "Er is geen opgeslagen geheim aangetroffen voor deze server." + #: src/Views/ChannelChatView.vala:38 msgid "You must join this channel to begin chatting" msgstr "Neem deel aan het kanaal om te chatten" -#: src/Views/ChatView.vala:74 +#: src/Views/ChatView.vala:75 msgid "You have unread messages!" msgstr "Je hebt ongelezen berichten!" -#: src/Views/ChatView.vala:75 +#: src/Views/ChatView.vala:76 msgid "Take me there" msgstr "Ga ernaartoe" -#: src/Views/ChatView.vala:95 src/Widgets/Dialogs/BrowseChannelsDialog.vala:96 +#: src/Views/ChatView.vala:96 src/Widgets/Dialogs/BrowseChannelsDialog.vala:96 msgid "Clear" msgstr "Wissen" @@ -119,22 +135,30 @@ msgstr "Wissen" msgid "You are not connected to this server" msgstr "Je bent niet verbonden met deze server" -#: src/Views/Welcome.vala:29 +#: src/Views/Welcome.vala:31 msgid "Welcome to Iridium" msgstr "Welkom bij Iridium" -#: src/Views/Welcome.vala:30 +#: src/Views/Welcome.vala:32 msgid "Connect to Any IRC Server" msgstr "Maak verbinding met irc-servers" -#: src/Views/Welcome.vala:42 +#: src/Views/Welcome.vala:51 msgid "Add a New Server" msgstr "Server toevoegen" -#: src/Views/Welcome.vala:42 +#: src/Views/Welcome.vala:51 msgid "Connect to a server and save it in the server list" msgstr "Maak verbinding met een server en sla deze op op de serverlijst" +#: src/Views/Welcome.vala:52 +msgid "Browse Servers" +msgstr "Bladeren door servers" + +#: src/Views/Welcome.vala:52 +msgid "Browse a curated list of popular IRC servers" +msgstr "Blader door een lijst met populaire irc-servers" + #: src/Widgets/Dialogs/BrowseChannelsDialog.vala:53 #: src/Widgets/Dialogs/BrowseChannelsDialog.vala:73 msgid "Browse Channels" @@ -156,40 +180,40 @@ msgstr "Gebruikers" msgid "Topic" msgstr "Onderwerp" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:168 -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:112 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:169 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:113 #: src/Widgets/Dialogs/ForgetConnectionsWarningDialog.vala:38 -#: src/Widgets/Dialogs/NicknameEditDialog.vala:92 -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:124 +#: src/Widgets/Dialogs/NicknameEditDialog.vala:93 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:140 msgid "Cancel" msgstr "Annuleren" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:173 -#: src/Widgets/Dialogs/ChannelJoinDialog.vala:154 src/Widgets/HeaderBar.vala:38 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:174 +#: src/Widgets/Dialogs/ChannelJoinDialog.vala:155 msgid "Join" msgstr "Deelnemen" -#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:286 +#: src/Widgets/Dialogs/BrowseChannelsDialog.vala:287 msgid "Retrieving channels, this may take a minute…" msgstr "Bezig met ophalen van kanalen…" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:51 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:53 msgid "Untrusted Connection" msgstr "Onvertrouwde verbinding" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:55 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:57 msgid "Don't Connect" msgstr "Niet verbinden" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:56 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:58 msgid "Connect Anyway" msgstr "Tóch verbinden" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:99 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:101 msgid "View certificate" msgstr "Certificaat tonen" -#: src/Widgets/Dialogs/CertificateWarningDialog.vala:102 +#: src/Widgets/Dialogs/CertificateWarningDialog.vala:104 msgid "Remember my decision" msgstr "Keuze onthouden" @@ -203,10 +227,11 @@ msgid "Channel:" msgstr "Kanaal:" #: src/Widgets/Dialogs/ChannelJoinDialog.vala:121 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:206 msgid "Browse…" msgstr "Bladeren…" -#: src/Widgets/Dialogs/ChannelJoinDialog.vala:149 +#: src/Widgets/Dialogs/ChannelJoinDialog.vala:150 msgid "Not Now" msgstr "Niet nu" @@ -215,13 +240,13 @@ msgstr "Niet nu" msgid "Edit Channel Topic" msgstr "Kanaalonderwerp aanpassen" -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:117 -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:134 -#: src/Widgets/Dialogs/NicknameEditDialog.vala:97 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:118 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:135 +#: src/Widgets/Dialogs/NicknameEditDialog.vala:98 msgid "Submit" msgstr "Versturen" -#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:132 +#: src/Widgets/Dialogs/ChannelTopicEditDialog.vala:133 msgid "Clear topic" msgstr "Onderwerp verwijderen" @@ -259,14 +284,14 @@ msgstr "Gebruikersnaam" msgid "Real Name" msgstr "Echte naam" -#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:126 -#: src/Widgets/Dialogs/PreferencesDialog.vala:245 -#: src/Widgets/SidePanel/ChannelRow.vala:121 -#: src/Widgets/SidePanel/PrivateMessageRow.vala:84 +#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:127 +#: src/Widgets/Dialogs/PreferencesDialog.vala:254 +#: src/Widgets/SidePanel/ChannelRow.vala:125 +#: src/Widgets/SidePanel/PrivateMessageRow.vala:90 msgid "Close" msgstr "Sluiten" -#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:131 +#: src/Widgets/Dialogs/ManageConnectionsDialog.vala:132 msgid "Save" msgstr "Opslaan" @@ -297,14 +322,18 @@ msgid "Suppress join/part messages:" msgstr "Deelname- en actieberichten onderdrukken:" #: src/Widgets/Dialogs/PreferencesDialog.vala:109 +msgid "Mute mention notifications:" +msgstr "Geen melding tonen bij vermeldingen" + +#: src/Widgets/Dialogs/PreferencesDialog.vala:116 msgid "Security and Privacy" msgstr "Beveiliging en privacy" -#: src/Widgets/Dialogs/PreferencesDialog.vala:111 +#: src/Widgets/Dialogs/PreferencesDialog.vala:118 msgid "Unacceptable SSL/TLS Certificates:" msgstr "Onacceptabele ssl- en tls-certificaten:" -#: src/Widgets/Dialogs/PreferencesDialog.vala:157 +#: src/Widgets/Dialogs/PreferencesDialog.vala:164 msgid "" "If a server presents an unacceptable SSL/TLS certificate, no connection " "will be made. (Recommended)" @@ -312,7 +341,7 @@ msgstr "" "Als een server een onacceptabel ssl- of tls-certificaat aanbiedt, dan " "wordt er geen verbinding gemaakt. (aanbevolen)" -#: src/Widgets/Dialogs/PreferencesDialog.vala:174 +#: src/Widgets/Dialogs/PreferencesDialog.vala:181 msgid "" "If a server presents an unacceptable SSL/TLS certificate, the user will " "be warned and can choose whether or not to proceed." @@ -320,7 +349,7 @@ msgstr "" "Als een server een onacceptabel ssl- of tls-certificaat aanbiedt, dan " "wordt er een waarschuwing getoond." -#: src/Widgets/Dialogs/PreferencesDialog.vala:191 +#: src/Widgets/Dialogs/PreferencesDialog.vala:198 msgid "" "If a server presents an unacceptable SSL/TLS certificate, the connection " "will still be made. (Not recommended)" @@ -328,19 +357,15 @@ msgstr "" "Als een server een onacceptabel ssl- of tls-certificaat aanbiedt, dan " "wordt er tóch verbinding gemaakt. (niet aanbevolen)" -#: src/Widgets/Dialogs/PreferencesDialog.vala:205 +#: src/Widgets/Dialogs/PreferencesDialog.vala:212 msgid "Remember connections between sessions:" msgstr "Verbindingen onthouden tussen sessies:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:48 -msgid "Connect to a Server" -msgstr "Verbinden met server" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:66 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:81 msgid "Connection secure" msgstr "Beveiligde verbinding" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:68 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:83 msgid "" "Connection secure, provided only trusted certificates are accepted when " "prompted" @@ -348,7 +373,7 @@ msgstr "" "Beveiligde verbinding, maar alleen vertrouwde certificaten worden " "geaccepteerd" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:70 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:85 msgid "" "Connection may be insecure. Consider rejecting unacceptable certificates " "from the application preferences." @@ -356,114 +381,129 @@ msgstr "" "Mogelijk onbeveiligde verbinding. Overweeg onacceptabele certificaten te " "weigeren in de voorkeuren." -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:72 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:87 msgid "Connection insecure. Consider enabling SSL/TLS from the Advanced tab." msgstr "" "Onbeveiligde verbinding. Overweeg ssl/tls in te schakelen op het tabblad " -"'Geavanceerd'." - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:80 -msgid "New Connection" -msgstr "Nieuwe verbinding" +"‘Geavanceerd’." -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:104 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:117 msgid "Basic" msgstr "Algemeen" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:105 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:118 msgid "Advanced" msgstr "Geavanceerd" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:129 -#: src/Widgets/SidePanel/ServerRow.vala:163 -msgid "Connect" -msgstr "Verbinden" - -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:166 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:199 msgid "Server:" msgstr "Server:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:173 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:211 msgid "Nickname:" msgstr "Bijnaam:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:187 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:218 msgid "Real Name:" msgstr "Echte naam:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:192 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:223 msgid "Iridium IRC Client" msgstr "Iridium irc-client" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:194 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:228 msgid "Authentication Method:" msgstr "Verificatiemethode:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:222 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:260 +msgid "SASL External requires SSL/TLS" +msgstr "SASL vereist ssl/tls" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:260 +msgid "" +"To use SASL External authentication, you must enable SSL/TLS for this server " +"connection." +msgstr "" +"Om externe sasl-authenticatie te kunnen gebruiken, dien je ssl/tls in te " +"schakelen op deze serververbinding." + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:278 msgid "Password:" msgstr "Wachtwoord:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:263 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:300 +msgid "Identity File:" +msgstr "Identiteitsbestand" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:303 +msgid "Select Your Identity File…" +msgstr "Kies een identiteitsbestand…" + +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:356 msgid "Use SSL/TLS:" msgstr "SSL/TLS gebruiken:" -#: src/Widgets/Dialogs/ServerConnectionDialog.vala:279 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:379 msgid "Port:" msgstr "Poort:" -#: src/Widgets/SidePanel/ChannelRow.vala:96 +#: src/Widgets/Dialogs/ServerConnectionDialog.vala:408 +msgid "Invalid identity file" +msgstr "Ongeldig identiteitsbestand" + +#: src/Widgets/SidePanel/ChannelRow.vala:100 msgid "Edit topic…" msgstr "Onderwerp aanpassen…" -#: src/Widgets/SidePanel/ChannelRow.vala:101 +#: src/Widgets/SidePanel/ChannelRow.vala:105 msgid "Add to favorites" msgstr "Toevoegen aan favorieten" -#: src/Widgets/SidePanel/ChannelRow.vala:106 +#: src/Widgets/SidePanel/ChannelRow.vala:110 msgid "Remove from favorites" msgstr "Verwijderen uit favorieten" -#: src/Widgets/SidePanel/ChannelRow.vala:111 +#: src/Widgets/SidePanel/ChannelRow.vala:115 msgid "Join channel" msgstr "Deelnemen aan kanaal" -#: src/Widgets/SidePanel/ChannelRow.vala:116 +#: src/Widgets/SidePanel/ChannelRow.vala:120 msgid "Leave channel" msgstr "Kanaal verlaten" -#: src/Widgets/SidePanel/Panel.vala:58 +#: src/Widgets/SidePanel/Panel.vala:79 msgid "Favorite Channels" msgstr "Favoriete kanalen" -#: src/Widgets/SidePanel/Panel.vala:73 +#: src/Widgets/SidePanel/Panel.vala:94 msgid "Servers" msgstr "Servers" -#: src/Widgets/SidePanel/ServerRow.vala:56 -msgid "Dismiss" -msgstr "Verwerpen" - -#: src/Widgets/SidePanel/ServerRow.vala:153 src/Widgets/StatusBar.vala:28 +#: src/Widgets/SidePanel/ServerRow.vala:119 msgid "Join a Channel…" msgstr "Deelnemen aan kanaal…" -#: src/Widgets/SidePanel/ServerRow.vala:158 +#: src/Widgets/SidePanel/ServerRow.vala:124 msgid "Edit Connection…" msgstr "Verbinding bewerken…" -#: src/Widgets/SidePanel/ServerRow.vala:168 +#: src/Widgets/SidePanel/ServerRow.vala:129 +msgid "Connect" +msgstr "Verbinden" + +#: src/Widgets/SidePanel/ServerRow.vala:134 msgid "Disconnect" msgstr "Verbinding verbreken" -#: src/Widgets/SidePanel/ServerRow.vala:173 +#: src/Widgets/SidePanel/ServerRow.vala:139 msgid "Remove" msgstr "Verwijderen" -#: src/Widgets/SidePanel/ServerRow.vala:207 +#: src/Widgets/SidePanel/ServerRow.vala:173 msgid "Are you sure you want to proceed?" msgstr "Weet je zeker dat je wilt doorgaan?" -#: src/Widgets/SidePanel/ServerRow.vala:208 +#: src/Widgets/SidePanel/ServerRow.vala:174 msgid "" "By removing this connection you will be disconnected, and will not be able " "to recover the connection settings. If you wish to join this server again in " @@ -473,55 +513,45 @@ msgstr "" "onomkeerbaar. Als je weer verbinding wilt maken met deze server, dan moet je " "de verbindingsinstellingen opnieuw opgeven." -#: src/Widgets/SidePanel/ServerRow.vala:213 +#: src/Widgets/SidePanel/ServerRow.vala:179 msgid "Yes, remove" msgstr "Ja, verwijderen" -#: src/Widgets/SidePanel/ServerRow.vala:217 +#: src/Widgets/SidePanel/ServerRow.vala:183 msgid "Don't warn me again" msgstr "Niet meer tonen" -#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:38 -msgid "No users" +#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:98 +#, fuzzy, c-format +msgid "%d user" msgstr "Geen gebruikers" -#: src/Widgets/HeaderBar.vala:43 -msgid "New Server Connection…" -msgstr "Nieuwe serververbinding…" - -#: src/Widgets/HeaderBar.vala:53 -msgid "Join Channel…" -msgstr "Deelnemen aan kanaal…" +#: src/Widgets/UsersPopover/ChannelUsersPopover.vala:100 +#, fuzzy, c-format +msgid "%d users" +msgstr "Geen gebruikers" -#: src/Widgets/HeaderBar.vala:78 +#: src/Widgets/HeaderBar.vala:41 msgid "Channel users" msgstr "Kanaalgebruikers" -#: src/Widgets/HeaderBar.vala:88 +#: src/Widgets/HeaderBar.vala:53 msgid "Menu" msgstr "Menu" -#: src/Widgets/HeaderBar.vala:93 -msgid "Light background" -msgstr "Lichte achtergrond" - -#: src/Widgets/HeaderBar.vala:94 -msgid "Dark background" -msgstr "Donkere achtergrond" - -#: src/Widgets/HeaderBar.vala:103 +#: src/Widgets/HeaderBar.vala:58 msgid "Toggle Sidebar" msgstr "Zijpaneel tonen/verbergen" -#: src/Widgets/HeaderBar.vala:113 +#: src/Widgets/HeaderBar.vala:68 msgid "Reset Marker Line" msgstr "Markering terugzetten" -#: src/Widgets/HeaderBar.vala:123 +#: src/Widgets/HeaderBar.vala:78 msgid "Preferences…" msgstr "Voorkeuren…" -#: src/Widgets/HeaderBar.vala:133 +#: src/Widgets/HeaderBar.vala:88 msgid "Quit" msgstr "Afsluiten" @@ -537,100 +567,126 @@ msgstr "Maak verbinding met het internet om deel te nemen aan servers." msgid "Network Settings…" msgstr "Internetinstellingen…" -#: src/Widgets/StatusBar.vala:27 -msgid "Connect to a Server…" -msgstr "Verbinden met server…" +#: src/Widgets/StatusBar.vala:28 +msgid "New Server Connection…" +msgstr "Nieuwe serververbinding…" #: src/Widgets/StatusBar.vala:38 +msgid "Join Channel…" +msgstr "Deelnemen aan kanaal…" + +#: src/Widgets/StatusBar.vala:60 +#, fuzzy +msgid "Join…" +msgstr "Deelnemen" + +#: src/Widgets/StatusBar.vala:63 msgid "Join a Server or Channel" msgstr "Deelnemen aan server of kanaal" -#: src/Widgets/StatusBar.vala:43 -msgid "Manage connections…" -msgstr "Verbindingen beheren…" - -#: src/MainWindow.vala:410 src/MainWindow.vala:456 +#: src/MainWindow.vala:423 src/MainWindow.vala:474 msgid "Already connected to this server!" msgstr "Je bent al verbonden met deze server!" -#: src/MainWindow.vala:629 src/MainWindow.vala:631 +#: src/MainWindow.vala:674 src/MainWindow.vala:676 msgid "You've already joined this channel" msgstr "Je neemt al deel aan dit kanaal" -#: src/MainWindow.vala:640 +#: src/MainWindow.vala:685 msgid "Channel must begin with '#' or '&'" -msgstr "De kanaalnaam moet beginnen met '#' of '&'" +msgstr "De kanaalnaam moet beginnen met ‘#’ of ‘&’" -#: src/MainWindow.vala:644 +#: src/MainWindow.vala:689 msgid "Enter a channel name" msgstr "Voer een kanaalnaam in" -#: src/MainWindow.vala:674 +#: src/MainWindow.vala:719 msgid "Start your message with a /" msgstr "Begin je bericht met een /" -#: src/MainWindow.vala:740 +#: src/MainWindow.vala:785 msgid "No recipient nickname specified (Usage: /msg )" msgstr "" "Je hebt geen ontvanger opgegeven (gebruiksvoorbeeld: /msg " ")" -#: src/MainWindow.vala:744 +#: src/MainWindow.vala:789 msgid "No message specified (Usage: /msg )" msgstr "" "Je hebt geen bericht ingevoerd (gebruiksvoorbeeld: /msg )" -#: src/MainWindow.vala:755 +#: src/MainWindow.vala:800 msgid "No action specified (Usage: /me )" msgstr "Je hebt geen actie opgegeven (gebruiksvoorbeeld: /me )" -#: src/MainWindow.vala:944 src/MainWindow.vala:958 +#: src/MainWindow.vala:986 src/MainWindow.vala:1003 msgid " has quit" msgstr " is weggegaan" -#: src/MainWindow.vala:993 +#: src/MainWindow.vala:1039 msgid " has cleared the topic" msgstr " heeft het onderwerp verwijderd" -#: src/MainWindow.vala:995 +#: src/MainWindow.vala:1041 msgid " has changed the topic to: " msgstr " heeft het onderwerp gewijzigd in " -#: src/MainWindow.vala:1010 +#: src/MainWindow.vala:1056 msgid "Topic for " msgstr "Onderwerp over " -#: src/MainWindow.vala:1010 +#: src/MainWindow.vala:1056 msgid " is: " msgstr " is: " -#: src/MainWindow.vala:1012 +#: src/MainWindow.vala:1058 msgid "Topic set by " msgstr "Het onderwerp is ingesteld door " -#: src/MainWindow.vala:1024 src/MainWindow.vala:1027 +#: src/MainWindow.vala:1070 src/MainWindow.vala:1073 msgid "Nickname already in use." msgstr "Deze bijnaam is al in gebruik." -#: src/MainWindow.vala:1033 +#: src/MainWindow.vala:1079 msgid "Nickname already in use" msgstr "Deze bijnaam is al in gebruik" -#: src/MainWindow.vala:1033 +#: src/MainWindow.vala:1079 msgid "Choose a new nickname and retry the connection." msgstr "Kies een andere bijnaam en probeer het opnieuw." -#: src/MainWindow.vala:1038 +#: src/MainWindow.vala:1084 msgid " is not a valid nickname." msgstr " is geen geldige bijnaam." -#: src/MainWindow.vala:1099 +#: src/MainWindow.vala:1171 msgid " has joined" msgstr " neemt nu deel aan het kanaal" -#: src/MainWindow.vala:1112 +#: src/MainWindow.vala:1185 msgid " has left" msgstr " heeft het kanaal verlaten" +#~ msgid "Dismiss" +#~ msgstr "Verwerpen" + +#~ msgid "Connect to a Server…" +#~ msgstr "Verbinden met server…" + +#~ msgid "Manage connections…" +#~ msgstr "Verbindingen beheren…" + +#~ msgid "Connect to a Server" +#~ msgstr "Verbinden met server" + +#~ msgid "New Connection" +#~ msgstr "Nieuwe verbinding" + +#~ msgid "Light background" +#~ msgstr "Lichte achtergrond" + +#~ msgid "Dark background" +#~ msgstr "Donkere achtergrond" + #~ msgid "Join Channel" #~ msgstr "Deelnemen aan kanaal" diff --git a/reset-installation.sh b/reset-installation.sh new file mode 100755 index 0000000..a2f85a3 --- /dev/null +++ b/reset-installation.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -e + +read -p "Are you sure you want to reset all settings and data? (y/n): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + exit 1 +fi + +APP_ID=com.github.avojak.iridium +GSETTINGS_ID=$APP_ID +GSETTINGS_PATH=$APP_ID + +print_setting () { + echo -e " $1 = $(flatpak run --command=gsettings $GSETTINGS_ID get $GSETTINGS_PATH $1)" +} + +set_setting () { + flatpak run --command=gsettings $GSETTINGS_ID set $GSETTINGS_PATH $1 "$2" + print_setting $1 +} + +clear_sqlite_table () { + sqlite3 $DATABASE_PATH "DELETE FROM $1;" + echo -e " \u2714 Cleared $1" +} + +echo +echo "Resetting GSettings..." + +set_setting certificate-validation-policy "REJECT" +set_setting default-nickname "" +set_setting default-realname "" +set_setting suppress-connection-close-warnings false +set_setting remember-connections true +set_setting suppress-join-part-messages false +set_setting mute-mention-notifications false +set_setting font "Monospace Regular 9" +set_setting pos-x 360 +set_setting pos-y 360 +set_setting window-width 1000 +set_setting window-height 600 +set_setting last-server "" +set_setting last-channel "" + +echo +echo "Resetting database..." + +DATABASE_PATH=~/.var/app/$APP_ID/config/$APP_ID/iridium01.db + +clear_sqlite_table servers +clear_sqlite_table channels +clear_sqlite_table server_identities +clear_sqlite_table sqlite_sequence + +echo +echo -e "\033[1;32mDone\033[0m" +echo \ No newline at end of file diff --git a/src/Application.vala b/src/Application.vala index a674199..7a9ea90 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -30,6 +30,7 @@ public class Iridium.Application : Gtk.Application { private GLib.List windows; private bool is_network_available; + private bool is_first_network_availability = true; private Gee.List restore_state_servers = new Gee.ArrayList (); private Gee.List restore_state_channels = new Gee.ArrayList (); @@ -44,12 +45,12 @@ public class Iridium.Application : Gtk.Application { } static construct { - Granite.Services.Logger.initialize (Constants.APP_ID); - if (is_dev_mode ()) { - Granite.Services.Logger.DisplayLevel = Granite.Services.LogLevel.DEBUG; - } else { - Granite.Services.Logger.DisplayLevel = Granite.Services.LogLevel.WARN; - } + // Granite.Services.Logger.initialize (Constants.APP_ID); + // if (is_dev_mode ()) { + // Granite.Services.Logger.DisplayLevel = Granite.Services.LogLevel.DEBUG; + // } else { + // Granite.Services.Logger.DisplayLevel = Granite.Services.LogLevel.WARN; + // } info ("%s version: %s", Constants.APP_ID, Constants.VERSION); info ("Kernel version: %s", Posix.utsname ().release); } @@ -64,10 +65,6 @@ public class Iridium.Application : Gtk.Application { windows = new GLib.List (); - network_monitor.network_changed.connect (() => { - warning ("Network availability changed: %s", network_monitor.get_network_available ().to_string ()); - }); - startup.connect ((handler) => { Hdy.init (); }); @@ -95,29 +92,52 @@ public class Iridium.Application : Gtk.Application { private Iridium.MainWindow add_new_window () { var window = new Iridium.MainWindow (this); - window.ui_initialized.connect ((servers, channels, is_reconnecting) => { - window.open_connections (servers, channels, is_reconnecting); + window.ui_initialized.connect (() => { + // If we run into the GLib NetworkMonitor bug, or there really isn't network availability, don't connect yet + if (is_network_available) { + window.open_connections (connection_repository.get_servers (), connection_repository.get_channels (), false); + } }); + window.connections_opened.connect ((is_reconnecting) => { + // Don't handle command line arguments if we're just reconnecting + if (!is_reconnecting && (queued_command_line_arguments != null)) { + debug ("Sending queued command line arguments to window"); + handle_command_line_arguments (queued_command_line_arguments); + } + }); + window.initialize_ui (connection_repository.get_servers (), connection_repository.get_channels ()); this.add_window (window); return window; } protected override int command_line (ApplicationCommandLine command_line) { + string[] command_line_arguments = parse_command_line_arguments (command_line.get_arguments ()); // If the application wasn't already open, activate it now if (windows.length () == 0) { - debug ("Queueing command line arguments until initialization is complete"); - queued_command_line_arguments = command_line.get_arguments (); + queued_command_line_arguments = command_line_arguments; activate (); } else { - handle_command_line_arguments (command_line.get_arguments ()); + handle_command_line_arguments (command_line_arguments); } return 0; } + private string[] parse_command_line_arguments (string[] command_line_arguments) { + if (command_line_arguments.length == 0) { + return command_line_arguments; + } else { + // For Flatpak, the first commandline argument is the app ID, so we need to filter it out + if (command_line_arguments[0] == Constants.APP_ID) { + return command_line_arguments[1:command_line_arguments.length - 1]; + } { + return command_line_arguments; + } + } + } + private void handle_command_line_arguments (string[] argv) { - // string[] argv = command_line.get_arguments (); GLib.List uris = new GLib.List (); - foreach (var uri_string in argv[1:argv.length]) { + foreach (var uri_string in argv) { try { Soup.URI uri = new Soup.URI (uri_string); if (uri == null) { @@ -127,8 +147,6 @@ public class Iridium.Application : Gtk.Application { throw new OptionError.BAD_VALUE ("Cannot open non-irc: URL"); } debug ("Received command line URI: %s", uri.to_string (false)); - // debug ("host: %s", uri.get_host ()); - // debug ("port: %s", uri.get_port ().to_string ()); uris.append (new Iridium.Models.IRCURI (uri)); } catch (OptionError e) { warning ("Argument parsing error: %s", e.message); @@ -147,10 +165,9 @@ public class Iridium.Application : Gtk.Application { connection_repository.sql_client = Iridium.Services.SQLClient.instance; certificate_manager.sql_client = Iridium.Services.SQLClient.instance; - // TODO: Connect to signals to save window size and position in settings - // Handle changes to network connectivity (eg. losing internet connection) network_monitor.network_changed.connect (() => { + debug ("Network availability changed: %s", network_monitor.get_network_available ().to_string ()); // Don't react to duplicate signals bool updated_availability = network_monitor.get_network_available (); if (is_network_available == updated_availability) { @@ -161,41 +178,60 @@ public class Iridium.Application : Gtk.Application { if (is_network_available) { foreach (var window in windows) { window.network_connection_gained (); - restore_state (window, true); + // If this is the first time that the network has become available, it's not a reconnection, + // it's the first connection. The servers and channels have been stored away in the + // restore_state_* lists. + window.open_connections (restore_state_servers, restore_state_channels, !is_first_network_availability); } + is_first_network_availability = false; } else { foreach (var window in windows) { restore_state_servers = connection_repository.get_servers (); restore_state_channels = connection_repository.get_channels (); + connection_manager.close_all_connections (); window.network_connection_lost (); } } }); + // Respect the system style preference + var granite_settings = Granite.Settings.get_default (); + var gtk_settings = Gtk.Settings.get_default (); + gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + granite_settings.notify["prefers-color-scheme"].connect (() => { + gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + }); var window = this.add_new_window (); // Check the initial state of the network connection + // Note: There is a bug in GLib where the initial network availability property may report `false` + // incorrectly due to an asynchronous D-Bus call. + // See: https://gitlab.gnome.org/GNOME/glib/-/issues/1718 is_network_available = network_monitor.get_network_available (); + debug ("Initial network availability: %s", is_network_available.to_string ()); + + // If the network isn't initially available, grab the servers and channels for later if (!is_network_available) { - foreach (var _window in windows) { - _window.network_connection_lost (); - } + restore_state_servers = connection_repository.get_servers (); + restore_state_channels = connection_repository.get_channels (); + window.network_connection_lost (); + } else { + // If network is available, next `true` value for network availability will be a reconnection + is_first_network_availability = false; } - restore_state (window, false); + update_default_preferences (); } - private void restore_state (Iridium.MainWindow main_window, bool is_reconnecting) { - var servers = is_reconnecting ? restore_state_servers : connection_repository.get_servers (); - var channels = is_reconnecting ? restore_state_channels : connection_repository.get_channels (); - main_window.connections_opened.connect (() => { - if (queued_command_line_arguments != null) { - debug ("Sending queued command line arguments to main window"); - handle_command_line_arguments (queued_command_line_arguments); - } - }); - main_window.initialize_ui (servers, channels, is_reconnecting); + private void update_default_preferences () { + // Update values that cannot be defined as constants in the schema + if (settings.get_string ("default-nickname") == "") { + settings.set_string ("default-nickname", GLib.Environment.get_user_name ()); + } + if (settings.get_string ("default-realname") == "") { + settings.set_string ("default-realname", GLib.Environment.get_real_name ()); + } } public static int main (string[] args) { diff --git a/src/Layouts/MainLayout.vala b/src/Layouts/MainLayout.vala index 8edd07f..0ff3eaf 100644 --- a/src/Layouts/MainLayout.vala +++ b/src/Layouts/MainLayout.vala @@ -49,28 +49,36 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { construct { header_bar = new Iridium.Widgets.HeaderBar (); header_bar.set_channel_users_button_visible (false); - header_bar.nickname_selected.connect ((nickname) => { - nickname_selected (nickname); + header_bar.initiate_private_message.connect ((nickname) => { + initiate_private_message (nickname); }); side_panel = new Iridium.Widgets.SidePanel.Panel (window); welcome_view = new Iridium.Views.Welcome (window); main_stack = new Gtk.Stack (); main_stack.add_named (welcome_view, "welcome"); + overlay = new Gtk.Overlay (); + overlay.add (main_stack); - paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); - paned.position = 240; - paned.pack1 (side_panel, false, false); - paned.pack2 (main_stack, true, false); + // Create a header group that automatically assigns the right decoration controls to the + // right headerbar automatically + var header_group = new Hdy.HeaderGroup (); + header_group.add_header_bar (side_panel.header_bar); + header_group.add_header_bar (header_bar); network_info_bar = new Iridium.Widgets.NetworkInfoBar (); - overlay = new Gtk.Overlay (); - overlay.add (paned); + var main_grid = new Gtk.Grid (); + main_grid.attach (header_bar, 0, 0); + main_grid.attach (network_info_bar, 0, 1); + main_grid.attach (overlay, 0, 2); - attach (header_bar, 0, 0); - attach (network_info_bar, 0, 1); - attach (overlay, 0, 2); + paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); + paned.position = 240; + paned.pack1 (side_panel, false, false); + paned.pack2 (main_grid, true, false); + + attach (paned, 0, 0); nickname_mapping = new Gee.HashMap> (); server_child_views = new Gee.HashMap> (); @@ -308,9 +316,12 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { return; } - // Display the error on the side panel row + // Display the error icon on the side panel row, and show the error message in the chat view if (chat_view is Iridium.Views.ServerChatView) { - side_panel.error_server_row (server_name, error_message, error_details); + side_panel.error_server_row (server_name); + var message = new Iridium.Services.Message (); + message.message = (error_details == null) ? error_message : @"$error_message: $error_details"; + chat_view.display_server_error_msg (message); } else if (chat_view is Iridium.Views.ChannelChatView) { // TODO } else if (chat_view is Iridium.Views.PrivateMessageChatView) { @@ -347,6 +358,7 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { string? child_name = get_child_name (server_name, channel_name); if (child_name != null) { main_stack.set_visible_child_full (child_name, Gtk.StackTransitionType.SLIDE_RIGHT); + update_last_shown_view (server_name, channel_name); } // Notify the chat view that it has gained focus @@ -373,6 +385,11 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { }); } + private void update_last_shown_view (string server_name, string? channel_name) { + Iridium.Application.settings.set_string ("last-server", server_name); + Iridium.Application.settings.set_string ("last-channel", channel_name == null ? "" : channel_name); + } + public string? get_visible_server () { string? child_name = main_stack.get_visible_child_name (); if (child_name == null || child_name == "welcome") { @@ -402,12 +419,15 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { } public void show_connecting_overlay () { - if (restore_connections_overlay_bar == null) { - restore_connections_overlay_bar = new Granite.Widgets.OverlayBar (overlay); - restore_connections_overlay_bar.label = _("Restoring server connections…"); - restore_connections_overlay_bar.active = true; - overlay.show_all (); - } + Idle.add (() => { + if (restore_connections_overlay_bar == null) { + restore_connections_overlay_bar = new Granite.Widgets.OverlayBar (overlay); + restore_connections_overlay_bar.label = _("Restoring server connections…"); + restore_connections_overlay_bar.active = true; + overlay.show_all (); + } + return false; + }); } public void hide_connecting_overlay () { @@ -581,11 +601,16 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { side_panel.favorite_channel (server_name, channel_name); } - public void update_channel_users (string server_name, string channel_name, Gee.List nicknames) { + public void update_channel_users (string server_name, string channel_name, Gee.List nicknames, Gee.List operators) { var channel_chat_view = get_channel_chat_view (server_name, channel_name); if (channel_chat_view != null) { channel_chat_view.set_nicknames (nicknames); } + // Update the channel users popover if this call affects the current chat view + // TODO: Update this all the time + if ((get_visible_server () == server_name) && (get_visible_channel () == channel_name)) { + header_bar.set_channel_users (nicknames, operators); + } } private Iridium.Views.ChatView? get_chat_view (string server_name, string? channel_name) { @@ -596,7 +621,7 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { return (Iridium.Views.ChatView) main_stack.get_child_by_name (child_name); } - private string? get_child_name (string server_name, string? channel_name) { + public string? get_child_name (string server_name, string? channel_name) { if (channel_name == null) { return server_name; } else if (channel_name.has_prefix ("#") || channel_name.has_prefix ("&")) { @@ -631,7 +656,7 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { } } - public void update_title (string title, string? subtitle) { + public void update_title (string? title, string? subtitle) { header_bar.update_title (title, subtitle); } @@ -647,10 +672,6 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { header_bar.set_channel_users_button_enabled (enabled); } - public void set_channel_users (Gee.List nicknames, Gee.List operators) { - header_bar.set_channel_users (nicknames, operators); - } - /* * Handlers for the side panel signals */ @@ -690,5 +711,5 @@ public class Iridium.Layouts.MainLayout : Gtk.Grid { public signal void edit_channel_topic_button_clicked (string server_name, string channel_name); public signal void edit_connection_button_clicked (string server_name); - public signal void nickname_selected (string nickname); // TODO: Rename this - for selecting nickname from channel list + public signal void initiate_private_message (string nickname); // TODO: Rename this - for selecting nickname from channel list } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 64a4806..6eafd7d 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -21,12 +21,12 @@ public class Iridium.MainWindow : Hdy.Window { - public unowned Iridium.Application app { get; construct; } + public weak Iridium.Application app { get; construct; } private Iridium.Services.ActionManager action_manager; private Gtk.AccelGroup accel_group; - private Iridium.Widgets.ServerConnectionDialog? connection_dialog = null; + private Iridium.Widgets.NewServerConnectionDialog? connection_dialog = null; private Iridium.Widgets.EditServerConnectionDialog? edit_connection_dialog = null; private Iridium.Widgets.ChannelJoinDialog? channel_join_dialog = null; private Iridium.Widgets.ChannelTopicEditDialog? channel_topic_edit_dialog = null; @@ -34,16 +34,19 @@ public class Iridium.MainWindow : Hdy.Window { private Iridium.Widgets.PreferencesDialog? preferences_dialog = null; private Iridium.Widgets.NicknameEditDialog? nickname_edit_dialog = null; private Iridium.Widgets.BrowseChannelsDialog? browse_channels_dialog = null; + private Iridium.Widgets.BrowseServersDialog? browse_servers_dialog = null; private Iridium.Layouts.MainLayout main_layout; + private Gee.Map> notification_ids = new Gee.HashMap> (); + public MainWindow (Iridium.Application application) { Object ( + title: Constants.APP_NAME, application: application, app: application, border_width: 0, - resizable: true, - window_position: Gtk.WindowPosition.CENTER + resizable: true ); } @@ -55,8 +58,7 @@ public class Iridium.MainWindow : Hdy.Window { main_layout = new Iridium.Layouts.MainLayout (this); add (main_layout); - move (Iridium.Application.settings.get_int ("pos-x"), Iridium.Application.settings.get_int ("pos-y")); - resize (Iridium.Application.settings.get_int ("window-width"), Iridium.Application.settings.get_int ("window-height")); + restore_window_position (); // Connect to main layout signals main_layout.welcome_view_shown.connect (on_welcome_view_shown); @@ -73,7 +75,7 @@ public class Iridium.MainWindow : Hdy.Window { main_layout.disconnect_from_server_button_clicked.connect (on_disconnect_from_server_button_clicked); main_layout.edit_channel_topic_button_clicked.connect (on_edit_channel_topic_button_clicked); main_layout.edit_connection_button_clicked.connect (on_edit_connection_button_clicked); - main_layout.nickname_selected.connect (on_nickname_selected); + main_layout.initiate_private_message.connect (initiate_private_message); // Connect to connection handler signals Iridium.Application.connection_manager.unacceptable_certificate.connect (on_unacceptable_certificate); @@ -128,6 +130,15 @@ public class Iridium.MainWindow : Hdy.Window { } }); + // Handles the case where the application gains focus again, but no change in visible chat view + this.focus_in_event.connect (() => { + string? server_name = main_layout.get_visible_server (); + string? channel_name = main_layout.get_visible_channel (); + if (server_name != null && channel_name != null) { + withdraw_notifications (channel_name == null ? server_name : @"$server_name:$channel_name"); + } + }); + // Close connections when the window is closed this.destroy.connect (() => { // // Disconnect this signal so that we don't modify the setting to @@ -145,6 +156,11 @@ public class Iridium.MainWindow : Hdy.Window { show_app (); } + private void restore_window_position () { + move (Iridium.Application.settings.get_int ("pos-x"), Iridium.Application.settings.get_int ("pos-y")); + resize (Iridium.Application.settings.get_int ("window-width"), Iridium.Application.settings.get_int ("window-height")); + } + public void show_app () { show_all (); show (); @@ -179,37 +195,43 @@ public class Iridium.MainWindow : Hdy.Window { } // TODO: Restore private messages from the side panel - public void initialize_ui (Gee.List servers, Gee.List channels, bool is_reconnecting) { + public void initialize_ui (Gee.List servers, Gee.List channels) { // Initialize the UI with disabled rows and chat views for everything - if (!is_reconnecting) { - debug ("Initializing side panel and chat views…"); - foreach (Iridium.Services.Server server in servers) { - var server_id = server.id; - var server_name = server.connection_details.server; + debug ("Initializing side panel and chat views…"); + foreach (Iridium.Services.Server server in servers) { + var server_id = server.id; + var server_name = server.connection_details.server; + Idle.add (() => { + main_layout.add_server_chat_view (server_name, server.connection_details.nickname, server.network_name != null ? server.network_name : null); + return false; + }); + foreach (Iridium.Services.Channel channel in channels) { + // var channel_id = channel.id; + var channel_server_id = channel.server_id; + var channel_name = channel.name; + if (channel_server_id != server_id) { + // This channel isn't for the current server + continue; + } Idle.add (() => { - main_layout.add_server_chat_view (server_name, server.connection_details.nickname, server.network_name != null ? server.network_name : null); + main_layout.add_channel_chat_view (server_name, channel_name, server.connection_details.nickname); + if (channel.favorite) { + main_layout.favorite_channel (server_name, channel_name); + } return false; }); - foreach (Iridium.Services.Channel channel in channels) { - // var channel_id = channel.id; - var channel_server_id = channel.server_id; - var channel_name = channel.name; - if (channel_server_id != server_id) { - // This channel isn't for the current server - continue; - } - Idle.add (() => { - main_layout.add_channel_chat_view (server_name, channel_name, server.connection_details.nickname); - if (channel.favorite) { - main_layout.favorite_channel (server_name, channel_name); - } - return false; - }); - } } } - ui_initialized (servers, channels, is_reconnecting); + // Switch to the last shown view from the prior session + var last_server = Iridium.Application.settings.get_string ("last-server"); + var last_channel = Iridium.Application.settings.get_string ("last-channel"); + Idle.add (() => { + main_layout.show_chat_view (last_server, last_channel == "" ? null : last_channel); + return false; + }); + + ui_initialized (servers, channels); } public void open_connections (Gee.List servers, Gee.List channels, bool is_reconnecting) { @@ -218,7 +240,7 @@ public class Iridium.MainWindow : Hdy.Window { // Handle case were there's nothing to initialize! if (servers.size == 0) { main_layout.hide_connecting_overlay (); - connections_opened (); + connections_opened (is_reconnecting); return; } @@ -249,13 +271,15 @@ public class Iridium.MainWindow : Hdy.Window { num_enabled_servers++; } } - - if (is_reconnecting) { - debug ("Attempting reconnection for %d servers…", num_enabled_servers); + // No enabled servers to connect to - nothing to do + if (num_enabled_servers == 0) { + debug ("No enabled servers to connect to"); + completed_opening_connections (is_reconnecting); + return; } // Open connections to enabled servers - debug ("Opening server connections…"); + debug ("Opening %d server connections [reconnecting: %s]", num_enabled_servers, is_reconnecting.to_string ()); foreach (Iridium.Services.Server server in servers) { var server_id = server.id; var connection_details = server.connection_details; @@ -287,29 +311,22 @@ public class Iridium.MainWindow : Hdy.Window { server_connection.open_successful.connect (() => { connection_status.set (server_name, true); if (connection_status.size == num_enabled_servers) { - completed_opening_connections (); + completed_opening_connections (is_reconnecting); } }); server_connection.open_failed.connect (() => { - // TODO: Give some user feedback, maybe a toast? Don't want the UI to get too busy though connection_status.set (server_name, false); if (connection_status.size == num_enabled_servers) { - completed_opening_connections (); + completed_opening_connections (is_reconnecting); } }); } - - // We've initialized the UI, but if there aren't any connections to wait on, we're done - if (num_enabled_servers == 0) { - completed_opening_connections (); - return; - } } - private void completed_opening_connections () { + private void completed_opening_connections (bool is_reconnecting) { debug ("Done opening connections"); main_layout.hide_connecting_overlay (); - connections_opened (); + connections_opened (is_reconnecting); } public void handle_uris (GLib.List uris) { @@ -393,11 +410,15 @@ public class Iridium.MainWindow : Hdy.Window { } } - public void show_server_connection_dialog () { + public void show_server_connection_dialog (Iridium.Models.CuratedServer? curated_server = null) { if (connection_dialog == null) { - connection_dialog = new Iridium.Widgets.ServerConnectionDialog (this); + if (curated_server == null) { + connection_dialog = new Iridium.Widgets.NewServerConnectionDialog (this); + } else { + connection_dialog = new Iridium.Widgets.NewServerConnectionDialog.from_curated_server (this, curated_server); + } connection_dialog.show_all (); - connection_dialog.connect_button_clicked.connect ((server, nickname, realname, port, auth_method, tls, auth_token) => { + connection_dialog.primary_button_clicked.connect ((server, nickname, realname, port, auth_method, tls, auth_token) => { // Prevent duplicate connections if (Iridium.Application.connection_manager.has_connection (server)) { connection_dialog.display_error (_("Already connected to this server!")); @@ -418,6 +439,12 @@ public class Iridium.MainWindow : Hdy.Window { // Attempt the server connection Iridium.Application.connection_manager.connect_to_server (connection_details); }); + connection_dialog.browse_button_clicked.connect (() => { + connection_dialog.destroy.connect (() => { + show_browse_servers_dialog (); + }); + connection_dialog.dismiss (); + }); connection_dialog.destroy.connect (() => { connection_dialog = null; }); @@ -427,7 +454,6 @@ public class Iridium.MainWindow : Hdy.Window { public void show_edit_server_connection_dialog (string server_name) { if (edit_connection_dialog == null) { - edit_connection_dialog = new Iridium.Widgets.EditServerConnectionDialog (this); Iridium.Services.ServerConnectionDetails? existing_connection_details = Iridium.Application.connection_manager.get_connection_details (server_name); if (existing_connection_details == null) { Iridium.Services.Server? server = Iridium.Application.connection_repository.get_server (server_name); @@ -441,9 +467,9 @@ public class Iridium.MainWindow : Hdy.Window { if (existing_connection_details.auth_method.stores_secret ()) { existing_connection_details.auth_token = Iridium.Application.secret_manager.retrieve_secret (existing_connection_details.server, existing_connection_details.port, existing_connection_details.nickname); } - edit_connection_dialog.populate (existing_connection_details); + edit_connection_dialog = new Iridium.Widgets.EditServerConnectionDialog.from_connection_details (this, existing_connection_details); edit_connection_dialog.show_all (); - edit_connection_dialog.save_button_clicked.connect ((server, nickname, realname, port, auth_method, tls, auth_token) => { + edit_connection_dialog.primary_button_clicked.connect ((server, nickname, realname, port, auth_method, tls, auth_token) => { // Prevent duplicate connections if ((existing_connection_details.server != server) && (Iridium.Application.connection_manager.has_connection (server))) { connection_dialog.display_error (_("Already connected to this server!")); @@ -483,6 +509,11 @@ public class Iridium.MainWindow : Hdy.Window { // Disconnect and update the settings Iridium.Application.connection_manager.disconnect_from_server (existing_connection_details.server); Iridium.Application.connection_repository.update_server_connection_details (existing_connection_details.server, updated_connection_details); + try { + Iridium.Application.secret_manager.store_secret (server, port, nickname, auth_token); + } catch (GLib.Error e) { + warning ("Error while storing secret: %s", e.message); + } // Re-connect var new_server_connection = Iridium.Application.connection_manager.connect_to_server (updated_connection_details); new_server_connection.open_successful.connect (() => { @@ -501,6 +532,11 @@ public class Iridium.MainWindow : Hdy.Window { // Otherwise just write the changes straight to the repository debug ("Server connection details changed, updating details in repository"); Iridium.Application.connection_repository.update_server_connection_details (existing_connection_details.server, updated_connection_details); + try { + Iridium.Application.secret_manager.store_secret (server, port, nickname, auth_token); + } catch (GLib.Error e) { + warning ("Error while storing secret: %s", e.message); + } edit_connection_dialog.dismiss (); // In case the nickname changed, update the views if (existing_connection_details.nickname != nickname) { @@ -615,6 +651,23 @@ public class Iridium.MainWindow : Hdy.Window { Iridium.Application.connection_manager.request_channel_list (server_name); } + public void show_browse_servers_dialog () { + if (browse_servers_dialog == null) { + browse_servers_dialog = new Iridium.Widgets.BrowseServersDialog (this); + browse_servers_dialog.show_all (); + browse_servers_dialog.connect_button_clicked.connect ((curated_server) => { + browse_servers_dialog.destroy.connect (() => { + show_server_connection_dialog (curated_server); + }); + browse_servers_dialog.dismiss (); + }); + browse_servers_dialog.destroy.connect (() => { + browse_servers_dialog = null; + }); + } + browse_servers_dialog.present (); + } + private void join_channel (string server_name, string channel_name) { // Check if we're already in this channel if (Iridium.Application.connection_manager.get_joined_channels (server_name).index_of (channel_name) != -1) { @@ -776,6 +829,13 @@ public class Iridium.MainWindow : Hdy.Window { main_layout.reset_marker_line (); } + public void show_chat_view (string server_name, string? channel_name) { + Idle.add (() => { + main_layout.show_chat_view (server_name, channel_name); + return false; + }); + } + // // Respond to network connection changes // @@ -783,7 +843,6 @@ public class Iridium.MainWindow : Hdy.Window { public void network_connection_lost () { main_layout.show_network_info_bar (); // TODO: Disable server and channel buttons in header bar - Iridium.Application.connection_manager.close_all_connections (); } public void network_connection_gained () { @@ -807,59 +866,48 @@ public class Iridium.MainWindow : Hdy.Window { // HeaderBar Callbacks // - private void on_nickname_selected (string nickname) { + private void initiate_private_message (string nickname) { var server_name = main_layout.get_visible_server (); if (server_name == null) { return; } var self_nickname = main_layout.get_server_chat_view (server_name).nickname; - var trimmed_nickname = strip_nickname_prefix (nickname); Idle.add (() => { - main_layout.add_private_message_chat_view (server_name, trimmed_nickname, self_nickname); - main_layout.enable_chat_view (server_name, trimmed_nickname); - main_layout.show_chat_view (server_name, trimmed_nickname); + main_layout.add_private_message_chat_view (server_name, nickname, self_nickname); + main_layout.enable_chat_view (server_name, nickname); + main_layout.show_chat_view (server_name, nickname); return false; }); } - private string strip_nickname_prefix (string nickname) { - var prefixes = new string[] { "@", "&" }; - foreach (string prefix in prefixes) { - if (nickname.has_prefix (prefix)) { - return nickname.substring (1, nickname.length - 1); - } - } - return nickname; - } - // // ServerConnectionManager Callbacks // - private bool on_unacceptable_certificate (TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable) { - int result = -1; - bool remember_decision = false; + private void on_unacceptable_certificate (string server_name, TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable) { Idle.add (() => { + bool remember_decision = false; var dialog = new Iridium.Widgets.CertificateWarningDialog (this, peer_cert, errors, connectable); dialog.remember_decision_toggled.connect ((remember) => { remember_decision = remember; }); - result = dialog.run (); + int result = dialog.run (); dialog.dismiss (); + var is_accepted = (result == Gtk.ResponseType.OK); + if (is_accepted) { + Iridium.Application.connection_manager.accept_certificate (server_name); + } else { + Iridium.Application.connection_manager.reject_certificate (server_name); + } + if (remember_decision) { + var server_identity = new Iridium.Models.ServerIdentity (); + server_identity.host = Iridium.Services.CertificateManager.parse_host (connectable); + server_identity.certificate_pem = peer_cert.certificate_pem; + server_identity.is_accepted = is_accepted; + Iridium.Application.certificate_manager.store_identity (server_identity); + } return false; }); - while (result == -1) { - // Block until a selection is made - } - var is_accepted = (result == Gtk.ResponseType.OK); - if (remember_decision) { - var server_identity = new Iridium.Models.ServerIdentity (); - server_identity.host = Iridium.Services.CertificateManager.parse_host (connectable); - server_identity.certificate_pem = peer_cert.certificate_pem; - server_identity.is_accepted = is_accepted; - Iridium.Application.certificate_manager.store_identity (server_identity); - } - return is_accepted; } private void on_server_connection_successful (string server_name, string nickname, Iridium.Services.Message message) { @@ -935,6 +983,7 @@ public class Iridium.MainWindow : Hdy.Window { foreach (string channel in channels) { if (!Iridium.Application.settings.get_boolean ("suppress-join-part-messages")) { var message_to_display = new Iridium.Services.Message (); + message_to_display.command = Iridium.Services.MessageCommands.QUIT; message_to_display.message = nickname + _(" has quit"); if (message.message != null && message.message.strip () != "") { message_to_display.message += " (" + message.message + ")"; @@ -951,6 +1000,7 @@ public class Iridium.MainWindow : Hdy.Window { Idle.add (() => { // Display a message in the channel chat view var message_to_display = new Iridium.Services.Message (); + message_to_display.command = Iridium.Services.MessageCommands.QUIT; message_to_display.message = nickname + _(" has quit"); if (message.message != null && message.message.strip () != "") { message_to_display.message += " (" + message.message + ")"; @@ -1084,16 +1134,43 @@ public class Iridium.MainWindow : Hdy.Window { private void on_channel_message_received (string server_name, string channel_name, Iridium.Services.Message message) { Idle.add (() => { main_layout.display_channel_message (server_name, channel_name, message); + var nickname = Iridium.Application.connection_manager.get_connection_details (server_name).nickname; + var is_user_mentioned = Iridium.Models.Text.TextBufferUtils.search_word_in_string (nickname, message.message, () => { + return false; + }); + if (is_user_mentioned) { + on_user_mentioned (server_name, channel_name, _(@"Mentioned in $channel_name"), message.message); + } return false; }); } + private void on_user_mentioned (string server_name, string channel_name, string title, string message) { + // Only send the notification if (1) the application is not in focus, and (2) mentions are not muted + if (((get_window ().get_state () & Gdk.WindowState.FOCUSED) == 0) && !Iridium.Application.settings.get_boolean ("mute-mention-notifications")) { + var notification = new GLib.Notification (title); + notification.set_body (message); + var target = new GLib.Variant.tuple ({new GLib.Variant.string (server_name), new GLib.Variant.string (channel_name)}); + notification.set_default_action_and_target_value ("app.action-show-chat-view", target); + var id = GLib.Uuid.string_random (); + var key = main_layout.get_child_name (server_name, channel_name); + if (!notification_ids.has_key (key)) { + notification_ids.set (key, new Gee.ArrayList ()); + } + notification_ids.get (key).add (id); + debug ("Sending notification: id=%s, key=%s", id, key); + update_app_badge (); + app.send_notification (id, notification); + } + } + private void on_user_joined_channel (string server_name, string channel_name, string nickname) { if (!Iridium.Application.settings.get_boolean ("suppress-join-part-messages")) { Idle.add (() => { // Display a message in the channel chat view var message = new Iridium.Services.Message (); message.message = nickname + _(" has joined"); + message.command = Iridium.Services.MessageCommands.JOIN; main_layout.display_server_message (server_name, channel_name, message); return false; }); @@ -1107,6 +1184,7 @@ public class Iridium.MainWindow : Hdy.Window { // Display a message in the channel chat view var message = new Iridium.Services.Message (); message.message = nickname + _(" has left"); + message.command = Iridium.Services.MessageCommands.PART; main_layout.display_server_message (server_name, channel_name, message); return false; }); @@ -1119,6 +1197,12 @@ public class Iridium.MainWindow : Hdy.Window { main_layout.add_private_message_chat_view (server_name, nickname, self_nickname); main_layout.enable_chat_view (server_name, nickname); main_layout.display_private_message (server_name, nickname, message); + var is_user_mentioned = Iridium.Models.Text.TextBufferUtils.search_word_in_string (self_nickname, message.message, () => { + return false; + }); + if (is_user_mentioned) { + on_user_mentioned (server_name, nickname, _(@"Mentioned by $nickname"), message.message); + } return false; }); } @@ -1128,10 +1212,7 @@ public class Iridium.MainWindow : Hdy.Window { var nicknames = Iridium.Application.connection_manager.get_users (server_name, channel_name); var operators = Iridium.Application.connection_manager.get_operators (server_name, channel_name); Idle.add (() => { - main_layout.update_channel_users (server_name, channel_name, nicknames); - if (main_layout.get_visible_server () == server_name && main_layout.get_visible_channel () == channel_name) { - main_layout.set_channel_users (nicknames, operators); - } + main_layout.update_channel_users (server_name, channel_name, nicknames, operators); return false; }); } @@ -1216,8 +1297,11 @@ public class Iridium.MainWindow : Hdy.Window { private void on_action_message_received (string server_name, string channel_name, string nickname, string self_nickname, string action) { Idle.add (() => { - main_layout.add_private_message_chat_view (server_name, nickname, self_nickname); - main_layout.enable_chat_view (server_name, nickname); + // If the channel name matches the nickname, it's an action message in a private message, so make sure that a chat view exists + if (channel_name == nickname) { + main_layout.add_private_message_chat_view (server_name, nickname, self_nickname); + main_layout.enable_chat_view (server_name, nickname); + } var message = new Iridium.Services.Message (); message.message = "%s %s".printf (nickname, action); @@ -1240,7 +1324,7 @@ public class Iridium.MainWindow : Hdy.Window { // private void on_welcome_view_shown () { - main_layout.update_title (Constants.APP_NAME, null); + main_layout.update_title (null, null); main_layout.set_channel_users_button_visible (false); main_layout.set_header_tooltip (null); } @@ -1259,6 +1343,7 @@ public class Iridium.MainWindow : Hdy.Window { main_layout.set_channel_users_button_visible (true); main_layout.set_channel_users_button_enabled (main_layout.is_view_enabled (server_name, channel_name)); update_channel_users_list (server_name, channel_name); + withdraw_notifications (main_layout.get_child_name (server_name, channel_name)); } private void on_private_message_chat_view_shown (string server_name, string nickname) { @@ -1266,6 +1351,55 @@ public class Iridium.MainWindow : Hdy.Window { main_layout.update_title (nickname, network_name != null ? network_name : server_name); main_layout.set_channel_users_button_visible (false); main_layout.set_header_tooltip (null); + withdraw_notifications (main_layout.get_child_name (server_name, nickname)); + } + + private void withdraw_notifications (string key) { + if (!notification_ids.has_key (key)) { + return; + } + foreach (var id in notification_ids.get (key)) { + debug ("Withdrawing notification: id=%s, key=%s", id, key); + app.withdraw_notification (id); + } + notification_ids.get (key).clear (); + update_app_badge (); + } + + private void update_app_badge () { + // Notifications are 1:1 with the badge count + int64 count = 0; + foreach (var entry in notification_ids.entries) { + count += entry.value.size; + } + // Perform actions in different order depending on showing or hiding to prevent seeing a 0 count + if (count > 0) { + set_app_badge (count); + set_app_badge_visible (true); + } else { + set_app_badge_visible (false); + set_app_badge (count); + } + } + + private void set_app_badge (int64 count) { + Granite.Services.Application.set_badge.begin (count, (obj, res) => { + try { + Granite.Services.Application.set_badge.end (res); + } catch (GLib.Error e) { + warning (e.message); + } + }); + } + + private void set_app_badge_visible (bool visible) { + Granite.Services.Application.set_badge_visible.begin (visible, (obj, res) => { + try { + Granite.Services.Application.set_badge_visible.end (res); + } catch (GLib.Error e) { + warning (e.message); + } + }); } private void on_nickname_button_clicked (string server_name) { @@ -1328,7 +1462,7 @@ public class Iridium.MainWindow : Hdy.Window { return network_name; } - public signal void ui_initialized (Gee.List servers, Gee.List channels, bool is_reconnecting); - public signal void connections_opened (); + public signal void ui_initialized (Gee.List servers, Gee.List channels); + public signal void connections_opened (bool is_reconnecting); } diff --git a/src/Models/AuthenticationMethod.vala b/src/Models/AuthenticationMethod.vala index 936ab74..ac2b9b0 100644 --- a/src/Models/AuthenticationMethod.vala +++ b/src/Models/AuthenticationMethod.vala @@ -23,7 +23,9 @@ public enum Iridium.Models.AuthenticationMethod { NONE, SERVER_PASSWORD, - NICKSERV_MSG; + NICKSERV_MSG, + SASL_PLAIN, + SASL_EXTERNAL; public string get_display_string () { switch (this) { @@ -33,6 +35,10 @@ public enum Iridium.Models.AuthenticationMethod { return _("Server Password"); case NICKSERV_MSG: return _("NickServ"); + case SASL_PLAIN: + return _("SASL (Plain)"); + case SASL_EXTERNAL: + return _("SASL (External)"); default: assert_not_reached (); } @@ -44,6 +50,8 @@ public enum Iridium.Models.AuthenticationMethod { return false; case SERVER_PASSWORD: case NICKSERV_MSG: + case SASL_PLAIN: + case SASL_EXTERNAL: return true; default: assert_not_reached (); diff --git a/src/Models/ColorPalette.vala b/src/Models/ColorPalette.vala index 465d204..c9ed8f3 100644 --- a/src/Models/ColorPalette.vala +++ b/src/Models/ColorPalette.vala @@ -27,7 +27,7 @@ public enum Iridium.Models.ColorPalette { COLOR_BLUEBERRY; public string get_value () { - var prefer_dark_style = Iridium.Application.settings.get_boolean ("prefer-dark-style"); + var prefer_dark_style = Gtk.Settings.get_default ().gtk_application_prefer_dark_theme; // Colors defined by the elementary OS Human Interface Guidelines // When in the "dark style", use shades that are one step lighter than the "middle" value switch (this) { diff --git a/src/Models/CuratedServer.vala b/src/Models/CuratedServer.vala new file mode 100644 index 0000000..8b90672 --- /dev/null +++ b/src/Models/CuratedServer.vala @@ -0,0 +1,618 @@ +/* + * Copyright (c) 2020 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +public class Iridium.Models.CuratedServer : GLib.Object { + + // Entries largely found at https://github.com/hexchat/hexchat/blob/master/src/common/servlist.c + + public enum Servers { + 2600NET, + ACN, + AFTERNET, + AITVARAS, + ANTHROCHAT, + ARCNET, + AUSTNET, + AZZURRANET, + CANTERNET, + CHAT4ALL, + CHATJUNKIES, + CHATPAT, + CHATSPIKE, + DAIRC, + DALNET, + DARKMYST, + DARKSCIENCE, + DARK_TOU_NET, + DIGITALIRC, + DOSERSNET, + EFNET, + ENTERTHEGAME, + ENTROPYNET, + ESPERNET, + EUIRC, + EUROPNET, + FDFNET, + GAMESURGE, + GEEKSHED, + GERMAN_ELITE, + GIMPNET, + GLOBALGAMERS, + HACKINT, + HASHMARK, + ICQ_CHAT, + INTERLINKED, + IRC_NERDS, + IRC4FUN, + IRCHIGHWAY, + IRCNET, + IRCTOO, + KEYBOARD_FAILURE, + LIBERA_CHAT, + LIBERTACASA, + LIBRAIRC, + LINKNET, + MINDFORGE, + MIXXNET, + OCEANIUS, + OFTC, + OTHERNET, + OZORG, + PIK, + PIRC_PL, + PTNET, + QUAKENET, + RIZON, + RUSNET, + SERENITY_IRC, + SIMOSNAP, + SLASHNET, + SNOONET, + SOHBET_NET, + SORCERYNET, + SPOTCHAT, + STATION51, + STORMBIT, + SWIFTIRC, + SYNIRC, + TECHTRONIX, + TILDE_CHAT, + TURLINET, + TRIPSIT, + UNDERNET, + XERTION; + + public CuratedServer get_details () { + switch (this) { + case 2600NET: + return new CuratedServer () { + network_name = "2600net", + server_host = "irc.2600.net", + }; + case ACN: + return new CuratedServer () { + network_name = "ACN", + server_host = "global.acn.gr", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case AFTERNET: + return new CuratedServer () { + network_name = "AfterNET", + server_host = "irc.afternet.org", + auth_method = Iridium.Models.AuthenticationMethod.NICKSERV_MSG + }; + case AITVARAS: + return new CuratedServer () { + network_name = "Aitvaras", + server_host = "irc.data.lt", + }; + case ANTHROCHAT: + return new CuratedServer () { + network_name = "Anthrochat", + server_host = "irc.anthrochat.net", + }; + case ARCNET: + return new CuratedServer () { + network_name = "ARCNet", + server_host = "arcnet-irc.org", + }; + case AUSTNET: + return new CuratedServer () { + network_name = "AustNet", + server_host = "irc.austnet.org", + }; + case AZZURRANET: + return new CuratedServer () { + network_name = "AzzurraNet", + server_host = "irc.azzurra.org", + }; + case CANTERNET: + return new CuratedServer () { + network_name = "Canternet", + server_host = "irc.canternet.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case CHAT4ALL: + return new CuratedServer () { + network_name = "Chat4all", + server_host = "irc.chat4all.org", + }; + case CHATJUNKIES: + return new CuratedServer () { + network_name = "ChatJunkies", + server_host = "irc.chatjunkies.org", + }; + case CHATPAT: + return new CuratedServer () { + network_name = "Chatpat", + server_host = "irc.unibg.net", + }; + case CHATSPIKE: + return new CuratedServer () { + network_name = "ChatSpike", + server_host = "irc.chatspike.net", + }; + case DAIRC: + return new CuratedServer () { + network_name = "DaIRC", + server_host = "irc.dairc.net", + }; + case DALNET: + return new CuratedServer () { + network_name = "DALnet", + server_host = "us.dal.net", + auth_method = Iridium.Models.AuthenticationMethod.NICKSERV_MSG + }; + case DARKMYST: + return new CuratedServer () { + network_name = "DarkMyst", + server_host = "irc.darkmyst.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case DARKSCIENCE: + return new CuratedServer () { + network_name = "darkscience", + server_host = "irc.darkscience.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case DARK_TOU_NET: + return new CuratedServer () { + network_name = "Dark-Tou-Net", + server_host = "irc.d-t-net.de", + }; + case DIGITALIRC: + return new CuratedServer () { + network_name = "DigitalIRC", + server_host = "irc.digitalirc.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case DOSERSNET: + return new CuratedServer () { + network_name = "DosersNET", + server_host = "irc.dosers.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case EFNET: + return new CuratedServer () { + network_name = "EFnet", + server_host = "irc.choopa.net", + }; + case ENTERTHEGAME: + return new CuratedServer () { + network_name = "EnterTheGame", + server_host = "irc.enterthegame.com", + }; + case ENTROPYNET: + return new CuratedServer () { + network_name = "EntropyNet", + server_host = "irc.entropynet.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case ESPERNET: + return new CuratedServer () { + network_name = "EsperNet", + server_host = "irc.esper.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case EUIRC: + return new CuratedServer () { + network_name = "euIRC", + server_host = "irc.euirc.net", + }; + case EUROPNET: + return new CuratedServer () { + network_name = "EuropNet", + server_host = "irc.europnet.org", + }; + case FDFNET: + return new CuratedServer () { + network_name = "FDFNet", + server_host = "irc.fdfnet.net", + }; + case GAMESURGE: + return new CuratedServer () { + network_name = "GameSurge", + server_host = "irc.gamesurge.net", + }; + case GEEKSHED: + return new CuratedServer () { + network_name = "GeekShed", + server_host = "irc.geekshed.net", + }; + case GERMAN_ELITE: + return new CuratedServer () { + network_name = "German-Elite", + server_host = "irc.german-elite.net", + }; + case GIMPNET: + return new CuratedServer () { + network_name = "GIMPNet", + server_host = "irc.gimp.org", + }; + case GLOBALGAMERS: + return new CuratedServer () { + network_name = "GlobalGamers", + server_host = "irc.globalgamers.net", + }; + case HACKINT: + return new CuratedServer () { + network_name = "hackint", + server_host = "irc.hackint.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case HASHMARK: + return new CuratedServer () { + network_name = "Hashmark", + server_host = "irc.hashmark.net", + }; + case ICQ_CHAT: + return new CuratedServer () { + network_name = "ICQ-Chat", + server_host = "irc.icq-chat.com", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case INTERLINKED: + return new CuratedServer () { + network_name = "Interlinked", + server_host = "irc.interlinked.me", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case IRC_NERDS: + return new CuratedServer () { + network_name = "IRC-nERDs", + server_host = "irc.irc-nerds.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case IRC4FUN: + return new CuratedServer () { + network_name = "IRC4Fun", + server_host = "irc.irc4fun.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case IRCHIGHWAY: + return new CuratedServer () { + network_name = "IRCHighWay", + server_host = "irc.irchighway.net", + }; + case IRCNET: + return new CuratedServer () { + network_name = "IRCnet", + server_host = "open.ircnet.net", + }; + case IRCTOO: + return new CuratedServer () { + network_name = "IRCtoo", + server_host = "irc.irctoo.net", + }; + case KEYBOARD_FAILURE: + return new CuratedServer () { + network_name = "Keyboard-Failure", + server_host = "irc.kbfail.net", + }; + case LIBERA_CHAT: + return new CuratedServer () { + network_name = "Libera.Chat", + server_host = "irc.libera.chat", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case LIBERTACASA: + return new CuratedServer () { + network_name = "LibertaCasa", + server_host = "irc.liberta.casa", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case LIBRAIRC: + return new CuratedServer () { + network_name = "LibraIRC", + server_host = "irc.librairc.net", + }; + case LINKNET: + return new CuratedServer () { + network_name = "LinkNet", + server_host = "irc.link-net.org", + port = 7000, + }; + case MINDFORGE: + return new CuratedServer () { + network_name = "MindForge", + server_host = "irc.mindforge.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case MIXXNET: + return new CuratedServer () { + network_name = "MIXXnet", + server_host = "irc.mixxnet.net", + }; + case OCEANIUS: + return new CuratedServer () { + network_name = "Oceanius", + server_host = "irc.oceanius.com", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case OFTC: + return new CuratedServer () { + network_name = "OFTC", + server_host = "irc.oftc.net", + }; + case OTHERNET: + return new CuratedServer () { + network_name = "OtherNet", + server_host = "irc.othernet.org", + }; + case OZORG: + return new CuratedServer () { + network_name = "OzOrg", + server_host = "irc.oz.org", + }; + case PIK: + return new CuratedServer () { + network_name = "PIK", + server_host = "irc.krstarica.com", + }; + case PIRC_PL: + return new CuratedServer () { + network_name = "pirc.pl", + server_host = "irc.pirc.pl", + }; + case PTNET: + return new CuratedServer () { + network_name = "PTnet", + server_host = "irc.ptnet.org", + }; + case QUAKENET: + return new CuratedServer () { + network_name = "QuakeNet", + server_host = "irc.quakenet.org", + port = 6668, + tls = false, + }; + case RIZON: + return new CuratedServer () { + network_name = "Rizon", + server_host = "irc.rizon.net", + }; + case RUSNET: + return new CuratedServer () { + network_name = "RusNet", + server_host = "irc.tomsk.net", + }; + case SERENITY_IRC: + return new CuratedServer () { + network_name = "Serenity-IRC", + server_host = "irc.serenity-irc.net", + }; + case SIMOSNAP: + return new CuratedServer () { + network_name = "SimosNap", + server_host = "irc.simosnap.com", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case SLASHNET: + return new CuratedServer () { + network_name = "SlashNET", + server_host = "irc.slashnet.org", + }; + case SNOONET: + return new CuratedServer () { + network_name = "Snoonet", + server_host = "irc.snoonet.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case SOHBET_NET: + return new CuratedServer () { + network_name = "Sohbet.net", + server_host = "irc.sohbet.net", + }; + case SORCERYNET: + return new CuratedServer () { + network_name = "SorceryNet", + server_host = "irc.sorcery.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case SPOTCHAT: + return new CuratedServer () { + network_name = "SpotChat", + server_host = "irc.spotchat.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case STATION51: + return new CuratedServer () { + network_name = "Station51", + server_host = "irc.station51.net", + }; + case STORMBIT: + return new CuratedServer () { + network_name = "StormBit", + server_host = "irc.stormbit.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case SWIFTIRC: + return new CuratedServer () { + network_name = "SwiftIRC", + server_host = "irc.swiftirc.net", + }; + case SYNIRC: + return new CuratedServer () { + network_name = "synIRC", + server_host = "irc.synirc.net", + }; + case TECHTRONIX: + return new CuratedServer () { + network_name = "Techtronix", + server_host = "irc.techtronix.net", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case TILDE_CHAT: + return new CuratedServer () { + network_name = "tilde.chat", + server_host = "irc.tilde.chat", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case TURLINET: + return new CuratedServer () { + network_name = "TURLINet", + server_host = "irc.servx.org", + }; + case TRIPSIT: + return new CuratedServer () { + network_name = "TripSit", + server_host = "irc.tripsit.me", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + case UNDERNET: + return new CuratedServer () { + network_name = "UnderNet", + server_host = "us.undernet.org", + }; + case XERTION: + return new CuratedServer () { + network_name = "Xertion", + server_host = "irc.xertion.org", + auth_method = Iridium.Models.AuthenticationMethod.SASL_PLAIN + }; + default: + assert_not_reached (); + } + } + + public static Servers[] all () { + return { + 2600NET, + ACN, + AFTERNET, + AITVARAS, + ANTHROCHAT, + ARCNET, + AUSTNET, + AZZURRANET, + CANTERNET, + CHAT4ALL, + CHATJUNKIES, + CHATPAT, + CHATSPIKE, + DAIRC, + DALNET, + DARKMYST, + DARKSCIENCE, + DARK_TOU_NET, + DIGITALIRC, + DOSERSNET, + EFNET, + ENTERTHEGAME, + ENTROPYNET, + ESPERNET, + EUIRC, + EUROPNET, + FDFNET, + GAMESURGE, + GEEKSHED, + GERMAN_ELITE, + GIMPNET, + GLOBALGAMERS, + HACKINT, + HASHMARK, + ICQ_CHAT, + INTERLINKED, + IRC_NERDS, + IRC4FUN, + IRCHIGHWAY, + IRCNET, + IRCTOO, + KEYBOARD_FAILURE, + LIBERA_CHAT, + LIBERTACASA, + LIBRAIRC, + LINKNET, + MINDFORGE, + MIXXNET, + OCEANIUS, + OFTC, + OTHERNET, + OZORG, + PIK, + PIRC_PL, + PTNET, + QUAKENET, + RIZON, + RUSNET, + SERENITY_IRC, + SIMOSNAP, + SLASHNET, + SNOONET, + SOHBET_NET, + SORCERYNET, + SPOTCHAT, + STATION51, + STORMBIT, + SWIFTIRC, + SYNIRC, + TECHTRONIX, + TILDE_CHAT, + TURLINET, + TRIPSIT, + UNDERNET, + XERTION + }; + } + + public static Servers? get_for_network_name (string network_name) { + foreach (var server in all ()) { + if (server.get_details ().network_name == network_name) { + return server; + } + } + return null; + } + } + + public string network_name { get; set; } + public string server_host { get; set; } + public uint16 port { get; set; } + public bool tls { get; set; } + public Iridium.Models.AuthenticationMethod auth_method { get; set; } + + private CuratedServer () { + port = Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT; + tls = true; + auth_method = Iridium.Models.AuthenticationMethod.NONE; + } + +} diff --git a/src/Models/MessageCommands.vala b/src/Models/MessageCommands.vala index 0825017..a5894c9 100644 --- a/src/Models/MessageCommands.vala +++ b/src/Models/MessageCommands.vala @@ -21,6 +21,17 @@ public class Iridium.Services.MessageCommands : GLib.Object { + public class CAPSubcommands { + public const string LS = "LS"; + public const string LIST = "LIST"; + public const string REQ = "REQ"; + public const string ACK = "ACK"; + public const string NAK = "NAK"; + public const string END = "END"; + public const string NEW = "NEW"; + public const string DEL = "DEL"; + } + // Connection messages public const string CAP = "CAP"; public const string AUTHENTICATE = "AUTHENTICATE"; diff --git a/src/Models/Text/RichText.vala b/src/Models/Text/RichText.vala index 5ad7f65..d57ccc5 100644 --- a/src/Models/Text/RichText.vala +++ b/src/Models/Text/RichText.vala @@ -114,24 +114,20 @@ public abstract class Iridium.Models.Text.RichText : GLib.Object { } private void apply_nickname_tags (Gtk.TextBuffer buffer) { - Gtk.TextIter search_start; - Gtk.TextIter search_end; - Gtk.TextIter match_start; - Gtk.TextIter match_end; foreach (var nickname in nicknames) { + Gtk.TextIter search_start; + Gtk.TextIter search_end; // Set start_iter and end_iter for the portion of the buffer with the new message buffer.get_end_iter (out search_start); search_start.backward_chars (message.message.length + 1); // +1 for newline char buffer.get_end_iter (out search_end); search_end.backward_chars (1); - while (search_start.forward_search (nickname, Gtk.TextSearchFlags.CASE_INSENSITIVE, out match_start, out match_end, search_end)) { - if (match_start.starts_word () && match_end.ends_word ()) { - buffer.apply_tag_by_name ("inline-nickname", match_start, match_end); - buffer.apply_tag_by_name ("selectable", match_start, match_end); - } - search_start = match_end; - } + Iridium.Models.Text.TextBufferUtils.search_word_in_buffer (nickname, buffer, search_start, search_end, (match_start, match_end) => { + buffer.apply_tag_by_name ("inline-nickname", match_start, match_end); + buffer.apply_tag_by_name ("selectable", match_start, match_end); + return true; + }); } // TODO: Check for our nickname and style the whole message diff --git a/src/Models/Text/TextBufferUtils.vala b/src/Models/Text/TextBufferUtils.vala new file mode 100644 index 0000000..e2d9069 --- /dev/null +++ b/src/Models/Text/TextBufferUtils.vala @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +public class Iridium.Models.Text.TextBufferUtils { + + public static bool search_word_in_string (string needle, string haystack, WordMatchDelegate handler) { + Gtk.TextBuffer text_buffer = new Gtk.TextBuffer (null) { + text = haystack + }; + Gtk.TextIter search_start; + Gtk.TextIter search_end; + text_buffer.get_end_iter (out search_start); + search_start.backward_chars (haystack.length); + text_buffer.get_end_iter (out search_end); + + return search_word_in_buffer (needle, text_buffer, search_start, search_end, handler); + } + + public static bool search_word_in_buffer (string needle, Gtk.TextBuffer text_buffer, Gtk.TextIter start, Gtk.TextIter end, WordMatchDelegate handler) { + bool has_match = false; + Gtk.TextIter search_start = start; + Gtk.TextIter search_end = end; + Gtk.TextIter match_start; + Gtk.TextIter match_end; + while (search_start.forward_search (needle, Gtk.TextSearchFlags.CASE_INSENSITIVE, out match_start, out match_end, search_end)) { + if (match_start.starts_word () && match_end.ends_word ()) { + has_match = true; + if (!handler (match_start, match_end)) { + return has_match; + } + } + search_start = match_end; + } + return has_match; + } + + // Return whether or not to continue searching after finding a match + public delegate bool WordMatchDelegate (Gtk.TextIter match_start, Gtk.TextIter match_end); + +} diff --git a/src/Services/ActionManager.vala b/src/Services/ActionManager.vala index bd6db32..57a362a 100644 --- a/src/Services/ActionManager.vala +++ b/src/Services/ActionManager.vala @@ -35,6 +35,9 @@ public class Iridium.Services.ActionManager : GLib.Object { public const string ACTION_PREFERENCES = "action_preferences"; public const string ACTION_TOGGLE_SIDEBAR = "action_toggle_sidebar"; public const string ACTION_RESET_MARKER = "action_reset_marker"; + public const string ACTION_BROWSE_SERVERS = "action_browse_servers"; + + public const string ACTION_SHOW_CHAT_VIEW = "action-show-chat-view"; private const GLib.ActionEntry[] ACTION_ENTRIES = { { ACTION_NEW_WINDOW, action_new_window }, @@ -49,7 +52,12 @@ public class Iridium.Services.ActionManager : GLib.Object { { ACTION_FAVORITE_CHANNEL, action_favorite_channel }, { ACTION_PREFERENCES, action_preferences }, { ACTION_TOGGLE_SIDEBAR, action_toggle_sidebar }, - { ACTION_RESET_MARKER, action_reset_marker } + { ACTION_RESET_MARKER, action_reset_marker }, + { ACTION_BROWSE_SERVERS, action_browse_servers } + }; + + private const GLib.ActionEntry[] APP_ACTION_ENTRIES = { + { ACTION_SHOW_CHAT_VIEW, action_show_chat_view, "(ss)" } }; private static Gee.MultiMap accelerators; @@ -97,6 +105,8 @@ public class Iridium.Services.ActionManager : GLib.Object { accelerators_array += null; application.set_accels_for_action (ACTION_PREFIX + action, accelerators_array); } + + application.add_action_entries (APP_ACTION_ENTRIES, this); } public static void action_from_group (string action_name, ActionGroup action_group, Variant? parameter = null) { @@ -165,4 +175,21 @@ public class Iridium.Services.ActionManager : GLib.Object { window.reset_marker_line (); } + private void action_browse_servers () { + window.show_browse_servers_dialog (); + } + + private void action_show_chat_view (SimpleAction action, Variant? parameter) { + if (parameter == null) { + return; + } + if (parameter.n_children () != 2) { + warning ("Expected 2 variant children"); + return; + } + string server_name = parameter.get_child_value (0).get_string (); + string channel_name = parameter.get_child_value (1).get_string (); + window.show_chat_view (server_name, channel_name); + } + } diff --git a/src/Services/SQLClient.vala b/src/Services/SQLClient.vala index 4d52206..f29a10c 100644 --- a/src/Services/SQLClient.vala +++ b/src/Services/SQLClient.vala @@ -21,6 +21,8 @@ public class Iridium.Services.SQLClient : GLib.Object { + // The 01 suffix was originall intended to be a versioning scheme for the database schema, however + // we will instead use the user_version pragma to check for necessary updates. private const string DATABASE_FILE = "iridium01.db"; private Sqlite.Database database; @@ -92,8 +94,63 @@ public class Iridium.Services.SQLClient : GLib.Object { ); """; database.exec (sql); + + do_upgrades (); } + private void do_upgrades () { + int? user_version = get_user_version (); + if (user_version == null) { + warning ("Null user_version, skipping upgrades"); + return; + } + if (user_version == 0) { + debug ("SQLite user_version: %d, no upgrades to perform", user_version); + } + } + + private int? get_user_version () { + var sql = "PRAGMA user_version"; + Sqlite.Statement statement; + if (database.prepare_v2 (sql, sql.length, out statement) != Sqlite.OK) { + log_database_error (database.errcode (), database.errmsg ()); + return null; + } + + if (statement.step () != Sqlite.ROW) { + return null; + } + var num_columns = statement.column_count (); + int? user_version = null; + for (int i = 0; i < num_columns; i++) { + switch (statement.column_name (i)) { + case "user_version": + user_version = statement.column_int (i); + break; + default: + break; + } + } + statement.reset (); + return user_version; + } + + // private void set_user_version (int user_version) { + // var sql = @"PRAGMA user_version = $user_version"; + // Sqlite.Statement statement; + // if (database.prepare_v2 (sql, sql.length, out statement) != Sqlite.OK) { + // log_database_error (database.errcode (), database.errmsg ()); + // return; + // } + // string err_msg; + // int ec = database.exec (statement.expanded_sql (), null, out err_msg); + // if (ec != Sqlite.OK) { + // log_database_error (ec, err_msg); + // debug ("SQL statement: %s", statement.expanded_sql ()); + // } + // statement.reset (); + // } + public void insert_server (Iridium.Services.Server server) { var sql = """ INSERT INTO servers (hostname, port, nickname, username, realname, auth_method, tls, enabled, network_name) diff --git a/src/Services/ServerConnection.vala b/src/Services/ServerConnection.vala index 3a4ce44..f14c300 100644 --- a/src/Services/ServerConnection.vala +++ b/src/Services/ServerConnection.vala @@ -47,6 +47,9 @@ public class Iridium.Services.ServerConnection : GLib.Object { private string? connection_error_message = null; private string? connection_error_details = null; + private bool has_sasl_failed = false; + private bool? should_accept_certificate = null; + public ServerConnection (Iridium.Services.ServerConnectionDetails connection_details) { Object ( connection_details: connection_details @@ -69,7 +72,7 @@ public class Iridium.Services.ServerConnection : GLib.Object { input_stream = new DataInputStream (connection.input_stream); output_stream = new DataOutputStream (connection.output_stream); - register (connection_details); + register (); string line = ""; do { @@ -142,6 +145,30 @@ public class Iridium.Services.ServerConnection : GLib.Object { break; case SocketClientEvent.TLS_HANDSHAKING: debug ("[SocketClientEvent] %s TLS_HANDSHAKING", connectable.to_string ()); + if (connection_details.auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL) { + try { + debug ("Providing certificate to connection for SASL external authentication"); + string? uri = get_auth_token (); + if (uri == null) { + warning ("Null certificate file URI"); + // connection_error_details already set for null token + break; + } + string? file_path = GLib.File.new_for_uri (uri).get_path (); + if (file_path == null) { + warning ("Certificate file no longer present"); + connection_error_details = _("Certificate file not found"); + break; + } + var certificate = new GLib.TlsCertificate.from_file (file_path); + var fingerprint = GLib.Checksum.compute_for_data (GLib.ChecksumType.SHA512, certificate.certificate.data); + debug (@"Certificate fingerprint: $fingerprint"); + ((TlsClientConnection) connection).set_certificate (certificate); + } catch (GLib.Error e) { + warning ("Error setting certificate: %s", e.message); + connection_error_details = e.message; + } + } ((TlsClientConnection) connection).accept_certificate.connect ((peer_cert, errors) => { return on_invalid_certificate (peer_cert, errors, connectable); }); @@ -171,7 +198,7 @@ public class Iridium.Services.ServerConnection : GLib.Object { if (flag in errors) { encountered_errors.add (flag); error_string += @"$(flag), "; - formatted_error_string += " • " + Iridium.Models.CertificateErrorMapping.get_description (flag) + "\n"; + formatted_error_string += " \u2022 " + Iridium.Models.CertificateErrorMapping.get_description (flag) + "\n"; } } warning (@"TLS certificate errors: $(error_string)"); @@ -193,8 +220,14 @@ public class Iridium.Services.ServerConnection : GLib.Object { return identity.is_accepted; } + // Prompt the user to accept/reject the certificate + unacceptable_certificate (peer_cert, encountered_errors, connectable); + while (should_keeping_waiting_on_user_certificate_verification ()) { + // Do nothing + } + // Identity is not known, so prompt the user - if (unacceptable_certificate (peer_cert, encountered_errors, connectable)) { + if (should_accept_certificate) { return true; } else { connection_error_details = _("Certificate was rejected by the user."); @@ -213,79 +246,94 @@ public class Iridium.Services.ServerConnection : GLib.Object { } - private void register (Iridium.Services.ServerConnectionDetails connection_details) { + private bool should_keeping_waiting_on_user_certificate_verification () { + lock (should_accept_certificate) { + return should_accept_certificate == null; + } + } + + public void accept_certificate () { + lock (should_accept_certificate) { + should_accept_certificate = true; + } + } + + public void reject_certificate () { + lock (should_accept_certificate) { + should_accept_certificate = false; + } + } + + private void register () { var nickname = connection_details.nickname; var username = connection_details.nickname; // Use nickname for both var realname = connection_details.realname; var mode = "+i"; // Handle the various auth methods + debug ("AuthenticationMethod is %s", connection_details.auth_method.to_string ()); switch (connection_details.auth_method) { case Iridium.Models.AuthenticationMethod.NONE: - debug ("AuthenticationMethod is NONE"); send_output (@"NICK $nickname"); send_output (@"USER $username 0 * :$realname"); send_output (@"MODE $nickname $mode"); break; case Iridium.Models.AuthenticationMethod.SERVER_PASSWORD: - debug ("AuthenticationMethod is SERVER_PASSWORD"); - string password = null; - // Check if we're passed an auth token - if (connection_details.auth_token != null) { - debug ("Server password passed with request to open connection"); - password = connection_details.auth_token; - } else { - debug ("Retrieving server password from secret manager"); - var server = connection_details.server; - var port = connection_details.port; - password = Iridium.Application.secret_manager.retrieve_secret (server, port, nickname); - if (password == null) { - // TODO: Handle this better! - warning ("No password found for server: " + server); - } - } + string? password = get_auth_token (); send_output (@"PASS $password"); send_output (@"NICK $nickname"); send_output (@"USER $username 0 * :$realname"); send_output (@"MODE $nickname $mode"); - break; case Iridium.Models.AuthenticationMethod.NICKSERV_MSG: - debug ("AuthenticationMethod is NICKSERV_MSG"); - string password = null; - // Check if we're passed an auth token - if (connection_details.auth_token != null) { - debug ("NickServ password passed with request to open connection"); - password = connection_details.auth_token; - } else { - debug ("Retrieving NickServ password from secret manager"); - var server = connection_details.server; - var port = connection_details.port; - password = Iridium.Application.secret_manager.retrieve_secret (server, port, nickname); - if (password == null) { - // TODO: Handle this better! - warning ("No password found for server: " + server + ", port: " + port.to_string () + ", nickname: " + nickname + "\n"); - } - } + string? password = get_auth_token (); send_output (@"NICK $nickname"); send_output (@"USER $username 0 * :$realname"); send_output (@"MODE $nickname $mode"); send_output (@"NickServ identify $password"); break; + case Iridium.Models.AuthenticationMethod.SASL_PLAIN: + case Iridium.Models.AuthenticationMethod.SASL_EXTERNAL: + send_output ("CAP REQ :sasl"); + send_output (@"NICK $nickname"); + send_output (@"USER $username 0 * :$realname"); + break; default: assert_not_reached (); } } + private string? get_auth_token () { + string? password = null; + if (connection_details.auth_token != null) { + debug ("Password passed with request to open connection"); + password = connection_details.auth_token; + } else if (!connection_details.auth_method.stores_secret ()) { + debug ("Authentication type %s does not store a secret", connection_details.auth_method.get_display_string ()); + return null; + } else { + debug ("Retrieving password from secret manager"); + var server = connection_details.server; + var port = connection_details.port; + var nickname = connection_details.nickname; + password = Iridium.Application.secret_manager.retrieve_secret (server, port, nickname); + if (password == null) { + // TODO: Handle this better! + warning ("No auth token found for server: " + server + ", port: " + port.to_string () + ", nickname: " + nickname + "\n"); + connection_error_details = _("No stored secret found for this server."); + return null; + } + } + return password; + } + private void handle_line (string? line) { if (line == null) { close (); return; } var message = new Iridium.Services.Message (line); - if (Iridium.Application.is_dev_mode ()) { - print (@"$line\n"); - } + debug (line); switch (message.command) { case "PING": send_output ("PONG " + message.message); @@ -296,6 +344,79 @@ public class Iridium.Services.ServerConnection : GLib.Object { } server_error_received (message); break; + case Iridium.Services.MessageCommands.CAP: + string subcommand = message.params[1]; + switch (subcommand) { + case Iridium.Services.MessageCommands.CAPSubcommands.LS: + case Iridium.Services.MessageCommands.CAPSubcommands.LIST: + case Iridium.Services.MessageCommands.CAPSubcommands.NEW: + case Iridium.Services.MessageCommands.CAPSubcommands.DEL: + warning (@"Unhandled CAP subcommand response from the server: $subcommand"); + break; + case Iridium.Services.MessageCommands.CAPSubcommands.ACK: + string capability = message.message; + debug (@"Capability accepted by the server: $capability"); + if (!is_registered && (capability == "sasl")) { + if (connection_details.auth_method == Iridium.Models.AuthenticationMethod.SASL_PLAIN) { + string mechanism = "PLAIN"; + send_output (@"AUTHENTICATE $mechanism"); + } else if (connection_details.auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL) { + string mechanism = "EXTERNAL"; + send_output (@"AUTHENTICATE $mechanism"); + } else { + warning ("Unsupported auth method for response to SASL CAP ACK"); + } + } + break; + case Iridium.Services.MessageCommands.CAPSubcommands.NAK: + if (!is_registered) { + string capability = message.message; + open_failed (_(@"Capability was rejected by the server: $capability")); + } + server_error_received (message); + break; + default: + warning ("Unexpected CAP subcommand from server: " + subcommand); + break; + } + break; + case Iridium.Services.MessageCommands.AUTHENTICATE: + if (is_registered) { + warning ("Received AUTHENTICATE command while in a registered state"); + break; + } + // Some servers send the + as a param rather than the message + if (message.message == "+" || (message.params.length > 0 && message.params[0] == "+")) { + if (connection_details.auth_method == Iridium.Models.AuthenticationMethod.SASL_PLAIN) { + string nickname = connection_details.nickname; + string? auth_token = get_auth_token (); + if (auth_token == null) { + // Abort the SASL exchange + send_output ("AUTHENTICATE *"); + send_output ("CAP END"); + break; + } + // Create an unencoded array with separators, because we can't use \0 with the string without breaking things + var sep = 0x0; + uint8[] unencoded = @"$nickname$sep$nickname$sep$auth_token".data; + // Now fill in the \0 separators in the data array + unencoded[nickname.length] = '\0'; + unencoded[2 * nickname.length + 1] = '\0'; + // Base64 encode the data + string encoded = GLib.Base64.encode (unencoded); + // Prevent free() errors + unencoded = null; + send_output (@"AUTHENTICATE $encoded"); + } else if (connection_details.auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL) { + send_output ("AUTHENTICATE +"); + } else { + warning ("Unhandled AUTHENTICATE from server for auth method %s", connection_details.auth_method.to_string ()); + } + } + break; + case Iridium.Services.NumericCodes.RPL_SASLSUCCESS: + send_output ("CAP END"); + break; case Iridium.Services.MessageCommands.NOTICE: case Iridium.Services.NumericCodes.RPL_CREATED: case Iridium.Services.NumericCodes.RPL_MOTD: @@ -305,6 +426,7 @@ public class Iridium.Services.ServerConnection : GLib.Object { case Iridium.Services.NumericCodes.RPL_SERVLIST: case Iridium.Services.NumericCodes.RPL_ENDOFSTATS: case Iridium.Services.NumericCodes.RPL_STATSLINKINFO: + case Iridium.Services.NumericCodes.RPL_LOGGEDIN: server_message_received (message); break; case Iridium.Services.NumericCodes.RPL_WELCOME: @@ -392,7 +514,6 @@ public class Iridium.Services.ServerConnection : GLib.Object { // If the first param is our nickname, it's a PM. Otherwise, it's // a general message on a channel if (message.params[0] == connection_details.nickname) { - // print ("received message from %s to %s\n", message.nickname, connection_details.nickname); private_message_received (message.nickname, connection_details.nickname, message); } else { channel_message_received (message.params[0], message); @@ -438,18 +559,26 @@ public class Iridium.Services.ServerConnection : GLib.Object { case Iridium.Services.MessageCommands.MODE: // If the first param is our nickname, this is being set on the server rather than for a channel if (message.params[0] == connection_details.nickname) { - char modifier = message.message[0]; - for (int i = 1; i < message.message.length; i++) { - var display_message = new Iridium.Services.Message (); - display_message.message = "%s sets mode %c%c on %s".printf (message.prefix, modifier, message.message[i], message.params[0]); - server_message_received (display_message); + // The conventional MODE message sends the modifier and mode chars as params, but in some + // cases (such as Libera.chat), they are sent in the message body + if (message.message != null) { + char modifier = message.message[0]; + for (int i = 1; i < message.message.length; i++) { + var display_message = new Iridium.Services.Message (); + display_message.message = "%s sets mode %c%c on %s".printf (message.prefix, modifier, message.message[i], message.params[0]); + server_message_received (display_message); + } + } else { + char modifier = message.params[1][0]; + for (int i = 1; i < message.params[1].length; i++) { + var display_message = new Iridium.Services.Message (); + display_message.message = "%s sets mode %c%c on %s".printf (message.prefix.split ("!")[0], modifier, message.params[1][i], message.params[0]); + server_message_received (display_message); + } } break; } - // params[0] = channel - // params[1] = mode chars - // params[2] = params string channel = message.params[0]; string mode_chars = message.params[1]; @@ -506,6 +635,33 @@ public class Iridium.Services.ServerConnection : GLib.Object { break; // Errors + case Iridium.Services.NumericCodes.ERR_SASLABORTED: + // This is received when we (the client) abort the SASL authentication attempt, typically + // after receiving an ERR_SASLFAIL message. At this point, if we're attempting to establish + // a new server connection, we should terminate the connection. + server_error_received (message); + if (!is_registered) { + // If we didn't receive an ERR_SASLFAIL message, ensure that we still send the open_failed signal + if (!has_sasl_failed) { + open_failed (message.message, connection_error_details); + } + do_close (); + } + break; + case Iridium.Services.NumericCodes.ERR_SASLFAIL: + // The pair of 904 and 906 (ERR_SASLFAIL and ERR_SASLABORTED respectively) is delicate. When + // an attempt to use SASL auth fails, first we (the client) receive ERR_SASLFAIL. Because the + // authentication attempt has failed, this means our new server connection attempt has failed, + // so we send the open_failed signal with the appropriate message. At this point, we should + // terminate the SASL authentication attempt by sending "AUTHENTICATE *" and "CAP END". + has_sasl_failed = true; + if (!is_registered) { + open_failed (message.message, connection_error_details); + } + send_output ("AUTHENTICATE *"); + send_output ("CAP END"); + server_error_received (message); + break; case Iridium.Services.NumericCodes.ERR_ERRONEOUSNICKNAME: // If this error occurs during the initial connection, the current // nickname will be an asterisk (*) @@ -567,31 +723,36 @@ public class Iridium.Services.ServerConnection : GLib.Object { public void close () { debug ("Closing connection for server: " + connection_details.server); + // Do this first to ensure we don't have a race condition of new messages coming in while trying to close lock (should_exit) { should_exit = true; } send_output (Iridium.Services.MessageCommands.QUIT + " :Iridium IRC Client"); channel_users.clear (); + leave_channels (); do_close (); + connection_closed (); + } + + private void leave_channels () { + foreach (var channel in joined_channels) { + channel_left (channel); + } + joined_channels.clear (); } private void do_close () { + // Stop reading lock (should_exit) { should_exit = true; } - + // Close the socket connection try { connection.close (); } catch (GLib.IOError e) { warning ("Error while closing connection: %s", e.message); } cancellable.cancel (); - - foreach (var channel in joined_channels) { - channel_left (channel); - } - joined_channels.clear (); - connection_closed (); } public void send_user_message (string text) { @@ -694,11 +855,22 @@ public class Iridium.Services.ServerConnection : GLib.Object { } // Add each new nickname to the buffer foreach (string nickname in nicknames) { - nickname_buffer.get (channel_name).add (nickname); + var trimmed_nickname = strip_nickname_prefix (nickname); + nickname_buffer.get (channel_name).add (trimmed_nickname); if (nickname.has_prefix ("@")) { - operators_buffer.get (channel_name).add (nickname); + operators_buffer.get (channel_name).add (trimmed_nickname); + } + } + } + + private string strip_nickname_prefix (string nickname) { + var prefixes = new string[] { "@", "&", "~", "%", "+" }; + foreach (string prefix in prefixes) { + if (nickname.has_prefix (prefix)) { + return nickname.substring (1, -1); } } + return nickname; } private void end_of_nicknames (string channel_name) { @@ -786,7 +958,7 @@ public class Iridium.Services.ServerConnection : GLib.Object { send_output (Iridium.Services.MessageCommands.LIST); } - public signal bool unacceptable_certificate (TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable); + public signal void unacceptable_certificate (TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable); public signal void open_successful (string nickname, Iridium.Services.Message message); public signal void open_failed (string error_message, string? error_details = null); public signal void connection_closed (); diff --git a/src/Services/ServerConnectionManager.vala b/src/Services/ServerConnectionManager.vala index 8c25e62..1dac1c6 100644 --- a/src/Services/ServerConnectionManager.vala +++ b/src/Services/ServerConnectionManager.vala @@ -78,6 +78,9 @@ public class Iridium.Services.ServerConnectionManager : GLib.Object { server_connection.open_failed.connect (() => { open_connections.unset (server); }); + server_connection.connection_closed.connect (() => { + open_connections.unset (server); + }); server_connection.open (); return server_connection; @@ -101,7 +104,6 @@ public class Iridium.Services.ServerConnectionManager : GLib.Object { return; } connection.close (); - open_connections.unset (server); } public void fail_server_connection (string server, string error_message, string? error_details) { @@ -110,7 +112,6 @@ public class Iridium.Services.ServerConnectionManager : GLib.Object { return; } connection.close (); - open_connections.unset (server); connection.open_failed (error_message, error_details); } @@ -177,10 +178,12 @@ public class Iridium.Services.ServerConnectionManager : GLib.Object { public void close_all_connections () { debug ("Closing all connections…"); - foreach (var connection in open_connections.entries) { + // Create an intermediate map so we're not modifying the same map we're iterating through + var intermediate = new Gee.HashMap (); + intermediate.set_all (open_connections); + foreach (var connection in intermediate.entries) { connection.value.close (); } - open_connections.clear (); } public void send_user_message (string server_name, string message) { @@ -248,12 +251,28 @@ public class Iridium.Services.ServerConnectionManager : GLib.Object { connection.request_channel_list (); } + public void accept_certificate (string server_name) { + var connection = open_connections.get (server_name); + if (connection == null) { + return; + } + connection.accept_certificate (); + } + + public void reject_certificate (string server_name) { + var connection = open_connections.get (server_name); + if (connection == null) { + return; + } + connection.reject_certificate (); + } + // // ServerConnection Callbacks // - private bool on_unacceptable_certificate (TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable) { - return unacceptable_certificate (peer_cert, errors, connectable); + private void on_unacceptable_certificate (Iridium.Services.ServerConnection source, TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable) { + unacceptable_certificate (source.connection_details.server, peer_cert, errors, connectable); } private void on_server_connection_successful (Iridium.Services.ServerConnection source, string nickname, Iridium.Services.Message message) { @@ -364,7 +383,7 @@ public class Iridium.Services.ServerConnectionManager : GLib.Object { // Signals // - public signal bool unacceptable_certificate (TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable); + public signal void unacceptable_certificate (string server_name, TlsCertificate peer_cert, Gee.List errors, SocketConnectable connectable); public signal void server_connection_successful (string server_name, string nickname, Iridium.Services.Message message); public signal void server_connection_failed (string server_name, string error_message, string? error_details); public signal void server_connection_closed (string server_name); diff --git a/src/Views/ChatView.vala b/src/Views/ChatView.vala index 8762ee3..4c07772 100644 --- a/src/Views/ChatView.vala +++ b/src/Views/ChatView.vala @@ -45,6 +45,7 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { private bool is_enabled = true; private bool has_unread_messages = false; + private double prev_upper_adj = 0; private DateTime? last_message_time = null; protected ChatView (Iridium.MainWindow window, string nickname) { @@ -164,7 +165,9 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { } else { clear_selectable_underlining (); } - + }); + text_view.size_allocate.connect (() => { + attempt_autoscroll (); }); // Clear the underlining when the mouse leaves the event box around the text view @@ -187,7 +190,7 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { return false; }); - Iridium.Application.settings.changed["prefer-dark-style"].connect (update_tag_colors); + Granite.Settings.get_default ().notify["prefers-color-scheme"].connect (update_tag_colors); show_all (); } @@ -379,21 +382,14 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { } last_message_time = new DateTime.now_utc (); do_display_server_msg (message); - // Always auto-scroll server messages (The large number of messages received upon connecting - // break the auto-scrolling's ability to keep up) - do_autoscroll (); } public void display_server_error_msg (Iridium.Services.Message message) { - bool should_autoscroll = should_autoscroll (); if (should_display_datetime ()) { do_display_datetime (); } last_message_time = new DateTime.now_utc (); do_display_server_error_msg (message); - if (should_autoscroll) { - do_autoscroll (); - } } public void display_private_msg (Iridium.Services.Message message) { @@ -401,16 +397,11 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { update_last_read_message_mark (); has_unread_messages = true; } - - bool should_autoscroll = should_autoscroll (); if (should_display_datetime ()) { do_display_datetime (); } last_message_time = new DateTime.now_utc (); do_display_private_msg (message); - if (should_autoscroll) { - do_autoscroll (); - } } public void focus_gained () { @@ -441,6 +432,19 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { return is_view_in_focus && is_window_in_focus; } + private void attempt_autoscroll () { + var adj = scrolled_window.get_vadjustment (); + var units_from_end = prev_upper_adj - adj.page_size - adj.value; + var view_size_difference = adj.upper - prev_upper_adj; + if (view_size_difference < 0) { + view_size_difference = 0; + } + if (prev_upper_adj <= adj.page_size || units_from_end <= 50) { + do_autoscroll (); + } + prev_upper_adj = adj.upper; + } + private void do_autoscroll () { var buffer_end_mark = text_view.get_buffer ().get_mark ("buffer-end"); if (buffer_end_mark != null) { @@ -448,17 +452,8 @@ public abstract class Iridium.Views.ChatView : Gtk.Grid { } } - private bool should_autoscroll () { - // If we've never opened this view the adjustment won't return the values you'd expect, - // so instead simply check whether there is a last read message and if the view has focus - if (!is_view_in_focus && text_view.get_buffer ().get_mark ("last-read-message") == null) { - return true; - } - - return at_bottom_of_screen (); - } - private bool at_bottom_of_screen () { + // Not ideal, but it works... var adjustment = scrolled_window.get_vadjustment (); double page_size = adjustment.get_page_size (); double max_view_size = adjustment.get_upper (); diff --git a/src/Views/Welcome.vala b/src/Views/Welcome.vala index d01d657..75e0cd6 100644 --- a/src/Views/Welcome.vala +++ b/src/Views/Welcome.vala @@ -21,6 +21,8 @@ public class Iridium.Views.Welcome : Granite.Widgets.Welcome { + private static Gtk.CssProvider provider; + public unowned Iridium.MainWindow window { get; construct; } public Welcome (Iridium.MainWindow window) { @@ -31,25 +33,35 @@ public class Iridium.Views.Welcome : Granite.Widgets.Welcome { ); } + static construct { + provider = new Gtk.CssProvider (); + provider.load_from_resource ("com/github/avojak/iridium/WelcomeView.css"); + } + construct { + unowned Gtk.StyleContext style_context = get_style_context (); + style_context.add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + valign = Gtk.Align.FILL; halign = Gtk.Align.FILL; vexpand = true; // TODO: Instead, simply have an option to connect to a new server. We // can maybe have a separate star icon for favoriting? - // TODO: Revisit the wording based on human interface guidelines append (Constants.APP_ID + ".network-server-new", _("Add a New Server"), _("Connect to a server and save it in the server list")); + append ("folder-remote", _("Browse Servers"), _("Browse a curated list of popular IRC servers")); // append ("document-open-recent", _("Recently Connected"), _("Connect to a recently connected server")); - // TODO: Have an option for connecting to a server from a list of - // popular/common servers? - activated.connect (index => { switch (index) { case 0: Iridium.Services.ActionManager.action_from_group (Iridium.Services.ActionManager.ACTION_NEW_SERVER_CONNECTION, window.get_action_group ("win")); - break; + break; + case 1: + Iridium.Services.ActionManager.action_from_group (Iridium.Services.ActionManager.ACTION_BROWSE_SERVERS, window.get_action_group ("win")); + break; + default: + assert_not_reached (); } }); } diff --git a/src/Widgets/Dialogs/BrowseChannelsDialog.vala b/src/Widgets/Dialogs/BrowseChannelsDialog.vala index ecd0113..aa3ab22 100644 --- a/src/Widgets/Dialogs/BrowseChannelsDialog.vala +++ b/src/Widgets/Dialogs/BrowseChannelsDialog.vala @@ -19,7 +19,7 @@ * Authored by: Andrew Vojak */ -public class Iridium.Widgets.BrowseChannelsDialog : Gtk.Dialog { +public class Iridium.Widgets.BrowseChannelsDialog : Granite.Dialog { // TODO: At some point it might be nice to add the ability to sort the columns @@ -158,6 +158,7 @@ public class Iridium.Widgets.BrowseChannelsDialog : Gtk.Dialog { status_label.halign = Gtk.Align.CENTER; status_label.valign = Gtk.Align.CENTER; status_label.justify = Gtk.Justification.CENTER; + status_label.set_max_width_chars (50); status_label.set_line_wrap (true); status_label.margin_bottom = 10; diff --git a/src/Widgets/Dialogs/BrowseServersDialog.vala b/src/Widgets/Dialogs/BrowseServersDialog.vala new file mode 100644 index 0000000..3781fba --- /dev/null +++ b/src/Widgets/Dialogs/BrowseServersDialog.vala @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2021 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +public class Iridium.Widgets.BrowseServersDialog : Granite.Dialog { + + // TODO: At some point it might be nice to add the ability to sort the columns + + public unowned Iridium.MainWindow main_window { get; construct; } + public string server_name { get; construct; } + + // I'm not 100% sure why this needs to be static - but they did it over + // here: https://github.com/xfce-mirror/xfmpc/blob/921fa89585d61b7462e30bac5caa9b2f583dd491/src/playlist.vala + // And it doesn't work otherwise... + private static Gtk.Entry search_entry; + + private Gtk.TreeView tree_view; + private Gtk.ListStore list_store; + private Gtk.TreeModelFilter filter; + + enum Column { + NAME, + HOST + } + + public BrowseServersDialog (Iridium.MainWindow main_window) { + Object ( + deletable: false, + resizable: false, + title: _("Browse Servers"), + transient_for: main_window, + modal: true, + main_window: main_window + ); + } + + construct { + var body = get_content_area (); + + // Create the header + var header_grid = new Gtk.Grid (); + header_grid.margin_start = 30; + header_grid.margin_end = 30; + header_grid.margin_bottom = 10; + header_grid.column_spacing = 10; + + var header_image = new Gtk.Image.from_icon_name ("system-search", Gtk.IconSize.DIALOG); + + var header_title = new Gtk.Label (_("Browse Servers")); + header_title.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); + header_title.halign = Gtk.Align.START; + header_title.hexpand = true; + header_title.margin_end = 10; + header_title.set_line_wrap (true); + + header_grid.attach (header_image, 0, 0, 1, 1); + header_grid.attach (header_title, 1, 0, 1, 1); + + body.add (header_grid); + + var main_grid = new Gtk.Grid (); + main_grid.margin = 30; + main_grid.row_spacing = 12; + main_grid.column_spacing = 10; + + var search_label = new Gtk.Label (_("Search:")); + search_label.halign = Gtk.Align.START; + + search_entry = new Gtk.Entry (); + search_entry.sensitive = true; + search_entry.hexpand = true; + search_entry.secondary_icon_tooltip_text = _("Clear"); + search_entry.changed.connect (() => { + if (search_entry.text != "") { + search_entry.secondary_icon_name = "edit-clear-symbolic"; + } else { + search_entry.secondary_icon_name = null; + } + filter.refilter (); + }); + search_entry.icon_release.connect ((icon_pos, event) => { + if (icon_pos == Gtk.EntryIconPosition.SECONDARY) { + search_entry.set_text (""); + } + }); + + Gtk.ScrolledWindow scrolled_window = new Gtk.ScrolledWindow (null, null); + scrolled_window.set_shadow_type (Gtk.ShadowType.ETCHED_IN); + scrolled_window.max_content_height = 250; + scrolled_window.max_content_width = 250; + scrolled_window.height_request = 250; + scrolled_window.width_request = 350; + scrolled_window.propagate_natural_height = true; + + tree_view = new Gtk.TreeView (); + tree_view.expand = true; + tree_view.headers_visible = true; + tree_view.enable_tree_lines = true; + tree_view.fixed_height_mode = true; + + list_store = new Gtk.ListStore (3, typeof (string), typeof (string), typeof (string)); + filter = new Gtk.TreeModelFilter (list_store, null); + filter.set_visible_func ((Gtk.TreeModelFilterVisibleFunc) filter_func); + + Gtk.CellRendererText name_column_renderer = new Gtk.CellRendererText (); + name_column_renderer.ellipsize = Pango.EllipsizeMode.END; + Gtk.CellRendererText host_column_renderer = new Gtk.CellRendererText (); + host_column_renderer.ellipsize = Pango.EllipsizeMode.END; + + tree_view.insert_column_with_attributes (-1, _("Network"), name_column_renderer, "text", Column.NAME); + tree_view.insert_column_with_attributes (-1, _("Host"), host_column_renderer, "text", Column.HOST); + + foreach (var column in tree_view.get_columns ()) { + column.resizable = true; + } + tree_view.get_column (Column.NAME).min_width = 150; + + scrolled_window.add (tree_view); + + main_grid.attach (search_label, 0, 0, 1, 1); + main_grid.attach (search_entry, 1, 0, 1, 1); + main_grid.attach (scrolled_window, 0, 1, 2, 1); + + body.add (main_grid); + + // Add action buttons + var cancel_button = new Gtk.Button.with_label (_("Cancel")); + cancel_button.clicked.connect (() => { + close (); + }); + + var connect_button = new Gtk.Button.with_label (_("Connect")); + connect_button.sensitive = false; + connect_button.get_style_context ().add_class ("suggested-action"); + connect_button.clicked.connect (() => { + string? network_name = get_selection (); + if (network_name == null) { + return; + } + + Iridium.Models.CuratedServer.Servers? server = Iridium.Models.CuratedServer.Servers.get_for_network_name (network_name); + if (server != null) { + connect_button_clicked (server.get_details ()); + } + }); + + tree_view.get_selection ().changed.connect (() => { + connect_button.sensitive = tree_view.get_selection ().count_selected_rows () > 0; + }); + + add_action_widget (cancel_button, 0); + add_action_widget (connect_button, 1); + + set_servers (); + } + + private void set_servers () { + foreach (var entry in Iridium.Models.CuratedServer.Servers.all ()) { + Gtk.TreeIter iter; + list_store.append (out iter); + list_store.set (iter, Column.NAME, entry.get_details ().network_name, + Column.HOST, entry.get_details ().server_host); + } + tree_view.set_model (filter); + } + + private string? get_selection () { + var selection = tree_view.get_selection (); + if (selection.count_selected_rows () > 1) { + // This should never happen + return null; + } + Gtk.TreeModel model = filter; + var list = selection.get_selected_rows (out model); + if (list.length () == 0) { + return null; + } + Gtk.TreeIter iter; + var path = list.nth_data (0); + if (filter.get_iter (out iter, path)) { + string network_name = ""; + filter.get (iter, Column.NAME, out network_name, -1); + return network_name; + } + return null; + } + + // I'm not 100% sure why this needs to be static - but they did it over + // here: https://github.com/xfce-mirror/xfmpc/blob/921fa89585d61b7462e30bac5caa9b2f583dd491/src/playlist.vala + // And it doesn't work otherwise... + private static bool filter_func (Gtk.TreeModel model, Gtk.TreeIter iter) { + if (search_entry == null) { + return true; + } + string search_string = search_entry.get_text () == null ? "" : search_entry.get_text ().strip ().down (); + if (search_string == "") { + return true; + } + string name = ""; + string host = ""; + model.get (iter, Column.NAME, out name, -1); + model.get (iter, Column.HOST, out host, -1); + if (name == null || host == null) { + return true; + } + if (name.down ().contains (search_string) || host.down ().contains (search_string)) { + return true; + } + return false; + } + + public void dismiss () { + close (); + } + + public signal void connect_button_clicked (Iridium.Models.CuratedServer server); + +} diff --git a/src/Widgets/Dialogs/CertificateWarningDialog.vala b/src/Widgets/Dialogs/CertificateWarningDialog.vala index 264023f..a92a6bc 100644 --- a/src/Widgets/Dialogs/CertificateWarningDialog.vala +++ b/src/Widgets/Dialogs/CertificateWarningDialog.vala @@ -21,6 +21,8 @@ public class Iridium.Widgets.CertificateWarningDialog : Granite.MessageDialog { + // TODO: It would be helpful to provide more user-friendly feedback here rather than just the raw cert + private static Gtk.CssProvider provider; public unowned Iridium.MainWindow main_window { get; construct; } diff --git a/src/Widgets/Dialogs/ChannelJoinDialog.vala b/src/Widgets/Dialogs/ChannelJoinDialog.vala index be33784..023bb49 100644 --- a/src/Widgets/Dialogs/ChannelJoinDialog.vala +++ b/src/Widgets/Dialogs/ChannelJoinDialog.vala @@ -139,6 +139,7 @@ public class Iridium.Widgets.ChannelJoinDialog : Granite.Dialog { status_label.halign = Gtk.Align.CENTER; status_label.valign = Gtk.Align.CENTER; status_label.justify = Gtk.Justification.CENTER; + status_label.set_max_width_chars (50); status_label.set_line_wrap (true); status_label.margin_bottom = 10; diff --git a/src/Widgets/Dialogs/ChannelTopicEditDialog.vala b/src/Widgets/Dialogs/ChannelTopicEditDialog.vala index a221bb2..a5ba43c 100644 --- a/src/Widgets/Dialogs/ChannelTopicEditDialog.vala +++ b/src/Widgets/Dialogs/ChannelTopicEditDialog.vala @@ -104,7 +104,8 @@ public class Iridium.Widgets.ChannelTopicEditDialog : Granite.Dialog { status_label.halign = Gtk.Align.CENTER; status_label.valign = Gtk.Align.CENTER; status_label.justify = Gtk.Justification.CENTER; - status_label.set_line_wrap (true); // TODO: Fix this - it's not working as expected for long error messages + status_label.set_max_width_chars (50); + status_label.set_line_wrap (true); status_label.margin_bottom = 10; body.add (status_label); diff --git a/src/Widgets/Dialogs/EditServerConnectionDialog.vala b/src/Widgets/Dialogs/EditServerConnectionDialog.vala index f150d6c..020d290 100644 --- a/src/Widgets/Dialogs/EditServerConnectionDialog.vala +++ b/src/Widgets/Dialogs/EditServerConnectionDialog.vala @@ -19,351 +19,60 @@ * Authored by: Andrew Vojak */ -public class Iridium.Widgets.EditServerConnectionDialog : Granite.Dialog { +public class Iridium.Widgets.EditServerConnectionDialog : Iridium.Widgets.ServerConnectionDialog { - // TODO: Could refactor this with the shared elements of the ServerConnectionDialog - - private Gtk.Entry server_entry; - private Gtk.Entry nickname_entry; - private Gtk.Entry realname_entry; - private Gee.Map auth_methods; - private Gee.Map auth_method_display_strings; - private Gtk.ComboBox auth_method_combo; - private Gtk.Entry password_entry; - private Gtk.Switch ssl_tls_switch; - private Gtk.Entry port_entry; - private Gtk.Button save_button; - - private Gtk.Stack header_image_stack; - private Gtk.Spinner spinner; - private Gtk.Label status_label; - - enum AuthColumn { - AUTH_METHOD - } + public Iridium.Services.ServerConnectionDetails? connection_details { get; construct; } public EditServerConnectionDialog (Iridium.MainWindow main_window) { Object ( deletable: false, resizable: false, title: _("Edit a Server Connection"), + header: _("Edit Connection"), + primary_button_text: _("Save"), transient_for: main_window, modal: true ); } - construct { - var body = get_content_area (); - - // Create the header - var header_grid = new Gtk.Grid (); - header_grid.margin_start = 30; - header_grid.margin_end = 30; - header_grid.margin_bottom = 10; - header_grid.column_spacing = 10; - - header_image_stack = new Gtk.Stack (); - var tls_reject_header_image = new Gtk.Image.from_icon_name (Constants.APP_ID + ".network-server-security-high", Gtk.IconSize.DIALOG); - tls_reject_header_image.tooltip_text = _("Connection secure"); - var tls_warn_header_image = new Gtk.Image.from_icon_name (Constants.APP_ID + ".network-server-security-medium", Gtk.IconSize.DIALOG); - tls_warn_header_image.tooltip_text = _("Connection secure, provided only trusted certificates are accepted when prompted"); - var tls_allow_header_image = new Gtk.Image.from_icon_name (Constants.APP_ID + ".network-server-security-low", Gtk.IconSize.DIALOG); - tls_allow_header_image.tooltip_text = _("Connection may be insecure. Consider rejecting unacceptable certificates from the application preferences."); - var no_tls_header_image = new Gtk.Image.from_icon_name (Constants.APP_ID + ".network-server-security-low", Gtk.IconSize.DIALOG); - no_tls_header_image.tooltip_text = _("Connection insecure. Consider enabling SSL/TLS from the Advanced tab."); - header_image_stack.add_named (tls_reject_header_image, "tls-reject"); - header_image_stack.add_named (tls_warn_header_image, "tls-warn"); - header_image_stack.add_named (tls_allow_header_image, "tls-allow"); - header_image_stack.add_named (no_tls_header_image, "no-tls"); - header_image_stack.show_all (); // Required in order to set the visible child from preferences - - - var header_title = new Gtk.Label (_("Edit Connection")); - header_title.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); - header_title.halign = Gtk.Align.START; - header_title.hexpand = true; - // header_title.margin_end = 10; - header_title.set_line_wrap (true); - - header_grid.attach (header_image_stack, 0, 0, 1, 1); - header_grid.attach (header_title, 1, 0, 1, 1); - - body.add (header_grid); - - var stack_grid = new Gtk.Grid (); - stack_grid.expand = true; - stack_grid.margin_top = 20; - - var stack_switcher = new Gtk.StackSwitcher (); - stack_switcher.halign = Gtk.Align.CENTER; - stack_grid.attach (stack_switcher, 0, 0, 1, 1); - - var stack = new Gtk.Stack (); - stack.expand = true; - stack_switcher.stack = stack; - - stack.add_titled (create_basic_form (), "basic", _("Basic")); - stack.add_titled (create_advanced_form (), "advanced", _("Advanced")); - stack_grid.attach (stack, 0, 1, 1, 1); - - body.add (stack_grid); - - spinner = new Gtk.Spinner (); - - status_label = new Gtk.Label (""); - status_label.get_style_context ().add_class ("h4"); - status_label.halign = Gtk.Align.CENTER; - status_label.valign = Gtk.Align.CENTER; - status_label.justify = Gtk.Justification.CENTER; - status_label.set_line_wrap (true); - status_label.margin_bottom = 10; - - body.add (spinner); - body.add (status_label); - - // Add action buttons - var cancel_button = new Gtk.Button.with_label (_("Cancel")); - cancel_button.clicked.connect (() => { - close (); - }); - - save_button = new Gtk.Button.with_label (_("Save")); - save_button.get_style_context ().add_class ("suggested-action"); - save_button.sensitive = false; - save_button.clicked.connect (do_save); - - // Connect to signals to determine whether the connect button should be sensitive - server_entry.changed.connect (update_save_button); - nickname_entry.changed.connect (update_save_button); - realname_entry.changed.connect (update_save_button); - port_entry.changed.connect (update_save_button); - - - add_action_widget (cancel_button, 0); - add_action_widget (save_button, 1); - - load_settings (); - } - - private void update_save_button () { - if (server_entry.get_text ().chomp ().chug () != "" && - nickname_entry.get_text ().chomp ().chug () != "" && - realname_entry.get_text ().chomp ().chug () != "" && - port_entry.get_text ().chomp ().chug () != "") { - save_button.sensitive = true; - } else { - save_button.sensitive = false; - } + public EditServerConnectionDialog.from_connection_details (Iridium.MainWindow main_window, Iridium.Services.ServerConnectionDetails connection_details) { + Object ( + deletable: false, + resizable: false, + title: _("Edit a Server Connection"), + header: _("Edit Connection"), + primary_button_text: _("Save"), + transient_for: main_window, + connection_details: connection_details, + modal: true + ); } - private Gtk.Grid create_basic_form () { - var basic_form_grid = new Gtk.Grid (); - basic_form_grid.margin = 30; - basic_form_grid.row_spacing = 12; - basic_form_grid.column_spacing = 20; - - var server_label = new Gtk.Label (_("Server:")); - server_label.halign = Gtk.Align.END; - - server_entry = new Gtk.Entry (); - server_entry.hexpand = true; - - var nickname_label = new Gtk.Label (_("Nickname:")); - nickname_label.halign = Gtk.Align.END; - - nickname_entry = new Gtk.Entry (); - nickname_entry.hexpand = true; - - var realname_label = new Gtk.Label (_("Real Name:")); - realname_label.halign = Gtk.Align.END; - - realname_entry = new Gtk.Entry (); - realname_entry.hexpand = true; - - var auth_method_label = new Gtk.Label (_("Authentication Method:")); - auth_method_label.halign = Gtk.Align.END; - - var list_store = new Gtk.ListStore (1, typeof (string)); - // TODO: This can be handled better - auth_methods = new Gee.HashMap (); - auth_method_display_strings = new Gee.HashMap (); - auth_methods.set (0, Iridium.Models.AuthenticationMethod.NONE); - auth_method_display_strings.set (0, Iridium.Models.AuthenticationMethod.NONE.get_display_string ()); - auth_methods.set (1, Iridium.Models.AuthenticationMethod.SERVER_PASSWORD); - auth_method_display_strings.set (1, Iridium.Models.AuthenticationMethod.SERVER_PASSWORD.get_display_string ()); - auth_methods.set (2, Iridium.Models.AuthenticationMethod.NICKSERV_MSG); - auth_method_display_strings.set (2, Iridium.Models.AuthenticationMethod.NICKSERV_MSG.get_display_string ()); - for (int i = 0; i < auth_method_display_strings.size; i++) { - Gtk.TreeIter iter; - list_store.append (out iter); - list_store.set (iter, AuthColumn.AUTH_METHOD, auth_method_display_strings[i]); - } - auth_method_combo = new Gtk.ComboBox.with_model (list_store); - var auth_method_cell = new Gtk.CellRendererText (); - auth_method_combo.pack_start (auth_method_cell, false); - auth_method_combo.set_attributes (auth_method_cell, "text", 0); - auth_method_combo.set_active (0); - - auth_method_combo.changed.connect (() => { - password_entry.set_sensitive (auth_methods.get (auth_method_combo.get_active ()) != Iridium.Models.AuthenticationMethod.NONE); - }); - - var password_label = new Gtk.Label (_("Password:")); - password_label.halign = Gtk.Align.END; - - password_entry = new Gtk.Entry (); - password_entry.hexpand = true; - password_entry.visibility = false; - password_entry.sensitive = false; - password_entry.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "changes-prevent-symbolic"); - password_entry.icon_press.connect ((pos, event) => { - if (pos == Gtk.EntryIconPosition.SECONDARY) { - password_entry.visibility = !password_entry.visibility; - } - if (password_entry.visibility) { - password_entry.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "changes-allow-symbolic"); + construct { + if (connection_details != null) { + server_entry.set_text (connection_details.server); + nickname_entry.set_text (connection_details.nickname); + realname_entry.set_text (connection_details.realname); + ssl_tls_switch.set_active (connection_details.tls); // Set this before port because changing it modifies the port number + port_entry.set_text (connection_details.port.to_string ()); + auth_method_combo.set_active (get_auth_method_index (connection_details.auth_method)); + if (connection_details.auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL) { + if (connection_details.auth_token != null) { + verify_certificate_file (connection_details.auth_token); + certificate_file_entry.set_uri (connection_details.auth_token); + } + certificate_file_entry.sensitive = true; + password_entry.sensitive = false; + show_certificate_stack (); } else { - password_entry.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "changes-prevent-symbolic"); - } - }); - - basic_form_grid.attach (server_label, 0, 0, 1, 1); - basic_form_grid.attach (server_entry, 1, 0, 1, 1); - basic_form_grid.attach (nickname_label, 0, 1, 1, 1); - basic_form_grid.attach (nickname_entry, 1, 1, 1, 1); - basic_form_grid.attach (realname_label, 0, 2, 1, 1); - basic_form_grid.attach (realname_entry, 1, 2, 1, 1); - basic_form_grid.attach (auth_method_label, 0, 3, 1, 1); - basic_form_grid.attach (auth_method_combo, 1, 3, 1, 1); - basic_form_grid.attach (password_label, 0, 4, 1, 1); - basic_form_grid.attach (password_entry, 1, 4, 1, 1); - - return basic_form_grid; - } - - private Gtk.Grid create_advanced_form () { - var advanced_form_grid = new Gtk.Grid (); - advanced_form_grid.margin = 30; - advanced_form_grid.row_spacing = 12; - advanced_form_grid.column_spacing = 20; - - var ssl_tls_label = new Gtk.Label (_("Use SSL/TLS:")); - ssl_tls_label.halign = Gtk.Align.END; - - ssl_tls_switch = new Gtk.Switch (); - var ssl_tls_switch_container = new Gtk.Grid (); - ssl_tls_switch_container.add (ssl_tls_switch); - ssl_tls_switch.state = true; - ssl_tls_switch.active = true; - - ssl_tls_switch.notify["active"].connect (() => { - port_entry.text = ssl_tls_switch.get_active () - ? Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT.to_string () - : Iridium.Services.ServerConnectionDetails.DEFAULT_INSECURE_PORT.to_string (); - }); - ssl_tls_switch.notify["active"].connect (on_security_posture_changed); - - var port_label = new Gtk.Label (_("Port:")); - port_label.halign = Gtk.Align.END; - - // TODO: Force numeric input - port_entry = new Gtk.Entry (); - port_entry.hexpand = true; - port_entry.text = Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT.to_string (); - - advanced_form_grid.attach (ssl_tls_label, 0, 0, 1, 1); - advanced_form_grid.attach (ssl_tls_switch_container, 1, 0, 1, 1); - advanced_form_grid.attach (port_label, 0, 1, 1, 1); - advanced_form_grid.attach (port_entry, 1, 1, 1, 1); - - return advanced_form_grid; - } - - private void on_security_posture_changed () { - if (ssl_tls_switch.get_active ()) { - var cert_policy = Iridium.Application.settings.get_string ("certificate-validation-policy"); - switch (Iridium.Models.InvalidCertificatePolicy.get_value_by_short_name (cert_policy)) { - case REJECT: - header_image_stack.set_visible_child_name ("tls-reject"); - break; - case WARN: - header_image_stack.set_visible_child_name ("tls-warn"); - break; - case ALLOW: - header_image_stack.set_visible_child_name ("tls-allow"); - break; - default: - assert_not_reached (); + if (connection_details.auth_token != null) { + password_entry.set_text (connection_details.auth_token); + } + certificate_file_entry.sensitive = false; + password_entry.sensitive = true; + show_password_stack (); } - } else { - header_image_stack.set_visible_child_name ("no-tls"); } } - private void load_settings () { - on_security_posture_changed (); - nickname_entry.text = Iridium.Application.settings.get_string ("default-nickname"); - realname_entry.text = Iridium.Application.settings.get_string ("default-realname"); - } - - private void do_save () { - // TODO: Validate entries first! - spinner.start (); - status_label.label = ""; - var server_name = server_entry.get_text ().chomp ().chug (); - var nickname = nickname_entry.get_text ().chomp ().chug (); - var realname = realname_entry.get_text ().chomp ().chug (); - var port = (uint16) int.parse (port_entry.get_text ().chomp ().chug ()); - if (port == 0) { - port = Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT; - } - var auth_method = auth_methods.get (auth_method_combo.get_active ()); - var auth_token = password_entry.get_text (); - var tls = ssl_tls_switch.get_active (); - save_button_clicked (server_name, nickname, realname, port, auth_method, tls, auth_token); - } - - public string get_server () { - return server_entry.get_text ().chomp ().chug (); - } - - public void populate (Iridium.Services.ServerConnectionDetails connection_details) { - server_entry.set_text (connection_details.server); - nickname_entry.set_text (connection_details.nickname); - realname_entry.set_text (connection_details.realname); - port_entry.set_text (connection_details.port.to_string ()); - auth_method_combo.set_active (get_auth_method_index (connection_details.auth_method)); - if (connection_details.auth_token != null) { - password_entry.set_text (connection_details.auth_token); - } - ssl_tls_switch.set_active (connection_details.tls); - } - - private int get_auth_method_index (Iridium.Models.AuthenticationMethod auth_method) { - foreach (Gee.Map.Entry entry in auth_methods.entries) { - if (entry.value == auth_method) { - return entry.key; - } - } - return -1; - } - - public void dismiss () { - spinner.stop (); - close (); - } - - public void display_error (string message, string? details = null) { - // TODO: We can make the error messaging better (wrap text!) - spinner.stop (); - status_label.label = message; - if (details != null) { - status_label.label += "\n"; - status_label.label += details; - } - } - - public signal void save_button_clicked (string server, string nickname, string realname, - uint16 port, Iridium.Models.AuthenticationMethod auth_method, bool tls, string auth_token); - } diff --git a/src/Widgets/Dialogs/ManageConnectionsDialog.vala b/src/Widgets/Dialogs/ManageConnectionsDialog.vala index 1dab1fe..5bb0519 100644 --- a/src/Widgets/Dialogs/ManageConnectionsDialog.vala +++ b/src/Widgets/Dialogs/ManageConnectionsDialog.vala @@ -118,6 +118,7 @@ public class Iridium.Widgets.ManageConnectionsDialog : Granite.Dialog { status_label.halign = Gtk.Align.CENTER; status_label.valign = Gtk.Align.CENTER; status_label.justify = Gtk.Justification.CENTER; + status_label.set_max_width_chars (50); status_label.set_line_wrap (true); status_label.margin_bottom = 10; body.add (status_label); diff --git a/src/Widgets/Dialogs/NewServerConnectionDialog.vala b/src/Widgets/Dialogs/NewServerConnectionDialog.vala new file mode 100644 index 0000000..c362989 --- /dev/null +++ b/src/Widgets/Dialogs/NewServerConnectionDialog.vala @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +public class Iridium.Widgets.NewServerConnectionDialog : Iridium.Widgets.ServerConnectionDialog { + + public Iridium.Models.CuratedServer? curated_server { get; construct; } + + public NewServerConnectionDialog (Iridium.MainWindow main_window) { + Object ( + deletable: false, + resizable: false, + title: _("Connect to a Server"), + header: _("New Connection"), + primary_button_text: _("Connect"), + transient_for: main_window, + modal: true + ); + } + + public NewServerConnectionDialog.from_curated_server (Iridium.MainWindow main_window, Iridium.Models.CuratedServer curated_server) { + Object ( + deletable: false, + resizable: false, + title: _("Connect to a Server"), + header: _("Open Connection"), + primary_button_text: _("Connect"), + transient_for: main_window, + curated_server: curated_server, + modal: true + ); + } + + construct { + // Set placeholder text + server_entry.placeholder_text = "irc.example.com"; + nickname_entry.placeholder_text = "iridium"; + realname_entry.placeholder_text = _("Iridium IRC Client"); + + if (curated_server != null) { + server_entry.set_text (curated_server.server_host); + port_entry.set_text (curated_server.port.to_string ()); + ssl_tls_switch.set_active (curated_server.tls); + auth_method_combo.set_active (get_auth_method_index (curated_server.auth_method)); + } + } + +} diff --git a/src/Widgets/Dialogs/NicknameEditDialog.vala b/src/Widgets/Dialogs/NicknameEditDialog.vala index 42ef6d1..c503837 100644 --- a/src/Widgets/Dialogs/NicknameEditDialog.vala +++ b/src/Widgets/Dialogs/NicknameEditDialog.vala @@ -84,7 +84,8 @@ public class Iridium.Widgets.NicknameEditDialog : Granite.Dialog { status_label.halign = Gtk.Align.CENTER; status_label.valign = Gtk.Align.CENTER; status_label.justify = Gtk.Justification.CENTER; - status_label.set_line_wrap (true); // TODO: Fix this - it's not working as expected for long error messages + status_label.set_max_width_chars (50); + status_label.set_line_wrap (true); status_label.margin_bottom = 10; body.add (status_label); diff --git a/src/Widgets/Dialogs/PreferencesDialog.vala b/src/Widgets/Dialogs/PreferencesDialog.vala index 1fe55b9..d5c9683 100644 --- a/src/Widgets/Dialogs/PreferencesDialog.vala +++ b/src/Widgets/Dialogs/PreferencesDialog.vala @@ -106,6 +106,13 @@ public class Iridium.Widgets.PreferencesDialog : Granite.Dialog { suppress_join_part_switch.valign = Gtk.Align.CENTER; Iridium.Application.settings.bind ("suppress-join-part-messages", suppress_join_part_switch, "active", SettingsBindFlags.DEFAULT); + var mute_mentions_label = new Gtk.Label (_("Mute mention notifications:")); + mute_mentions_label.halign = Gtk.Align.END; + var mute_mentions_switch = new Gtk.Switch (); + mute_mentions_switch.halign = Gtk.Align.START; + mute_mentions_switch.valign = Gtk.Align.CENTER; + Iridium.Application.settings.bind ("mute-mention-notifications", mute_mentions_switch, "active", SettingsBindFlags.DEFAULT); + var security_header_label = new Granite.HeaderLabel (_("Security and Privacy")); var cert_validation_policy_label = new Gtk.Label (_("Unacceptable SSL/TLS Certificates:")); @@ -232,12 +239,14 @@ public class Iridium.Widgets.PreferencesDialog : Granite.Dialog { form_grid.attach (default_realname_entry, 1, 2, 1); form_grid.attach (suppress_join_part_label, 0, 3, 1); form_grid.attach (suppress_join_part_switch, 1, 3, 1); - form_grid.attach (security_header_label, 0, 4, 1); - form_grid.attach (cert_validation_policy_label, 0, 5, 1); - form_grid.attach (cert_validation_policy_combo, 1, 5, 1); - form_grid.attach (security_posture_stack, 0, 6, 2); - form_grid.attach (remember_connections_label, 0, 7, 1); - form_grid.attach (remember_connections_switch, 1, 7, 1); + form_grid.attach (mute_mentions_label, 0, 4, 1); + form_grid.attach (mute_mentions_switch, 1, 4, 1); + form_grid.attach (security_header_label, 0, 5, 1); + form_grid.attach (cert_validation_policy_label, 0, 6, 1); + form_grid.attach (cert_validation_policy_combo, 1, 6, 1); + form_grid.attach (security_posture_stack, 0, 7, 2); + form_grid.attach (remember_connections_label, 0, 8, 1); + form_grid.attach (remember_connections_switch, 1, 8, 1); body.add (header_grid); body.add (form_grid); diff --git a/src/Widgets/Dialogs/ServerConnectionDialog.vala b/src/Widgets/Dialogs/ServerConnectionDialog.vala index 14ebfb8..7e38773 100644 --- a/src/Widgets/Dialogs/ServerConnectionDialog.vala +++ b/src/Widgets/Dialogs/ServerConnectionDialog.vala @@ -19,20 +19,35 @@ * Authored by: Andrew Vojak */ -public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { - - private Gtk.Entry server_entry; - private Gtk.Entry nickname_entry; - // private Gtk.Entry username_entry; - private Gtk.Entry realname_entry; - private Gee.Map auth_methods; - private Gee.Map auth_method_display_strings; - private Gtk.ComboBox auth_method_combo; - private Gtk.Entry password_entry; - private Gtk.Switch ssl_tls_switch; - private Gtk.Entry port_entry; - private Gtk.Button connect_button; +public abstract class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { + private static GLib.Regex SERVER_REGEX; // vala-lint=naming-convention + private static GLib.Regex NICKNAME_REGEX; // vala-lint=naming-convention + + protected Gee.Map auth_methods; + protected Gee.Map auth_method_display_strings; + + protected Gtk.Label password_label; + protected Gtk.Label certificate_file_label; + + protected Granite.ValidatedEntry server_entry; + protected Granite.ValidatedEntry nickname_entry; + protected Granite.ValidatedEntry realname_entry; + protected Granite.ValidatedEntry password_entry; + protected Iridium.Widgets.NumberEntry port_entry; + protected Gtk.FileChooserButton certificate_file_entry; + + protected Gtk.Stack auth_token_label_stack; + protected Gtk.Stack auth_token_entry_stack; + + protected Gtk.ComboBox auth_method_combo; + + protected Gtk.Switch ssl_tls_switch; + + public string primary_button_text { get; construct; } + private Gtk.Button primary_button; + + public string header { get; construct; } private Gtk.Stack header_image_stack; private Gtk.Spinner spinner; private Gtk.Label status_label; @@ -41,14 +56,14 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { AUTH_METHOD } - public ServerConnectionDialog (Iridium.MainWindow main_window) { - Object ( - deletable: false, - resizable: false, - title: _("Connect to a Server"), - transient_for: main_window, - modal: true - ); + static construct { + try { + SERVER_REGEX = new GLib.Regex ("""^[a-zA-Z0-9\.-]+$""", GLib.RegexCompileFlags.OPTIMIZE); + // See RFC 2812 Section 2.3.1 + NICKNAME_REGEX = new GLib.Regex ("""^[a-zA-Z\[\]\\\`\_\^\{\|\}][a-zA-Z0-9\[\]\\\`\_\^\{\|\}]*$""", GLib.RegexCompileFlags.OPTIMIZE); + } catch (GLib.Error e) { + critical (e.message); + } } construct { @@ -76,12 +91,10 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { header_image_stack.add_named (no_tls_header_image, "no-tls"); header_image_stack.show_all (); // Required in order to set the visible child from preferences - - var header_title = new Gtk.Label (_("New Connection")); + var header_title = new Gtk.Label (header); header_title.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); header_title.halign = Gtk.Align.START; header_title.hexpand = true; - // header_title.margin_end = 10; header_title.set_line_wrap (true); header_grid.attach (header_image_stack, 0, 0, 1, 1); @@ -114,8 +127,11 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { status_label.halign = Gtk.Align.CENTER; status_label.valign = Gtk.Align.CENTER; status_label.justify = Gtk.Justification.CENTER; + status_label.set_max_width_chars (50); status_label.set_line_wrap (true); status_label.margin_bottom = 10; + status_label.margin_start = 8; + status_label.margin_end = 8; body.add (spinner); body.add (status_label); @@ -126,35 +142,52 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { close (); }); - connect_button = new Gtk.Button.with_label (_("Connect")); - connect_button.get_style_context ().add_class ("suggested-action"); - connect_button.sensitive = false; - connect_button.clicked.connect (() => { - do_connect (); - }); + primary_button = new Gtk.Button.with_label (primary_button_text); + primary_button.get_style_context ().add_class ("suggested-action"); + primary_button.sensitive = false; + primary_button.clicked.connect (do_primary_action); // Connect to signals to determine whether the connect button should be sensitive - server_entry.changed.connect (update_connect_button); - nickname_entry.changed.connect (update_connect_button); - realname_entry.changed.connect (update_connect_button); - port_entry.changed.connect (update_connect_button); - + // Note: Can't use the preferred Granite.ValidatedEntry way, because that seems to limit + // one widget per button, not a set of widgets like in this case. + server_entry.changed.connect (update_primary_button); + nickname_entry.changed.connect (update_primary_button); + realname_entry.changed.connect (update_primary_button); + port_entry.changed.connect (update_primary_button); + auth_method_combo.changed.connect (update_primary_button); + password_entry.changed.connect (update_primary_button); + certificate_file_entry.file_set.connect (update_primary_button); add_action_widget (cancel_button, 0); - add_action_widget (connect_button, 1); + add_action_widget (primary_button, 1); load_settings (); } - private void update_connect_button () { - if (server_entry.get_text ().chomp ().chug () != "" && - nickname_entry.get_text ().chomp ().chug () != "" && - realname_entry.get_text ().chomp ().chug () != "" && - port_entry.get_text ().chomp ().chug () != "") { - connect_button.sensitive = true; - } else { - connect_button.sensitive = false; + private void update_primary_button () { + // Set the update button as sensitive only when all fields are marked as valid + bool is_auth_token_valid = false; + var auth_method = auth_methods.get (auth_method_combo.get_active ()); + switch (auth_method) { + case Iridium.Models.AuthenticationMethod.NONE: + is_auth_token_valid = true; + break; + case Iridium.Models.AuthenticationMethod.SERVER_PASSWORD: + case Iridium.Models.AuthenticationMethod.NICKSERV_MSG: + case Iridium.Models.AuthenticationMethod.SASL_PLAIN: + is_auth_token_valid = password_entry.is_valid; + break; + case Iridium.Models.AuthenticationMethod.SASL_EXTERNAL: + is_auth_token_valid = verify_certificate_file (certificate_file_entry.get_uri ()); + break; + default: + assert_not_reached (); } + primary_button.sensitive = server_entry.is_valid && + nickname_entry.is_valid && + realname_entry.is_valid && + port_entry.is_valid && + is_auth_token_valid; } private Gtk.Grid create_basic_form () { @@ -166,30 +199,31 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { var server_label = new Gtk.Label (_("Server:")); server_label.halign = Gtk.Align.END; - server_entry = new Gtk.Entry (); + server_entry = new Granite.ValidatedEntry.from_regex (SERVER_REGEX); server_entry.hexpand = true; server_entry.placeholder_text = "irc.example.com"; + var browse_button = new Gtk.Button.with_label (_("Browse…")); + browse_button.clicked.connect (() => { + browse_button_clicked (); + }); + var nickname_label = new Gtk.Label (_("Nickname:")); nickname_label.halign = Gtk.Align.END; - nickname_entry = new Gtk.Entry (); + nickname_entry = new Granite.ValidatedEntry.from_regex (NICKNAME_REGEX); nickname_entry.hexpand = true; nickname_entry.placeholder_text = "iridium"; - // var username_label = new Gtk.Label (_("Username:")); - // username_label.halign = Gtk.Align.END; - - // username_entry = new Gtk.Entry (); - // username_entry.hexpand = true; - // username_entry.placeholder_text = "iridium"; - var realname_label = new Gtk.Label (_("Real Name:")); realname_label.halign = Gtk.Align.END; - realname_entry = new Gtk.Entry (); + realname_entry = new Granite.ValidatedEntry (); realname_entry.hexpand = true; realname_entry.placeholder_text = _("Iridium IRC Client"); + realname_entry.changed.connect (() => { + realname_entry.is_valid = realname_entry.get_text ().strip ().length > 0; + }); var auth_method_label = new Gtk.Label (_("Authentication Method:")); auth_method_label.halign = Gtk.Align.END; @@ -204,6 +238,10 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { auth_method_display_strings.set (1, Iridium.Models.AuthenticationMethod.SERVER_PASSWORD.get_display_string ()); auth_methods.set (2, Iridium.Models.AuthenticationMethod.NICKSERV_MSG); auth_method_display_strings.set (2, Iridium.Models.AuthenticationMethod.NICKSERV_MSG.get_display_string ()); + auth_methods.set (3, Iridium.Models.AuthenticationMethod.SASL_PLAIN); + auth_method_display_strings.set (3, Iridium.Models.AuthenticationMethod.SASL_PLAIN.get_display_string ()); + auth_methods.set (4, Iridium.Models.AuthenticationMethod.SASL_EXTERNAL); + auth_method_display_strings.set (4, Iridium.Models.AuthenticationMethod.SASL_EXTERNAL.get_display_string ()); for (int i = 0; i < auth_method_display_strings.size; i++) { Gtk.TreeIter iter; list_store.append (out iter); @@ -216,13 +254,31 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { auth_method_combo.set_active (0); auth_method_combo.changed.connect (() => { - password_entry.set_sensitive (auth_methods.get (auth_method_combo.get_active ()) != Iridium.Models.AuthenticationMethod.NONE); + var auth_method = auth_methods.get (auth_method_combo.get_active ()); + if (!ssl_tls_switch.get_active () && (auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL)) { + // Alert user to the SSL/TLS requirement for SASL External + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (_("SASL External requires SSL/TLS"), _("To use SASL External authentication, you must enable SSL/TLS for this server connection."), "dialog-information", Gtk.ButtonsType.CLOSE); + message_dialog.run (); + message_dialog.destroy (); + // Fall back to SASL Plain in the dialog + auth_method_combo.set_active (auth_method_combo.get_active () - 1); + return; + } + // Update auth token entry sensitivity + password_entry.set_sensitive ((auth_method != Iridium.Models.AuthenticationMethod.NONE) && (auth_method != Iridium.Models.AuthenticationMethod.SASL_EXTERNAL)); + certificate_file_entry.set_sensitive (auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL); + // Update the visible auth token label and entry + if (auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL) { + show_certificate_stack (); + } else { + show_password_stack (); + } }); - var password_label = new Gtk.Label (_("Password:")); + password_label = new Gtk.Label (_("Password:")); password_label.halign = Gtk.Align.END; - password_entry = new Gtk.Entry (); + password_entry = new Granite.ValidatedEntry (); password_entry.hexpand = true; password_entry.visibility = false; password_entry.sensitive = false; @@ -237,28 +293,65 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { password_entry.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "changes-prevent-symbolic"); } }); + password_entry.changed.connect (() => { + password_entry.is_valid = password_entry.get_text ().strip ().length > 0; + }); + + certificate_file_label = new Gtk.Label (_("Identity File:")); + certificate_file_label.halign = Gtk.Align.END; + + certificate_file_entry = new Gtk.FileChooserButton (_("Select Your Identity File\u2026"), Gtk.FileChooserAction.OPEN); + certificate_file_entry.hexpand = true; + certificate_file_entry.sensitive = false; + certificate_file_entry.set_uri (GLib.Environment.get_home_dir ()); + + auth_token_label_stack = new Gtk.Stack (); + auth_token_label_stack.add (password_label); + auth_token_label_stack.add (certificate_file_label); + + auth_token_entry_stack = new Gtk.Stack (); + auth_token_entry_stack.add (password_entry); + auth_token_entry_stack.add (certificate_file_entry); + + show_password_stack (); basic_form_grid.attach (server_label, 0, 0, 1, 1); basic_form_grid.attach (server_entry, 1, 0, 1, 1); + basic_form_grid.attach (browse_button, 2, 0, 1, 1); basic_form_grid.attach (nickname_label, 0, 1, 1, 1); - basic_form_grid.attach (nickname_entry, 1, 1, 1, 1); - // basic_form_grid.attach (username_label, 0, 2, 1, 1); - // basic_form_grid.attach (username_entry, 1, 2, 1, 1); + basic_form_grid.attach (nickname_entry, 1, 1, 2, 1); basic_form_grid.attach (realname_label, 0, 2, 1, 1); - basic_form_grid.attach (realname_entry, 1, 2, 1, 1); + basic_form_grid.attach (realname_entry, 1, 2, 2, 1); basic_form_grid.attach (auth_method_label, 0, 3, 1, 1); - basic_form_grid.attach (auth_method_combo, 1, 3, 1, 1); - basic_form_grid.attach (password_label, 0, 4, 1, 1); - basic_form_grid.attach (password_entry, 1, 4, 1, 1); + basic_form_grid.attach (auth_method_combo, 1, 3, 2, 1); + basic_form_grid.attach (auth_token_label_stack, 0, 4, 1, 1); + basic_form_grid.attach (auth_token_entry_stack, 1, 4, 2, 1); return basic_form_grid; } + protected void show_password_stack () { + Idle.add (() => { + auth_token_label_stack.set_visible_child (password_label); + auth_token_entry_stack.set_visible_child (password_entry); + return false; + }); + } + + protected void show_certificate_stack () { + Idle.add (() => { + auth_token_label_stack.set_visible_child (certificate_file_label); + auth_token_entry_stack.set_visible_child (certificate_file_entry); + return false; + }); + } + private Gtk.Grid create_advanced_form () { var advanced_form_grid = new Gtk.Grid (); advanced_form_grid.margin = 30; advanced_form_grid.row_spacing = 12; advanced_form_grid.column_spacing = 20; + advanced_form_grid.halign = Gtk.Align.CENTER; var ssl_tls_label = new Gtk.Label (_("Use SSL/TLS:")); ssl_tls_label.halign = Gtk.Align.END; @@ -274,15 +367,26 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { ? Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT.to_string () : Iridium.Services.ServerConnectionDetails.DEFAULT_INSECURE_PORT.to_string (); }); + ssl_tls_switch.notify["active"].connect (() => { + // If the user has selected SASL External, fall back to SASL Plain + var auth_method = auth_methods.get (auth_method_combo.get_active ()); + if (!ssl_tls_switch.get_active () && (auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL)) { + auth_method_combo.set_active (auth_method_combo.get_active () - 1); + } + }); ssl_tls_switch.notify["active"].connect (on_security_posture_changed); var port_label = new Gtk.Label (_("Port:")); port_label.halign = Gtk.Align.END; - // TODO: Force numeric input - port_entry = new Gtk.Entry (); - port_entry.hexpand = true; + port_entry = new Iridium.Widgets.NumberEntry (); + port_entry.is_valid = true; + port_entry.hexpand = false; port_entry.text = Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT.to_string (); + port_entry.changed.connect (() => { + int port = int.parse (port_entry.get_text ().strip ()); + port_entry.is_valid = port >= 1 && port <= 65535; + }); advanced_form_grid.attach (ssl_tls_label, 0, 0, 1, 1); advanced_form_grid.attach (ssl_tls_switch_container, 1, 0, 1, 1); @@ -292,6 +396,20 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { return advanced_form_grid; } + protected bool verify_certificate_file (string? uri) { + if (uri == null) { + return false; + } + display_error (""); + try { + new GLib.TlsCertificate.from_file (GLib.File.new_for_uri (uri).get_path ()); + return true; + } catch (GLib.Error e) { + display_error (_("Invalid identity file"), e.message); + return false; + } + } + private void on_security_posture_changed () { if (ssl_tls_switch.get_active ()) { var cert_policy = Iridium.Application.settings.get_string ("certificate-validation-policy"); @@ -316,30 +434,36 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { private void load_settings () { on_security_posture_changed (); nickname_entry.text = Iridium.Application.settings.get_string ("default-nickname"); - // username_entry.text = Iridium.Application.settings.get_string ("default-nickname"); realname_entry.text = Iridium.Application.settings.get_string ("default-realname"); } - private void do_connect () { - // TODO: Validate entries first! + private void do_primary_action () { spinner.start (); status_label.label = ""; - var server_name = server_entry.get_text ().chomp ().chug (); - var nickname = nickname_entry.get_text ().chomp ().chug (); - // var username = username_entry.get_text ().chomp ().chug (); - var realname = realname_entry.get_text ().chomp ().chug (); - var port = (uint16) int.parse (port_entry.get_text ().chomp ().chug ()); + var server_name = server_entry.get_text ().strip (); + var nickname = nickname_entry.get_text ().strip (); + var realname = realname_entry.get_text ().strip (); + var port = (uint16) int.parse (port_entry.get_text ().strip ()); if (port == 0) { port = Iridium.Services.ServerConnectionDetails.DEFAULT_SECURE_PORT; } var auth_method = auth_methods.get (auth_method_combo.get_active ()); - var auth_token = password_entry.get_text (); + var auth_token = (auth_method == Iridium.Models.AuthenticationMethod.SASL_EXTERNAL) ? certificate_file_entry.get_uri () : password_entry.get_text (); var tls = ssl_tls_switch.get_active (); - connect_button_clicked (server_name, nickname, realname, port, auth_method, tls, auth_token); + primary_button_clicked (server_name, nickname, realname, port, auth_method, tls, auth_token); + } + + protected int get_auth_method_index (Iridium.Models.AuthenticationMethod auth_method) { + foreach (Gee.Map.Entry entry in auth_methods.entries) { + if (entry.value == auth_method) { + return entry.key; + } + } + return -1; } public string get_server () { - return server_entry.get_text ().chomp ().chug (); + return server_entry.get_text ().strip (); } public void dismiss () { @@ -357,7 +481,8 @@ public class Iridium.Widgets.ServerConnectionDialog : Granite.Dialog { } } - public signal void connect_button_clicked (string server, string nickname, string realname, + public signal void primary_button_clicked (string server, string nickname, string realname, uint16 port, Iridium.Models.AuthenticationMethod auth_method, bool tls, string auth_token); + public signal void browse_button_clicked (); } diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index 91d266d..a24c8f9 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -27,78 +27,33 @@ public class Iridium.Widgets.HeaderBar : Hdy.HeaderBar { public HeaderBar () { Object ( - title: Constants.APP_NAME, + has_subtitle: true, show_close_button: true ); } construct { - var join_button = new Gtk.MenuButton (); - join_button.set_image (new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.BUTTON)); - join_button.tooltip_text = _("Join"); - join_button.relief = Gtk.ReliefStyle.NONE; - join_button.valign = Gtk.Align.CENTER; - - var new_server_connection_accellabel = new Granite.AccelLabel.from_action_name ( - _("New Server Connection…"), - Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_NEW_SERVER_CONNECTION - ); - - var new_server_connection_menu_item = new Gtk.ModelButton (); - new_server_connection_menu_item.action_name = Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_NEW_SERVER_CONNECTION; - new_server_connection_menu_item.get_child ().destroy (); - new_server_connection_menu_item.add (new_server_connection_accellabel); - - var join_channel_accellabel = new Granite.AccelLabel.from_action_name ( - _("Join Channel…"), - Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_JOIN_CHANNEL - ); - - var join_channel_menu_item = new Gtk.ModelButton (); - join_channel_menu_item.action_name = Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_JOIN_CHANNEL; - join_channel_menu_item.get_child ().destroy (); - join_channel_menu_item.add (join_channel_accellabel); - - var join_popover_grid = new Gtk.Grid (); - join_popover_grid.margin_top = 3; - join_popover_grid.margin_bottom = 3; - join_popover_grid.orientation = Gtk.Orientation.VERTICAL; - join_popover_grid.width_request = 200; - join_popover_grid.attach (new_server_connection_menu_item, 0, 0, 1, 1); - join_popover_grid.attach (join_channel_menu_item, 0, 1, 1, 1); - join_popover_grid.show_all (); - - var join_popover = new Gtk.Popover (null); - join_popover.add (join_popover_grid); - - join_button.popover = join_popover; + unowned Gtk.StyleContext style_context = get_style_context (); + style_context.add_class (Gtk.STYLE_CLASS_FLAT); channel_users_button = new Gtk.MenuButton (); - channel_users_button.set_image (new Gtk.Image.from_icon_name ("system-users-symbolic", Gtk.IconSize.BUTTON)); + channel_users_button.set_image (new Gtk.Image.from_icon_name ("system-users-symbolic", Gtk.IconSize.SMALL_TOOLBAR)); channel_users_button.tooltip_text = _("Channel users"); // TODO: Enable accelerator channel_users_button.relief = Gtk.ReliefStyle.NONE; channel_users_button.valign = Gtk.Align.CENTER; channel_users_popover = new Iridium.Widgets.UsersPopover.ChannelUsersPopover (channel_users_button); - channel_users_popover.nickname_selected.connect (on_nickname_selected); + channel_users_popover.initiate_private_message.connect ((nickname) => { + initiate_private_message (nickname); + }); channel_users_button.popover = channel_users_popover; var settings_button = new Gtk.MenuButton (); - settings_button.image = new Gtk.Image.from_icon_name ("preferences-system-symbolic", Gtk.IconSize.BUTTON); + settings_button.image = new Gtk.Image.from_icon_name ("preferences-system-symbolic", Gtk.IconSize.SMALL_TOOLBAR); settings_button.tooltip_text = _("Menu"); settings_button.relief = Gtk.ReliefStyle.NONE; settings_button.valign = Gtk.Align.CENTER; - var mode_switch = new Granite.ModeSwitch.from_icon_name ("display-brightness-symbolic", "weather-clear-night-symbolic"); - mode_switch.primary_icon_tooltip_text = _("Light background"); - mode_switch.secondary_icon_tooltip_text = _("Dark background"); - mode_switch.valign = Gtk.Align.CENTER; - mode_switch.halign = Gtk.Align.CENTER; - mode_switch.margin = 12; - mode_switch.margin_bottom = 6; - mode_switch.bind_property ("active", Gtk.Settings.get_default (), "gtk_application_prefer_dark_theme"); - Iridium.Application.settings.bind ("prefer-dark-style", mode_switch, "active", GLib.SettingsBindFlags.DEFAULT); - var toggle_sidebar_accellabel = new Granite.AccelLabel.from_action_name ( _("Toggle Sidebar"), Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_TOGGLE_SIDEBAR @@ -143,13 +98,11 @@ public class Iridium.Widgets.HeaderBar : Hdy.HeaderBar { settings_popover_grid.margin_bottom = 3; settings_popover_grid.orientation = Gtk.Orientation.VERTICAL; settings_popover_grid.width_request = 200; - settings_popover_grid.attach (mode_switch, 0, 0, 1, 1); - settings_popover_grid.attach (create_menu_separator (12), 0, 1, 1, 1); - settings_popover_grid.attach (toggle_sidebar_menu_item, 0, 2, 1, 1); - settings_popover_grid.attach (reset_marker_menu_item, 0, 3, 1, 1); - settings_popover_grid.attach (preferences_menu_item, 0, 4, 1, 1); - settings_popover_grid.attach (create_menu_separator (), 0, 5, 1, 1); - settings_popover_grid.attach (quit_menu_item, 0, 6, 1, 1); + settings_popover_grid.attach (toggle_sidebar_menu_item, 0, 0, 1, 1); + settings_popover_grid.attach (reset_marker_menu_item, 0, 1, 1, 1); + settings_popover_grid.attach (preferences_menu_item, 0, 2, 1, 1); + settings_popover_grid.attach (create_menu_separator (), 0, 3, 1, 1); + settings_popover_grid.attach (quit_menu_item, 0, 4, 1, 1); settings_popover_grid.show_all (); var settings_popover = new Gtk.Popover (null); @@ -157,11 +110,10 @@ public class Iridium.Widgets.HeaderBar : Hdy.HeaderBar { settings_button.popover = settings_popover; + pack_start (new Gtk.Separator (Gtk.Orientation.VERTICAL)); pack_end (settings_button); pack_end (channel_users_button); pack_end (new Gtk.Separator (Gtk.Orientation.VERTICAL)); - pack_start (join_button); - pack_start (new Gtk.Separator (Gtk.Orientation.VERTICAL)); } private Gtk.Separator create_menu_separator (int margin_top = 0) { @@ -170,7 +122,7 @@ public class Iridium.Widgets.HeaderBar : Hdy.HeaderBar { return menu_separator; } - public void update_title (string title, string? subtitle) { + public void update_title (string? title, string? subtitle) { this.title = title; this.subtitle = subtitle; } @@ -188,10 +140,6 @@ public class Iridium.Widgets.HeaderBar : Hdy.HeaderBar { channel_users_popover.set_users (nicknames, operators); } - private void on_nickname_selected (string nickname) { - nickname_selected (nickname); - } - - public signal void nickname_selected (string nickname); + public signal void initiate_private_message (string nickname); } diff --git a/src/Widgets/NumberEntry.vala b/src/Widgets/NumberEntry.vala new file mode 100644 index 0000000..e05d4f7 --- /dev/null +++ b/src/Widgets/NumberEntry.vala @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +public class Iridium.Widgets.NumberEntry : Granite.ValidatedEntry { + + private static GLib.Regex? NUMBER_REGEX = null; // vala-lint=naming-convention + + static construct { + try { + NUMBER_REGEX = new GLib.Regex ("^[0-9]*$", GLib.RegexCompileFlags.OPTIMIZE); + } catch (GLib.Error e) { + critical (e.message); + } + } + + construct { + // Force input to be strictly numeric + this.insert_text.connect ((new_text, new_text_length, ref position) => { + try { + if (!NUMBER_REGEX.match_full (new_text)) { + GLib.Signal.stop_emission_by_name (this, "insert-text"); + } + } catch (GLib.Error e) { + warning (e.message); + } + }); + } + +} diff --git a/src/Widgets/SidePanel/ChannelRow.vala b/src/Widgets/SidePanel/ChannelRow.vala index 56bfa56..0db463d 100644 --- a/src/Widgets/SidePanel/ChannelRow.vala +++ b/src/Widgets/SidePanel/ChannelRow.vala @@ -21,12 +21,25 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.Item, Iridium.Widgets.SidePanel.Row { + private Iridium.Widgets.SidePanel.Row.State _state; + public string channel_name { get; construct; } public string server_name { get; construct; } public string? network_name { get; set; } - public Iridium.Widgets.SidePanel.Row.State state { get; set; } - private bool is_enabled = true; + public Iridium.Widgets.SidePanel.Row.State state { + get { + lock (_state) { + return _state; + } + } + set { + lock (_state) { + _state = value; + } + } + } + private bool is_favorite = false; public ChannelRow (string channel_name, string server_name) { @@ -34,7 +47,7 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.I name: channel_name, channel_name: channel_name, server_name: server_name, - icon: new GLib.ThemedIcon ("user-available"), + icon: new GLib.ThemedIcon ("emblem-disabled"), state: Iridium.Widgets.SidePanel.Row.State.DISABLED ); } @@ -48,41 +61,32 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.I } public new void enable () { - if (is_enabled) { - return; - } - icon = new GLib.ThemedIcon ("user-available"); - // icon = new GLib.ThemedIcon ("internet-chat"); - // icon = null; - this.is_enabled = true; + state = Iridium.Widgets.SidePanel.Row.State.ENABLED; + update_icon ("emblem-enabled"); update_markup (); } public new void disable () { - if (!is_enabled) { - return; - } - icon = new GLib.ThemedIcon ("user-offline"); - // icon = new GLib.ThemedIcon ("internet-chat"); - // icon = null; - this.is_enabled = false; + state = Iridium.Widgets.SidePanel.Row.State.DISABLED; + update_icon ("emblem-disabled"); update_markup (); } - public new void error (string error_message, string? error_details) { + public new void error () { + state = Iridium.Widgets.SidePanel.Row.State.ERROR; } public new void updating () { - icon = new GLib.ThemedIcon ("mail-unread"); - // Maybe add the symbolic chat and user icons so we can specifically use them when not loading? - // Could also create "disabled" versions of each that are greyed out slightly - // icon = new GLib.ThemedIcon (Constants.APP_ID + ".image-loading-symbolic"); - this.is_enabled = false; + state = Iridium.Widgets.SidePanel.Row.State.UPDATING; update_markup (); } public new bool get_enabled () { - return is_enabled; + return state == Iridium.Widgets.SidePanel.Row.State.ENABLED; + } + + public new Iridium.Widgets.SidePanel.Row.State get_state () { + return state; } public void set_favorite (bool favorite) { @@ -120,13 +124,13 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.I var close_item = new Gtk.MenuItem.with_label (_("Close")); close_item.activate.connect (() => { - if (is_enabled) { + if (state == Iridium.Widgets.SidePanel.Row.State.ENABLED) { leave_channel (); } remove_channel (); }); - if (is_enabled) { + if (state == Iridium.Widgets.SidePanel.Row.State.ENABLED) { menu.append (edit_topic_item); menu.append (new Gtk.SeparatorMenuItem ()); } @@ -136,7 +140,7 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.I menu.append (favorite_item); } menu.append (new Gtk.SeparatorMenuItem ()); - if (is_enabled) { + if (state == Iridium.Widgets.SidePanel.Row.State.ENABLED) { menu.append (leave_item); } else { menu.append (join_item); @@ -156,13 +160,13 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.I private void update_markup () { if (is_favorite) { var server_text = network_name == null ? server_name : network_name; - if (is_enabled) { + if (state == Iridium.Widgets.SidePanel.Row.State.ENABLED) { markup = channel_name + " " + server_text + ""; } else { markup = "" + channel_name + " " + server_text + ""; } } else { - if (is_enabled) { + if (state == Iridium.Widgets.SidePanel.Row.State.ENABLED) { markup = null; } else { markup = "" + channel_name + ""; @@ -170,6 +174,13 @@ public class Iridium.Widgets.SidePanel.ChannelRow : Granite.Widgets.SourceList.I } } + private void update_icon (string icon_name) { + Idle.add (() => { + icon = new GLib.ThemedIcon (icon_name); + return false; + }); + } + public signal void edit_topic (); public signal void favorite_channel (); public signal void remove_favorite_channel (); diff --git a/src/Widgets/SidePanel/Panel.vala b/src/Widgets/SidePanel/Panel.vala index daa0937..e5eae69 100644 --- a/src/Widgets/SidePanel/Panel.vala +++ b/src/Widgets/SidePanel/Panel.vala @@ -25,8 +25,12 @@ public class Iridium.Widgets.SidePanel.Panel : Gtk.Grid { // the row items. Might allow us to display a spinner while connecting to a server or joining // a channel. + private const int NUM_SPINNER_IMAGES = 12; + + public Hdy.HeaderBar header_bar { get; construct; } + private Granite.Widgets.SourceList source_list; - public Iridium.Widgets.StatusBar status_bar; + private Iridium.Widgets.StatusBar status_bar; private Granite.Widgets.SourceList.ExpandableItem favorites_category; private Granite.Widgets.SourceList.ExpandableItem servers_category; @@ -40,6 +44,10 @@ public class Iridium.Widgets.SidePanel.Panel : Gtk.Grid { private Gee.Map> channel_items; private Gee.Map> private_message_items; + private Thread spinner_thread; + private Cancellable spinner_cancellable = new Cancellable (); + private Gee.Map spinner_images; + public unowned Iridium.MainWindow window { get; construct; } public Panel (Iridium.MainWindow window) { @@ -50,6 +58,19 @@ public class Iridium.Widgets.SidePanel.Panel : Gtk.Grid { } construct { + unowned Gtk.StyleContext style_context = get_style_context (); + style_context.add_class (Gtk.STYLE_CLASS_SIDEBAR); + + // Technically this header bar doesn't have a subtitle, but set to true so that the close button + // is in a consistent position with the maximize button on the other header bar (which *does* have + // a subtitle) + header_bar = new Hdy.HeaderBar () { + has_subtitle = true, + show_close_button = true + }; + unowned Gtk.StyleContext header_bar_context = header_bar.get_style_context (); + header_bar_context.add_class (Gtk.STYLE_CLASS_FLAT); + source_list = new Granite.Widgets.SourceList (); status_bar = new Iridium.Widgets.StatusBar (); @@ -100,8 +121,51 @@ public class Iridium.Widgets.SidePanel.Panel : Gtk.Grid { item_selected (item); }); - add (source_list); - // add (status_bar); + attach (header_bar, 0, 0); + attach (source_list, 0, 1); + attach (status_bar, 0, 2); + + // This is a bit of a hack since Gtk.Spinner isn't supported by Granite.SourceList, but far + // easier than re-implementing SourceList + spinner_images = new Gee.HashMap (); + for (int i = 0; i < NUM_SPINNER_IMAGES; i++) { + spinner_images.set (i, new GLib.ThemedIcon ("%s.process-working-%d-symbolic".printf (Constants.APP_ID, i + 1))); + } + spinner_thread = new Thread ("Side panel spinner", do_spin); + + this.destroy.connect (() => { + spinner_cancellable.cancel (); + }); + } + + private void do_spin () { + int image_index = 0; + while (!spinner_cancellable.is_cancelled ()) { + foreach (var server_entry in server_items.entries) { + var server_row = (Iridium.Widgets.SidePanel.Row) server_entry.value; + if (server_row.get_state () == Iridium.Widgets.SidePanel.Row.State.UPDATING) { + set_item_icon (server_entry.value, spinner_images.get (image_index)); + } + foreach (var channel_item in channel_items.get (server_entry.key)) { + var channel_row = (Iridium.Widgets.SidePanel.Row) channel_item; + if (channel_row.get_state () == Iridium.Widgets.SidePanel.Row.State.UPDATING) { + set_item_icon (channel_item, spinner_images.get (image_index)); + } + } + } + image_index++; + if (image_index == NUM_SPINNER_IMAGES) { + image_index = 0; + } + GLib.Thread.usleep (50000); + } + } + + private void set_item_icon (Granite.Widgets.SourceList.Item item, GLib.ThemedIcon icon) { + Idle.add (() => { + item.icon = icon; + return false; + }); } public void add_server_row (string server_name, string? network_name) { @@ -349,13 +413,13 @@ public class Iridium.Widgets.SidePanel.Panel : Gtk.Grid { server_row_disabled (server_name); } - public void error_server_row (string server_name, string error_message, string? error_details) { + public void error_server_row (string server_name) { var server_item = server_items.get (server_name); if (server_item == null) { return; } unowned Iridium.Widgets.SidePanel.Row server_row = (Iridium.Widgets.SidePanel.Row) server_item; - server_row.error (error_message, error_details); + server_row.error (); } public void updating_server_row (string server_name) { diff --git a/src/Widgets/SidePanel/PrivateMessageRow.vala b/src/Widgets/SidePanel/PrivateMessageRow.vala index 6d50a95..177fa82 100644 --- a/src/Widgets/SidePanel/PrivateMessageRow.vala +++ b/src/Widgets/SidePanel/PrivateMessageRow.vala @@ -21,11 +21,23 @@ public class Iridium.Widgets.SidePanel.PrivateMessageRow : Granite.Widgets.SourceList.ExpandableItem, Iridium.Widgets.SidePanel.Row { + private Iridium.Widgets.SidePanel.Row.State _state; + public string nickname { get; set; } public string server_name { get; construct; } - public Iridium.Widgets.SidePanel.Row.State state { get; set; } - private bool is_enabled = true; + public Iridium.Widgets.SidePanel.Row.State state { + get { + lock (_state) { + return _state; + } + } + set { + lock (_state) { + _state = value; + } + } + } public PrivateMessageRow (string nickname, string server_name) { Object ( @@ -46,36 +58,30 @@ public class Iridium.Widgets.SidePanel.PrivateMessageRow : Granite.Widgets.Sourc } public new void enable () { - if (is_enabled) { - return; - } - // icon = new GLib.ThemedIcon ("user-available"); - icon = new GLib.ThemedIcon ("system-users"); + state = Iridium.Widgets.SidePanel.Row.State.ENABLED; + update_icon ("system-users"); markup = null; - is_enabled = true; } public new void disable () { - if (!is_enabled) { - return; - } - // icon = new GLib.ThemedIcon ("user-offline"); + state = Iridium.Widgets.SidePanel.Row.State.DISABLED; markup = "" + nickname + ""; - is_enabled = false; } - public new void error (string error_message, string? error_details) { + public new void error () { + // Private messages don't have an error state } public new void updating () { - // icon = new GLib.ThemedIcon ("mail-unread"); - icon = new GLib.ThemedIcon (Constants.APP_ID + ".image-loading-symbolic"); - markup = "" + nickname + ""; - is_enabled = false; + // Private messages don't have an updating state } public new bool get_enabled () { - return is_enabled; + return state == Iridium.Widgets.SidePanel.Row.State.ENABLED; + } + + public new Iridium.Widgets.SidePanel.Row.State get_state () { + return state; } public override Gtk.Menu? get_context_menu () { @@ -97,6 +103,13 @@ public class Iridium.Widgets.SidePanel.PrivateMessageRow : Granite.Widgets.Sourc this.nickname = nickname; } + private void update_icon (string icon_name) { + Idle.add (() => { + icon = new GLib.ThemedIcon (icon_name); + return false; + }); + } + public signal void close_private_message (); } diff --git a/src/Widgets/SidePanel/Row.vala b/src/Widgets/SidePanel/Row.vala index 0eb2c1d..a3277e1 100644 --- a/src/Widgets/SidePanel/Row.vala +++ b/src/Widgets/SidePanel/Row.vala @@ -24,17 +24,18 @@ public interface Iridium.Widgets.SidePanel.Row : GLib.Object { protected enum State { ENABLED, DISABLED, - UPDATING + UPDATING, + ERROR } public abstract string get_server_name (); public abstract string? get_channel_name (); public abstract void enable (); public abstract void disable (); - public abstract void error (string error_message, string? error_details); + public abstract void error (); // TODO: Maybe remove this from interface and add to implementations as 'joining', 'connecting', etc. public abstract void updating (); - // public abstract State get_state (); + public abstract State get_state (); public abstract bool get_enabled (); } diff --git a/src/Widgets/SidePanel/ServerRow.vala b/src/Widgets/SidePanel/ServerRow.vala index 93b118c..43b807f 100644 --- a/src/Widgets/SidePanel/ServerRow.vala +++ b/src/Widgets/SidePanel/ServerRow.vala @@ -21,14 +21,25 @@ public class Iridium.Widgets.SidePanel.ServerRow : Granite.Widgets.SourceList.ExpandableItem, Granite.Widgets.SourceListSortable, Iridium.Widgets.SidePanel.Row { + private Iridium.Widgets.SidePanel.Row.State _state; + public string server_name { get; construct; } public string? network_name { get; set; } - public Iridium.Widgets.SidePanel.Row.State state { get; set; } - public unowned Iridium.MainWindow window { get; construct; } + public Iridium.Widgets.SidePanel.Row.State state { + get { + lock (_state) { + return _state; + } + } + set { + lock (_state) { + _state = value; + } + } + } - private string? error_message = null; - private string? error_details = null; + public unowned Iridium.MainWindow window { get; construct; } public ServerRow (string server_name, Iridium.MainWindow window, string? network_name) { Object ( @@ -36,39 +47,11 @@ public class Iridium.Widgets.SidePanel.ServerRow : Granite.Widgets.SourceList.Ex network_name: network_name, server_name: server_name, window: window, - icon: new GLib.ThemedIcon ("user-available"), + icon: new GLib.ThemedIcon (Constants.APP_ID + ".network-server-disconnected"), state: Iridium.Widgets.SidePanel.Row.State.DISABLED ); } - construct { - action_activated.connect (() => { - if (error_message != null) { - var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( - activatable_tooltip, - error_details == null ? "" : error_details, - "network-server", - Gtk.ButtonsType.CANCEL - ); - message_dialog.badge_icon = new ThemedIcon ("dialog-error"); - message_dialog.transient_for = window; - - var suggested_button = new Gtk.Button.with_label (_("Dismiss")); - suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); - - message_dialog.show_all (); - if (message_dialog.run () == Gtk.ResponseType.ACCEPT) { - activatable = null; - activatable_tooltip = null; - error_message = null; - error_details = null; - }; - message_dialog.destroy (); - } - }); - } - public new bool allow_dnd_sorting () { return false; } @@ -101,52 +84,35 @@ public class Iridium.Widgets.SidePanel.ServerRow : Granite.Widgets.SourceList.Ex } public new void enable () { - // if (state == Iridium.Widgets.SidePanel.Row.State.ENABLED) { - // return; - // } - icon = new GLib.ThemedIcon ("user-available"); - // icon = new GLib.ThemedIcon ("network-server"); - markup = null; - - clear_error (); - state = Iridium.Widgets.SidePanel.Row.State.ENABLED; + update_icon (Constants.APP_ID + ".network-server-connected"); + markup = null; } public new void disable () { - icon = new GLib.ThemedIcon ("user-offline"); - // icon = new GLib.ThemedIcon ("network-server"); - markup = "" + (network_name == null ? server_name : network_name) + ""; - - clear_error (); - state = Iridium.Widgets.SidePanel.Row.State.DISABLED; + update_icon (Constants.APP_ID + ".network-server-disconnected"); + markup = "" + (network_name == null ? server_name : network_name) + ""; } - public new void error (string error_message, string? error_details) { - // icon = new GLib.ThemedIcon ("dialog-error"); - // markup = "" + server_name + ""; - - activatable = new GLib.ThemedIcon ("dialog-error"); - activatable_tooltip = error_message; - this.error_message = error_message; - this.error_details = error_details; + public new void error () { + state = Iridium.Widgets.SidePanel.Row.State.ERROR; + update_icon (Constants.APP_ID + ".network-server-error"); } public new void updating () { - icon = new GLib.ThemedIcon ("mail-unread"); - // icon = new GLib.ThemedIcon (Constants.APP_ID + ".image-loading-symbolic"); - markup = "" + (network_name == null ? server_name : network_name) + ""; - - clear_error (); - state = Iridium.Widgets.SidePanel.Row.State.UPDATING; + markup = "" + (network_name == null ? server_name : network_name) + ""; } public new bool get_enabled () { return state == Iridium.Widgets.SidePanel.Row.State.ENABLED; } + public new Iridium.Widgets.SidePanel.Row.State get_state () { + return state; + } + public override Gtk.Menu? get_context_menu () { var menu = new Gtk.Menu (); @@ -231,11 +197,11 @@ public class Iridium.Widgets.SidePanel.ServerRow : Granite.Widgets.SourceList.Ex this.name = network_name; } - private void clear_error () { - activatable = null; - activatable_tooltip = null; - error_message = null; - error_details = null; + private void update_icon (string icon_name) { + Idle.add (() => { + icon = new GLib.ThemedIcon (icon_name); + return false; + }); } public signal void join_channel (); diff --git a/src/Widgets/StatusBar.vala b/src/Widgets/StatusBar.vala index 1404680..7e7f362 100644 --- a/src/Widgets/StatusBar.vala +++ b/src/Widgets/StatusBar.vala @@ -21,54 +21,51 @@ public class Iridium.Widgets.StatusBar : Gtk.ActionBar { - private Gtk.MenuItem channel_join_menu_item; - construct { - var server_connect_menu_item = new Gtk.MenuItem.with_label (_("Connect to a Server…")); - channel_join_menu_item = new Gtk.MenuItem.with_label (_("Join a Channel…")); - - var menu = new Gtk.Menu (); - menu.append (server_connect_menu_item); - menu.append (channel_join_menu_item); - menu.show_all (); + get_style_context ().add_class (Gtk.STYLE_CLASS_INLINE_TOOLBAR); - var add_menu_button = new Gtk.MenuButton (); - add_menu_button.direction = Gtk.ArrowType.UP; - add_menu_button.popup = menu; - add_menu_button.tooltip_text = _("Join a Server or Channel"); - add_menu_button.add (new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.MENU)); - add_menu_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + var server_connect_accellabel = new Granite.AccelLabel.from_action_name ( + _("New Server Connection…"), + Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_NEW_SERVER_CONNECTION + ); - var manage_connections_button = new Gtk.Button.from_icon_name ("edit-symbolic", Gtk.IconSize.MENU); - manage_connections_button.tooltip_text = _("Manage connections…"); - manage_connections_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + var server_connect_menu_item = new Gtk.ModelButton (); + server_connect_menu_item.action_name = Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_NEW_SERVER_CONNECTION; + server_connect_menu_item.get_child ().destroy (); + server_connect_menu_item.add (server_connect_accellabel); - pack_start (add_menu_button); - // pack_end (manage_connections_button); + var channel_join_accellabel = new Granite.AccelLabel.from_action_name ( + _("Join Channel…"), + Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_JOIN_CHANNEL + ); - server_connect_menu_item.activate.connect (() => { - server_connect_button_clicked (); - }); + var channel_join_menu_item = new Gtk.ModelButton (); + channel_join_menu_item.action_name = Iridium.Services.ActionManager.ACTION_PREFIX + Iridium.Services.ActionManager.ACTION_JOIN_CHANNEL; + channel_join_menu_item.get_child ().destroy (); + channel_join_menu_item.add (channel_join_accellabel); - channel_join_menu_item.activate.connect (() => { - channel_join_button_clicked (); - }); + var join_popover_grid = new Gtk.Grid (); + join_popover_grid.margin_top = 3; + join_popover_grid.margin_bottom = 3; + join_popover_grid.orientation = Gtk.Orientation.VERTICAL; + join_popover_grid.width_request = 200; + join_popover_grid.attach (server_connect_menu_item, 0, 0, 1, 1); + join_popover_grid.attach (channel_join_menu_item, 0, 1, 1, 1); + join_popover_grid.show_all (); - manage_connections_button.clicked.connect (() => { - manage_connections_button_clicked (); - }); - } + var join_popover = new Gtk.Popover (null); + join_popover.add (join_popover_grid); - public void enable_channel_join_item () { - channel_join_menu_item.sensitive = true; - } + var add_menu_button = new Gtk.MenuButton (); + add_menu_button.label = _("Join…"); + add_menu_button.direction = Gtk.ArrowType.UP; + add_menu_button.popover = join_popover; + add_menu_button.tooltip_text = _("Join a Server or Channel"); + add_menu_button.image = new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.SMALL_TOOLBAR); + add_menu_button.always_show_image = true; + add_menu_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - public void disable_channel_join_item () { - channel_join_menu_item.sensitive = false; + pack_start (add_menu_button); } - public signal void server_connect_button_clicked (); - public signal void channel_join_button_clicked (); - public signal void manage_connections_button_clicked (); - } diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index 9513cff..6e93d99 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -93,16 +93,19 @@ public class Iridium.Widgets.TextView : Gtk.SourceView { Gdk.Rectangle rect; get_iter_location (iter, out rect); + int line_y; // Don't use this - use line_y_window instead + int line_height; + get_line_yrange (iter, out line_y, out line_height); // Convert to window coordinates - int window_x; - int window_y; - buffer_to_window_coords (Gtk.TextWindowType.TEXT, rect.x, rect.y, out window_x, out window_y); + int line_x_window; + int line_y_window; + buffer_to_window_coords (Gtk.TextWindowType.TEXT, rect.x, rect.y, out line_x_window, out line_y_window); // Don't include the border_width, because it gets buggy and sometimes doesn't update the part of the line in the border - double line_width = hadjustment.upper + left_margin + right_margin; - double line_x = left_margin + border_width; - double line_y = window_y + 26; // + 10; // TODO: Compute this based on font size and padding between lines + double render_width = hadjustment.upper + left_margin + right_margin; + double render_x = left_margin + border_width; + double render_y = line_y_window + line_height + border_width; ctx.save (); @@ -111,8 +114,8 @@ public class Iridium.Widgets.TextView : Gtk.SourceView { ctx.set_source_rgba (rgba.red, rgba.green, rgba.blue, 1); ctx.set_line_width (1); - ctx.move_to (line_x, line_y); - ctx.line_to (line_width, line_y); + ctx.move_to (render_x, render_y); + ctx.line_to (render_width, render_y); ctx.stroke (); ctx.restore (); diff --git a/src/Widgets/UsersPopover/ChannelUsersList.vala b/src/Widgets/UsersPopover/ChannelUsersList.vala new file mode 100644 index 0000000..c8fd5c8 --- /dev/null +++ b/src/Widgets/UsersPopover/ChannelUsersList.vala @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019 Andrew Vojak (https://avojak.com) + * + * 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 + * + * Authored by: Andrew Vojak + */ + +public class Iridium.Widgets.UsersPopover.ChannelUsersList : Gtk.TreeView { + + // This class is heavily influenced by the Granite.Widgets.SourceList, but contains + // some optimization to allow efficiently adding a large number of items. + // https://github.com/elementary/granite/blob/master/lib/Widgets/SourceList.vala + + enum Column { + // STATUS_ICON, + NICKNAME, + OP_BADGE, + IS_OP + } + + private const string DEFAULT_STYLESHEET = """ + .sidebar.badge { + border-radius: 10px; + border-width: 0; + padding: 1px 2px 1px 2px; + font-weight: bold; + } + """; + + private static string search_text = ""; + + private Gtk.ListStore list_store; + private Gtk.TreeModelFilter filter; + + public ChannelUsersList () { + Object ( + expand: true, + headers_visible: false, + enable_tree_lines: false, + fixed_height_mode: true + ); + } + + construct { + unowned Gtk.StyleContext style_context = get_style_context (); + style_context.add_class (Gtk.STYLE_CLASS_SIDEBAR); + style_context.add_class (Granite.STYLE_CLASS_SOURCE_LIST); + + var css_provider = new Gtk.CssProvider (); + try { + css_provider.load_from_data (DEFAULT_STYLESHEET, -1); + style_context.add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK); + } catch (Error e) { + warning ("Could not create CSS Provider: %s\nStylesheet:\n%s", e.message, DEFAULT_STYLESHEET); + } + + list_store = new Gtk.ListStore (3, typeof (string), typeof (string), typeof (bool)); + filter = new Gtk.TreeModelFilter (list_store, null); + filter.set_visible_func ((Gtk.TreeModelFilterVisibleFunc) filter_func); + + var nickname_renderer = new Gtk.CellRendererText (); + nickname_renderer.ellipsize = Pango.EllipsizeMode.END; + + var badge_renderer = new Granite.Widgets.CellRendererBadge (); + + // insert_column_with_attributes (-1, null, new Gtk.CellRendererPixbuf (), "icon-name", Column.STATUS_ICON); + insert_column_with_attributes (-1, null, nickname_renderer, "text", Column.NICKNAME); + insert_column_with_attributes (-1, null, badge_renderer, "text", Column.OP_BADGE); + insert_column_with_attributes (-1, null, new Gtk.CellRendererToggle (), "active", Column.IS_OP); + + set_tooltip_column (Column.NICKNAME); + + for (int i = 0; i < get_n_columns (); i++) { + unowned var column = get_column (i); + column.expand = (i == Column.NICKNAME); + column.sizing = Gtk.TreeViewColumnSizing.FIXED; + column.visible = (i != Column.IS_OP); + } + + get_column (Column.OP_BADGE).set_cell_data_func (badge_renderer, badge_cell_data_func); + + button_press_event.connect ((event) => { + if ((event.type == Gdk.EventType.BUTTON_PRESS) && (event.button == Gdk.BUTTON_SECONDARY)) { + // Get the path within the tree where the click event occurred + Gtk.TreePath? path; + Gtk.TreeViewColumn? column; + int cell_x; + int cell_y; + get_path_at_pos ((int) event.x, (int) event.y, out path, out column, out cell_x, out cell_y); + if (path == null) { + return false; + } + // Get the iter for the path + Gtk.TreeIter iter; + if (!filter.get_iter (out iter, path)) { + return false; + } + // Determine the nickname that was selected based on the iter + string nickname = ""; + filter.get (iter, Column.NICKNAME, out nickname, -1); + + var menu = create_popover (nickname); + menu.popup_at_pointer (event); + return true; + } + return false; + }); + } + + private Gtk.Menu create_popover (string nickname) { + var menu = new Gtk.Menu (); + menu.attach_widget = this; + var private_message_item = new Gtk.MenuItem.with_label (_("Send private message")); + private_message_item.activate.connect (() => { + initiate_private_message (nickname); + }); + menu.append (private_message_item); + menu.show_all (); + return menu; + } + + private void badge_cell_data_func (Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { + var badge_renderer = renderer as Granite.Widgets.CellRendererBadge; + assert (badge_renderer != null); + + string op_badge = ""; + model.get (iter, Column.OP_BADGE, out op_badge, -1); + bool is_visible = (op_badge != null) && (op_badge.strip () != ""); + + badge_renderer.visible = is_visible; + badge_renderer.text = is_visible ? op_badge : ""; + } + + private static bool filter_func (Gtk.TreeModel model, Gtk.TreeIter iter) { + if (search_text == "") { + return true; + } + string? nickname = null; + string? op_badge = null; + model.get (iter, Column.NICKNAME, out nickname, -1); + model.get (iter, Column.OP_BADGE, out op_badge, -1); + if (nickname == null) { + return true; + } + if (nickname.down ().contains (search_text) || ((op_badge != null) && op_badge.down ().contains (search_text))) { + return true; + } + return false; + } + + public int update_search_text (string _search_text) { + search_text = _search_text; + filter.refilter (); + // Return the number of visible children + return filter.iter_n_children (null); + } + + public int set_users (Gee.List nicknames, Gee.List operators) { + set_model (null); + list_store.clear (); + foreach (var nickname in nicknames) { + bool is_op = operators.contains (nickname); + Gtk.TreeIter iter; + list_store.append (out iter); + list_store.set (iter, /*Column.STATUS_ICON, "user-available",*/ + Column.NICKNAME, nickname, + Column.OP_BADGE, is_op ? _("OP") : null, + Column.IS_OP, is_op); + } + // With the model fully populated, we can now update the view + set_model (filter); + // Return the number of visible children + return filter.iter_n_children (null); + } + + public signal void initiate_private_message (string nickname); + +} diff --git a/src/Widgets/UsersPopover/ChannelUsersPopover.vala b/src/Widgets/UsersPopover/ChannelUsersPopover.vala index f306eee..f509751 100644 --- a/src/Widgets/UsersPopover/ChannelUsersPopover.vala +++ b/src/Widgets/UsersPopover/ChannelUsersPopover.vala @@ -21,12 +21,12 @@ public class Iridium.Widgets.UsersPopover.ChannelUsersPopover : Gtk.Popover { - // TODO: Need to handle nicknames for OPs and other special cases where the - // nickname starts with a symbol (e.g. @) - private Gtk.SearchEntry search_entry; + + private Gtk.Box box; private Gtk.ScrolledWindow scrolled_window; - private Gtk.ListBox list_box; + private Iridium.Widgets.UsersPopover.ChannelUsersList tree_view; + private Gtk.Label label; public ChannelUsersPopover (Gtk.Widget? relative_to) { Object ( @@ -35,87 +35,72 @@ public class Iridium.Widgets.UsersPopover.ChannelUsersPopover : Gtk.Popover { } construct { - var placeholder = new Gtk.Label (_("No users")); - placeholder.margin_top = 4; - placeholder.margin_bottom = 4; - placeholder.show_all (); + search_entry = new Gtk.SearchEntry (); + search_entry.margin_bottom = 6; scrolled_window = new Gtk.ScrolledWindow (null, null); + scrolled_window.set_shadow_type (Gtk.ShadowType.ETCHED_IN); + scrolled_window.min_content_height = 50; scrolled_window.max_content_height = 250; scrolled_window.propagate_natural_height = true; + scrolled_window.margin_bottom = 6; - list_box = new Gtk.ListBox (); - list_box.expand = true; - list_box.activate_on_single_click = true; - list_box.selection_mode = Gtk.SelectionMode.SINGLE; - list_box.set_placeholder (placeholder); - scrolled_window.add (list_box); - - list_box.set_filter_func (filter_func); - list_box.set_sort_func (sort_func); - // TODO: User header_func to add header for Ops/Owners/Others? - // list_box.set_header_func (); + tree_view = new Iridium.Widgets.UsersPopover.ChannelUsersList (); + tree_view.initiate_private_message.connect ((nickname) => { + initiate_private_message (nickname); + }); + scrolled_window.add (tree_view); - search_entry = new Gtk.SearchEntry (); - search_entry.margin = 6; + label = new Gtk.Label (""); + label.get_style_context ().add_class ("h4"); + label.halign = Gtk.Align.CENTER; + label.valign = Gtk.Align.CENTER; + label.justify = Gtk.Justification.CENTER; + label.set_max_width_chars (50); + label.set_line_wrap (true); - var users_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - users_box.pack_start (search_entry, true, false, 0); - users_box.pack_start (scrolled_window, true, false, 0); + box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + box.margin = 6; + box.pack_start (search_entry, true, false, 0); + box.pack_start (scrolled_window, true, false, 0); + box.pack_start (label, true, false, 0); - add (users_box); + add (box); - users_box.show_all (); + box.show_all (); search_entry.search_changed.connect (() => { - list_box.invalidate_filter (); - }); - list_box.row_selected.connect ((row) => { - if (row == null) { - return; - } - Iridium.Widgets.UsersPopover.UserListBoxRow user_row = (Iridium.Widgets.UsersPopover.UserListBoxRow) row; - nickname_selected (user_row.nickname); - popdown (); + var search_text = search_entry.get_text (); + var num_users = tree_view.update_search_text (search_text == null ? "" : search_text.strip ().down ()); + update_user_count (num_users); }); this.closed.connect (() => { search_entry.set_text (""); - list_box.select_row (null); }); } - private bool filter_func (Gtk.ListBoxRow row) { - if (search_entry.text == null || search_entry.text.length == 0) { - return true; - } - Iridium.Widgets.UsersPopover.UserListBoxRow user_row = (Iridium.Widgets.UsersPopover.UserListBoxRow) row; - return user_row.nickname.contains (search_entry.text); - } - - private int sort_func (Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) { - Iridium.Widgets.UsersPopover.UserListBoxRow user_row1 = (Iridium.Widgets.UsersPopover.UserListBoxRow) row1; - Iridium.Widgets.UsersPopover.UserListBoxRow user_row2 = (Iridium.Widgets.UsersPopover.UserListBoxRow) row2; - return user_row1.nickname.collate (user_row2.nickname); - } - public void set_users (Gee.List nicknames, Gee.List operators) { - list_box.foreach ((widget) => { - widget.destroy (); + nicknames.sort ((a, b) => { + return a.down ().ascii_casecmp (b.down ()); }); - foreach (string nickname in nicknames) { - if (nickname == null || nickname.chomp ().length == 0) { - continue; - } - bool is_op = operators.contains (nickname); - var row = new Iridium.Widgets.UsersPopover.UserListBoxRow (nickname, is_op); - list_box.insert (row, -1); + search_entry.sensitive = false; + double scroll_offset = scrolled_window.vadjustment.get_value (); + update_user_count (tree_view.set_users (nicknames, operators)); + Idle.add (() => { + scrolled_window.vadjustment.set_value (scroll_offset); + return false; + }); + search_entry.sensitive = true; + } + + private void update_user_count (int num_users) { + if (num_users == 1) { + label.set_text (_("%d user").printf (num_users)); + } else { + label.set_text (_("%d users").printf (num_users)); } - list_box.show_all (); - list_box.invalidate_sort (); - list_box.invalidate_filter (); - scrolled_window.check_resize (); } - public signal void nickname_selected (string nickname); + public signal void initiate_private_message (string nickname); } diff --git a/src/Widgets/UsersPopover/UserListBoxRow.vala b/src/Widgets/UsersPopover/UserListBoxRow.vala deleted file mode 100644 index 71d392b..0000000 --- a/src/Widgets/UsersPopover/UserListBoxRow.vala +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2019 Andrew Vojak (https://avojak.com) - * - * 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 - * - * Authored by: Andrew Vojak - */ - -public class Iridium.Widgets.UsersPopover.UserListBoxRow : Gtk.ListBoxRow { - - public string nickname { get; construct; } - public bool is_op { get; construct; } - - private Gtk.EventBox event_box; - - public UserListBoxRow (string nickname, bool is_op) { - Object ( - nickname: nickname, - is_op: is_op - ); - } - - construct { - event_box = new Gtk.EventBox (); - event_box.enter_notify_event.connect (() => { - event_box.set_state_flags (Gtk.StateFlags.PRELIGHT | Gtk.StateFlags.SELECTED, true); - return false; - }); - event_box.leave_notify_event.connect (() => { - event_box.set_state_flags (Gtk.StateFlags.NORMAL, true); - return false; - }); - - var label = new Gtk.Label (nickname); - label.single_line_mode = true; - label.xalign = 0; - label.margin_top = 4; - label.margin_bottom = 4; - - var icon = new Gtk.Image (); - icon.icon_size = Gtk.IconSize.MENU; - if (is_op) { - icon = new Gtk.Image.from_icon_name ("user-available", Gtk.IconSize.MENU); - icon.tooltip_text = _("Operator"); - } - - Gtk.Box box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); - box.pack_start (icon, false, false, 8); - box.pack_start (label, true, true); - event_box.add (box); - this.add (event_box); - } - -} diff --git a/src/meson.build b/src/meson.build index 05b49f1..4b0c43e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -19,6 +19,7 @@ iridium_files = files( join_paths('Models', 'ChannelListEntry.vala'), join_paths('Models', 'ColorPalette.vala'), join_paths('Models', 'ConnectionState.vala'), + join_paths('Models', 'CuratedServer.vala'), join_paths('Models', 'Server.vala'), join_paths('Models', 'InvalidCertificatePolicy.vala'), join_paths('Models', 'IRCURI.vala'), @@ -38,6 +39,7 @@ iridium_files = files( join_paths('Models', 'Text', 'OthersPrivateMessageText.vala'), join_paths('Models', 'Text', 'ServerErrorMessageText.vala'), join_paths('Models', 'Text', 'ServerMessageText.vala'), + join_paths('Models', 'Text', 'TextBufferUtils.vala'), join_paths('Services', 'ActionManager.vala'), join_paths('Services', 'CertificateManager.vala'), join_paths('Services', 'PreemptKeyringThread.vala'), @@ -55,9 +57,11 @@ iridium_files = files( join_paths('Widgets', 'FontUtils.vala'), join_paths('Widgets', 'HeaderBar.vala'), join_paths('Widgets', 'NetworkInfoBar.vala'), + join_paths('Widgets', 'NumberEntry.vala'), join_paths('Widgets', 'StatusBar.vala'), join_paths('Widgets', 'TextView.vala'), join_paths('Widgets', 'Dialogs', 'BrowseChannelsDialog.vala'), + join_paths('Widgets', 'Dialogs', 'BrowseServersDialog.vala'), join_paths('Widgets', 'Dialogs', 'CertificateWarningDialog.vala'), join_paths('Widgets', 'Dialogs', 'ChannelJoinDialog.vala'), join_paths('Widgets', 'Dialogs', 'ChannelTopicEditDialog.vala'), @@ -67,13 +71,14 @@ iridium_files = files( join_paths('Widgets', 'Dialogs', 'NicknameEditDialog.vala'), join_paths('Widgets', 'Dialogs', 'PreferencesDialog.vala'), join_paths('Widgets', 'Dialogs', 'ServerConnectionDialog.vala'), + join_paths('Widgets', 'Dialogs', 'NewServerConnectionDialog.vala'), join_paths('Widgets', 'SidePanel', 'Panel.vala'), join_paths('Widgets', 'SidePanel', 'Row.vala'), join_paths('Widgets', 'SidePanel', 'ServerRow.vala'), join_paths('Widgets', 'SidePanel', 'ChannelRow.vala'), join_paths('Widgets', 'SidePanel', 'PrivateMessageRow.vala'), - join_paths('Widgets', 'UsersPopover', 'ChannelUsersPopover.vala'), - join_paths('Widgets', 'UsersPopover', 'UserListBoxRow.vala') + join_paths('Widgets', 'UsersPopover', 'ChannelUsersList.vala'), + join_paths('Widgets', 'UsersPopover', 'ChannelUsersPopover.vala') ) # Create a new executable, list the files we want to compile, list the dependencies we need, and install @@ -87,7 +92,7 @@ executable( dependency('gee-0.8', version: '>= 0.8.5'), dependency('glib-2.0', version: '>= 2.30.0'), dependency('granite', version: '>= 0.6.0'), - dependency('libsecret-1', version: '>= 0.18.6'), + dependency('libsecret-1', version: '>= 0.20.4'), dependency('sqlite3', version: '>= 3.22.0'), dependency('gtksourceview-4'), dependency('libsoup-2.4'),