├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── Chad.jsx ├── assets │ ├── botavatar.png │ └── chad.png ├── components │ ├── ContentBar.jsx │ ├── ContentBarDrafts.jsx │ ├── ContentBarPostWrite.jsx │ ├── ContentBarPosts.jsx │ ├── SideBar.jsx │ ├── SideBarBotProfile.jsx │ ├── SideBarChannels.jsx │ ├── SideBarMenu.jsx │ ├── WPostWrite.jsx │ ├── WPostWriteInput.jsx │ └── WPostWritePreview.jsx ├── css │ └── style.css ├── fonts │ ├── Roboto-Black.ttf │ ├── Roboto-BlackItalic.ttf │ ├── Roboto-Bold.ttf │ ├── Roboto-BoldItalic.ttf │ ├── Roboto-Italic.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-LightItalic.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-MediumItalic.ttf │ ├── Roboto-Regular.ttf │ ├── Roboto-Thin.ttf │ └── Roboto-ThinItalic.ttf ├── index.html ├── js │ ├── API.js │ ├── DataRepair.js │ ├── Signal.js │ ├── Utils.js │ └── parser.js ├── langs │ ├── en.js │ └── ru.js └── main.jsx ├── images ├── main.png ├── step4.png ├── step5.png └── step6.png ├── linux-config.json ├── package-lock.json ├── package.json └── src ├── checkUpdates.js ├── icons ├── chad.icns ├── chad.ico └── chad.png ├── index.js ├── menu.js ├── package.json └── preload.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | builds 3 | package 4 | .vscode 5 | .cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 [Free Software Foundation, Inc.](http://fsf.org/) 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license 7 | document, but changing it is not allowed. 8 | 9 | ## Preamble 10 | 11 | The GNU General Public License is a free, copyleft license for software and 12 | other kinds of works. 13 | 14 | The licenses for most software and other practical works are designed to take 15 | away your freedom to share and change the works. By contrast, the GNU General 16 | Public License is intended to guarantee your freedom to share and change all 17 | versions of a program--to make sure it remains free software for all its users. 18 | We, the Free Software Foundation, use the GNU General Public License for most 19 | of our software; it applies also to any other work released this way by its 20 | authors. You can apply it to your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not price. Our 23 | General Public Licenses are designed to make sure that you have the freedom to 24 | distribute copies of free software (and charge for them if you wish), that you 25 | receive source code or can get it if you want it, that you can change the 26 | software or use pieces of it in new free programs, and that you know you can do 27 | these things. 28 | 29 | To protect your rights, we need to prevent others from denying you these rights 30 | or asking you to surrender the rights. Therefore, you have certain 31 | responsibilities if you distribute copies of the software, or if you modify it: 32 | responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether gratis or for 35 | a fee, you must pass on to the recipients the same freedoms that you received. 36 | You must make sure that they, too, receive or can get the source code. And you 37 | must show them these terms so they know their rights. 38 | 39 | Developers that use the GNU GPL protect your rights with two steps: 40 | 41 | 1. assert copyright on the software, and 42 | 2. offer you this License giving you legal permission to copy, distribute 43 | and/or modify it. 44 | 45 | For the developers' and authors' protection, the GPL clearly explains that 46 | there is no warranty for this free software. For both users' and authors' sake, 47 | the GPL requires that modified versions be marked as changed, so that their 48 | problems will not be attributed erroneously to authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run modified 51 | versions of the software inside them, although the manufacturer can do so. This 52 | is fundamentally incompatible with the aim of protecting users' freedom to 53 | change the software. The systematic pattern of such abuse occurs in the area of 54 | products for individuals to use, which is precisely where it is most 55 | unacceptable. Therefore, we have designed this version of the GPL to prohibit 56 | the practice for those products. If such problems arise substantially in other 57 | domains, we stand ready to extend this provision to those domains in future 58 | versions of the GPL, as needed to protect the freedom of users. 59 | 60 | Finally, every program is threatened constantly by software patents. States 61 | should not allow patents to restrict development and use of software on 62 | general-purpose computers, but in those that do, we wish to avoid the special 63 | danger that patents applied to a free program could make it effectively 64 | proprietary. To prevent this, the GPL assures that patents cannot be used to 65 | render the program non-free. 66 | 67 | The precise terms and conditions for copying, distribution and modification 68 | follow. 69 | 70 | ## TERMS AND CONDITIONS 71 | 72 | ### 0. Definitions. 73 | 74 | *This License* refers to version 3 of the GNU General Public License. 75 | 76 | *Copyright* also means copyright-like laws that apply to other kinds of works, 77 | such as semiconductor masks. 78 | 79 | *The Program* refers to any copyrightable work licensed under this License. 80 | Each licensee is addressed as *you*. *Licensees* and *recipients* may be 81 | individuals or organizations. 82 | 83 | To *modify* a work means to copy from or adapt all or part of the work in a 84 | fashion requiring copyright permission, other than the making of an exact copy. 85 | The resulting work is called a *modified version* of the earlier work or a work 86 | *based on* the earlier work. 87 | 88 | A *covered work* means either the unmodified Program or a work based on the 89 | Program. 90 | 91 | To *propagate* a work means to do anything with it that, without permission, 92 | would make you directly or secondarily liable for infringement under applicable 93 | copyright law, except executing it on a computer or modifying a private copy. 94 | Propagation includes copying, distribution (with or without modification), 95 | making available to the public, and in some countries other activities as well. 96 | 97 | To *convey* a work means any kind of propagation that enables other parties to 98 | make or receive copies. Mere interaction with a user through a computer 99 | network, with no transfer of a copy, is not conveying. 100 | 101 | An interactive user interface displays *Appropriate Legal Notices* to the 102 | extent that it includes a convenient and prominently visible feature that 103 | 104 | 1. displays an appropriate copyright notice, and 105 | 2. tells the user that there is no warranty for the work (except to the 106 | extent that warranties are provided), that licensees may convey the work 107 | under this License, and how to view a copy of this License. 108 | 109 | If the interface presents a list of user commands or options, such as a menu, a 110 | prominent item in the list meets this criterion. 111 | 112 | ### 1. Source Code. 113 | 114 | The *source code* for a work means the preferred form of the work for making 115 | modifications to it. *Object code* means any non-source form of a work. 116 | 117 | A *Standard Interface* means an interface that either is an official standard 118 | defined by a recognized standards body, or, in the case of interfaces specified 119 | for a particular programming language, one that is widely used among developers 120 | working in that language. 121 | 122 | The *System Libraries* of an executable work include anything, other than the 123 | work as a whole, that (a) is included in the normal form of packaging a Major 124 | Component, but which is not part of that Major Component, and (b) serves only 125 | to enable use of the work with that Major Component, or to implement a Standard 126 | Interface for which an implementation is available to the public in source code 127 | form. A *Major Component*, in this context, means a major essential component 128 | (kernel, window system, and so on) of the specific operating system (if any) on 129 | which the executable work runs, or a compiler used to produce the work, or an 130 | object code interpreter used to run it. 131 | 132 | The *Corresponding Source* for a work in object code form means all the source 133 | code needed to generate, install, and (for an executable work) run the object 134 | code and to modify the work, including scripts to control those activities. 135 | However, it does not include the work's System Libraries, or general-purpose 136 | tools or generally available free programs which are used unmodified in 137 | performing those activities but which are not part of the work. For example, 138 | Corresponding Source includes interface definition files associated with source 139 | files for the work, and the source code for shared libraries and dynamically 140 | linked subprograms that the work is specifically designed to require, such as 141 | by intimate data communication or control flow between those subprograms and 142 | other parts of the work. 143 | 144 | The Corresponding Source need not include anything that users can regenerate 145 | automatically from other parts of the Corresponding Source. 146 | 147 | The Corresponding Source for a work in source code form is that same work. 148 | 149 | ### 2. Basic Permissions. 150 | 151 | All rights granted under this License are granted for the term of copyright on 152 | the Program, and are irrevocable provided the stated conditions are met. This 153 | License explicitly affirms your unlimited permission to run the unmodified 154 | Program. The output from running a covered work is covered by this License only 155 | if the output, given its content, constitutes a covered work. This License 156 | acknowledges your rights of fair use or other equivalent, as provided by 157 | copyright law. 158 | 159 | You may make, run and propagate covered works that you do not convey, without 160 | conditions so long as your license otherwise remains in force. You may convey 161 | covered works to others for the sole purpose of having them make modifications 162 | exclusively for you, or provide you with facilities for running those works, 163 | provided that you comply with the terms of this License in conveying all 164 | material for which you do not control copyright. Those thus making or running 165 | the covered works for you must do so exclusively on your behalf, under your 166 | direction and control, on terms that prohibit them from making any copies of 167 | your copyrighted material outside their relationship with you. 168 | 169 | Conveying under any other circumstances is permitted solely under the 170 | conditions stated below. Sublicensing is not allowed; section 10 makes it 171 | unnecessary. 172 | 173 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 174 | 175 | No covered work shall be deemed part of an effective technological measure 176 | under any applicable law fulfilling obligations under article 11 of the WIPO 177 | copyright treaty adopted on 20 December 1996, or similar laws prohibiting or 178 | restricting circumvention of such measures. 179 | 180 | When you convey a covered work, you waive any legal power to forbid 181 | circumvention of technological measures to the extent such circumvention is 182 | effected by exercising rights under this License with respect to the covered 183 | work, and you disclaim any intention to limit operation or modification of the 184 | work as a means of enforcing, against the work's users, your or third parties' 185 | legal rights to forbid circumvention of technological measures. 186 | 187 | ### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you receive it, 190 | in any medium, provided that you conspicuously and appropriately publish on 191 | each copy an appropriate copyright notice; keep intact all notices stating that 192 | this License and any non-permissive terms added in accord with section 7 apply 193 | to the code; keep intact all notices of the absence of any warranty; and give 194 | all recipients a copy of this License along with the Program. 195 | 196 | You may charge any price or no price for each copy that you convey, and you may 197 | offer support or warranty protection for a fee. 198 | 199 | ### 5. Conveying Modified Source Versions. 200 | 201 | You may convey a work based on the Program, or the modifications to produce it 202 | from the Program, in the form of source code under the terms of section 4, 203 | provided that you also meet all of these conditions: 204 | 205 | - a) The work must carry prominent notices stating that you modified it, and 206 | giving a relevant date. 207 | - b) The work must carry prominent notices stating that it is released under 208 | this License and any conditions added under section 7. This requirement 209 | modifies the requirement in section 4 to *keep intact all notices*. 210 | - c) You must license the entire work, as a whole, under this License to 211 | anyone who comes into possession of a copy. This License will therefore 212 | apply, along with any applicable section 7 additional terms, to the whole 213 | of the work, and all its parts, regardless of how they are packaged. This 214 | License gives no permission to license the work in any other way, but it 215 | does not invalidate such permission if you have separately received it. 216 | - d) If the work has interactive user interfaces, each must display 217 | Appropriate Legal Notices; however, if the Program has interactive 218 | interfaces that do not display Appropriate Legal Notices, your work need 219 | not make them do so. 220 | 221 | A compilation of a covered work with other separate and independent works, 222 | which are not by their nature extensions of the covered work, and which are not 223 | combined with it such as to form a larger program, in or on a volume of a 224 | storage or distribution medium, is called an *aggregate* if the compilation and 225 | its resulting copyright are not used to limit the access or legal rights of the 226 | compilation's users beyond what the individual works permit. Inclusion of a 227 | covered work in an aggregate does not cause this License to apply to the other 228 | parts of the aggregate. 229 | 230 | ### 6. Conveying Non-Source Forms. 231 | 232 | You may convey a covered work in object code form under the terms of sections 4 233 | and 5, provided that you also convey the machine-readable Corresponding Source 234 | under the terms of this License, in one of these ways: 235 | 236 | - a) Convey the object code in, or embodied in, a physical product (including 237 | a physical distribution medium), accompanied by the Corresponding Source 238 | fixed on a durable physical medium customarily used for software 239 | interchange. 240 | - b) Convey the object code in, or embodied in, a physical product (including 241 | a physical distribution medium), accompanied by a written offer, valid for 242 | at least three years and valid for as long as you offer spare parts or 243 | customer support for that product model, to give anyone who possesses the 244 | object code either 245 | 1. a copy of the Corresponding Source for all the software in the product 246 | that is covered by this License, on a durable physical medium 247 | customarily used for software interchange, for a price no more than your 248 | reasonable cost of physically performing this conveying of source, or 249 | 2. access to copy the Corresponding Source from a network server at no 250 | charge. 251 | - c) Convey individual copies of the object code with a copy of the written 252 | offer to provide the Corresponding Source. This alternative is allowed only 253 | occasionally and noncommercially, and only if you received the object code 254 | with such an offer, in accord with subsection 6b. 255 | - d) Convey the object code by offering access from a designated place 256 | (gratis or for a charge), and offer equivalent access to the Corresponding 257 | Source in the same way through the same place at no further charge. You 258 | need not require recipients to copy the Corresponding Source along with the 259 | object code. If the place to copy the object code is a network server, the 260 | Corresponding Source may be on a different server operated by you or a 261 | third party) that supports equivalent copying facilities, provided you 262 | maintain clear directions next to the object code saying where to find the 263 | Corresponding Source. Regardless of what server hosts the Corresponding 264 | Source, you remain obligated to ensure that it is available for as long as 265 | needed to satisfy these requirements. 266 | - e) Convey the object code using peer-to-peer transmission, provided you 267 | inform other peers where the object code and Corresponding Source of the 268 | work are being offered to the general public at no charge under subsection 269 | 6d. 270 | 271 | A separable portion of the object code, whose source code is excluded from the 272 | Corresponding Source as a System Library, need not be included in conveying the 273 | object code work. 274 | 275 | A *User Product* is either 276 | 277 | 1. a *consumer product*, which means any tangible personal property which is 278 | normally used for personal, family, or household purposes, or 279 | 2. anything designed or sold for incorporation into a dwelling. 280 | 281 | In determining whether a product is a consumer product, doubtful cases shall be 282 | resolved in favor of coverage. For a particular product received by a 283 | particular user, *normally used* refers to a typical or common use of that 284 | class of product, regardless of the status of the particular user or of the way 285 | in which the particular user actually uses, or expects or is expected to use, 286 | the product. A product is a consumer product regardless of whether the product 287 | has substantial commercial, industrial or non-consumer uses, unless such uses 288 | represent the only significant mode of use of the product. 289 | 290 | *Installation Information* for a User Product means any methods, procedures, 291 | authorization keys, or other information required to install and execute 292 | modified versions of a covered work in that User Product from a modified 293 | version of its Corresponding Source. The information must suffice to ensure 294 | that the continued functioning of the modified object code is in no case 295 | prevented or interfered with solely because modification has been made. 296 | 297 | If you convey an object code work under this section in, or with, or 298 | specifically for use in, a User Product, and the conveying occurs as part of a 299 | transaction in which the right of possession and use of the User Product is 300 | transferred to the recipient in perpetuity or for a fixed term (regardless of 301 | how the transaction is characterized), the Corresponding Source conveyed under 302 | this section must be accompanied by the Installation Information. But this 303 | requirement does not apply if neither you nor any third party retains the 304 | ability to install modified object code on the User Product (for example, the 305 | work has been installed in ROM). 306 | 307 | The requirement to provide Installation Information does not include a 308 | requirement to continue to provide support service, warranty, or updates for a 309 | work that has been modified or installed by the recipient, or for the User 310 | Product in which it has been modified or installed. Access to a network may be 311 | denied when the modification itself materially and adversely affects the 312 | operation of the network or violates the rules and protocols for communication 313 | across the network. 314 | 315 | Corresponding Source conveyed, and Installation Information provided, in accord 316 | with this section must be in a format that is publicly documented (and with an 317 | implementation available to the public in source code form), and must require 318 | no special password or key for unpacking, reading or copying. 319 | 320 | ### 7. Additional Terms. 321 | 322 | *Additional permissions* are terms that supplement the terms of this License by 323 | making exceptions from one or more of its conditions. Additional permissions 324 | that are applicable to the entire Program shall be treated as though they were 325 | included in this License, to the extent that they are valid under applicable 326 | law. If additional permissions apply only to part of the Program, that part may 327 | be used separately under those permissions, but the entire Program remains 328 | governed by this License without regard to the additional permissions. 329 | 330 | When you convey a copy of a covered work, you may at your option remove any 331 | additional permissions from that copy, or from any part of it. (Additional 332 | permissions may be written to require their own removal in certain cases when 333 | you modify the work.) You may place additional permissions on material, added 334 | by you to a covered work, for which you have or can give appropriate copyright 335 | permission. 336 | 337 | Notwithstanding any other provision of this License, for material you add to a 338 | covered work, you may (if authorized by the copyright holders of that material) 339 | supplement the terms of this License with terms: 340 | 341 | - a) Disclaiming warranty or limiting liability differently from the terms of 342 | sections 15 and 16 of this License; or 343 | - b) Requiring preservation of specified reasonable legal notices or author 344 | attributions in that material or in the Appropriate Legal Notices displayed 345 | by works containing it; or 346 | - c) Prohibiting misrepresentation of the origin of that material, or 347 | requiring that modified versions of such material be marked in reasonable 348 | ways as different from the original version; or 349 | - d) Limiting the use for publicity purposes of names of licensors or authors 350 | of the material; or 351 | - e) Declining to grant rights under trademark law for use of some trade 352 | names, trademarks, or service marks; or 353 | - f) Requiring indemnification of licensors and authors of that material by 354 | anyone who conveys the material (or modified versions of it) with 355 | contractual assumptions of liability to the recipient, for any liability 356 | that these contractual assumptions directly impose on those licensors and 357 | authors. 358 | 359 | All other non-permissive additional terms are considered *further restrictions* 360 | within the meaning of section 10. If the Program as you received it, or any 361 | part of it, contains a notice stating that it is governed by this License along 362 | with a term that is a further restriction, you may remove that term. If a 363 | license document contains a further restriction but permits relicensing or 364 | conveying under this License, you may add to a covered work material governed 365 | by the terms of that license document, provided that the further restriction 366 | does not survive such relicensing or conveying. 367 | 368 | If you add terms to a covered work in accord with this section, you must place, 369 | in the relevant source files, a statement of the additional terms that apply to 370 | those files, or a notice indicating where to find the applicable terms. 371 | 372 | Additional terms, permissive or non-permissive, may be stated in the form of a 373 | separately written license, or stated as exceptions; the above requirements 374 | apply either way. 375 | 376 | ### 8. Termination. 377 | 378 | You may not propagate or modify a covered work except as expressly provided 379 | under this License. Any attempt otherwise to propagate or modify it is void, 380 | and will automatically terminate your rights under this License (including any 381 | patent licenses granted under the third paragraph of section 11). 382 | 383 | However, if you cease all violation of this License, then your license from a 384 | particular copyright holder is reinstated 385 | 386 | - a) provisionally, unless and until the copyright holder explicitly and 387 | finally terminates your license, and 388 | - b) permanently, if the copyright holder fails to notify you of the 389 | violation by some reasonable means prior to 60 days after the cessation. 390 | 391 | Moreover, your license from a particular copyright holder is reinstated 392 | permanently if the copyright holder notifies you of the violation by some 393 | reasonable means, this is the first time you have received notice of violation 394 | of this License (for any work) from that copyright holder, and you cure the 395 | violation prior to 30 days after your receipt of the notice. 396 | 397 | Termination of your rights under this section does not terminate the licenses 398 | of parties who have received copies or rights from you under this License. If 399 | your rights have been terminated and not permanently reinstated, you do not 400 | qualify to receive new licenses for the same material under section 10. 401 | 402 | ### 9. Acceptance Not Required for Having Copies. 403 | 404 | You are not required to accept this License in order to receive or run a copy 405 | of the Program. Ancillary propagation of a covered work occurring solely as a 406 | consequence of using peer-to-peer transmission to receive a copy likewise does 407 | not require acceptance. However, nothing other than this License grants you 408 | permission to propagate or modify any covered work. These actions infringe 409 | copyright if you do not accept this License. Therefore, by modifying or 410 | propagating a covered work, you indicate your acceptance of this License to do 411 | so. 412 | 413 | ### 10. Automatic Licensing of Downstream Recipients. 414 | 415 | Each time you convey a covered work, the recipient automatically receives a 416 | license from the original licensors, to run, modify and propagate that work, 417 | subject to this License. You are not responsible for enforcing compliance by 418 | third parties with this License. 419 | 420 | An *entity transaction* is a transaction transferring control of an 421 | organization, or substantially all assets of one, or subdividing an 422 | organization, or merging organizations. If propagation of a covered work 423 | results from an entity transaction, each party to that transaction who receives 424 | a copy of the work also receives whatever licenses to the work the party's 425 | predecessor in interest had or could give under the previous paragraph, plus a 426 | right to possession of the Corresponding Source of the work from the 427 | predecessor in interest, if the predecessor has it or can get it with 428 | reasonable efforts. 429 | 430 | You may not impose any further restrictions on the exercise of the rights 431 | granted or affirmed under this License. For example, you may not impose a 432 | license fee, royalty, or other charge for exercise of rights granted under this 433 | License, and you may not initiate litigation (including a cross-claim or 434 | counterclaim in a lawsuit) alleging that any patent claim is infringed by 435 | making, using, selling, offering for sale, or importing the Program or any 436 | portion of it. 437 | 438 | ### 11. Patents. 439 | 440 | A *contributor* is a copyright holder who authorizes use under this License of 441 | the Program or a work on which the Program is based. The work thus licensed is 442 | called the contributor's *contributor version*. 443 | 444 | A contributor's *essential patent claims* are all patent claims owned or 445 | controlled by the contributor, whether already acquired or hereafter acquired, 446 | that would be infringed by some manner, permitted by this License, of making, 447 | using, or selling its contributor version, but do not include claims that would 448 | be infringed only as a consequence of further modification of the contributor 449 | version. For purposes of this definition, *control* includes the right to grant 450 | patent sublicenses in a manner consistent with the requirements of this 451 | License. 452 | 453 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent 454 | license under the contributor's essential patent claims, to make, use, sell, 455 | offer for sale, import and otherwise run, modify and propagate the contents of 456 | its contributor version. 457 | 458 | In the following three paragraphs, a *patent license* is any express agreement 459 | or commitment, however denominated, not to enforce a patent (such as an express 460 | permission to practice a patent or covenant not to sue for patent 461 | infringement). To *grant* such a patent license to a party means to make such 462 | an agreement or commitment not to enforce a patent against the party. 463 | 464 | If you convey a covered work, knowingly relying on a patent license, and the 465 | Corresponding Source of the work is not available for anyone to copy, free of 466 | charge and under the terms of this License, through a publicly available 467 | network server or other readily accessible means, then you must either 468 | 469 | 1. cause the Corresponding Source to be so available, or 470 | 2. arrange to deprive yourself of the benefit of the patent license for this 471 | particular work, or 472 | 3. arrange, in a manner consistent with the requirements of this License, to 473 | extend the patent license to downstream recipients. 474 | 475 | *Knowingly relying* means you have actual knowledge that, but for the patent 476 | license, your conveying the covered work in a country, or your recipient's use 477 | of the covered work in a country, would infringe one or more identifiable 478 | patents in that country that you have reason to believe are valid. 479 | 480 | If, pursuant to or in connection with a single transaction or arrangement, you 481 | convey, or propagate by procuring conveyance of, a covered work, and grant a 482 | patent license to some of the parties receiving the covered work authorizing 483 | them to use, propagate, modify or convey a specific copy of the covered work, 484 | then the patent license you grant is automatically extended to all recipients 485 | of the covered work and works based on it. 486 | 487 | A patent license is *discriminatory* if it does not include within the scope of 488 | its coverage, prohibits the exercise of, or is conditioned on the non-exercise 489 | of one or more of the rights that are specifically granted under this License. 490 | You may not convey a covered work if you are a party to an arrangement with a 491 | third party that is in the business of distributing software, under which you 492 | make payment to the third party based on the extent of your activity of 493 | conveying the work, and under which the third party grants, to any of the 494 | parties who would receive the covered work from you, a discriminatory patent 495 | license 496 | 497 | - a) in connection with copies of the covered work conveyed by you (or copies 498 | made from those copies), or 499 | - b) primarily for and in connection with specific products or compilations 500 | that contain the covered work, unless you entered into that arrangement, or 501 | that patent license was granted, prior to 28 March 2007. 502 | 503 | Nothing in this License shall be construed as excluding or limiting any implied 504 | license or other defenses to infringement that may otherwise be available to 505 | you under applicable patent law. 506 | 507 | ### 12. No Surrender of Others' Freedom. 508 | 509 | If conditions are imposed on you (whether by court order, agreement or 510 | otherwise) that contradict the conditions of this License, they do not excuse 511 | you from the conditions of this License. If you cannot convey a covered work so 512 | as to satisfy simultaneously your obligations under this License and any other 513 | pertinent obligations, then as a consequence you may not convey it at all. For 514 | example, if you agree to terms that obligate you to collect a royalty for 515 | further conveying from those to whom you convey the Program, the only way you 516 | could satisfy both those terms and this License would be to refrain entirely 517 | from conveying the Program. 518 | 519 | ### 13. Use with the GNU Affero General Public License. 520 | 521 | Notwithstanding any other provision of this License, you have permission to 522 | link or combine any covered work with a work licensed under version 3 of the 523 | GNU Affero General Public License into a single combined work, and to convey 524 | the resulting work. The terms of this License will continue to apply to the 525 | part which is the covered work, but the special requirements of the GNU Affero 526 | General Public License, section 13, concerning interaction through a network 527 | will apply to the combination as such. 528 | 529 | ### 14. Revised Versions of this License. 530 | 531 | The Free Software Foundation may publish revised and/or new versions of the GNU 532 | General Public License from time to time. Such new versions will be similar in 533 | spirit to the present version, but may differ in detail to address new problems 534 | or concerns. 535 | 536 | Each version is given a distinguishing version number. If the Program specifies 537 | that a certain numbered version of the GNU General Public License *or any later 538 | version* applies to it, you have the option of following the terms and 539 | conditions either of that numbered version or of any later version published by 540 | the Free Software Foundation. If the Program does not specify a version number 541 | of the GNU General Public License, you may choose any version ever published by 542 | the Free Software Foundation. 543 | 544 | If the Program specifies that a proxy can decide which future versions of the 545 | GNU General Public License can be used, that proxy's public statement of 546 | acceptance of a version permanently authorizes you to choose that version for 547 | the Program. 548 | 549 | Later license versions may give you additional or different permissions. 550 | However, no additional obligations are imposed on any author or copyright 551 | holder as a result of your choosing to follow a later version. 552 | 553 | ### 15. Disclaimer of Warranty. 554 | 555 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE 556 | LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER 557 | PARTIES PROVIDE THE PROGRAM *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER 558 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 559 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 560 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 561 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 562 | CORRECTION. 563 | 564 | ### 16. Limitation of Liability. 565 | 566 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 567 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 568 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 569 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE 570 | THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED 571 | INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE 572 | PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY 573 | HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 574 | 575 | ### 17. Interpretation of Sections 15 and 16. 576 | 577 | If the disclaimer of warranty and limitation of liability provided above cannot 578 | be given local legal effect according to their terms, reviewing courts shall 579 | apply local law that most closely approximates an absolute waiver of all civil 580 | liability in connection with the Program, unless a warranty or assumption of 581 | liability accompanies a copy of the Program in return for a fee. 582 | 583 | ## END OF TERMS AND CONDITIONS ### 584 | 585 | ### How to Apply These Terms to Your New Programs 586 | 587 | If you develop a new program, and you want it to be of the greatest possible 588 | use to the public, the best way to achieve this is to make it free software 589 | which everyone can redistribute and change under these terms. 590 | 591 | To do so, attach the following notices to the program. It is safest to attach 592 | them to the start of each source file to most effectively state the exclusion 593 | of warranty; and each file should have at least the *copyright* line and a 594 | pointer to where the full notice is found. 595 | 596 | 597 | Copyright (C) 598 | 599 | This program is free software: you can redistribute it and/or modify 600 | it under the terms of the GNU General Public License as published by 601 | the Free Software Foundation, either version 3 of the License, or 602 | (at your option) any later version. 603 | 604 | This program is distributed in the hope that it will be useful, 605 | but WITHOUT ANY WARRANTY; without even the implied warranty of 606 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 607 | GNU General Public License for more details. 608 | 609 | You should have received a copy of the GNU General Public License 610 | along with this program. If not, see . 611 | 612 | Also add information on how to contact you by electronic and paper mail. 613 | 614 | If the program does terminal interaction, make it output a short notice like 615 | this when it starts in an interactive mode: 616 | 617 | Copyright (C) 618 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 619 | This is free software, and you are welcome to redistribute it 620 | under certain conditions; type `show c' for details. 621 | 622 | The hypothetical commands `show w` and `show c` should show the appropriate 623 | parts of the General Public License. Of course, your program's commands might 624 | be different; for a GUI interface, you would use an *about box*. 625 | 626 | You should also get your employer (if you work as a programmer) or school, if 627 | any, to sign a *copyright disclaimer* for the program, if necessary. For more 628 | information on this, and how to apply and follow the GNU GPL, see 629 | [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). 630 | 631 | The GNU General Public License does not permit incorporating your program into 632 | proprietary programs. If your program is a subroutine library, you may consider 633 | it more useful to permit linking proprietary applications with the library. If 634 | this is what you want to do, use the GNU Lesser General Public License instead 635 | of this License. But first, please read 636 | [http://www.gnu.org/philosophy/why-not-lgpl.html](http://www.gnu.org/philosophy/why-not-lgpl.html). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chad [![version](https://img.shields.io/github/release/40PK/Chad.svg?style=flat-square)](https://github.com/40PK/Chad/releases) [![build](https://img.shields.io/circleci/project/40PK/Chad.svg?style=flat-square)](https://circleci.com/gh/40PK/Chad) [![downloads](https://img.shields.io/github/downloads/40PK/Chad/total.svg?style=flat-square)](https://github.com/40PK/Chad/releases) [![download latest](https://img.shields.io/github/downloads/40PK/Chad/latest/total.svg?style=flat-square)](https://github.com/40PK/Chad/releases/latest) [![Dependency Status](https://www.versioneye.com/user/projects/57c2a38c939fc60037ebcb7f/badge.svg?style=flat-square)](https://www.versioneye.com/user/projects/57c2a38c939fc60037ebcb7f) 2 | 3 | Cross-platform tool for writing posts in telegram channels with markdown and HTML supporting. 4 | 5 | [Download](https://github.com/Perkovec/Chad/releases) - [Changelog](https://github.com/Perkovec/Chad/blob/master/CHANGELOG.md) - [Features](#features) - [How to use](#how-to-use) - [Roadmap](#roadmap) - [Used libraries](#used-libraries-and-frameworks) - [How to build](#how-to-build) 6 | 7 | ![Main window](images/main.png) 8 | 9 | ## Features 10 | - Post preview (HTML, markdown, plain text) 11 | - Sending posts in multiple channels 12 | - Drafts of posts 13 | - Change sent posts 14 | - Sending post options 15 | - Formatting controls (link, bold, italic) 16 | - Multilanguage support 17 | - Material design 😊 18 | 19 | ## How to use 20 | If you have bot token in telegram, you can start from step 4 21 | 22 | 1. Goto `@BotFather` in telegram 23 | 2. Create new bot with `/newbot` command 24 | 3. `@BotFather` give you bot `token` (`'123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'`) 25 | 4. Setting up bot, that will send posts in channel ![Setup bot](images/step4.png) 26 | 5. Add channels where the posts will be sent ![Add channel](images/step5.png) 27 | 6. Write your post and send ![Write and send post](images/step6.png) 28 | 29 | ## Roadmap 30 | - Attachments 31 | - Link preview 32 | - Sync scroll 33 | - Two-column writing design 34 | - Autosave 35 | - Hotkeys and other user settings 36 | 37 | ## Used libraries and frameworks 38 | - [Electron](http://electron.atom.io/) - framework for create cross-platform desktop applications using JavaScript, HTML and CSS 39 | - [React](https://facebook.github.io/react/) - JavaScript library for building user interface 40 | - react-dom 41 | - react-addons-shallow-compare 42 | - [Material-UI](http://www.material-ui.com) - A Set of React Components that Implement Google's Material Design 43 | - [react-tap-event-plugin](https://github.com/zilverline/react-tap-event-plugin) - Instant TapEvents for React 44 | - [XSS](https://github.com/leizongmin/js-xss) - HTML sanitizer 45 | - [electron-contextmenu-middleware](https://github.com/parro-it/electron-contextmenu-middleware) - `Electron` context menu 46 | - [electron-input-menu](https://github.com/parro-it/electron-input-menu) - Context menu for `Electron` input elements 47 | - [react-layout-pane](https://github.com/tomkp/react-layout-pane) - React layout component using flexbox 48 | - [superagent](https://github.com/visionmedia/superagent) - small progressive client-side HTTP request library 49 | - [velocity-animate](https://github.com/julianshapiro/velocity) - accelerated JavaScript animation 50 | - [react-deep-force-update](https://github.com/gaearon/react-deep-force-update) - force-updates React component tree recursively 51 | - [semver-compare](https://github.com/substack/semver-compare) - compare two semver version strings 52 | 53 | 54 | ## How to build 55 | #### 1. Install `gulp`: 56 | ``` 57 | npm install gulp -g 58 | ``` 59 | #### 2. Install dependencies: 60 | ``` 61 | npm install 62 | ``` 63 | #### 3. Compile code: 64 | ``` 65 | # Debug 66 | gulp build-debug 67 | 68 | # Release 69 | gulp build-release 70 | ``` 71 | #### 4. Run build tasks: 72 | ``` 73 | # OS X 74 | gulp package:mac 75 | 76 | # Linux 77 | gulp package:linux 78 | 79 | # Windows 80 | gulp package:windows 81 | ``` 82 | Builds destionation folder - `'builds'` 83 | -------------------------------------------------------------------------------- /app/Chad.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | const React = require('react'); 3 | const { Paper, Snackbar } = require('material-ui'); 4 | const { Layout, Fixed, Flex } = require('react-layout-pane'); 5 | const { ipcRenderer } = window; 6 | const { MuiThemeProvider, getMuiTheme } = require('material-ui/styles'); 7 | const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default; 8 | const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default; 9 | const SideBar = require('./components/SideBar'); 10 | const ContentBar = require('./components/ContentBar'); 11 | const PropTypes = require('prop-types'); 12 | const botavatarURL = require('./assets/botavatar.png'); 13 | 14 | const langs = { 15 | /* eslint-disable global-require */ 16 | ru: require('./langs/ru'), 17 | en: require('./langs/en'), 18 | /* eslint-enable global-require */ 19 | }; 20 | 21 | const TGAPI = require('./js/API'); 22 | const Signal = require('./js/Signal'); 23 | const Utils = require('./js/Utils'); 24 | 25 | const usernameRegex = /[@-].{5,}/; 26 | const tokenRegex = /\d{9}:.{30,}/; 27 | 28 | const tags = { 29 | paperStyle: { height: '100%', width: 256 }, 30 | contentStyle: { overflow: 'auto' }, 31 | }; 32 | 33 | class Chad extends React.Component { 34 | constructor(props) { 35 | super(props); 36 | 37 | this.state = { 38 | posts: JSON.parse(localStorage.getItem('posts') || '[]'), 39 | drafts: JSON.parse(localStorage.getItem('drafts') || '[]'), 40 | settings: JSON.parse(localStorage.getItem('settings')), 41 | botavatar: (localStorage.getItem('botavatar') || botavatarURL), 42 | bot: JSON.parse(localStorage.getItem('bot') || '{}'), 43 | channels: JSON.parse(localStorage.getItem('channels') || '[]'), 44 | snackbar: { 45 | open: false, 46 | text: '', 47 | }, 48 | }; 49 | 50 | this.local = langs[this.state.settings.lang]; 51 | ipcRenderer.send('build-menu', this.local); 52 | ipcRenderer.send('check-updates', this.local); 53 | this.signal = new Signal(); 54 | 55 | this.token = this.state.bot.token || null; 56 | this.API = this.token !== null ? new TGAPI(this.token) : null; 57 | 58 | this.signal.register('LanguageChange', v => this.languageChange(v)); 59 | this.signal.register('DarkThemeChange', v => this.darkThemeChange(v)); 60 | this.signal.register('PostWriteDefaultsChange', v => this.postWriteDefaultsChange(v)); 61 | this.signal.register('SendPost', d => this.sendPost(d)); 62 | this.signal.register('NewChannel', d => this.newChannel(d)); 63 | this.signal.register('RemoveChannel', (i, h) => this.removeChannel(i, h)); 64 | this.signal.register('SetAdminBot', (t, o) => this.setAdminBot(t, o)); 65 | this.signal.register('RemoveAdminBot', () => this.removeAdminBot()); 66 | this.signal.register('DeletePost', (i, h) => this.deletePost(i, h)); 67 | this.signal.register('ChangePost', d => this.changePost(d)); 68 | this.signal.register('DeleteDraft', (i, h) => this.deleteDraft(i, h)); 69 | this.signal.register('SaveDraft', d => this.saveDraft(d)); 70 | this.signal.register('ChangeDraft', d => this.changeDraft(d)); 71 | 72 | // Binding context 73 | this.closeSnackbar = this.closeSnackbar.bind(this); 74 | } 75 | 76 | getPostIndexByUid(uid) { 77 | for (let i = 0; i < this.state.posts.length; i += 1) { 78 | if (this.state.posts[i].uid === uid) return i; 79 | } 80 | return null; 81 | } 82 | 83 | getDraftIndexByUid(uid) { 84 | for (let i = 0; i < this.state.drafts.length; i += 1) { 85 | if (this.state.drafts[i].uid === uid) return i; 86 | } 87 | return null; 88 | } 89 | 90 | setAdminBot(token, onPass) { 91 | if (!token || token.length < 30) { 92 | return alert(this.loca.alert_invalid_token); 93 | } 94 | 95 | if (!tokenRegex.test(token)) { 96 | return alert(this.local.alert_invalid_token_format); 97 | } 98 | 99 | if (onPass) onPass(); 100 | 101 | this.API = new TGAPI(token); 102 | const API = this.API; 103 | 104 | const signal = this.signal; 105 | signal.call('SetLoadState', [true]); 106 | 107 | API.getMe().then((res) => { 108 | const getMe = res.body; 109 | if (getMe.ok) { 110 | getMe.result.token = token; 111 | 112 | localStorage.setItem('bot', JSON.stringify(getMe.result)); 113 | this.token = token; 114 | this.setState({ 115 | bot: getMe.result, 116 | }, () => signal.call('SetLoadState', [false])); 117 | this.updateBotProfilePhoto(getMe.result.id); 118 | } 119 | }, () => { 120 | signal.call('SetLoadState', [false]); 121 | alert(this.local.alert_cant_find_bot); 122 | }); 123 | 124 | return null; 125 | } 126 | 127 | removeChannel(id, onRemove) { 128 | const channels = this.state.channels; 129 | 130 | for (let i = 0; i < channels.length; i += 1) { 131 | if (channels[i].uid === id) { 132 | channels.splice(i, 1); 133 | return this.setState({ 134 | channels, 135 | }, () => { 136 | localStorage.setItem('channels', JSON.stringify(channels)); 137 | onRemove(); 138 | }); 139 | } 140 | } 141 | 142 | return null; 143 | } 144 | 145 | newChannel(data) { 146 | if (!data.name || data.name.length === 0) { 147 | return alert(this.local.alert_channel_name_short); 148 | } 149 | 150 | if (!data.username || !usernameRegex.test(data.username)) { 151 | return alert(this.local.alert_invalid_channel_name); 152 | } 153 | 154 | const channels = this.state.channels; 155 | channels.push({ 156 | name: data.name, 157 | username: data.username, 158 | uid: Utils.uid(), 159 | }); 160 | 161 | this.setState({ 162 | channels, 163 | }, () => localStorage.setItem('channels', JSON.stringify(channels))); 164 | 165 | return null; 166 | } 167 | 168 | changePost(data) { 169 | if (this.token === null) { 170 | return alert(this.local.alert_add_admin_bot); 171 | } 172 | 173 | if (data.text.length < 1) { 174 | return alert(this.loca.alert_post_text_empty); 175 | } 176 | 177 | data.onStart.apply(null); 178 | 179 | const params = { 180 | text: data.text, 181 | disable_web_page_preview: data.disablePreview, 182 | parse_mode: data.parser, 183 | }; 184 | 185 | const listToSend = data.post.chats.slice(0); 186 | 187 | const API = this.API; 188 | function sendRec(list, onend) { 189 | params.chat_id = list[0].chat_id; 190 | params.message_id = list[0].message_id; 191 | 192 | if (typeof params.chat_id === 'string') { 193 | params.chat_id = `@${params.chat_id}`; 194 | } 195 | 196 | API.editMessageText(params).then((res) => { 197 | const success = res.body; 198 | if (success.ok) { 199 | list.splice(0, 1); 200 | if (list.length) { 201 | sendRec(list, onend); 202 | } 203 | } else { 204 | alert(this.local.alert_something_wrong); 205 | onend(success); 206 | } 207 | 208 | if (!list.length) { 209 | onend(success); 210 | } 211 | }); 212 | } 213 | 214 | sendRec(listToSend, (success) => { 215 | data.onEnd.apply(null); 216 | const post = data.post; 217 | post.date = success.result.date; 218 | post.edit_date = success.result.edit_date || null; 219 | post.text = data.text || null; 220 | post.parse_mode = params.parse_mode; 221 | post.disableNotification = params.disable_notification; 222 | post.disablePreview = params.disable_web_page_preview; 223 | const posts = this.state.posts; 224 | posts[this.getPostIndexByUid(data.post.uid)] = post; 225 | localStorage.setItem('posts', JSON.stringify(posts)); 226 | this.setState({ 227 | posts, 228 | }); 229 | }); 230 | 231 | return null; 232 | } 233 | 234 | sendPost(data) { 235 | const state = this.state; 236 | const listToSend = this.signal.call('SelectedChannels'); 237 | 238 | if (listToSend.length < 1) { 239 | return alert(this.local.alert_select_channel_to_send); 240 | } 241 | 242 | if (this.token === null) { 243 | return alert(this.local.alert_add_admin_bot); 244 | } 245 | 246 | if (data.text.length < 1) { 247 | return alert(this.local.alert_post_text_empty); 248 | } 249 | 250 | data.onStart.apply(null); 251 | 252 | const params = { 253 | text: data.text, 254 | disable_web_page_preview: data.disablePreview, 255 | disable_notification: data.disableNotification, 256 | parse_mode: data.parser, 257 | }; 258 | 259 | const post = { 260 | chats: [], 261 | }; 262 | 263 | const API = this.API; 264 | function sendRec(list, onend) { 265 | params.chat_id = list[0]; 266 | 267 | API.sendMessage(params).then((res) => { 268 | const success = res.body; 269 | if (success.ok) { 270 | post.chats.push({ 271 | name: success.result.chat.title || '', 272 | message_id: success.result.message_id, 273 | chat_id: (success.result.chat.type === 'channel') ? 274 | success.result.chat.username : 275 | success.result.chat.id, 276 | }); 277 | list.splice(0, 1); 278 | if (list.length) { 279 | sendRec(list, onend); 280 | } 281 | } else { 282 | alert(this.local.alert_something_wrong); 283 | onend(success); 284 | } 285 | 286 | if (!list.length) { 287 | onend(success); 288 | } 289 | }); 290 | } 291 | 292 | sendRec(listToSend, (success) => { 293 | data.onEnd.apply(null); 294 | post.date = success.result.date; 295 | post.edit_date = success.result.edit_date || null; 296 | post.text = data.text || null; 297 | post.parse_mode = params.parse_mode; 298 | post.disableNotification = params.disable_notification; 299 | post.disablePreview = params.disable_web_page_preview; 300 | post.uid = Utils.uid2(); 301 | state.posts.unshift(post); 302 | localStorage.setItem('posts', JSON.stringify(state.posts)); 303 | this.setState(state); 304 | }); 305 | return null; 306 | } 307 | 308 | closeSnackbar() { 309 | this.setState({ 310 | snackbar: { 311 | open: false, 312 | text: '', 313 | }, 314 | }); 315 | } 316 | 317 | makeSnackbar(text) { 318 | this.setState({ 319 | snackbar: { 320 | open: true, 321 | text, 322 | }, 323 | }); 324 | } 325 | 326 | postWriteDefaultsChange(value) { 327 | const state = this.state; 328 | state.settings.postWriteDefaults = value; 329 | localStorage.setItem('settings', JSON.stringify(state.settings)); 330 | this.setState(state); 331 | } 332 | 333 | darkThemeChange(value) { 334 | const state = this.state; 335 | state.settings.darkTheme = value; 336 | localStorage.setItem('settings', JSON.stringify(state.settings)); 337 | this.setState(state, this.props.deepForceUpdate); 338 | } 339 | 340 | languageChange(value) { 341 | const state = this.state; 342 | state.settings.lang = value; 343 | localStorage.setItem('settings', JSON.stringify(state.settings)); 344 | this.local = langs[value]; 345 | this.setState(state); 346 | ipcRenderer.send('build-menu', this.local); 347 | } 348 | 349 | updateBotProfilePhoto(id) { 350 | const userId = id !== undefined ? id : this.props.bot.id; 351 | 352 | const API = this.API; 353 | 354 | this.signal.call('SetAvatarLoadState', [true]); 355 | API.getBase64Avatar(userId).then((res) => { 356 | const botavatar = res === null ? botavatarURL : res; 357 | localStorage.setItem('botavatar', botavatar); 358 | this.setState({ 359 | botavatar, 360 | }, () => this.signal.call('SetAvatarLoadState', [false])); 361 | }); 362 | } 363 | 364 | removeAdminBot() { 365 | localStorage.removeItem('bot'); 366 | localStorage.removeItem('botavatar'); 367 | this.setState({ 368 | bot: {}, 369 | botavatar: botavatarURL, 370 | }); 371 | } 372 | 373 | deletePost(uid, onDelete) { 374 | const posts = this.state.posts; 375 | for (let i = 0; i < posts.length; i += 1) { 376 | if (posts[i].uid === uid) { 377 | posts.splice(i, 1); 378 | return this.setState({ 379 | posts, 380 | }, () => { 381 | localStorage.setItem('posts', JSON.stringify(posts)); 382 | onDelete(); 383 | }); 384 | } 385 | } 386 | return null; 387 | } 388 | 389 | deleteDraft(uid, onDelete) { 390 | const drafts = this.state.drafts; 391 | for (let i = 0; i < drafts.length; i += 1) { 392 | if (drafts[i].uid === uid) { 393 | drafts.splice(i, 1); 394 | return this.setState({ 395 | drafts, 396 | }, () => { 397 | localStorage.setItem('drafts', JSON.stringify(drafts)); 398 | onDelete(); 399 | }); 400 | } 401 | } 402 | return null; 403 | } 404 | 405 | saveDraft(data) { 406 | if (data.text.length < 1) { 407 | return alert(this.local.alert_post_text_empty); 408 | } 409 | 410 | const draftData = data; 411 | draftData.uid = Utils.uid2(); 412 | const drafts = this.state.drafts; 413 | drafts.unshift(draftData); 414 | this.setState({ 415 | drafts, 416 | }, () => this.makeSnackbar(this.local.snackbar_draft_saved)); 417 | localStorage.setItem('drafts', JSON.stringify(drafts)); 418 | return null; 419 | } 420 | 421 | changeDraft(data) { 422 | if (data.text.length < 1) { 423 | return alert(this.local.alert_post_text_empty); 424 | } 425 | 426 | const drafts = this.state.drafts; 427 | drafts[this.getDraftIndexByUid(data.uid)] = data; 428 | this.setState({ 429 | drafts, 430 | }); 431 | localStorage.setItem('drafts', JSON.stringify(drafts)); 432 | return null; 433 | } 434 | 435 | render() { 436 | const SideBarData = { 437 | lang: this.state.settings.lang, 438 | darkTheme: this.state.settings.darkTheme, 439 | postWriteDefaults: this.state.settings.postWriteDefaults, 440 | channels: this.state.channels, 441 | bot: { 442 | avatar: this.state.botavatar, 443 | name: this.state.bot.first_name, 444 | username: `@${this.state.bot.username}`, 445 | token: this.token, 446 | }, 447 | }; 448 | 449 | const ContentBarData = { 450 | posts: this.state.posts, 451 | drafts: this.state.drafts, 452 | postWriteDefaults: this.state.settings.postWriteDefaults, 453 | }; 454 | 455 | const theme = this.state.settings.darkTheme ? darkBaseTheme : lightBaseTheme; 456 | 457 | return ( 458 | 459 | 460 | 461 | 462 | 463 | 464 | 469 | 470 | 471 | 472 | 477 | 478 | 479 | 480 | 486 | 487 | 488 | ); 489 | } 490 | } 491 | 492 | /* eslint-disable react/no-unused-prop-types */ 493 | Chad.propTypes = { 494 | deepForceUpdate: PropTypes.func, 495 | bot: PropTypes.shape({ 496 | id: PropTypes.number, 497 | first_name: PropTypes.string, 498 | last_name: PropTypes.string, 499 | username: PropTypes.string, 500 | }), 501 | }; 502 | /* eslint-enable react/no-unused-prop-types */ 503 | 504 | module.exports = Chad; 505 | -------------------------------------------------------------------------------- /app/assets/botavatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/assets/botavatar.png -------------------------------------------------------------------------------- /app/assets/chad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/assets/chad.png -------------------------------------------------------------------------------- /app/components/ContentBar.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ContentBarPostWrite = require('./ContentBarPostWrite'); 3 | const ContentBarPosts = require('./ContentBarPosts'); 4 | const ContentBarDrafts = require('./ContentBarDrafts'); 5 | const shallowCompare = require('react-addons-shallow-compare'); 6 | const PropTypes = require('prop-types'); 7 | 8 | class ContentBar extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | content: 'PostWrite', 14 | }; 15 | 16 | this.props.signal.register('MenuWritePost', () => this.setState({ content: 'PostWrite' })); 17 | this.props.signal.register('MenuPosts', () => this.setState({ content: 'Posts' })); 18 | this.props.signal.register('MenuDrafts', () => this.setState({ content: 'Drafts' })); 19 | } 20 | 21 | shouldComponentUpdate(nextProps, nextState) { 22 | return shallowCompare(this, nextProps, nextState); 23 | } 24 | 25 | render() { 26 | let content; 27 | if (this.state.content === 'PostWrite') { 28 | content = (); 33 | } else if (this.state.content === 'Posts') { 34 | content = (); 39 | } else if (this.state.content === 'Drafts') { 40 | content = (); 45 | } 46 | 47 | return ( 48 |
49 | {content} 50 |
51 | ); 52 | } 53 | } 54 | ContentBar.propTypes = { 55 | signal: PropTypes.object, 56 | data: PropTypes.object, 57 | local: PropTypes.object, 58 | }; 59 | 60 | module.exports = ContentBar; 61 | -------------------------------------------------------------------------------- /app/components/ContentBarDrafts.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | Card, 4 | CardText, 5 | CardActions, 6 | FlatButton, 7 | CircularProgress, 8 | } = require('material-ui'); 9 | const { grey300 } = require('material-ui/styles/colors'); 10 | const EmptyIcon = require('material-ui/svg-icons/action/assignment-late').default; 11 | const WPostWrite = require('./WPostWrite'); 12 | const parser = require('../js/parser'); 13 | const shallowCompare = require('react-addons-shallow-compare'); 14 | const PropTypes = require('prop-types'); 15 | 16 | const tags = { 17 | circProgressStyle: { marginTop: -8 }, 18 | cardStyle: { marginTop: 8 }, 19 | contentStyle: { padding: '0px 8px 8px 8px' }, 20 | }; 21 | 22 | class ContentBarDraft extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = { 27 | change: null, 28 | sending: null, 29 | sendButtonContent: this.props.local.drafts_send, 30 | }; 31 | 32 | this.props.signal.register('PostDraftBtn', (v) => this.setState({ sendButtonContent: v })); 33 | 34 | // Binding context 35 | this.changeDraft = this.changeDraft.bind(this); 36 | this.cancelEditDraft = this.cancelEditDraft.bind(this); 37 | this.sendDraft = this.sendDraft.bind(this); 38 | } 39 | 40 | shouldComponentUpdate(nextProps, nextState) { 41 | return shallowCompare(this, nextProps, nextState); 42 | } 43 | 44 | onSendDraftEnd(uid) { 45 | this.setState({ 46 | change: null, 47 | sending: null, 48 | sendButtonContent: this.props.local.post_send, 49 | }); 50 | this.deleteDraft(uid); 51 | } 52 | 53 | getDraftByUid(uid) { 54 | for (let i = 0; i < this.props.drafts.length; ++i) { 55 | if (this.props.drafts[i].uid === uid) return this.props.drafts[i]; 56 | } 57 | return null; 58 | } 59 | 60 | changeDraft(data) { 61 | const draftData = data; 62 | draftData.uid = this.state.change; 63 | 64 | this.props.signal.call('ChangeDraft', [draftData]); 65 | this.setState({ 66 | change: null, 67 | }); 68 | } 69 | 70 | sendDraftByUid(uid) { 71 | const data = this.getDraftByUid(uid); 72 | data.onEnd = () => this.onSendDraftEnd(uid); 73 | data.onStart = () => this.setState({ 74 | sending: uid, 75 | }); 76 | 77 | this.props.signal.call('SendPost', [data]); 78 | } 79 | 80 | sendDraft(data) { 81 | const uid = this.state.change; 82 | const draftData = data; 83 | draftData.onEnd = () => this.onSendDraftEnd(uid); 84 | draftData.onStart = () => this.setState({ 85 | sendButtonContent: 86 | , 91 | }); 92 | 93 | this.props.signal.call('SendPost', [draftData]); 94 | } 95 | 96 | editDraft(uid) { 97 | this.setState({ 98 | change: uid, 99 | }); 100 | } 101 | 102 | deleteDraft(uid) { 103 | this.props.signal.call('DeleteDraft', [uid, () => this.forceUpdate()]); 104 | } 105 | 106 | cancelEditDraft() { 107 | this.setState({ 108 | change: null, 109 | }); 110 | } 111 | 112 | render() { 113 | let content; 114 | 115 | if (this.state.change === null) { 116 | if (this.props.drafts.length > 0) { 117 | content = []; 118 | this.props.drafts.map(draft => { 119 | const html = parser({ 120 | mode: draft.parse_mode, 121 | data: draft.text, 122 | }); 123 | const sendButtonContent = this.state.sending === draft.uid ? 124 | ( this.sendDraftByUid(draft.uid)} 126 | primary 127 | icon={} 128 | />) : 129 | ( this.sendDraftByUid(draft.uid)} 131 | primary 132 | label={this.props.local.drafts_send} 133 | />); 134 | 135 | content.push( 136 | 137 | 138 |
139 |               
140 |               
141 |                  this.deleteDraft(draft.uid)}
143 |                   secondary
144 |                   label={this.props.local.drafts_delete}
145 |                 />
146 |                  this.editDraft(draft.uid)}
148 |                   label={this.props.local.drafts_edit}
149 |                 />
150 |                 {sendButtonContent}
151 |               
152 |             
153 |           );
154 |           return null;
155 |         });
156 |       } else {
157 |         content = (
158 |           
159 | 160 |

{this.props.local.drafts_empty}

161 |
162 | ); 163 | } 164 | } else { 165 | const draft = this.getDraftByUid(this.state.change); 166 | const settings = { 167 | disableNotification: draft.disableNotification, 168 | disablePreview: draft.disablePreview, 169 | parser: draft.parser, 170 | }; 171 | content = ( 172 | ); 181 | } 182 | 183 | return ( 184 |
185 | {content} 186 |
187 | ); 188 | } 189 | } 190 | ContentBarDraft.propTypes = { 191 | signal: PropTypes.object, 192 | local: PropTypes.object, 193 | drafts: PropTypes.array, 194 | }; 195 | 196 | module.exports = ContentBarDraft; 197 | -------------------------------------------------------------------------------- /app/components/ContentBarPostWrite.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const WPostWrite = require('./WPostWrite'); 3 | const { CircularProgress } = require('material-ui'); 4 | const shallowCompare = require('react-addons-shallow-compare'); 5 | const PropTypes = require('prop-types'); 6 | 7 | const tags = { 8 | circProgressStyle: { marginTop: -8 }, 9 | }; 10 | 11 | class ContentBarPostWrite extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.wPostWriteRef = null; 16 | 17 | this.state = { 18 | sendButtonContent: this.props.local.post_send, 19 | }; 20 | 21 | // Binding context 22 | this.onSend = this.onSend.bind(this); 23 | this.onSaveDraft = this.onSaveDraft.bind(this); 24 | this.onWPostRef = this.onWPostRef.bind(this); 25 | } 26 | 27 | componentWillReceiveProps(nextProps) { 28 | this.setState({ 29 | sendButtonContent: nextProps.local.post_send, 30 | }); 31 | } 32 | 33 | shouldComponentUpdate(nextProps, nextState) { 34 | return shallowCompare(this, nextProps, nextState); 35 | } 36 | 37 | onSendEnd() { 38 | this.wPostWriteRef.clearText(); 39 | this.setState({ 40 | sendButtonContent: this.props.local.post_send, 41 | }); 42 | } 43 | 44 | onSend(data) { 45 | const postData = data; 46 | postData.onEnd = () => this.onSendEnd(); 47 | postData.onStart = () => this.setState({ 48 | sendButtonContent: 49 | , 55 | }); 56 | 57 | this.props.signal.call('SendPost', [postData]); 58 | } 59 | 60 | onSaveDraft(data) { 61 | this.props.signal.call('SaveDraft', [data]); 62 | } 63 | 64 | onWPostRef(ref) { 65 | this.wPostWriteRef = ref; 66 | } 67 | 68 | render() { 69 | return ( 70 |
71 | 79 |
80 | ); 81 | } 82 | } 83 | ContentBarPostWrite.propTypes = { 84 | local: PropTypes.object, 85 | signal: PropTypes.object, 86 | defaults: PropTypes.object, 87 | }; 88 | 89 | module.exports = ContentBarPostWrite; 90 | -------------------------------------------------------------------------------- /app/components/ContentBarPosts.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | Card, 4 | CardHeader, 5 | CardText, 6 | CardActions, 7 | FlatButton, 8 | Chip, 9 | CircularProgress, 10 | } = require('material-ui'); 11 | const { grey300 } = require('material-ui/styles/colors'); 12 | const EmptyIcon = require('material-ui/svg-icons/action/assignment-late').default; 13 | const WPostWrite = require('./WPostWrite'); 14 | const parser = require('../js/parser'); 15 | const Utils = require('../js/Utils'); 16 | const shallowCompare = require('react-addons-shallow-compare'); 17 | const PropTypes = require('prop-types'); 18 | 19 | const tags = { 20 | circProgressStyel: { marginTop: -8 }, 21 | cardStyle: { marginTop: 8 }, 22 | emptyStyle: { width: 180, height: 180 }, 23 | h1Style: { color: grey300, fontWeight: 300 }, 24 | contentStyle: { padding: '0px 8px 8px 8px' }, 25 | }; 26 | 27 | class ContentBarPosts extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | 31 | this.state = { 32 | change: null, 33 | sendButtonContent: this.props.local.post_send, 34 | }; 35 | 36 | // Binding context 37 | this.onChange = this.onChange.bind(this); 38 | this.cancelEditPost = this.cancelEditPost.bind(this); 39 | } 40 | 41 | shouldComponentUpdate(nextProps, nextState) { 42 | return shallowCompare(this, nextProps, nextState); 43 | } 44 | 45 | onChangeEnd() { 46 | this.setState({ 47 | change: null, 48 | sendButtonContent: this.props.local.post_send, 49 | }); 50 | } 51 | 52 | onChange(data) { 53 | const postData = data; 54 | postData.onEnd = () => this.onChangeEnd(); 55 | postData.onStart = () => this.setState({ 56 | sendButtonContent: 57 | , 62 | }); 63 | postData.post = this.getPostByUid(this.state.change.uid); 64 | 65 | this.props.signal.call('ChangePost', [postData]); 66 | } 67 | 68 | getPostByUid(uid) { 69 | for (let i = 0; i < this.props.posts.length; ++i) { 70 | if (this.props.posts[i].uid === uid) return this.props.posts[i]; 71 | } 72 | return null; 73 | } 74 | 75 | editPost(uid) { 76 | this.setState({ 77 | change: { 78 | uid, 79 | }, 80 | }); 81 | } 82 | 83 | cancelEditPost() { 84 | this.setState({ 85 | change: null, 86 | }); 87 | } 88 | 89 | deletePost(uid) { 90 | this.props.signal.call('DeletePost', [uid, () => this.forceUpdate()]); 91 | } 92 | 93 | render() { 94 | let content; 95 | 96 | if (this.state.change === null) { 97 | if (this.props.posts.length > 0) { 98 | content = []; 99 | this.props.posts.map(post => { 100 | const html = parser({ 101 | mode: post.parse_mode, 102 | data: post.text, 103 | }); 104 | 105 | content.push( 106 | 107 | {chat.name})} 109 | subtitle={Utils.getDateString(new Date(post.date * 1000))} 110 | /> 111 | 112 |
113 |               
114 |               
115 |                  this.deletePost(post.uid)}
117 |                   secondary
118 |                   label={this.props.local.posts_delete}
119 |                 />
120 |                  this.editPost(post.uid)}
122 |                   label={this.props.local.posts_edit}
123 |                 />
124 |               
125 |             
126 |           );
127 |           return null;
128 |         });
129 |       } else {
130 |         content = (
131 |           
132 | 133 |

{this.props.local.posts_empty}

134 |
135 | ); 136 | } 137 | } else { 138 | const post = this.getPostByUid(this.state.change.uid); 139 | const settings = { 140 | disableNotification: post.disableNotification, 141 | disablePreview: post.disablePreview, 142 | parser: post.parse_mode, 143 | }; 144 | content = ( 145 | ); 153 | } 154 | 155 | return ( 156 |
157 | {content} 158 |
159 | ); 160 | } 161 | } 162 | ContentBarPosts.propTypes = { 163 | signal: PropTypes.object, 164 | local: PropTypes.object, 165 | posts: PropTypes.array, 166 | }; 167 | 168 | module.exports = ContentBarPosts; 169 | -------------------------------------------------------------------------------- /app/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | List, 4 | Divider, 5 | Dialog, 6 | FlatButton, 7 | SelectField, 8 | MenuItem, 9 | Toggle, 10 | Checkbox, 11 | } = require('material-ui'); 12 | const { Layout, Fixed, Flex } = require('react-layout-pane'); 13 | const SideBarMenu = require('./SideBarMenu'); 14 | const SideBarChannels = require('./SideBarChannels'); 15 | const SideBarBotProfile = require('./SideBarBotProfile'); 16 | const { shell } = window; 17 | const shallowCompare = require('react-addons-shallow-compare'); 18 | const PropTypes = require('prop-types'); 19 | 20 | const tags = { 21 | topSideBarStyle: { height: '100%', overflow: 'auto' }, 22 | listStyle: { width: '100%' }, 23 | h4Style: { margin: 0 }, 24 | dialogStyle: { width: 310 }, 25 | }; 26 | 27 | class SideBar extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | 31 | this.state = { 32 | preferencesDialog: false, 33 | aboutDialog: false, 34 | }; 35 | 36 | this.props.signal.register('MenuPreferences', () => this.setState({ preferencesDialog: true })); 37 | this.props.signal.register('MenuAbout', () => this.setState({ aboutDialog: true })); 38 | 39 | // Binding context 40 | this.closePreferencesDialog = this.closePreferencesDialog.bind(this); 41 | this.closeAboutDialog = this.closeAboutDialog.bind(this); 42 | this.languageChange = this.languageChange.bind(this); 43 | this.darkThemeChange = this.darkThemeChange.bind(this); 44 | this.formattingStyleChange = this.formattingStyleChange.bind(this); 45 | 46 | this.openGithub = () => this.openBrowser('https://github.com/40PK/Chad'); 47 | this.openSite = () => this.openBrowser('https://40pk.github.io/Chad/'); 48 | this.openTelegram = () => this.openBrowser('https://telegram.me/perkovec/'); 49 | this.openVK = () => this.openBrowser('https://vk.com/id120146182'); 50 | this.openEmail = () => this.openBrowser('mailto:perkovec24@gmail.com'); 51 | this.openLinkedIn = () => this.openBrowser('https://www.linkedin.com/in/perkovec'); 52 | } 53 | 54 | shouldComponentUpdate(nextProps, nextState) { 55 | return shallowCompare(this, nextProps, nextState); 56 | } 57 | 58 | formattingStyleChange(event, index, value) { 59 | const data = this.props.data.postWriteDefaults; 60 | data.parser = value; 61 | this.props.signal.call('PostWriteDefaultsChange', [data]); 62 | } 63 | 64 | checkboxChange(type, event, isInputChecked) { 65 | const data = this.props.data.postWriteDefaults; 66 | data[type] = isInputChecked; 67 | this.props.signal.call('PostWriteDefaultsChange', [data]); 68 | } 69 | 70 | openBrowser(url) { 71 | shell.openExternal(url); 72 | } 73 | 74 | closePreferencesDialog() { 75 | this.setState({ preferencesDialog: false }); 76 | } 77 | 78 | closeAboutDialog() { 79 | this.setState({ aboutDialog: false }); 80 | } 81 | 82 | languageChange(event, index, value) { 83 | this.props.signal.call('LanguageChange', [value]); 84 | } 85 | 86 | darkThemeChange(event) { 87 | this.props.signal.call('DarkThemeChange', [event.target.checked]); 88 | } 89 | 90 | render() { 91 | const actionsPreferences = [ 92 | , 97 | ]; 98 | 99 | const actionsAbout = [ 100 | , 105 | ]; 106 | 107 | return ( 108 | 109 | 110 | 111 | 115 | 116 | 121 | 122 | 123 | 124 | 125 | 133 | 134 | 135 | 142 |

{this.props.local.d_preferences_general}:

143 | 148 | 149 | 150 | 151 | 156 |
157 |

{this.props.local.d_preferences_default_postwrite}:

158 | 163 | 164 | 165 | 166 | 167 | this.checkboxChange('disablePreview', e, i)} 171 | label={this.props.local.post_settings_disable_link_preview} 172 | /> 173 | this.checkboxChange('disableNotification', e, i)} 177 | label={this.props.local.post_settings_disable_notification} 178 | /> 179 |
180 | 181 | 188 | Source code: Github
189 | Site: perkovec.github.io/Chad
190 |
191 | Developed by Perkovec:
192 | Telegram: @Perkovec
193 | VK: Ilya Perkovec
194 | E-mail: perkovec24@gmail.com
195 | LinkedIn: Ilya Perkovec 196 |
197 |
198 | ); 199 | } 200 | } 201 | SideBar.propTypes = { 202 | signal: PropTypes.object, 203 | data: PropTypes.object, 204 | local: PropTypes.object, 205 | }; 206 | 207 | module.exports = SideBar; 208 | -------------------------------------------------------------------------------- /app/components/SideBarBotProfile.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | Divider, 4 | Avatar, 5 | ListItem, 6 | IconButton, 7 | IconMenu, 8 | MenuItem, 9 | FlatButton, 10 | TextField, 11 | Dialog, 12 | CircularProgress, 13 | } = require('material-ui'); 14 | const MoreVertIcon = require('material-ui/svg-icons/navigation/more-vert').default; 15 | const RefreshIcon = require('material-ui/svg-icons/navigation/refresh').default; 16 | const ChangeIcon = require('material-ui/svg-icons/editor/mode-edit').default; 17 | const RemoveIcon = require('material-ui/svg-icons/action/delete').default; 18 | const shallowCompare = require('react-addons-shallow-compare'); 19 | const PropTypes = require('prop-types'); 20 | 21 | const tags = { 22 | circProgressStyle: { marginTop: -5 }, 23 | origin: { horizontal: 'right', vertical: 'top' }, 24 | dialogStyle: { width: 310 }, 25 | }; 26 | 27 | class SideBarBotProfile extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | 31 | this.state = { 32 | load: false, 33 | avatarLoad: false, 34 | setAdminBotDialog: false, 35 | token: this.props.token || '', 36 | }; 37 | 38 | this.props.signal.register('SetLoadState', (s) => this.setState({ load: s })); 39 | this.props.signal.register('SetAvatarLoadState', (s) => this.setState({ avatarLoad: s })); 40 | 41 | // Binding context 42 | this.setAdminBot = this.setAdminBot.bind(this); 43 | this.openSetAdminBotDialog = this.openSetAdminBotDialog.bind(this); 44 | this.closeSetAdminBotDialog = this.closeSetAdminBotDialog.bind(this); 45 | this.removeAdminBot = this.removeAdminBot.bind(this); 46 | this.tokenChange = this.tokenChange.bind(this); 47 | } 48 | 49 | shouldComponentUpdate(nextProps, nextState) { 50 | return shallowCompare(this, nextProps, nextState); 51 | } 52 | 53 | setAdminBot() { 54 | this.props.signal.call('SetAdminBot', [this.state.token, this.closeSetAdminBotDialog]); 55 | } 56 | 57 | removeAdminBot() { 58 | this.props.signal.call('RemoveAdminBot'); 59 | } 60 | 61 | openSetAdminBotDialog() { 62 | this.setState({ setAdminBotDialog: true }); 63 | } 64 | 65 | closeSetAdminBotDialog() { 66 | this.setState({ setAdminBotDialog: false }); 67 | } 68 | 69 | tokenChange(event) { 70 | this.setState({ token: event.target.value }); 71 | } 72 | 73 | render() { 74 | let BotProfile; 75 | 76 | const BotAvatar = this.state.avatarLoad ? 77 | () : 78 | (); 79 | 80 | if (this.state.load) { 81 | BotProfile = (} 84 | />); 85 | } else if (this.props.name) { 86 | BotProfile = ( 87 | {this.props.name}} 90 | secondaryText={this.props.username} 91 | rightIconButton={ 92 | } 94 | targetOrigin={tags.origin} 95 | anchorOrigin={tags.origin} 96 | > 97 | } 99 | onClick={this.setAdminBot} 100 | primaryText={this.props.local.bot_refresh} 101 | /> 102 | } 104 | onClick={this.openSetAdminBotDialog} 105 | primaryText={this.props.local.bot_change} 106 | /> 107 | } 109 | onClick={this.removeAdminBot} 110 | primaryText={this.props.local.bot_remove} 111 | /> 112 | 113 | } 114 | />); 115 | } else { 116 | BotProfile = ( 117 | } 120 | primaryText={
{this.props.local.bot_set}
} 121 | />); 122 | } 123 | 124 | const actions = [ 125 | , 129 | , 134 | ]; 135 | 136 | return ( 137 |
138 | 139 | {BotProfile} 140 | 147 | 153 | 154 |
155 | ); 156 | } 157 | } 158 | SideBarBotProfile.propTypes = { 159 | token: PropTypes.string, 160 | avatar: PropTypes.string, 161 | name: PropTypes.string, 162 | username: PropTypes.string, 163 | local: PropTypes.object, 164 | signal: PropTypes.object, 165 | }; 166 | 167 | module.exports = SideBarBotProfile; 168 | -------------------------------------------------------------------------------- /app/components/SideBarChannels.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | ListItem, 4 | Checkbox, 5 | Dialog, 6 | FlatButton, 7 | TextField, 8 | IconButton, 9 | IconMenu, 10 | MenuItem, 11 | } = require('material-ui'); 12 | const { grey400 } = require('material-ui/styles/colors'); 13 | const ContentAdd = require('material-ui/svg-icons/content/add').default; 14 | const MoreVertIcon = require('material-ui/svg-icons/navigation/more-vert').default; 15 | const shallowCompare = require('react-addons-shallow-compare'); 16 | const PropTypes = require('prop-types'); 17 | 18 | const tags = { 19 | dialogStyle: { width: 310 }, 20 | }; 21 | 22 | class SideBarChannels extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = { 27 | newChannelDialog: false, 28 | newChannelName: '', 29 | newChannelUsername: '', 30 | }; 31 | this.checkboxRefs = {}; 32 | 33 | this.props.signal.register('SelectedChannels', () => this.selectedChannels()); 34 | 35 | // Binding context 36 | this.closeNewChannelDialog = this.closeNewChannelDialog.bind(this); 37 | this.newChannel = this.newChannel.bind(this); 38 | this.openNewChannelDialog = this.openNewChannelDialog.bind(this); 39 | } 40 | 41 | shouldComponentUpdate(nextProps, nextState) { 42 | return shallowCompare(this, nextProps, nextState); 43 | } 44 | 45 | openNewChannelDialog() { 46 | this.setState({ newChannelDialog: true }); 47 | } 48 | 49 | closeNewChannelDialog() { 50 | this.setState({ 51 | newChannelDialog: false, 52 | }, this.resetFields); 53 | } 54 | 55 | selectedChannels() { 56 | const checked = []; 57 | for (let i = 0; i < this.props.channels.length; ++i) { 58 | if (this.checkboxRefs[this.props.channels[i].uid].state.switched) { 59 | checked.push(this.props.channels[i].username); 60 | } 61 | } 62 | 63 | return checked; 64 | } 65 | 66 | textFieldChange(type, event) { 67 | const state = {}; 68 | state[type] = event.target.value; 69 | this.setState(state); 70 | } 71 | 72 | resetFields() { 73 | this.setState({ 74 | newChannelName: '', 75 | newChannelUsername: '', 76 | }); 77 | } 78 | 79 | newChannel() { 80 | this.props.signal.call('NewChannel', [ 81 | { 82 | name: this.state.newChannelName, 83 | username: this.state.newChannelUsername, 84 | }, 85 | ]); 86 | this.closeNewChannelDialog(); 87 | } 88 | 89 | removeChannel(uid) { 90 | this.props.signal.call('RemoveChannel', [uid, () => this.forceUpdate()]); 91 | } 92 | 93 | render() { 94 | const nestedItems = []; 95 | 96 | const actions = [ 97 | , 101 | , 106 | ]; 107 | 108 | this.props.channels.map((channel) => { 109 | nestedItems.push( 110 | }> 113 | this.removeChannel(channel.uid)}>Remove 114 | 115 | } 116 | leftCheckbox={ { this.checkboxRefs[channel.uid] = ref; }} />} 117 | key={channel.uid} 118 | primaryText={channel.name} 119 | />); 120 | return null; 121 | }); 122 | 123 | nestedItems.push(} 126 | key={100500} 127 | primaryText={this.props.local.channels_add_channel} 128 | />); 129 | 130 | return ( 131 |
132 | 137 | 144 | this.textFieldChange('newChannelName', e)} 147 | floatingLabelText={this.props.local.d_add_channel_name} 148 | hintText={this.props.local.d_add_channel_name_placeholder} 149 | /> 150 | this.textFieldChange('newChannelUsername', e)} 153 | floatingLabelText={this.props.local.d_add_channel_username} 154 | hintText="@mychannel" 155 | /> 156 | 157 |
158 | ); 159 | } 160 | } 161 | SideBarChannels.propTypes = { 162 | signal: PropTypes.object, 163 | channels: PropTypes.array, 164 | local: PropTypes.object, 165 | }; 166 | 167 | module.exports = SideBarChannels; 168 | -------------------------------------------------------------------------------- /app/components/SideBarMenu.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { ListItem } = require('material-ui'); 3 | const shallowCompare = require('react-addons-shallow-compare'); 4 | const PropTypes = require('prop-types'); 5 | 6 | class SideBarMenu extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.items = [ 11 | { 12 | key: 1, 13 | text: 'menu_write_post', 14 | onClick: () => this.props.signal.call('MenuWritePost'), 15 | }, { 16 | key: 2, 17 | text: 'menu_posts', 18 | onClick: () => this.props.signal.call('MenuPosts'), 19 | }, { 20 | key: 3, 21 | text: 'menu_drafts', 22 | onClick: () => this.props.signal.call('MenuDrafts'), 23 | }, { 24 | key: 4, 25 | text: 'menu_preferences', 26 | onClick: () => this.props.signal.call('MenuPreferences'), 27 | }, { 28 | key: 5, 29 | text: 'menu_about', 30 | onClick: () => this.props.signal.call('MenuAbout'), 31 | }, 32 | ]; 33 | } 34 | 35 | shouldComponentUpdate(nextProps, nextState) { 36 | return shallowCompare(this, nextProps, nextState); 37 | } 38 | 39 | render() { 40 | const nestedItems = []; 41 | 42 | this.items.map((item) => { 43 | nestedItems.push( 44 | ); 49 | return null; 50 | }); 51 | 52 | return ( 53 | 58 | ); 59 | } 60 | } 61 | SideBarMenu.propTypes = { 62 | signal: PropTypes.object, 63 | local: PropTypes.object, 64 | }; 65 | 66 | module.exports = SideBarMenu; 67 | -------------------------------------------------------------------------------- /app/components/WPostWrite.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | const React = require('react'); 3 | const { 4 | SelectField, 5 | MenuItem, 6 | RaisedButton, 7 | FlatButton, 8 | Dialog, 9 | Checkbox, 10 | } = require('material-ui'); 11 | const { Layout, Fixed, Flex } = require('react-layout-pane'); 12 | const shallowCompare = require('react-addons-shallow-compare'); 13 | const WPostWriteInput = require('./WPostWriteInput'); 14 | const WPostWritePreview = require('./WPostWritePreview'); 15 | const PropTypes = require('prop-types'); 16 | 17 | const tags = { 18 | buttonStyle: { 19 | margin: '8px 0 0 8px', 20 | float: 'right', 21 | }, 22 | constentStyle: { padding: '0 8px 8px 8px' }, 23 | settingsButtonStyle: { margin: '8px 8px 0 0' }, 24 | previewContainerStyle: { height: 100 }, 25 | dialogStyle: { width: 350 }, 26 | }; 27 | 28 | class WPostWrite extends React.Component { 29 | constructor(props) { 30 | super(props); 31 | 32 | const state = { 33 | parser: 'none', 34 | disablePreview: false, 35 | disableNotification: false, 36 | preview: '', 37 | settingsDialog: false, 38 | }; 39 | 40 | if (this.props.settings) { 41 | if (this.props.settings.parser) state.parser = this.props.settings.parser; 42 | if (this.props.settings.disablePreview) state.disablePreview = true; 43 | if (this.props.settings.disableNotification) state.disableNotification = true; 44 | } 45 | 46 | this.state = state; 47 | 48 | this.inputRef = null; 49 | this.previewRef = null; 50 | 51 | // Binding context 52 | this.onSend = this.onSend.bind(this); 53 | this.saveDraft = this.saveDraft.bind(this); 54 | this.openSettingsDialog = this.openSettingsDialog.bind(this); 55 | this.closeSettingsDialog = this.closeSettingsDialog.bind(this); 56 | this.formattingStyleChange = this.formattingStyleChange.bind(this); 57 | this.checkParser = this.checkParser.bind(this); 58 | this.onInputRef = this.onInputRef.bind(this); 59 | this.onPreviewRef = this.onPreviewRef.bind(this); 60 | this.updatePreview = this.updatePreview.bind(this); 61 | this.getParser = this.getParser.bind(this); 62 | } 63 | 64 | componentWillReceiveProps(newProps) { 65 | const state = this.state; 66 | if (newProps.settings) { 67 | if (newProps.settings.parser) state.parser = newProps.settings.parser; 68 | if (newProps.settings.disablePreview) state.disablePreview = true; 69 | if (newProps.settings.disableNotification) state.disableNotification = true; 70 | } 71 | 72 | this.setState(state, () => this.updatePreview(this.inputRef.getText())); 73 | } 74 | 75 | shouldComponentUpdate(nextProps, nextState) { 76 | return shallowCompare(this, nextProps, nextState); 77 | } 78 | 79 | onSend() { 80 | this.props.onSend({ 81 | text: this.inputRef.getText(), 82 | parser: this.state.parser, 83 | disablePreview: this.state.disablePreview, 84 | disableNotification: this.state.disableNotification, 85 | }); 86 | } 87 | 88 | onInputRef(ref) { 89 | this.inputRef = ref; 90 | } 91 | 92 | onPreviewRef(ref) { 93 | this.previewRef = ref; 94 | } 95 | 96 | getParser() { 97 | return this.state.parser; 98 | } 99 | 100 | clearText() { 101 | this.inputRef.clearText(); 102 | } 103 | 104 | saveDraft() { 105 | this.props.onSaveDraft({ 106 | text: this.inputRef.getText(), 107 | parser: this.state.parser, 108 | disablePreview: this.state.disablePreview, 109 | disableNotification: this.state.disableNotification, 110 | }); 111 | } 112 | 113 | checkboxChange(type, event, isInputChecked) { 114 | const state = {}; 115 | state[type] = isInputChecked; 116 | this.setState(state); 117 | } 118 | 119 | updatePreview(text) { 120 | this.previewRef.updatePreview(text, this.state.parser); 121 | } 122 | 123 | formattingStyleChange(event, index, value) { 124 | this.setState({ 125 | parser: value, 126 | }, () => this.updatePreview(this.inputRef.getText())); 127 | } 128 | 129 | fieldChange(event, type) { 130 | const state = {}; 131 | state[type] = event.target.value; 132 | this.setState(state); 133 | } 134 | 135 | closeSettingsDialog() { 136 | this.setState({ settingsDialog: false }); 137 | } 138 | 139 | openSettingsDialog() { 140 | this.setState({ settingsDialog: true }); 141 | } 142 | 143 | checkParser() { 144 | if (this.state.parser === 'none') { 145 | return alert(this.props.local.alert_select_formatting_style); 146 | } 147 | 148 | return true; 149 | } 150 | 151 | render() { 152 | const settingsActions = [ 153 | , 158 | ]; 159 | 160 | let sendButton; 161 | if (typeof this.props.sendButtonContent === 'string') { 162 | sendButton = (); 168 | } else { 169 | sendButton = (); 175 | } 176 | 177 | return ( 178 | 179 | 180 | 188 | 189 | 190 | 191 | 192 | 198 | 199 | 200 | {sendButton} 201 | 206 | {this.props.onCancel && ( 207 | 212 | )} 213 | 218 | 219 | 226 | 231 | 232 | 233 | 234 | 235 | this.checkboxChange('disablePreview', e, i)} 239 | label={this.props.local.post_settings_disable_link_preview} 240 | /> 241 | this.checkboxChange('disableNotification', e, i)} 245 | label={this.props.local.post_settings_disable_notification} 246 | /> 247 | 248 | 249 | 250 | 251 | 252 | ); 253 | } 254 | } 255 | WPostWrite.propTypes = { 256 | settings: PropTypes.object, 257 | local: PropTypes.object, 258 | text: PropTypes.string, 259 | sendButtonContent: PropTypes.oneOfType([ 260 | PropTypes.string, 261 | PropTypes.element, 262 | ]), 263 | onSend: PropTypes.func, 264 | onSaveDraft: PropTypes.func, 265 | onCancel: PropTypes.func, 266 | }; 267 | 268 | module.exports = WPostWrite; 269 | -------------------------------------------------------------------------------- /app/components/WPostWriteInput.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | RaisedButton, 4 | TextField, 5 | Divider, 6 | FlatButton, 7 | Dialog, 8 | } = require('material-ui'); 9 | const shallowCompare = require('react-addons-shallow-compare'); 10 | const PropTypes = require('prop-types'); 11 | 12 | const tags = { 13 | buttonStyle: { 14 | margin: '8px 0 0 8px', 15 | float: 'right', 16 | }, 17 | constrolButtonStyle: { margin: '8px 0 0 8px' }, 18 | dialogStyle: { width: 350 }, 19 | dividerStyle: { marginTop: 10 }, 20 | submitStyle: { display: 'none' }, 21 | }; 22 | 23 | class WPostWriteInput extends React.Component { 24 | constructor(props) { 25 | super(props); 26 | 27 | this.state = { 28 | text: this.props.text || '', 29 | insertLinkTitle: '', 30 | insertLinkURL: '', 31 | insertLinkDialog: false, 32 | }; 33 | 34 | this.inputRef = null; 35 | this.insertLinkTitleRef = null; 36 | this.insertLinkURLRef = null; 37 | 38 | // Binding context 39 | this.insertLink = this.insertLink.bind(this); 40 | this.insertBold = this.insertBold.bind(this); 41 | this.insertItalic = this.insertItalic.bind(this); 42 | this.insertStrikethrough = this.insertStrikethrough.bind(this); 43 | this.insertUnderline = this.insertUnderline.bind(this); 44 | this.insertHiddenLink = this.insertHiddenLink.bind(this); 45 | this.onInputRef = this.onInputRef.bind(this); 46 | this.postTextChange = this.postTextChange.bind(this); 47 | this.titleFieldChange = this.titleFieldChange.bind(this); 48 | this.urlFieldChange = this.urlFieldChange.bind(this); 49 | this.closeInsertLinkDialog = this.closeInsertLinkDialog.bind(this); 50 | this.insertLinkInText = this.insertLinkInText.bind(this); 51 | this.clearText = this.clearText.bind(this); 52 | this.getText = this.getText.bind(this); 53 | } 54 | 55 | shouldComponentUpdate(nextProps, nextState) { 56 | return shallowCompare(this, nextProps, nextState); 57 | } 58 | 59 | onInputRef(ref) { 60 | if (this.inputRef === null) this.inputRef = ref.input.refs.input; 61 | } 62 | 63 | getText() { 64 | return this.state.text; 65 | } 66 | 67 | clearText() { 68 | this.setState({ 69 | text: '', 70 | }, () => this.props.updatePreview('')); 71 | } 72 | 73 | insertUnderline() { 74 | if (!this.props.checkParser()) return; 75 | 76 | const selStart = this.inputRef.selectionStart; 77 | const selEnd = this.inputRef.selectionEnd; 78 | 79 | let text = this.state.text.substring(selStart, selEnd); 80 | if (text.length === 0) text = 'underline'; 81 | 82 | const parser = this.props.getParser(); 83 | 84 | let res; 85 | let padLeft; 86 | if (parser === 'markdown') { 87 | res = `__${text}__`; 88 | padLeft = 2; 89 | } else if (parser === 'HTML') { 90 | res = `${text}`; 91 | padLeft = 3; 92 | } 93 | 94 | this.replaceText(selStart, selEnd, padLeft, text.length, res); 95 | } 96 | 97 | insertStrikethrough() { 98 | if (!this.props.checkParser()) return; 99 | 100 | const selStart = this.inputRef.selectionStart; 101 | const selEnd = this.inputRef.selectionEnd; 102 | 103 | let text = this.state.text.substring(selStart, selEnd); 104 | if (text.length === 0) text = 'strikethrough'; 105 | 106 | const parser = this.props.getParser(); 107 | 108 | let res; 109 | let padLeft; 110 | if (parser === 'markdown') { 111 | res = `~${text}~`; 112 | padLeft = 1; 113 | } else if (parser === 'HTML') { 114 | res = `${text}`; 115 | padLeft = 3; 116 | } 117 | 118 | this.replaceText(selStart, selEnd, padLeft, text.length, res); 119 | } 120 | 121 | insertItalic() { 122 | if (!this.props.checkParser()) return; 123 | 124 | const selStart = this.inputRef.selectionStart; 125 | const selEnd = this.inputRef.selectionEnd; 126 | 127 | let text = this.state.text.substring(selStart, selEnd); 128 | if (text.length === 0) text = 'italic'; 129 | 130 | const parser = this.props.getParser(); 131 | 132 | let res; 133 | let padLeft; 134 | if (parser === 'markdown') { 135 | res = `_${text}_`; 136 | padLeft = 1; 137 | } else if (parser === 'HTML') { 138 | res = `${text}`; 139 | padLeft = 3; 140 | } 141 | 142 | this.replaceText(selStart, selEnd, padLeft, text.length, res); 143 | } 144 | 145 | insertBold() { 146 | if (!this.props.checkParser()) return; 147 | 148 | const selStart = this.inputRef.selectionStart; 149 | const selEnd = this.inputRef.selectionEnd; 150 | 151 | let text = this.state.text.substring(selStart, selEnd); 152 | if (text.length === 0) text = 'bold'; 153 | 154 | const parser = this.props.getParser(); 155 | 156 | let res; 157 | let padLeft; 158 | if (parser === 'markdown') { 159 | res = `*${text}*`; 160 | padLeft = 1; 161 | } else if (parser === 'HTML') { 162 | res = `${text}`; 163 | padLeft = 3; 164 | } 165 | 166 | this.replaceText(selStart, selEnd, padLeft, text.length, res); 167 | } 168 | 169 | insertLinkInText(e) { 170 | e.preventDefault(); 171 | 172 | const selStart = this.inputRef.selectionStart; 173 | const selEnd = this.inputRef.selectionEnd; 174 | 175 | const parser = this.props.getParser(); 176 | 177 | let res; 178 | let padLeft; 179 | if (parser === 'markdown') { 180 | res = `[${this.state.insertLinkTitle}](${this.state.insertLinkURL})`; 181 | padLeft = 1; 182 | } else if (parser === 'HTML') { 183 | res = `${this.state.insertLinkTitle}`; 184 | padLeft = 9; 185 | } 186 | 187 | this.replaceText(selStart, selEnd, padLeft, this.state.insertLinkTitle.length, res); 188 | this.closeInsertLinkDialog(); 189 | 190 | return false; 191 | } 192 | 193 | insertLink() { 194 | if (!this.props.checkParser()) return; 195 | 196 | const selStart = this.inputRef.selectionStart; 197 | const selEnd = this.inputRef.selectionEnd; 198 | 199 | const title = (selStart === selEnd) ? '' : this.state.text.substring(selStart, selEnd); 200 | 201 | this.setState({ 202 | insertLinkTitle: title, 203 | insertLinkURL: '', 204 | }, this.openInsertLinkDialog); 205 | } 206 | 207 | insertHiddenLink() { 208 | const parser = this.props.getParser(); 209 | if (parser != "HTML") 210 | return alert(this.props.local.alert_hidden_link_not_allowed); 211 | 212 | const selStart = this.inputRef.selectionStart; 213 | const selEnd = this.inputRef.selectionEnd; 214 | 215 | let text = this.state.text.substring(selStart, selEnd); 216 | if (text.length === 0) text = 'your_link'; 217 | 218 | const res = ``; 219 | const padLeft = 9; 220 | 221 | this.replaceText(selStart, selEnd, padLeft, text.length, res); 222 | } 223 | 224 | replaceText(start, end, paddingLeft, dataLength, text) { 225 | const postText = this.state.text; 226 | this.setState({ 227 | text: postText.substring(0, start) + 228 | text + postText.substring(end, postText.length), 229 | }, () => { 230 | this.props.updatePreview(this.state.text); 231 | this.inputRef.focus(); 232 | let padLeft = start + paddingLeft; 233 | this.inputRef.setSelectionRange(padLeft, padLeft + dataLength); 234 | }); 235 | } 236 | 237 | closeInsertLinkDialog() { 238 | this.setState({ insertLinkDialog: false }); 239 | } 240 | 241 | openInsertLinkDialog() { 242 | this.setState({ 243 | insertLinkDialog: true, 244 | }, () => { 245 | if (this.state.insertLinkTitle) { 246 | this.insertLinkURLRef.focus(); 247 | } else { 248 | this.insertLinkTitleRef.focus(); 249 | } 250 | }); 251 | } 252 | 253 | postTextChange(event) { 254 | this.setState({ 255 | text: event.target.value, 256 | }, () => this.props.updatePreview(this.state.text)); 257 | } 258 | 259 | titleFieldChange(event) { 260 | this.setState({ insertLinkTitle: event.target.value }); 261 | } 262 | 263 | urlFieldChange(event) { 264 | this.setState({ insertLinkURL: event.target.value }); 265 | } 266 | 267 | render() { 268 | const linkActions = [ 269 | , 274 | , 279 | ]; 280 | 281 | return ( 282 |
283 | 288 | 293 | 298 | 303 | 308 | 313 | 314 | 323 | 324 | 331 |
332 | { this.insertLinkTitleRef = ref; }} 334 | value={this.state.insertLinkTitle} 335 | onChange={this.titleFieldChange} 336 | floatingLabelText={this.props.local.d_insert_link_title} 337 | /> 338 | { this.insertLinkURLRef = ref; }} 340 | value={this.state.insertLinkURL} 341 | onChange={this.urlFieldChange} 342 | floatingLabelText={this.props.local.d_insert_link_url} 343 | /> 344 |
347 |
348 | ); 349 | } 350 | } 351 | WPostWriteInput.propTypes = { 352 | text: PropTypes.string, 353 | local: PropTypes.object, 354 | updatePreview: PropTypes.func, 355 | checkParser: PropTypes.func, 356 | getParser: PropTypes.func, 357 | }; 358 | 359 | module.exports = WPostWriteInput; 360 | -------------------------------------------------------------------------------- /app/components/WPostWritePreview.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { 3 | Paper, 4 | Subheader, 5 | Divider, 6 | } = require('material-ui'); 7 | const { Layout, Fixed, Flex } = require('react-layout-pane'); 8 | const previewParser = require('../js/parser'); 9 | const shallowCompare = require('react-addons-shallow-compare'); 10 | const PropTypes = require('prop-types'); 11 | 12 | const tags = { 13 | previewStyle: { width: '100%', height: '100%' }, 14 | previewContainerStyle: { height: 100 }, 15 | }; 16 | 17 | class WPostWritePreview extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.state = { 22 | preview: previewParser({ 23 | data: this.props.text, 24 | mode: this.props.parser, 25 | }), 26 | }; 27 | 28 | this.updatePreview = this.updatePreview.bind(this); 29 | } 30 | 31 | shouldComponentUpdate(nextProps, nextState) { 32 | return shallowCompare(this, nextProps, nextState); 33 | } 34 | 35 | updatePreview(text, parser) { 36 | this.setState({ 37 | preview: previewParser({ 38 | data: text, 39 | mode: parser, 40 | }), 41 | }); 42 | } 43 | 44 | render() { 45 | return ( 46 | 47 | 48 | 49 | {this.props.local.preview} 50 | 51 | 52 | 53 |
54 |
58 |             
59 |
60 |
61 |
62 | ); 63 | } 64 | } 65 | WPostWritePreview.propTypes = { 66 | text: PropTypes.string, 67 | parser: PropTypes.string, 68 | local: PropTypes.object, 69 | }; 70 | 71 | module.exports = WPostWritePreview; 72 | -------------------------------------------------------------------------------- /app/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('../fonts/Roboto-Black.ttf') format('truetype'); 4 | font-weight: 900; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | src: url('../fonts/Roboto-BlackItalic.ttf') format('truetype'); 11 | font-weight: 900; 12 | font-style: italic; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | src: url('../fonts/Roboto-Bold.ttf') format('truetype'); 18 | font-weight: 700; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: 'Roboto'; 24 | src: url('../fonts/Roboto-BoldItalic.ttf') format('truetype'); 25 | font-weight: 700; 26 | font-style: italic; 27 | } 28 | 29 | @font-face { 30 | font-family: 'Roboto'; 31 | src: url('../fonts/Roboto-Medium.ttf') format('truetype'); 32 | font-weight: 500; 33 | font-style: normal; 34 | } 35 | 36 | @font-face { 37 | font-family: 'Roboto'; 38 | src: url('../fonts/Roboto-MediumItalic.ttf') format('truetype'); 39 | font-weight: 500; 40 | font-style: italic; 41 | } 42 | 43 | @font-face { 44 | font-family: 'Roboto'; 45 | src: url('../fonts/Roboto-Regular.ttf') format('truetype'); 46 | font-weight: 400; 47 | font-style: normal; 48 | } 49 | 50 | @font-face { 51 | font-family: 'Roboto'; 52 | src: url('../fonts/Roboto-Italic.ttf') format('truetype'); 53 | font-weight: 400; 54 | font-style: italic; 55 | } 56 | 57 | @font-face { 58 | font-family: 'Roboto'; 59 | src: url('../fonts/Roboto-Light.ttf') format('truetype'); 60 | font-weight: 300; 61 | font-style: normal; 62 | } 63 | 64 | @font-face { 65 | font-family: 'Roboto'; 66 | src: url('../fonts/Roboto-LightItalic.ttf') format('truetype'); 67 | font-weight: 300; 68 | font-style: italic; 69 | } 70 | 71 | @font-face { 72 | font-family: 'Roboto'; 73 | src: url('../fonts/Roboto-Thin.ttf') format('truetype'); 74 | font-weight: 100; 75 | font-style: normal; 76 | } 77 | 78 | @font-face { 79 | font-family: 'Roboto'; 80 | src: url('../fonts/Roboto-ThinItalic.ttf') format('truetype'); 81 | font-weight: 100; 82 | font-style: italic; 83 | } 84 | 85 | body { 86 | margin: 0 auto; 87 | } 88 | 89 | #container { 90 | height: 100%; 91 | width: 100%; 92 | font-family: 'Roboto'; 93 | } 94 | 95 | .content-bar { 96 | width: 100%; 97 | height: 100%; 98 | position: relative; 99 | } 100 | 101 | .empty-placeholder { 102 | text-align: center; 103 | position: absolute; 104 | top: 50%; 105 | left: 0; 106 | right: 0; 107 | transform: translateY(-50%); 108 | } 109 | 110 | .ellipsis { 111 | text-overflow: ellipsis; 112 | white-space: nowrap; 113 | overflow: hidden; 114 | } 115 | 116 | .preview-overflow { 117 | overflow: auto; 118 | height: 100%; 119 | } 120 | 121 | .preview { 122 | font-size: 16px; 123 | font-family: inherit; 124 | margin: 5px; 125 | white-space: pre-wrap; 126 | word-wrap: break-word; 127 | } 128 | 129 | .preview-code { 130 | margin: 1em 0; 131 | } 132 | 133 | .preview-code-inline { 134 | margin: 0; 135 | display: inline; 136 | } 137 | 138 | .overlay { 139 | position: absolute; 140 | width: 100%; 141 | height: 100%; 142 | background: #FFF; 143 | z-index: 100; 144 | } 145 | 146 | .overlay > img { 147 | position: absolute; 148 | top: 50%; 149 | left: 50%; 150 | transform: translate(-50%, -50%); 151 | } 152 | 153 | .aboutLink { 154 | color: rgb(0, 151, 167); 155 | cursor: pointer; 156 | } 157 | -------------------------------------------------------------------------------- /app/fonts/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Black.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/app/fonts/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chad 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/js/API.js: -------------------------------------------------------------------------------- 1 | const request = require('superagent'); 2 | 3 | class TelegramAPI { 4 | constructor(token) { 5 | this.token = token; 6 | this.methodURL = `https://api.telegram.org/bot${token}/`; 7 | this.fileURL = `https://api.telegram.org/file/bot${token}/`; 8 | } 9 | 10 | post(methodName, params) { 11 | return request 12 | .post(this.methodURL + methodName) 13 | .send(params); 14 | } 15 | 16 | getMe() { 17 | return this.post('getMe', {}); 18 | } 19 | 20 | getUserProfilePhotos(params) { 21 | return this.post('getUserProfilePhotos', params); 22 | } 23 | 24 | getFile(params) { 25 | return this.post('getFile', params); 26 | } 27 | 28 | getBlobFile(path) { 29 | return new Promise((resolve) => { 30 | const xhr = new XMLHttpRequest(); 31 | xhr.onreadystatechange = (res) => { 32 | if (res.target.readyState === 4 && res.target.status === 200) { 33 | resolve(res.target.response); 34 | } 35 | }; 36 | xhr.open('GET', this.fileURL + path); 37 | xhr.responseType = 'blob'; 38 | xhr.send(); 39 | }); 40 | } 41 | 42 | getBase64Avatar(id) { 43 | return new Promise((resolve) => { 44 | this.getUserProfilePhotos({ 45 | user_id: id, 46 | limit: 1, 47 | }) 48 | .then((res) => { 49 | const photos = res.body; 50 | if (photos.ok && photos.result.photos.length > 0) { 51 | return photos.result.photos[0]; 52 | } 53 | resolve(null); 54 | return null; 55 | }) 56 | .then((photo) => { 57 | if (photo) { 58 | return this.getFile({ 59 | file_id: photo[photo.length - 1].file_id, 60 | }); 61 | } 62 | resolve(null); 63 | return null; 64 | }) 65 | .then((res) => { 66 | if (res) { 67 | const file = res.body; 68 | if (file.ok) { 69 | return this.getBlobFile(file.result.file_path); 70 | } 71 | } 72 | resolve(null); 73 | return null; 74 | }) 75 | .then((blob) => { 76 | if (blob) { 77 | const reader = new FileReader(); 78 | reader.onloadend = () => resolve(reader.result); 79 | reader.readAsDataURL(blob); 80 | } else { 81 | resolve(null); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | sendMessage(params) { 88 | return this.post('sendMessage', params); 89 | } 90 | 91 | editMessageText(params) { 92 | return this.post('editMessageText', params); 93 | } 94 | } 95 | 96 | module.exports = TelegramAPI; 97 | -------------------------------------------------------------------------------- /app/js/DataRepair.js: -------------------------------------------------------------------------------- 1 | const { remote } = window; 2 | 3 | const initialVersions = { 4 | settings: '3.0', 5 | posts: '3.0', 6 | bot: '3.0', 7 | channels: '3.0', 8 | botavatar: '3.0', 9 | drafts: '3.0', 10 | }; 11 | 12 | const defaultSettings = { 13 | lang: 'en', 14 | darkTheme: false, 15 | postWriteDefaults: { 16 | parser: 'none', 17 | disablePreview: false, 18 | disableNotification: false, 19 | }, 20 | }; 21 | 22 | class DataRepair { 23 | static repair() { 24 | let lastVersion = localStorage.getItem('data_version'); 25 | if (!lastVersion) { 26 | lastVersion = initialVersions; 27 | } else { 28 | lastVersion = JSON.parse(lastVersion); 29 | } 30 | 31 | const currentVersion = remote.getGlobal('data_version'); 32 | const newSettingsVer = DataRepair.processSettings( 33 | lastVersion.settings, 34 | currentVersion.settings); 35 | 36 | lastVersion.settings = newSettingsVer; 37 | 38 | localStorage.setItem('data_version', JSON.stringify(lastVersion)); 39 | } 40 | 41 | static processSettings(oldVer, newVer) { 42 | let settings = localStorage.getItem('settings'); 43 | if (!settings) { 44 | settings = defaultSettings; 45 | localStorage.setItem('settings', JSON.stringify(settings)); 46 | return newVer; 47 | } 48 | settings = JSON.parse(settings); 49 | 50 | if (oldVer === newVer) return newVer; 51 | 52 | if (oldVer === '3.0' && newVer === '3.1') { 53 | settings.postWriteDefaults = defaultSettings.postWriteDefaults; 54 | } 55 | 56 | localStorage.setItem('settings', JSON.stringify(settings)); 57 | return newVer; 58 | } 59 | } 60 | 61 | module.exports = DataRepair.repair; 62 | -------------------------------------------------------------------------------- /app/js/Signal.js: -------------------------------------------------------------------------------- 1 | class Signal { 2 | constructor() { 3 | this.signals = {}; 4 | } 5 | 6 | register(type, cb) { 7 | this.signals[type] = cb; 8 | } 9 | 10 | call(type, args) { 11 | return this.signals[type].apply(null, args); 12 | } 13 | } 14 | 15 | module.exports = Signal; 16 | -------------------------------------------------------------------------------- /app/js/Utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-template */ 2 | class Utils { 3 | static getDateString(date) { 4 | return `${date.getFullYear()}-` + 5 | (`0${date.getMonth() + 1}`).slice(-2) + '-' + 6 | (`0${date.getDate()}`).slice(-2) + ' ' + 7 | (`0${date.getHours()}`).slice(-2) + ':' + 8 | (`0${date.getMinutes()}`).slice(-2); 9 | } 10 | 11 | static s4() { 12 | return Math.floor((1 + Math.random()) * 0x10000) 13 | .toString(16) 14 | .substring(1); 15 | } 16 | 17 | static uid() { 18 | return Utils.s4() + Utils.s4(); 19 | } 20 | 21 | static uid2() { 22 | return Utils.s4() + Utils.s4() + Utils.s4() + Utils.s4(); 23 | } 24 | } 25 | 26 | module.exports = Utils; 27 | -------------------------------------------------------------------------------- /app/js/parser.js: -------------------------------------------------------------------------------- 1 | const xss = require('xss'); 2 | 3 | const strongRegex = /\*([\s\S]+?)\*(?!\*)/g; 4 | const italicRegex = /_([\s\S]+?)_(?!_)/g; 5 | const underlineRegex = /__([\s\S]+?)__(?!__)/g; 6 | const strikethroughRegex = /~([\s\S]+?)~(?!~)/g; 7 | const urlRegex = /\[([\s\S]+?)\](?!\])\(([\s\S]+?)\)(?!\))/g; 8 | const inlinecodeRegex = /(`)\s*([\s\S]*?[^`])\s*\1(?!`)/g; 9 | const blockcodeRegex = /(```)\s*([\s\S]*?[^`])\s*\1(?!```)\n?/g; 10 | const linkRegex = /\((.*)\)/g; 11 | const titleRegex = /\[(.*)\]/g; 12 | 13 | function escapeHtml(str) { 14 | const div = document.createElement('div'); 15 | div.appendChild(document.createTextNode(str)); 16 | return div.innerHTML; 17 | } 18 | 19 | function parser(params = { mode: 'markdown', data: '' }) { 20 | const mode = params.mode; 21 | let data = params.data; 22 | 23 | if (mode === 'markdown') { 24 | data = escapeHtml(data); 25 | data = data 26 | .replace(blockcodeRegex, (sub) => { 27 | const code = sub.substring(3, sub.length - 4); 28 | 29 | return `
${code}
`; 30 | }) 31 | .replace(inlinecodeRegex, (sub) => { 32 | const code = sub.substring(1, sub.length - 1); 33 | 34 | return `
${code}
`; 35 | }) 36 | .replace(urlRegex, (sub) => { 37 | let link = sub.match(linkRegex)[0]; 38 | link = link.substring(1, link.length - 1); 39 | 40 | let title = sub.match(titleRegex)[0]; 41 | title = title.substring(1, title.length - 1); 42 | 43 | return `${title}`; 44 | }) 45 | .replace(underlineRegex, (sub) => { 46 | const text = sub.substring(2, sub.length - 2); 47 | 48 | return `${text}`; 49 | }) 50 | .replace(italicRegex, (sub) => { 51 | const text = sub.substring(1, sub.length - 1); 52 | 53 | return `${text}`; 54 | }) 55 | .replace(strongRegex, (sub) => { 56 | const text = sub.substring(1, sub.length - 1); 57 | 58 | return `${text}`; 59 | }) 60 | .replace(strikethroughRegex, (sub) => { 61 | const text = sub.substring(1, sub.length - 1); 62 | 63 | return `${text}`; 64 | }); 65 | 66 | return data; 67 | } else if (mode === 'HTML') { 68 | return xss(data, { 69 | allowedTags: ['b', 'i', 'em', 'strong', 'a', 'code', 'pre', 's', 'u'], 70 | whiteList: { 71 | b: [], 72 | i: [], 73 | em: [], 74 | strong: [], 75 | code: [], 76 | pre: [], 77 | s: [], 78 | u: [], 79 | a: ['href'], 80 | }, 81 | }); 82 | } 83 | 84 | return escapeHtml(data); 85 | } 86 | 87 | module.exports = parser; 88 | -------------------------------------------------------------------------------- /app/langs/en.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | menu: 'Menu', 3 | menu_write_post: 'Write post', 4 | menu_posts: 'Posts', 5 | menu_drafts: 'Drafts', 6 | menu_preferences: 'Preferences', 7 | menu_about: 'About Chad', 8 | 9 | channels: 'Channels', 10 | channels_add_channel: 'Add channel', 11 | 12 | bot_refresh: 'Refresh', 13 | bot_change: 'Change', 14 | bot_remove: 'Remove', 15 | bot_set: 'Set admin bot', 16 | 17 | settings: 'Settings', 18 | settings_formatting_styles: 'Formatting styles', 19 | settings_none: 'None', 20 | settings_html: 'HTML', 21 | settings_markdown: 'Markdown', 22 | settings_link: 'Link', 23 | settings_bold: 'Bold', 24 | settings_italic: 'Italic', 25 | settings_strikethrough: 'Strikethrough', 26 | settings_underline: 'Underline', 27 | settings_hidden_link: 'Hidden link', 28 | 29 | post_test: 'Post text', 30 | post_send: 'Send', 31 | post_save_draft: 'Save draft', 32 | post_cancel: 'Cancel', 33 | 34 | post_settings: 'Settings', 35 | post_settings_ok: 'OK', 36 | post_settings_disable_link_preview: 'Disable link preview', 37 | post_settings_disable_notification: 'Disable notification', 38 | 39 | preview: 'Preview', 40 | 41 | loading: 'Loading...', 42 | 43 | d_about: 'About Chad', 44 | d_about_close: 'Close', 45 | 46 | d_preferences: 'Preferences', 47 | d_preferences_language: 'Language', 48 | d_preferences_dark_theme: 'Dark theme', 49 | d_preferences_default_postwrite: 'Post options', 50 | d_preferences_general: 'General', 51 | d_preferences_ok: 'OK', 52 | 53 | d_set_admin_bot: 'Set admin bot', 54 | d_set_admin_bot_token: 'Bot token', 55 | d_set_admin_bot_cancel: 'Cancel', 56 | d_set_admin_bot_save: 'Save', 57 | 58 | d_add_channel: 'Add channel', 59 | d_add_channel_name: 'Channel name', 60 | d_add_channel_name_placeholder: 'My channel', 61 | d_add_channel_username: 'Channel username', 62 | d_add_channel_cancel: 'Cancel', 63 | d_add_channel_save: 'Save', 64 | 65 | d_insert_link: 'Insert link', 66 | d_insert_link_title: 'Link title', 67 | d_insert_link_url: 'Link url', 68 | d_insert_link_cancel: 'Cancel', 69 | d_insert_link_save: 'Save', 70 | 71 | posts_delete: 'Delete', 72 | posts_edit: 'Edit', 73 | posts_empty: 'Empty list', 74 | 75 | drafts_delete: 'Delete', 76 | drafts_edit: 'Edit', 77 | drafts_send: 'Send', 78 | drafts_empty: 'Empty list', 79 | 80 | tmenu_edit: 'Edit', 81 | tmenu_edit_undo: 'Undo', 82 | tmenu_edit_redo: 'Redo', 83 | tmenu_edit_cut: 'Cut', 84 | tmenu_edit_copy: 'Copy', 85 | tmenu_edit_paste: 'Paste', 86 | tmenu_edit_delete: 'Delete', 87 | tmenu_edit_selectall: 'Select all', 88 | 89 | tmenu_view: 'View', 90 | tmenu_view_reload: 'Reload', 91 | tmenu_view_toggledevtools: 'Toggle Developer Tools', 92 | tmenu_view_resetzoom: 'Reset zoom', 93 | tmenu_view_zoomin: 'Zoom in', 94 | tmenu_view_zoomout: 'Zoom out', 95 | tmenu_view_togglefullscreen: 'Enter Full Screen', 96 | 97 | tmenu_window: 'Window', 98 | tmenu_window_minimize: 'Minimize', 99 | tmenu_window_close: 'Close', 100 | tmenu_window_zoom: 'Zoom', 101 | tmenu_window_front: 'Bring All to Front', 102 | 103 | tmenu_help: 'Help', 104 | tmenu_help_learnmore: 'Learn More', 105 | 106 | tmenu_app_about: 'About Chad', 107 | tmenu_app_checkforupdates: 'Check for updates', 108 | tmenu_app_reportbugs: 'Report bugs', 109 | tmenu_app_services: 'Services', 110 | tmenu_app_hide: 'Hide Chad', 111 | tmenu_app_hideothers: 'Hide others', 112 | tmenu_app_unhide: 'Show all', 113 | tmenu_app_quit: 'Quit Chad', 114 | 115 | alert_select_formatting_style: 'Select formatting style', 116 | alert_select_channel_to_send: 'Select channel(s) to send post', 117 | alert_add_admin_bot: 'Add Admin bot', 118 | alert_post_text_empty: 'Post text is empty', 119 | alert_something_wrong: 'Something went wrong', 120 | alert_channel_name_short: 'Channel name is too short', 121 | alert_invalid_channel_name: 'Invalid channel username', 122 | alert_invalid_token: 'Invalid token', 123 | alert_invalid_token_format: 'Invalid token format', 124 | alert_cant_find_bot: 'Can\'t find bot with this token', 125 | alert_hidden_link_not_allowed: 'Hidden link are allowed only with "HTML" formatting style', 126 | 127 | snackbar_draft_saved: 'Draft saved!', 128 | 129 | update: 'Update', 130 | update_new: 'A newer version $newVer is available!', 131 | update_new_detail: 'Detail', 132 | update_new_cancel: 'Cancel', 133 | update_no: 'No Updates', 134 | update_no_text: 'Current version $version is already up to date!', 135 | update_no_ok: 'OK', 136 | }; 137 | -------------------------------------------------------------------------------- /app/langs/ru.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | menu: 'Меню', 3 | menu_write_post: 'Написать пост', 4 | menu_posts: 'Посты', 5 | menu_drafts: 'Черновики', 6 | menu_preferences: 'Настройки', 7 | menu_about: 'О Chad', 8 | 9 | channels: 'Каналы', 10 | channels_add_channel: 'Добавить канал', 11 | 12 | bot_refresh: 'Обновить', 13 | bot_change: 'Изменить', 14 | bot_remove: 'Удалить', 15 | bot_set: 'Указать админ бота', 16 | 17 | settings: 'Параметры', 18 | settings_formatting_styles: 'Стиль форматирования', 19 | settings_none: 'Нет', 20 | settings_html: 'HTML', 21 | settings_markdown: 'Markdown', 22 | settings_link: 'Ссылка', 23 | settings_bold: 'Жирный', 24 | settings_italic: 'Курсив', 25 | settings_strikethrough: 'Зачеркнутый', 26 | settings_underline: 'Подчеркнутый', 27 | settings_hidden_link: 'Скрытая ссылка', 28 | 29 | post_test: 'Текст поста', 30 | post_send: 'Отправить', 31 | post_save_draft: 'Сохранить черновик', 32 | post_cancel: 'Отмена', 33 | 34 | post_settings: 'Параметры', 35 | post_settings_ok: 'OK', 36 | post_settings_disable_link_preview: 'Отключить предпросмотр ссылок', 37 | post_settings_disable_notification: 'Отключить уведомление', 38 | 39 | preview: 'Предпросмотр', 40 | 41 | loading: 'Загрузка...', 42 | 43 | d_about: 'О Chad', 44 | d_about_close: 'Закрыть', 45 | 46 | d_preferences: 'Настройки', 47 | d_preferences_language: 'Язык', 48 | d_preferences_dark_theme: 'Тёмная тема', 49 | d_preferences_default_postwrite: 'Параметры поста', 50 | d_preferences_general: 'Основные', 51 | d_preferences_ok: 'OK', 52 | 53 | d_set_admin_bot: 'Указать админ бота', 54 | d_set_admin_bot_token: 'Ключ бота', 55 | d_set_admin_bot_cancel: 'Отмена', 56 | d_set_admin_bot_save: 'Сохранить', 57 | 58 | d_add_channel: 'Добавить канал', 59 | d_add_channel_name: 'Название канала', 60 | d_add_channel_name_placeholder: 'Мой канал', 61 | d_add_channel_username: 'Никнейм канала', 62 | d_add_channel_cancel: 'Отмена', 63 | d_add_channel_save: 'Сохранить', 64 | 65 | d_insert_link: 'Вставить ссылку', 66 | d_insert_link_title: 'Название ссылки', 67 | d_insert_link_url: 'Адрес ссылки', 68 | d_insert_link_cancel: 'Отмена', 69 | d_insert_link_save: 'Сохранить', 70 | 71 | posts_delete: 'Удалить', 72 | posts_edit: 'Изменить', 73 | posts_empty: 'Список пуст', 74 | 75 | drafts_delete: 'Удалить', 76 | drafts_edit: 'Изменить', 77 | drafts_send: 'Отправить', 78 | drafts_empty: 'Список пуст', 79 | 80 | tmenu_edit: 'Редактирование', 81 | tmenu_edit_undo: 'Шаг назад', 82 | tmenu_edit_redo: 'Шаг вперед', 83 | tmenu_edit_cut: 'Вырезать', 84 | tmenu_edit_copy: 'Копировать', 85 | tmenu_edit_paste: 'Вставить', 86 | tmenu_edit_delete: 'Очистить', 87 | tmenu_edit_selectall: 'Выделить все', 88 | 89 | tmenu_view: 'Просмотр', 90 | tmenu_view_reload: 'Обновить', 91 | tmenu_view_toggledevtools: 'Открыть панель разработчика', 92 | tmenu_view_resetzoom: 'Реальный размер', 93 | tmenu_view_zoomin: 'Увеличить', 94 | tmenu_view_zoomout: 'Уменьшить', 95 | tmenu_view_togglefullscreen: 'Во весь экран', 96 | 97 | tmenu_window: 'Окно', 98 | tmenu_window_minimize: 'Свернуть', 99 | tmenu_window_close: 'Закрыть', 100 | tmenu_window_zoom: 'Увеличить', 101 | tmenu_window_front: 'Все на передний план', 102 | 103 | tmenu_help: 'Помощь', 104 | tmenu_help_learnmore: 'Подробнее', 105 | 106 | tmenu_app_about: 'О Chad', 107 | tmenu_app_checkforupdates: 'Проверить обновления', 108 | tmenu_app_reportbugs: 'Сообщить об ошибке', 109 | tmenu_app_services: 'Сервисы', 110 | tmenu_app_hide: 'Скрыть Chad', 111 | tmenu_app_hideothers: 'Скрыть другие', 112 | tmenu_app_unhide: 'Показать все', 113 | tmenu_app_quit: 'Выйти из Chad', 114 | 115 | alert_select_formatting_style: 'Выберите стиль форматирования', 116 | alert_select_channel_to_send: 'Выберите канал(ы) для отправки поста', 117 | alert_add_admin_bot: 'Добавьте админ бота', 118 | alert_post_text_empty: 'Текст поста пуст', 119 | alert_something_wrong: 'Что-то пошло не так...', 120 | alert_channel_name_short: 'Имя канала слишком короткое', 121 | alert_invalid_channel_name: 'Неверный никнейм канала', 122 | alert_invalid_token: 'Неверный ключ', 123 | alert_invalid_token_format: 'Неверный формат ключа', 124 | alert_cant_find_bot: 'Бот с этим ключом не найден', 125 | alert_hidden_link_not_allowed: 'Скрытые ссылки доступны только с "HTML" стилем форматирования', 126 | 127 | snackbar_draft_saved: 'Черновик сохранен!', 128 | 129 | update: 'Обновление', 130 | update_new: 'Новая версия $newVer уже доступна!', 131 | update_new_detail: 'Подробнее', 132 | update_new_cancel: 'Отмена', 133 | update_no: 'Нет обновлений', 134 | update_no_text: 'Текущая версия $version самая новая!', 135 | update_no_ok: 'OK', 136 | }; 137 | -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | const overlay = document.getElementById('overlay'); 2 | let instance = null; 3 | 4 | setTimeout(() => { 5 | /* eslint-disable global-require */ 6 | const React = require('react'); 7 | const { render } = require('react-dom'); 8 | const injectTapEventPlugin = require('react-tap-event-plugin'); 9 | const deepForceUpdate = require('react-deep-force-update'); 10 | const velocity = require('velocity-animate'); 11 | require('./js/DataRepair')(); 12 | /* eslint-enable global-require */ 13 | 14 | // injectTapEventPlugin(); 15 | 16 | function deepUpdate() { 17 | deepForceUpdate(instance); 18 | } 19 | 20 | /* eslint-disable global-require */ 21 | const Chad = require('./Chad'); 22 | /* eslint-enable global-require */ 23 | 24 | instance = render( 25 | , 26 | document.getElementById('container') 27 | ); 28 | 29 | velocity(overlay, 'fadeOut', { duration: 500 }); 30 | }, 10); 31 | -------------------------------------------------------------------------------- /images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/images/main.png -------------------------------------------------------------------------------- /images/step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/images/step4.png -------------------------------------------------------------------------------- /images/step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/images/step5.png -------------------------------------------------------------------------------- /images/step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/images/step6.png -------------------------------------------------------------------------------- /linux-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": "builds/Chad-linux-x64", 3 | "dest": "builds", 4 | "icon": "src/icons/chad.png", 5 | "categories": [ 6 | "Utility" 7 | ] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chad", 3 | "productName": "Chad", 4 | "version": "4.0.0", 5 | "description": "Telegram channel administration tool", 6 | "scripts": { 7 | "build:app": "parcel build app/index.html --out-dir package/app --public-url ./", 8 | "build:package": "rm -rf package && npm run build:app && cp -r src/* package && cd package && npm i", 9 | "electron": "cd package && electron index.js", 10 | "package:linux": "electron-packager ./package --overwrite --platform linux --arch x64 --app-bundle-id com.perkovec.chad --icon ./src/icons/chad.png --out ./builds --executable-name chad", 11 | "package:windows": "electron-packager ./package --overwrite --platform win32 --arch x64 --win32metadata.ProductName=\"Chad\" --icon ./src/icons/chad.ico --out ./builds", 12 | "package:mac": "electron-packager ./package --overwrite --platform darwin --arch x64 --app-bundle-id com.perkovec.chad --icon ./src/icons/chad.icns --out ./builds" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/40PK/Chad.git" 17 | }, 18 | "keywords": [ 19 | "Telegram", 20 | "tool", 21 | "administration", 22 | "channel", 23 | "Electron" 24 | ], 25 | "license": "GNU GPLv3", 26 | "author": "Perkovec", 27 | "bugs": { 28 | "url": "https://github.com/40PK/Chad/issues" 29 | }, 30 | "homepage": "https://github.com/40PK/Chad#readme", 31 | "devDependencies": { 32 | "electron": "^9.0.0", 33 | "electron-packager": "^14.2.1", 34 | "parcel-bundler": "^1.12.4" 35 | }, 36 | "dependencies": { 37 | "material-ui": "^0.20.2", 38 | "prop-types": "^15.7.2", 39 | "react": "^16.13.1", 40 | "react-addons-shallow-compare": "^15.6.2", 41 | "react-deep-force-update": "^2.1.3", 42 | "react-dom": "^16.13.1", 43 | "react-layout-pane": "^0.1.16", 44 | "react-tap-event-plugin": "^3.0.3", 45 | "semver-compare": "^1.0.0", 46 | "superagent": "^5.2.2", 47 | "velocity-animate": "^1.5.2", 48 | "xss": "^1.0.6" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/checkUpdates.js: -------------------------------------------------------------------------------- 1 | const { 2 | dialog, 3 | shell, 4 | app, 5 | } = require('electron'); 6 | const request = require('superagent'); 7 | const compare = require('semver-compare'); 8 | 9 | const version = app.getVersion(); 10 | 11 | module.exports = (local, focusedWindow, notifyUpToDate) => { 12 | request.get('https://api.github.com/repos/40PK/Chad/releases/latest') 13 | .set('User-Agent', "Chad") 14 | .end((err, res) => { 15 | const newVer = res.body.tag_name.substr(1); 16 | const hasUpdates = compare(newVer, version) === 1; 17 | const answerDialog = []; 18 | if (focusedWindow) answerDialog.push(focusedWindow); 19 | 20 | if (hasUpdates) { 21 | answerDialog.push({ 22 | type: 'question', 23 | message: local.update, 24 | detail: local.update_new.replace('$newVer', newVer), 25 | buttons: [local.update_new_detail, local.update_new_cancel], 26 | }); 27 | const answer = dialog.showMessageBox.apply(null, answerDialog); 28 | if (answer === 0) { 29 | shell.openExternal(`https://github.com/40PK/Chad/releases/tag/v${newVer}`); 30 | } 31 | } else if (notifyUpToDate) { 32 | answerDialog.push({ 33 | type: 'info', 34 | message: local.update_no, 35 | detail: local.update_no_text.replace('$version', version), 36 | buttons: [local.update_no_ok], 37 | }); 38 | dialog.showMessageBox.apply(null, answerDialog); 39 | } 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/icons/chad.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/src/icons/chad.icns -------------------------------------------------------------------------------- /src/icons/chad.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/src/icons/chad.ico -------------------------------------------------------------------------------- /src/icons/chad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/40PK/Chad/f34fd96290b04c53c330277db9a133f7b1831bf5/src/icons/chad.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | BrowserWindow, 3 | ipcMain, 4 | Menu, 5 | app, 6 | } = require('electron'); 7 | const checkUpdates = require('./checkUpdates'); 8 | const menu = require('./menu'); 9 | const path = require('path'); 10 | 11 | let mainWindow; 12 | 13 | global.data_version = { 14 | settings: '3.1', 15 | posts: '3.0', 16 | bot: '3.0', 17 | channels: '3.0', 18 | botavatar: '3.0', 19 | drafts: '3.0', 20 | }; 21 | 22 | function createWindow() { 23 | mainWindow = new BrowserWindow({ 24 | width: 850, 25 | height: 650, 26 | 27 | minWidth: 850, 28 | minHeight: 600, 29 | 30 | 'title-bar-style': 'hidden', 31 | icon: path.join(__dirname, '/icons/chad.png'), 32 | show: true, 33 | webPreferences: { 34 | nodeIntegration: true, 35 | preload: __dirname + '/preload.js' 36 | } 37 | }); 38 | 39 | mainWindow.setMenu(null); 40 | mainWindow.loadFile('app/index.html'); 41 | 42 | mainWindow.on('closed', () => { 43 | mainWindow = null; 44 | }); 45 | 46 | ipcMain.on('build-menu', (event, locals) => { 47 | Menu.setApplicationMenu(menu(locals)); 48 | }); 49 | 50 | ipcMain.on('check-updates', (event, locals) => { 51 | checkUpdates(locals); 52 | }); 53 | } 54 | 55 | app.whenReady().then(createWindow) 56 | 57 | app.on('window-all-closed', () => { 58 | if (process.platform !== 'darwin') { 59 | app.quit(); 60 | } 61 | }); 62 | 63 | app.on('activate', () => { 64 | if (BrowserWindow.getAllWindows().length === 0) { 65 | createWindow() 66 | } 67 | }) -------------------------------------------------------------------------------- /src/menu.js: -------------------------------------------------------------------------------- 1 | const checkUpdates = require('./checkUpdates'); 2 | const { 3 | shell, 4 | Menu, 5 | app, 6 | } = require('electron'); 7 | 8 | module.exports = (local) => { 9 | const template = [ 10 | { 11 | label: local.tmenu_edit, 12 | submenu: [ 13 | { 14 | role: 'undo', 15 | label: local.tmenu_edit_undo, 16 | }, 17 | { 18 | role: 'redo', 19 | label: local.tmenu_edit_redo, 20 | }, 21 | { 22 | type: 'separator', 23 | }, 24 | { 25 | role: 'cut', 26 | label: local.tmenu_edit_cut, 27 | }, 28 | { 29 | role: 'copy', 30 | label: local.tmenu_edit_copy, 31 | }, 32 | { 33 | role: 'paste', 34 | label: local.tmenu_edit_paste, 35 | }, 36 | { 37 | role: 'delete', 38 | label: local.tmenu_edit_delete, 39 | }, 40 | { 41 | role: 'selectall', 42 | label: local.tmenu_edit_selectall, 43 | }, 44 | ], 45 | }, 46 | { 47 | label: local.tmenu_view, 48 | submenu: [ 49 | { 50 | label: local.tmenu_view_reload, 51 | accelerator: 'CmdOrCtrl+R', 52 | click(item, focusedWindow) { 53 | if (focusedWindow) focusedWindow.reload(); 54 | }, 55 | }, 56 | { 57 | label: local.tmenu_view_toggledevtools, 58 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', 59 | click(item, focusedWindow) { 60 | if (focusedWindow) focusedWindow.webContents.toggleDevTools(); 61 | }, 62 | }, 63 | { 64 | type: 'separator', 65 | }, 66 | { 67 | role: 'resetzoom', 68 | label: local.tmenu_view_resetzoom, 69 | }, 70 | { 71 | role: 'zoomin', 72 | label: local.tmenu_view_zoomin, 73 | }, 74 | { 75 | role: 'zoomout', 76 | label: local.tmenu_view_zoomout, 77 | }, 78 | { 79 | type: 'separator', 80 | }, 81 | { 82 | role: 'togglefullscreen', 83 | label: local.tmenu_view_togglefullscreen, 84 | }, 85 | ], 86 | }, 87 | { 88 | role: 'window', 89 | label: local.tmenu_window, 90 | submenu: [ 91 | { 92 | role: 'minimize', 93 | label: local.tmenu_window_minimize, 94 | }, 95 | { 96 | role: 'close', 97 | label: local.tmenu_window_close, 98 | }, 99 | ], 100 | }, 101 | { 102 | role: 'help', 103 | label: local.tmenu_help, 104 | submenu: [ 105 | { 106 | label: local.tmenu_help_learnmore, 107 | click() { shell.openExternal('https://perkovec.github.io/Chad'); }, 108 | }, 109 | ], 110 | }, 111 | ]; 112 | 113 | if (process.platform === 'darwin') { 114 | const name = app.getName(); 115 | template.unshift({ 116 | label: name, 117 | submenu: [ 118 | { 119 | label: local.tmenu_app_about, 120 | role: 'about', 121 | }, 122 | { 123 | label: local.tmenu_app_checkforupdates, 124 | click(item, focusedWindow) { 125 | if (focusedWindow) checkUpdates(local, focusedWindow, true); 126 | }, 127 | }, 128 | { 129 | label: local.tmenu_app_reportbugs, 130 | click() { 131 | shell.openExternal('http://github.com/40PK/Chad/issues'); 132 | }, 133 | }, 134 | { 135 | type: 'separator', 136 | }, 137 | { 138 | role: 'services', 139 | label: local.tmenu_app_services, 140 | submenu: [], 141 | }, 142 | { 143 | type: 'separator', 144 | }, 145 | { 146 | role: 'hide', 147 | label: local.tmenu_app_hide, 148 | }, 149 | { 150 | role: 'hideothers', 151 | label: local.tmenu_app_hideothers, 152 | }, 153 | { 154 | role: 'unhide', 155 | label: local.tmenu_app_unhide, 156 | }, 157 | { 158 | type: 'separator', 159 | }, 160 | { 161 | role: 'quit', 162 | label: local.tmenu_app_quit, 163 | }, 164 | ], 165 | }); 166 | 167 | // Window menu. 168 | template[3].submenu = [ 169 | { 170 | label: local.tmenu_window_close, 171 | accelerator: 'CmdOrCtrl+W', 172 | role: 'close', 173 | }, 174 | { 175 | label: local.tmenu_window_minimize, 176 | accelerator: 'CmdOrCtrl+M', 177 | role: 'minimize', 178 | }, 179 | { 180 | label: local.tmenu_window_zoom, 181 | role: 'zoom', 182 | }, 183 | { 184 | type: 'separator', 185 | }, 186 | { 187 | label: local.tmenu_window_front, 188 | role: 'front', 189 | }, 190 | ]; 191 | } else { 192 | template[template.length - 1].submenu.unshift({ 193 | label: local.tmenu_app_reportbugs, 194 | click() { 195 | shell.openExternal('http://github.com/40PK/Chad/issues'); 196 | }, 197 | }); 198 | template[template.length - 1].submenu.unshift({ 199 | label: local.tmenu_app_checkforupdates, 200 | click(item, focusedWindow) { 201 | if (focusedWindow) checkUpdates(focusedWindow, true); 202 | }, 203 | }); 204 | } 205 | 206 | return Menu.buildFromTemplate(template); 207 | }; 208 | 209 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chad", 3 | "productName": "Chad", 4 | "version": "4.0.0", 5 | "description": "Telegram channel administration tool", 6 | "scripts": { 7 | "build:app": "parcel build app/index.html --out-dir package/app --public-url ./", 8 | "build:package": "npm run build:app && cp -r src/* package && cp package.json package", 9 | "electron": "cd package && electron index.js", 10 | "package:linux": "electron-packager ./package --platform linux --arch all --app-bundle-id com.perkovec.chad --icon ./src/icons/chad.png --out ./builds" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/40PK/Chad.git" 15 | }, 16 | "keywords": [ 17 | "Telegram", 18 | "tool", 19 | "administration", 20 | "channel", 21 | "Electron" 22 | ], 23 | "license": "GNU GPLv3", 24 | "author": "Perkovec", 25 | "bugs": { 26 | "url": "https://github.com/40PK/Chad/issues" 27 | }, 28 | "homepage": "https://github.com/40PK/Chad#readme", 29 | "dependencies": { 30 | "semver-compare": "^1.0.0", 31 | "superagent": "^5.2.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/preload.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | 3 | window.ipcRenderer = electron.ipcRenderer; 4 | window.shell = electron.shell; 5 | window.remote = electron.remote; 6 | --------------------------------------------------------------------------------