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 @@
+
+
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 @@
+
+
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 @@
+
+
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#3335
- 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.cssstyles/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 suppressedIf 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'),