Compare commits
1087 Commits
Author | SHA1 | Date |
---|---|---|
Nicolas Constant | 822ef21985 | |
Nicolas Constant | a154028a53 | |
Nicolas Constant | 6a8d85f40c | |
Nicolas Constant | 04153543a9 | |
Nicolas Constant | 92ec089eab | |
Nicolas Constant | 12ce0a3a4a | |
Nicolas Constant | 7a6eb9c3d2 | |
Nicolas Constant | 63b7c6fdf1 | |
Nicolas Constant | bd75317417 | |
Nicolas Constant | 74eed7e8ba | |
Nicolas Constant | ebce6282c5 | |
Nicolas Constant | 702e4daa44 | |
Nicolas Constant | d2221d539c | |
Nicolas Constant | c4de387f86 | |
Nicolas Constant | c0f84ddc11 | |
Nicolas Constant | 1830212a91 | |
Nicolas Constant | 46adf207bb | |
Nicolas Constant | 909b190b33 | |
Nicolas Constant | cfc4d5f915 | |
Nicolas Constant | 0f58252c61 | |
Nicolas Constant | 0d2ac6b569 | |
Nicolas Constant | e62987b11a | |
Nicolas Constant | 8cee7289eb | |
Nicolas Constant | 0305cc6ac7 | |
Nicolas Constant | f215d027f9 | |
Nicolas Constant | 335cbf4956 | |
Nicolas Constant | b41c31b4ac | |
Nicolas Constant | 41faa36087 | |
Nicolas Constant | 024042959e | |
Nicolas Constant | f4c87df078 | |
Nicolas Constant | d24441343a | |
Nicolas Constant | 8c9685045e | |
Nicolas Constant | a0cb240446 | |
Nicolas Constant | 2def5725f5 | |
Nicolas Constant | 450a0088d5 | |
Nicolas Constant | d7f988ecb9 | |
Nicolas Constant | 8703df27d5 | |
Nicolas Constant | 10fa412173 | |
Nicolas Constant | 0b93ed7307 | |
Nicolas Constant | c3cd6fe79e | |
Nicolas Constant | 14287b476c | |
Nicolas Constant | 2b106ba546 | |
Nicolas Constant | 4a2b408c1b | |
Nicolas Constant | 92a3ac6ae3 | |
Nicolas Constant | ec0bed4606 | |
Nicolas Constant | 62d4140d63 | |
Nicolas Constant | 4a34063dc8 | |
Nicolas Constant | 9cd709f44c | |
Nicolas Constant | 64ceb3e095 | |
Nicolas Constant | cb58be5bd8 | |
Nicolas Constant | 7a8dfd0c6b | |
Nicolas Constant | 89c5c33de2 | |
Nicolas Constant | 590627bc58 | |
Nicolas Constant | 7013d9174c | |
Nicolas Constant | ba08c0d0b2 | |
Nicolas Constant | 26a01b5c30 | |
Nicolas Constant | 73ac37a8f4 | |
Nicolas Constant | 38b052f06b | |
Nicolas Constant | 4511363408 | |
Nicolas Constant | c0f03570a0 | |
Nicolas Constant | 3d5c91a12b | |
Nicolas Constant | 27b22338c9 | |
Nicolas Constant | 191bd936aa | |
Nicolas Constant | 1c42f54db0 | |
Nicolas Constant | e8dbe214f4 | |
Nicolas Constant | 8cd4d30ac8 | |
Nicolas Constant | 30f678af04 | |
Nicolas Constant | 16bbf9aa2f | |
Nicolas Constant | 74af61ad78 | |
Nicolas Constant | 449506092a | |
Nicolas Constant | b37a2a2f0c | |
Nicolas Constant | 32efac5aa4 | |
Nicolas Constant | 91b2f4a0f0 | |
Nicolas Constant | 0d7821cd01 | |
Nicolas Constant | 18d6b8d96c | |
Nicolas Constant | 503cb6c9d4 | |
Nicolas Constant | 98e7d54c33 | |
Nicolas Constant | dbb5d8e71b | |
Nicolas Constant | a77b46755f | |
Nicolas Constant | a5f9feb10b | |
Nicolas Constant | 95c4d8b249 | |
Nicolas Constant | 128dfd7fe5 | |
Nicolas Constant | 2dc77dd39a | |
Nicolas Constant | f71e175375 | |
HamzaFarooqArif | a1a56e49f5 | |
HamzaFarooqArif | 5dc98c677e | |
Nicolas Constant | b00c52ff83 | |
Nicolas Constant | f46d7d433a | |
Nicolas Constant | 06dbdef1dc | |
Nicolas Constant | 5e865ed9a4 | |
Nicolas Constant | 253ea52590 | |
Nicolas Constant | 84a4b8c00a | |
Nicolas Constant | 982a670352 | |
Nicolas Constant | 314c736cf4 | |
Nicolas Constant | 9999944d1f | |
Nicolas Constant | 2bcac4622a | |
Nicolas Constant | 5d6672f379 | |
Nicolas Constant | eac8c6120a | |
Nicolas Constant | 22cad9e22d | |
Nicolas Constant | 232a86566c | |
Nicolas Constant | 2cb443dd4d | |
Nicolas Constant | cb342ce9b5 | |
Nicolas Constant | 8c9fe07109 | |
Nicolas Constant | 00134a7407 | |
Nicolas Constant | db6b37eef3 | |
Nicolas Constant | e14852e087 | |
Nicolas Constant | 6001a26f02 | |
Nicolas Constant | 48677e8e6c | |
Nicolas Constant | 1ca603f211 | |
Nicolas Constant | d60bf804b8 | |
Nicolas Constant | 8bd71afc55 | |
Nicolas Constant | ed8c935285 | |
Nicolas Constant | b1cd975422 | |
Nicolas Constant | c5e3f4abac | |
Nicolas Constant | 4599d64c60 | |
Nicolas Constant | 522c1c0133 | |
Nicolas Constant | b6ea1d8d43 | |
Nicolas Constant | 55a855d046 | |
Nicolas Constant | 410007dc25 | |
Nicolas Constant | 54d4b300f4 | |
Nicolas Constant | f4ba3a168f | |
Nicolas Constant | f2e1478cfa | |
Nicolas Constant | ce71965b5c | |
Nicolas Constant | 65c147bc6f | |
Nicolas Constant | 57f863e2a1 | |
Nicolas Constant | 0ce8be99bd | |
Nicolas Constant | f5de97993b | |
Nicolas Constant | 0777c23124 | |
Nicolas Constant | 70c9e2564b | |
Nicolas Constant | 54772d8487 | |
Nicolas Constant | 30c81ae143 | |
Nicolas Constant | 9cc2324fd2 | |
Nicolas Constant | c912f12db5 | |
Rob Petti | 513bb1e684 | |
Rob Petti | ec233754dd | |
Rob Petti | 39187c82fb | |
Nicolas Constant | 78f0f3ab5f | |
Nicolas Constant | 39abd6a175 | |
Nicolas Constant | 644b0d0b86 | |
Rob Petti | 83f52391ae | |
Nicolas Constant | 33a61f7347 | |
Nicolas Constant | 0409431105 | |
Nicolas Constant | 42fb269c24 | |
Nicolas Constant | c3a5306e56 | |
Nicolas Constant | 76b911351c | |
Nicolas Constant | 7cb0887749 | |
Nicolas Constant | 5c52c9c4f2 | |
Nicolas Constant | 59c3b19271 | |
Nicolas Constant | 2f84471a3e | |
Nicolas Constant | 640028ca08 | |
Nicolas Constant | 3f01c70bc9 | |
Nicolas Constant | 70bef7b98e | |
Nicolas Constant | 0956b623ce | |
Nicolas Constant | 6554a359b5 | |
Nicolas Constant | 1ebbece7ab | |
Nicolas Constant | a85e24b77f | |
Nicolas Constant | c2812fae43 | |
Nicolas Constant | 9426bc9e38 | |
Nicolas Constant | 06d142c4a5 | |
Nicolas Constant | eb74e34cb0 | |
Nicolas Constant | 50dc938295 | |
Nicolas Constant | f1596bf04f | |
Nicolas Constant | 67e69c64a4 | |
Nicolas Constant | ba5fead320 | |
Nicolas Constant | 9bba8a3352 | |
Nicolas Constant | 14a9aade0b | |
Nicolas Constant | 93847df4d8 | |
Nicolas Constant | aa705d7c5b | |
Nicolas Constant | 21ad2cffb6 | |
Nicolas Constant | f8cea22693 | |
Nicolas Constant | 03bcc95d65 | |
Nicolas Constant | 8bbc58d9c8 | |
Nicolas Constant | 28065912b2 | |
Nicolas Constant | cd96324442 | |
Nicolas Constant | 30cb395bda | |
Nicolas Constant | 8d13822000 | |
Nicolas Constant | d82da3d180 | |
Nicolas Constant | 7653398642 | |
Nicolas Constant | fb4c99870e | |
Nicolas Constant | 47a8cdc096 | |
Nicolas Constant | 3724b0b4c2 | |
Nicolas Constant | 030ce2e568 | |
Nicolas Constant | 1d9e3c5130 | |
Nicolas Constant | 5eef9506fe | |
Nicolas Constant | f152a3dc6f | |
Nicolas Constant | d30f5a8261 | |
Nicolas Constant | 4babd219b4 | |
Nicolas Constant | 9ae1711093 | |
Nicolas Constant | 1b7853ec4d | |
Nicolas Constant | 6144d12740 | |
Nicolas Constant | 6696ca4274 | |
Nicolas Constant | 70032f55f1 | |
Nicolas Constant | 63175d1e60 | |
Nicolas Constant | 269b8b87cd | |
Nicolas Constant | b2a198c6d9 | |
Nicolas Constant | af026a444d | |
Nicolas Constant | 31527d3914 | |
Nicolas Constant | d74b030688 | |
Nicolas Constant | 438867e49a | |
Nicolas Constant | 82e86039b4 | |
Nicolas Constant | 8b849a6650 | |
Nicolas Constant | 8c76056747 | |
Nicolas Constant | 4083b1017a | |
Nicolas Constant | 5b7c2de8ba | |
Nicolas Constant | 3c4fc074ef | |
Nicolas Constant | e772613193 | |
Nicolas Constant | 8566966463 | |
Nicolas Constant | 5f4e822b64 | |
Nicolas Constant | 7d42737c27 | |
Nicolas Constant | e5ce5fb14e | |
Nicolas Constant | 50758f1170 | |
Nicolas Constant | d720dc06a9 | |
Nicolas Constant | 7841f72890 | |
Nicolas Constant | a303f16afe | |
Nicolas Constant | 3be94a842d | |
Nicolas Constant | d67ef4aaf2 | |
Nicolas Constant | 0c361d57fc | |
Nicolas Constant | e46c878e36 | |
Nicolas Constant | 49c776a67c | |
Nicolas Constant | 24c188aa80 | |
Nicolas Constant | 180f218eb0 | |
Nicolas Constant | c2f9c17189 | |
Nicolas Constant | 45d735835b | |
Nicolas Constant | 0d1a2e59d4 | |
Nicolas Constant | c950744a48 | |
Nicolas Constant | 23abf0e0b7 | |
Nicolas Constant | 3dbc5c57e1 | |
Nicolas Constant | f13e30ebaf | |
Nicolas Constant | 45620de391 | |
Nicolas Constant | 711e351543 | |
Miosame | 91f75f2b0f | |
Miosame | 0afa7a0998 | |
Nicolas Constant | 912d9e31b5 | |
Nicolas Constant | a0952de788 | |
Nicolas Constant | 628b9c6733 | |
Nicolas Constant | bcc4549b9a | |
Nicolas Constant | 54bac5e0ee | |
Nicolas Constant | e223c1d032 | |
Nicolas Constant | 0d851560b6 | |
Nicolas Constant | b09c2a0b81 | |
Nicolas Constant | 386058eceb | |
Nicolas Constant | 7b94b950d2 | |
Nicolas Constant | 496b2b7dd2 | |
Nicolas Constant | e600f096cd | |
Nicolas Constant | b4eb092181 | |
Nicolas Constant | f5f1c2e8f8 | |
Nicolas Constant | d46fa0ffca | |
Nicolas Constant | 63c2385644 | |
Nicolas Constant | b02979430c | |
Nicolas Constant | 39af84785f | |
Nicolas Constant | 5992ac7001 | |
Nicolas Constant | 5ae8d668df | |
Nicolas Constant | a37b814c16 | |
Nicolas Constant | 60e99a1c30 | |
Nicolas Constant | a8f940eea7 | |
Nicolas Constant | 9adb9c5c44 | |
Nicolas Constant | 8f03d7f19e | |
Nicolas Constant | 4e453903f2 | |
Nicolas Constant | d8398a4af6 | |
Nicolas Constant | efbc3d3cdc | |
Nicolas Constant | ac3acfb4fa | |
Nicolas Constant | 4e9730a4ae | |
Nicolas Constant | 16a92bcc56 | |
Nicolas Constant | 2b0da954a1 | |
Nicolas Constant | 0e23b64b63 | |
Nicolas Constant | 8f719c515b | |
Nicolas Constant | 4df59f0edb | |
Nicolas Constant | 7a0de67f5d | |
Nicolas Constant | 65068f5dc0 | |
Nicolas Constant | 54ed9a8c4a | |
Nicolas Constant | 73f6030a87 | |
Nicolas Constant | 713fa918be | |
Nicolas Constant | a82afc6bd1 | |
Nicolas Constant | fbe5a53f60 | |
Nicolas Constant | 505f0b025a | |
Nicolas Constant | 534a4b11e3 | |
Nicolas Constant | 69c6fbc145 | |
Nicolas Constant | 1f93817a6f | |
Nicolas Constant | 58c1f04609 | |
Nicolas Constant | f3f63f569a | |
Nicolas Constant | a809274756 | |
Nicolas Constant | 8710b0267e | |
Nicolas Constant | 95454e29a0 | |
Nicolas Constant | 1ae9cc282f | |
Nicolas Constant | 031b1d5631 | |
Nicolas Constant | 5ddf555172 | |
Nicolas Constant | ece9182e99 | |
Nicolas Constant | 9cc21a4b64 | |
Nicolas Constant | e3a7239522 | |
Nicolas Constant | 08dd0025c9 | |
Nicolas Constant | b0234435d4 | |
Nicolas Constant | bce2cd0527 | |
Nicolas Constant | 5230b3a115 | |
Nicolas Constant | d5b2a3e47d | |
Nicolas Constant | 9ad3ef81b4 | |
Nicolas Constant | e821c8a8dc | |
Nicolas Constant | 1de96741ad | |
Nicolas Constant | 9a8f24462c | |
Nicolas Constant | 34dcc3050a | |
Nicolas Constant | db5ee22615 | |
Nicolas Constant | 382cae866f | |
Nicolas Constant | fa0ae59e78 | |
Nicolas Constant | f6466a5c8f | |
Nicolas Constant | ff030e4669 | |
Nicolas Constant | 675dd0a3a2 | |
Nicolas Constant | 82a4452a41 | |
Nicolas Constant | c30ba1483a | |
Nicolas Constant | f073e4f224 | |
Nicolas Constant | b423ca6b31 | |
Nicolas Constant | fef906da72 | |
Nicolas Constant | 134d2c47f5 | |
Nicolas Constant | c51ea38c96 | |
Nicolas Constant | 734d2ae161 | |
Nicolas Constant | 5674127e28 | |
Nicolas Constant | c8c17ca44e | |
Nicolas Constant | b0b3c4ec21 | |
Nicolas Constant | fa0d89276a | |
Nicolas Constant | 9de28fad86 | |
Nicolas Constant | 185bb64a42 | |
Nicolas Constant | 391c515b30 | |
Nicolas Constant | 305833ddc2 | |
Nicolas Constant | ba7872b902 | |
Nicolas Constant | 0ee30eba76 | |
Nicolas Constant | 8000f51aaa | |
Nicolas Constant | 37325cb188 | |
Nicolas Constant | 164bf22484 | |
Nicolas Constant | 5f7f77a60e | |
Nicolas Constant | b0c16b3fa8 | |
Nicolas Constant | 879bfb7d23 | |
Nicolas Constant | 6088be077f | |
Yannick A | a0b43be0a7 | |
Nicolas Constant | 73c264e9e7 | |
Nicolas Constant | ab30bb1a06 | |
Nicolas Constant | 1e9f0d4137 | |
Nicolas Constant | 93a0d17fc0 | |
Nicolas Constant | f4ff3d0e94 | |
Nicolas Constant | 18f6caf24a | |
Nicolas Constant | 8f24e65c49 | |
Nicolas Constant | 84d32065a6 | |
Nicolas Constant | c14009692f | |
Nicolas Constant | 6a7d389b55 | |
Nicolas Constant | e40f5e41e5 | |
Nicolas Constant | b48e8e219e | |
Nicolas Constant | 8e5525741f | |
Nicolas Constant | b3d24b61d9 | |
Nicolas Constant | 9595e6f5db | |
Nicolas Constant | 698fd39aa6 | |
Nicolas Constant | 7169a30481 | |
Nicolas Constant | bdaa2068cf | |
Nicolas Constant | 79566d4856 | |
Nicolas Constant | 0635397087 | |
Nicolas Constant | 94fe3eff31 | |
Nicolas Constant | 3c93dcb709 | |
Nicolas Constant | 8139f1a601 | |
Nicolas Constant | f7187353bb | |
Nicolas Constant | 3c8805b876 | |
Nicolas Constant | 5bff9a12e2 | |
Nicolas Constant | 51ef48150f | |
Nicolas Constant | 8d9895b0c9 | |
Nicolas Constant | 16ea7205dd | |
Nicolas Constant | 586aef214c | |
Nicolas Constant | e064297187 | |
Nicolas Constant | b71743b8f6 | |
Nicolas Constant | fde017ccf6 | |
Nicolas Constant | 7008dbbbba | |
Nicolas Constant | 25d468ad21 | |
Nicolas Constant | 42217b42b8 | |
Nicolas Constant | 54bce7762e | |
Nicolas Constant | 6f96de22ce | |
Nicolas Constant | 560147743d | |
Nicolas Constant | bd44586d96 | |
Nicolas Constant | ed81e4b18a | |
Nicolas Constant | a6a8f4b876 | |
Nicolas Constant | 369d65769e | |
Nicolas Constant | 51709508f0 | |
Nicolas Constant | 9eb6fc61ae | |
Nicolas Constant | 7dab421c26 | |
Nicolas Constant | c7b543c780 | |
Nicolas Constant | 75cb0a1509 | |
Nicolas Constant | d8f2a38e5b | |
Nicolas Constant | af790dc646 | |
Nicolas Constant | 4269ca2700 | |
Nicolas Constant | 0a1156c137 | |
Nicolas Constant | a4a8efe3cb | |
Nicolas Constant | 2e8c9b5e9f | |
Nicolas Constant | 11c6ef955e | |
Nicolas Constant | 311a72454b | |
Nicolas Constant | c6fa7d1b27 | |
Nicolas Constant | d036a5cd9a | |
Nicolas Constant | 931bc92939 | |
Nicolas Constant | e0bfdb546d | |
Nicolas Constant | 607ad8c945 | |
Nicolas Constant | 8b202696b5 | |
Nicolas Constant | de0c7b1761 | |
Nicolas Constant | 1aec5e9438 | |
Nicolas Constant | df7e6c0d1f | |
Nicolas Constant | f1ec2a9068 | |
Nicolas Constant | 30ec7025c1 | |
Nicolas Constant | 51f048bfd5 | |
Nicolas Constant | 3bd2b7aaa8 | |
Nicolas Constant | 5860422d83 | |
Nicolas Constant | ed2cf94519 | |
Nicolas Constant | 861d2956e4 | |
Nicolas Constant | 5cc43fa2bd | |
Nicolas Constant | e2d1e1893a | |
Nicolas Constant | b1a8ffee2f | |
Nicolas Constant | 58a05e2b72 | |
Nicolas Constant | 628b3e3f1f | |
Nicolas Constant | 277a2a76df | |
Nicolas Constant | 179f0c8cd0 | |
Nicolas Constant | c2e8247a58 | |
Nicolas Constant | 19bb19c5b0 | |
Nicolas Constant | 82206bc9c9 | |
Nicolas Constant | 9c53010ae8 | |
Nicolas Constant | 8b397a3f7c | |
Nicolas Constant | b65737e237 | |
Nicolas Constant | c75245fabe | |
Nicolas Constant | 3d16be26c5 | |
Nicolas Constant | caf17f9662 | |
Nicolas Constant | 4b9cb381ce | |
Nicolas Constant | b848e28f11 | |
Nicolas Constant | c83a464901 | |
Nicolas Constant | ed55aa8c75 | |
Nicolas Constant | 2a57ee8164 | |
Nicolas Constant | 646888d9f2 | |
Nicolas Constant | 96508209aa | |
Nicolas Constant | 3fe127121e | |
Nicolas Constant | 1290d09440 | |
Nicolas Constant | 17ba7eb27b | |
Nicolas Constant | e3e8761e0e | |
Nicolas Constant | a3e453ae0e | |
Nicolas Constant | fbc10b049f | |
Nicolas Constant | d982c7a84b | |
Nicolas Constant | ea6343355e | |
Nicolas Constant | a4bcddf533 | |
Nicolas Constant | 1519cb0b3d | |
Nicolas Constant | 74b4b6c623 | |
Nicolas Constant | 85ac14351f | |
Nicolas Constant | a88cf6a50e | |
Nicolas Constant | 4ea25c4866 | |
Nicolas Constant | ef984fed18 | |
Nicolas Constant | ecd75676bb | |
Nicolas Constant | dbdb08dd48 | |
Nicolas Constant | 7d7fda2aca | |
Nicolas Constant | 2b2d467e74 | |
Nicolas Constant | 4e9858874c | |
Nicolas Constant | 75ad9752bf | |
Nicolas Constant | 15ab7ba56e | |
Nicolas Constant | 49a87aeef0 | |
Nicolas Constant | a98048b4fd | |
Nicolas Constant | f8868f1fed | |
Nicolas Constant | 918a969c2d | |
Nicolas Constant | 6268fa405b | |
Nicolas Constant | 68dd97e5a9 | |
Nicolas Constant | ce705f8c6b | |
Nicolas Constant | 9b03781c73 | |
Nicolas Constant | ef4e9b7ff8 | |
Nicolas Constant | 10c8189b77 | |
Nicolas Constant | e76d1d84bc | |
Nicolas Constant | 71139eb4c0 | |
Nicolas Constant | decb4316d0 | |
Nicolas Constant | 4ea7181590 | |
Nicolas Constant | ecc2db9eb6 | |
Nicolas Constant | f3b13879b3 | |
Nicolas Constant | 5e2ab0365f | |
Nicolas Constant | 7144463c5a | |
Nicolas Constant | ff7148c33b | |
Nicolas Constant | 239aa36109 | |
Nicolas Constant | 8d489cb5d7 | |
Nicolas Constant | 1272f2303a | |
Nicolas Constant | e6e75923b0 | |
Nicolas Constant | 687148bc3a | |
Nicolas Constant | b15cc6047b | |
Nicolas Constant | c4e0cfa877 | |
Nicolas Constant | 18c745c9d6 | |
Nicolas Constant | d1a85d05c8 | |
Nicolas Constant | e84164ef87 | |
Nicolas Constant | 2714af39ff | |
Nicolas Constant | 9ee6038bad | |
Nicolas Constant | 6329506950 | |
Nicolas Constant | cff9d41d9d | |
Nicolas Constant | da58d0723a | |
Nicolas Constant | e6de932764 | |
Nicolas Constant | 48e29110c3 | |
Nicolas Constant | 917564c067 | |
Nicolas Constant | 274857a522 | |
Nicolas Constant | 2f0cb2b959 | |
Nicolas Constant | 3a2761eeb1 | |
Nicolas Constant | fe9cb7fea1 | |
Nicolas Constant | a41a46168a | |
Nicolas Constant | 0aed10cbff | |
Nicolas Constant | 9aefdbe870 | |
Nicolas Constant | a9350ff31b | |
Nicolas Constant | 3b8448369d | |
Nicolas Constant | 57b98e9e4f | |
Nicolas Constant | 2cb2e8770e | |
Nicolas Constant | d9c570a1bf | |
Nicolas Constant | 8ae2edf164 | |
Nicolas Constant | 1cd2833ede | |
Nicolas Constant | 59944b68f2 | |
Nicolas Constant | c27ed4dc2d | |
Nicolas Constant | f43fcaa38b | |
Nicolas Constant | 06240c9374 | |
Nicolas Constant | c5b325ce8a | |
Nicolas Constant | a1c671dfbf | |
Nicolas Constant | b17f2742a4 | |
Nicolas Constant | 4d461b497e | |
Nicolas Constant | 49887613c8 | |
Nicolas Constant | e978a1534a | |
Nicolas Constant | 146ee28e79 | |
Nicolas Constant | 2fa9d95000 | |
Nicolas Constant | 3a287ee05e | |
Nicolas Constant | 939c59655f | |
Nicolas Constant | 6f0c949a91 | |
Nicolas Constant | 5e7d151cad | |
Nicolas Constant | 239e86332f | |
Nicolas Constant | 0cd16fd0de | |
Nicolas Constant | 55edfbd24a | |
Nicolas Constant | b72049fd28 | |
Nicolas Constant | 00cd6ae592 | |
Nicolas Constant | f9ac4319aa | |
Nicolas Constant | 6d83550557 | |
Nicolas Constant | 3d828e8a77 | |
Nicolas Constant | 611bccc383 | |
Nicolas Constant | 24821d9e94 | |
Nicolas Constant | c871b2afe2 | |
Nicolas Constant | c804fbce68 | |
Nicolas Constant | 8df8eb7de8 | |
Nicolas Constant | 78dd3f123b | |
Nicolas Constant | 5731962603 | |
Nicolas Constant | 6a2ec8bd23 | |
Nicolas Constant | 6ab3fc1bd8 | |
Nicolas Constant | 88b32f5b5a | |
Nicolas Constant | 993202bfff | |
Nicolas Constant | 153dc60f4f | |
Nicolas Constant | 1e9a7edf97 | |
Nicolas Constant | bf1b84e980 | |
Nicolas Constant | 5e67c8bdd0 | |
Nicolas Constant | 7ae94303da | |
Nicolas Constant | f8b376a001 | |
Nicolas Constant | 33ebfb0e78 | |
Nicolas Constant | 71e6e1ed18 | |
Nicolas Constant | 21bade1dc9 | |
Nicolas Constant | a7ec96c29f | |
Nicolas Constant | 252313aac2 | |
Nicolas Constant | fb79e53b96 | |
Nicolas Constant | 6723656371 | |
Nicolas Constant | 646cf49c8f | |
Nicolas Constant | 3abaa916b8 | |
Nicolas Constant | 3486b8eb14 | |
Nicolas Constant | 6c7915e4b1 | |
Nicolas Constant | be17f56140 | |
Nicolas Constant | a7f50b2a1f | |
Nicolas Constant | f925f08d00 | |
Nicolas Constant | ccbe9bb01a | |
Nicolas Constant | 0702063f69 | |
Nicolas Constant | 3bca277b0c | |
Mostafa Ahangarha | e6d780aa4b | |
Nicolas Constant | 92fc1d4831 | |
Nicolas Constant | 73f0628519 | |
Nicolas Constant | 882d123881 | |
Nicolas Constant | 975b3a6e2b | |
Nicolas Constant | df6213ee1a | |
Nicolas Constant | 3fc0b5f34f | |
Nicolas Constant | 345f80a462 | |
Nicolas Constant | 2064940361 | |
Nicolas Constant | 7a7b09b59d | |
Nicolas Constant | d82d5d3fcd | |
Nicolas Constant | 4ef26b2f61 | |
Nicolas Constant | 99011b5853 | |
Nicolas Constant | 6afde304d0 | |
Nicolas Constant | 6c71820bfc | |
Nicolas Constant | 6730de097c | |
Nicolas Constant | df9da325f1 | |
Nicolas Constant | 5fc7211adb | |
Nicolas Constant | bb0693bb24 | |
Nicolas Constant | 4ba5c98da1 | |
Nicolas Constant | c311bcee16 | |
Nicolas Constant | 342742b730 | |
Nicolas Constant | 4fd1689518 | |
Nicolas Constant | 7ee5cdb4f1 | |
Nicolas Constant | 52f5aa69df | |
Nicolas Constant | fb4a274956 | |
Nicolas Constant | fc6d994f42 | |
Nicolas Constant | fb018a7a80 | |
Nicolas Constant | 39b8b15f72 | |
Nicolas Constant | c182540ca8 | |
Nicolas Constant | 29d646477d | |
Nicolas Constant | c09b89b131 | |
Nicolas Constant | bd11d1dfea | |
Nicolas Constant | 3e9bd59a7f | |
Nicolas Constant | 756deb56f4 | |
Nicolas Constant | 7a001a7f67 | |
Nicolas Constant | 8ddd06facd | |
Nicolas Constant | ce277d57b6 | |
Nicolas Constant | f8eba5a479 | |
Nicolas Constant | 6eb3e44923 | |
Nicolas Constant | d7d1952c5e | |
Nicolas Constant | 97976a1f68 | |
Nicolas Constant | 24ed03f66a | |
Nicolas Constant | 0db82b4ce3 | |
Nicolas Constant | f8bd0adc53 | |
Nicolas Constant | 1dd52dde34 | |
Nicolas Constant | 36f9a1f87a | |
Nicolas Constant | c177efa2e7 | |
Nicolas Constant | 423a32a888 | |
Nicolas Constant | 93947744be | |
Nicolas Constant | 7414539f09 | |
Nicolas Constant | eab60d4449 | |
Nicolas Constant | 0cd17f25d6 | |
Nicolas Constant | de20c8f4f4 | |
Nicolas Constant | 9e3389ed11 | |
Nicolas Constant | 4390a5e89c | |
Nicolas Constant | a9bee0f9fb | |
Nicolas Constant | c846859461 | |
Nicolas Constant | b060c6f7c5 | |
Nicolas Constant | 7f82edf7be | |
Nicolas Constant | c5bf07a1a6 | |
Nicolas Constant | 903fdce232 | |
Nicolas Constant | 6d7ddec1bd | |
Nicolas Constant | 329bdffd20 | |
Nicolas Constant | 382a3df3bf | |
Nicolas Constant | 99b1ea3a2b | |
Nicolas Constant | e137317dc1 | |
Nicolas Constant | bbfb4bc026 | |
Nicolas Constant | a23db9b30e | |
Nicolas Constant | ce0e7f09cd | |
Nicolas Constant | 85cc61df8a | |
Nicolas Constant | 24324ddccb | |
Nicolas Constant | 4b0924dd3b | |
Mostafa Ahangarha | 900652e5e8 | |
Nicolas Constant | 832ed7e9bc | |
Nicolas Constant | 46873c7b8b | |
Nicolas Constant | f8e1a49218 | |
Nicolas Constant | 661019a5ec | |
Nicolas Constant | 81d716279a | |
Nicolas Constant | f9bf362935 | |
Nicolas Constant | 36fb623ace | |
Nicolas Constant | 0fe3cbce62 | |
Nicolas Constant | e88a568a99 | |
Nicolas Constant | eb41cbf8e9 | |
Nicolas Constant | 6cd3d7272a | |
Nicolas Constant | e3b2ffcd58 | |
Nicolas Constant | da759f0523 | |
Nicolas Constant | c56ae31964 | |
Nicolas Constant | 58414baa8e | |
Nicolas Constant | 5b6b463b90 | |
Nicolas Constant | dc101db6ef | |
Nicolas Constant | 9a9406cabb | |
Nicolas Constant | f52ed00db5 | |
Nicolas Constant | e5aea3c4a6 | |
Nicolas Constant | ed1e52232d | |
Nicolas Constant | 8c36620b0a | |
Nicolas Constant | 4ba42a0d0c | |
Nicolas Constant | 03be3db8c1 | |
Nicolas Constant | e0b85dd885 | |
Nicolas Constant | a5948b05e0 | |
Nicolas Constant | 201341f0e7 | |
Nicolas Constant | 55bc557512 | |
Nicolas Constant | b977134109 | |
Nicolas Constant | 216e433bec | |
Nicolas Constant | 7cfa11be51 | |
Nicolas Constant | 6528af69ab | |
Nicolas Constant | 6409a06970 | |
Nicolas Constant | 41b3c83af6 | |
Nicolas Constant | 11ddcacb8c | |
Nicolas Constant | c7ca6fb61c | |
Nicolas Constant | cf78c57b55 | |
Nicolas Constant | c85eaee78b | |
Nicolas Constant | 28ca6d7fa0 | |
Nicolas Constant | 68367a783a | |
Nicolas Constant | 8139a0ff72 | |
Nicolas Constant | 51cfefdd15 | |
Nicolas Constant | 6eac5b0042 | |
Nicolas Constant | 252724aa7d | |
Nicolas Constant | 27e4f44f22 | |
Nicolas Constant | 088f92a15b | |
Nicolas Constant | fde828e779 | |
Nicolas Constant | 24177f50b2 | |
Nicolas Constant | 086d60b49f | |
Nicolas Constant | 46383c5fda | |
Nicolas Constant | 7945c4166e | |
Nicolas Constant | ebbd5c944f | |
Mike C | d43426641d | |
Nicolas Constant | b28a023e47 | |
Nicolas Constant | 11beefa883 | |
Nicolas Constant | c69ff3dd3a | |
Nicolas Constant | 2d9b48dede | |
Nicolas Constant | 4589577119 | |
Nicolas Constant | 55d5f82e80 | |
Nicolas Constant | 0c00a2aa86 | |
Nicolas Constant | fc18ce5f1f | |
Nicolas Constant | 1e0a426bfd | |
Nicolas Constant | 1a243c3ee6 | |
Nicolas Constant | 05e4a87524 | |
Nicolas Constant | 2b9adece64 | |
Nicolas Constant | c5d3f6c97b | |
Nicolas Constant | f81c76c80c | |
Nicolas Constant | f96309ed01 | |
Nicolas Constant | 8ae06f1439 | |
Nicolas Constant | 5164d28d11 | |
Nicolas Constant | 21f32b91b0 | |
Nicolas Constant | 8299137b56 | |
Nicolas Constant | 9a0af9f246 | |
Nicolas Constant | 802e0afdd0 | |
Nicolas Constant | 5db27f2dbb | |
Nicolas Constant | d777b78063 | |
Nicolas Constant | a0bce355e7 | |
Nicolas Constant | b180c1d57e | |
Nicolas Constant | c5bb09a42e | |
Nicolas Constant | 8ba78b3c19 | |
Nicolas Constant | a06d178bae | |
Nicolas Constant | 0fdd1c9896 | |
Nicolas Constant | 0d0ed5d318 | |
Nicolas Constant | caaa301349 | |
Nicolas Constant | 5cd55393f8 | |
Nicolas Constant | b332955abf | |
Nicolas Constant | fa5b6dc562 | |
Nicolas Constant | 3cc0f8a6fc | |
Nicolas Constant | 113cefb36f | |
Nicolas Constant | 716819524b | |
Nicolas Constant | 733c21af7d | |
Nicolas Constant | 956b8894a0 | |
Nicolas Constant | 1ea39621ad | |
Nicolas Constant | 2b6ad187ac | |
Nicolas Constant | 560cf19903 | |
Nicolas Constant | 5858d96370 | |
Nicolas Constant | 88ebe89217 | |
Nicolas Constant | 7f748a7e12 | |
Nicolas Constant | f0035149eb | |
Nicolas Constant | ef0602e54a | |
Nicolas Constant | cc45d7fb25 | |
Nicolas Constant | 1c6b0784db | |
Nicolas Constant | f7f5678526 | |
Nicolas Constant | f6b7f95bd6 | |
Nicolas Constant | 3c798baab1 | |
Nicolas Constant | 6bfdc07dfa | |
Nicolas Constant | f02047a0bd | |
Nicolas Constant | 798f8f404d | |
Nicolas Constant | eb66cb7760 | |
Nicolas Constant | 2bedbd6cf2 | |
Nicolas Constant | 877449cd4b | |
Nicolas Constant | 7fef28ee8f | |
Nicolas Constant | bf687ebb62 | |
Nicolas Constant | 235e81cccb | |
Nicolas Constant | fc6724d57e | |
Nicolas Constant | 77ac1764cd | |
Nicolas Constant | 7a54e18bf7 | |
Nicolas Constant | d5e78dec0b | |
Nicolas Constant | ff02dea006 | |
Nicolas Constant | 5d61c44700 | |
Nicolas Constant | b5f543dac3 | |
Nicolas Constant | 5d99d37384 | |
Nicolas Constant | 52202af43e | |
Nicolas Constant | 98ac80dccf | |
Nicolas Constant | dc6cd2fb17 | |
Nicolas Constant | 23c9e6873b | |
Nicolas Constant | 78d3cc9631 | |
Nicolas Constant | c320518159 | |
Nicolas Constant | 5697445c08 | |
Nicolas Constant | b68f12d975 | |
Nicolas Constant | 94f7a52d3f | |
Nicolas Constant | 5c1617d673 | |
Nicolas Constant | 5be372a3e9 | |
Nicolas Constant | 985513335e | |
Nicolas Constant | e77858e021 | |
Nicolas Constant | e51973df54 | |
Nicolas Constant | 433917fae4 | |
Nicolas Constant | c38db1d1e1 | |
Nicolas Constant | 1351a06eb8 | |
Nicolas Constant | 0ce6cf8d4b | |
Nicolas Constant | 90d227efca | |
Nicolas Constant | 4cf824caca | |
Nicolas Constant | caa0964116 | |
Nicolas Constant | b3964fd334 | |
Nicolas Constant | 63475fd813 | |
Nicolas Constant | 0c62aba240 | |
Nicolas Constant | a7e55b5200 | |
Nicolas Constant | cb7d3d7079 | |
Nicolas Constant | 97808aacd1 | |
Nicolas Constant | ba1d965c2f | |
Nicolas Constant | a3204550f9 | |
Nicolas Constant | 7a66cbeeaa | |
Nicolas Constant | 25648da660 | |
Nicolas Constant | e4fb591170 | |
Nicolas Constant | 9ef0a9a9e8 | |
Nicolas Constant | dc134296cb | |
Nicolas Constant | fd94db2e75 | |
Nicolas Constant | 9f77d16b5b | |
Nicolas Constant | 965a05abc0 | |
Nicolas Constant | d53e6f8d48 | |
Nicolas Constant | b4c5c3b094 | |
Nicolas Constant | 801626df8d | |
Nicolas Constant | d3eb7073f7 | |
Nicolas Constant | bf3d88ce48 | |
Nicolas Constant | 58b265e0ac | |
Nicolas Constant | fd42a3c5d2 | |
Nicolas Constant | c42d63cc4c | |
Nicolas Constant | c0b21b3658 | |
Nicolas Constant | 59a423f952 | |
Nicolas Constant | 9c01350f07 | |
Nicolas Constant | 72cf69d9c0 | |
Nicolas Constant | f11d82c072 | |
Nicolas Constant | 761a910d31 | |
Nicolas Constant | 58e3f4a34c | |
Nicolas Constant | 1c77cd3594 | |
Nicolas Constant | 962557c1b0 | |
Nicolas Constant | faa7399d39 | |
Nicolas Constant | 2dea5544eb | |
Nicolas Constant | 30aec1645b | |
Nicolas Constant | 5778e7400d | |
Nicolas Constant | 59c323112c | |
Nicolas Constant | 535ad08438 | |
Nicolas Constant | bd970f2196 | |
Nicolas Constant | 1c4438dbb9 | |
Nicolas Constant | ed43487091 | |
Nicolas Constant | 0a3ddb0b87 | |
Nicolas Constant | 4ed3483b81 | |
Nicolas Constant | 9b7f60c0e2 | |
Nicolas Constant | fa7ea5212a | |
Nicolas Constant | 7a7f2488e3 | |
Nicolas Constant | 8b72d551ec | |
Nicolas Constant | f8d3f7b4f7 | |
Nicolas Constant | b4bec669fc | |
Nicolas Constant | 04a09dff83 | |
Nicolas Constant | c18be61fd1 | |
Nicolas Constant | 1ffda7f5cd | |
Nicolas Constant | 9a4c0ab6dc | |
Nicolas Constant | 6b0ef5cca1 | |
Nicolas Constant | 4ba6c1f119 | |
Nicolas Constant | 612e7526a7 | |
Nicolas Constant | 1bba36b474 | |
Nicolas Constant | f7ec1ce0a3 | |
Nicolas Constant | 0ae14edba5 | |
Nicolas Constant | 1885459eef | |
Nicolas Constant | 469d04d9bd | |
Nicolas Constant | bf9fe144b3 | |
Nicolas Constant | 2339ca55ea | |
Nicolas Constant | ed42426733 | |
Nicolas Constant | 50d32888b2 | |
Nicolas Constant | c97210b205 | |
Nicolas Constant | e44dc451bf | |
Nicolas Constant | 893fb73363 | |
Nicolas Constant | a33a01289a | |
Nicolas Constant | 4af542e713 | |
Nicolas Constant | dcb6858ce0 | |
Nicolas Constant | be62a172e2 | |
Nicolas Constant | 6b57f06381 | |
Nicolas Constant | f019b8d68b | |
Nicolas Constant | aa6b93eebb | |
Nicolas Constant | 80238b0e1f | |
Nicolas Constant | 1eaafb12a9 | |
Nicolas Constant | fdcdf9d813 | |
Nicolas Constant | b74fcdd814 | |
Nicolas Constant | 4e10edb274 | |
Nicolas Constant | 4f8657f043 | |
Nicolas Constant | ede2e13e48 | |
Nicolas Constant | 0ced6445b5 | |
Nicolas Constant | 6d4beceb32 | |
Nicolas Constant | b23888fbfa | |
Nicolas Constant | c98bd53496 | |
Nicolas Constant | 65f2154ea1 | |
Nicolas Constant | 0d44668b7a | |
Nicolas Constant | e1d1cddda7 | |
Nicolas Constant | 576d5fe61b | |
Nicolas Constant | 8f614ccecd | |
Nicolas Constant | 706bdb37c1 | |
Nicolas Constant | f0573e06cc | |
Nicolas Constant | c1dd3bba3f | |
Nicolas Constant | c991a8a387 | |
Nicolas Constant | 660d66ab6d | |
Nicolas Constant | 591bd34cac | |
Nicolas Constant | 8aee9c8245 | |
Nicolas Constant | 264ebcd951 | |
Nicolas Constant | eb84510404 | |
Nicolas Constant | 03870964f4 | |
Nicolas Constant | b93315092b | |
Nicolas Constant | 946382dd73 | |
Nicolas Constant | f8e99cefd0 | |
Nicolas Constant | 5936ef29fb | |
Nicolas Constant | 8d5dea08ff | |
Nicolas Constant | 374f0b208d | |
Nicolas Constant | 9ed3f83c8e | |
Nicolas Constant | 7f8098d9a4 | |
Nicolas Constant | 1956e06121 | |
Nicolas Constant | f3fe5f900b | |
Nicolas Constant | 9c8a1a829f | |
Nicolas Constant | a9ba1c6ba4 | |
Nicolas Constant | 370d98087a | |
Nicolas Constant | 09fcfab98a | |
Nicolas Constant | 6786c95d91 | |
Nicolas Constant | 2d4243f0dc | |
Nicolas Constant | fbf65b39c8 | |
Nicolas Constant | 2d6882b1f8 | |
Nicolas Constant | 568b6014c1 | |
Nicolas Constant | 18afb5e538 | |
Nicolas Constant | 421905cd41 | |
Nicolas Constant | 6ac168fbd9 | |
Nicolas Constant | af5bab34ac | |
Nicolas Constant | 394fe508a1 | |
Nicolas Constant | 020fbd9de3 | |
Nicolas Constant | 2d01b3297e | |
Nicolas Constant | b8fcfb6cb5 | |
Nicolas Constant | 69cb670dcc | |
Nicolas Constant | 74de21f5e6 | |
Nicolas Constant | 30e9fdc2d9 | |
Nicolas Constant | 60f8c23c5b | |
Nicolas Constant | b733631b28 | |
Nicolas Constant | 9e224efa88 | |
Nicolas Constant | d1c0488460 | |
Nicolas Constant | 8aac46a4c2 | |
Nicolas Constant | 68cc397b89 | |
Nicolas Constant | 13a895a114 | |
Nicolas Constant | 33e03a9207 | |
Nicolas Constant | 108e5c4b82 | |
Nicolas Constant | 35f8c30c26 | |
Nicolas Constant | 9011b593ee | |
Nicolas Constant | bdd6a1b1f3 | |
Nicolas Constant | 90157c1ee7 | |
Nicolas Constant | a2e0789a9c | |
Nicolas Constant | b2632e981d | |
Nicolas Constant | 84fe025f47 | |
Nicolas Constant | dac2cbfabd | |
Nicolas Constant | a35c7e911d | |
Nicolas Constant | 7bdcc6277f | |
Nicolas Constant | 145b8617b1 | |
Nicolas Constant | 3f8df79f33 | |
Nicolas Constant | 7d4e4e90c6 | |
Nicolas Constant | e840d5f1b7 | |
Nicolas Constant | 405031edd8 | |
Nicolas Constant | 5d9bdccdf9 | |
Nicolas Constant | cd699b9da4 | |
Nicolas Constant | e6f8d9f855 | |
Nicolas Constant | cbcc1f85e1 | |
Nicolas Constant | dcb929eac3 | |
Nicolas Constant | b5f063b158 | |
Nicolas Constant | 44ed56346e | |
Nicolas Constant | 801445cd8c | |
Nicolas Constant | 0fa56da2e4 | |
Nicolas Constant | af45279efa | |
Nicolas Constant | 06982bd891 | |
Nicolas Constant | 8ad9ecb95b | |
Nicolas Constant | 6c9653f945 | |
Nicolas Constant | b81d73f062 | |
Nicolas Constant | 04cdb6ebce | |
Nicolas Constant | 62c5d42493 | |
Nicolas Constant | 2bc4393488 | |
Nicolas Constant | da7ddc7f3d | |
Nicolas Constant | 1327860c07 | |
Nicolas Constant | 0e214f1204 | |
Nicolas Constant | 8d5d6ae5d1 | |
Nicolas Constant | 02caa33a8c | |
Nicolas Constant | fe8b14f94e | |
Nicolas Constant | 2e0f7cb5a5 | |
Nicolas Constant | 9feddaf819 | |
Nicolas Constant | 0e1ca8cf71 | |
Nicolas Constant | fdd9d751e7 | |
Nicolas Constant | 6e2bb144fc | |
Nicolas Constant | 1d4c8b5dad | |
Nicolas Constant | 8f540c48f8 | |
Nicolas Constant | 3c45a3de0b | |
Nicolas Constant | 1f1a86c692 | |
Nicolas Constant | b3ada67b11 | |
Nicolas Constant | 53b360b2cb | |
Nicolas Constant | 653c83f5bb | |
Nicolas Constant | f126cf2b37 | |
Nicolas Constant | ec86d38778 | |
Nicolas Constant | ab76d1de35 | |
Nicolas Constant | 83258ed553 | |
Nicolas Constant | ba4fa61667 | |
Nicolas Constant | 803913ccf0 | |
Nicolas Constant | 26bc0cf44b | |
Nicolas Constant | f591955e49 | |
Nicolas Constant | 6cb2132bf9 | |
Nicolas Constant | b8311371f9 | |
Nicolas Constant | f72141a794 | |
Nicolas Constant | c122d66fb6 | |
Nicolas Constant | 979d21bdfe | |
Nicolas Constant | 55684d5ada | |
Nicolas Constant | 131054514c | |
Nicolas Constant | 93fe06979c | |
Nicolas Constant | 43ecbde31a | |
Nicolas Constant | 34644e0652 | |
Nicolas Constant | dd03ea0e3b | |
Nicolas Constant | 9c87d6baa5 | |
Nicolas Constant | 656e2dca00 | |
Nicolas Constant | 3d017da166 | |
Nicolas Constant | 5ed1e562e4 | |
Nicolas Constant | 877f142238 | |
Nicolas Constant | 72c83a436c | |
Nicolas Constant | 6b12487b85 | |
Nicolas Constant | cf83f7367b | |
Nicolas Constant | 9e6bf71230 | |
Nicolas Constant | 1871ce4566 | |
Nicolas Constant | 839e9bb4b1 | |
Nicolas Constant | 00825aefb8 | |
Nicolas Constant | 7006f7453e | |
Nicolas Constant | 3dffcdad51 | |
Nicolas Constant | 0418ee6c2f | |
Nicolas Constant | 9b129ae7b4 | |
Nicolas Constant | 4f9f37c685 | |
Nicolas Constant | 6d0b78af92 | |
Nicolas Constant | 5ffd54470e | |
Nicolas Constant | 34bdd12029 | |
Nicolas Constant | 3fda1e7b9b | |
Nicolas Constant | b2cad5fd8a | |
Nicolas Constant | 0e9bff10f6 | |
Nicolas Constant | 86ce3e5ae0 | |
Nicolas Constant | 5a344531ce | |
Nicolas Constant | b7b84dae24 | |
Nicolas Constant | a928778ecb | |
Nicolas Constant | a8ed5ac79f | |
Nicolas Constant | 3448ae56fb | |
Nicolas Constant | 2170a5ec29 | |
Nicolas Constant | f24c60fa02 | |
Nicolas Constant | cdc81508e2 | |
Nicolas Constant | a1db774767 | |
Nicolas Constant | f592adc38c | |
Nicolas Constant | 5fcbeeceba | |
Nicolas Constant | 6302b07c96 | |
Nicolas Constant | 53d971ff88 | |
Nicolas Constant | 255c3ec6fa | |
Nicolas Constant | 12444aa603 | |
Nicolas Constant | b3a97bd75b | |
Nicolas Constant | a4331ff3d2 | |
Nicolas Constant | 274bd951f5 | |
Nicolas Constant | 2fd7beb0fb | |
Nicolas Constant | 8ed0e2655d | |
Nicolas Constant | 72e72cfc86 | |
Nicolas Constant | 5568c3c38d | |
Nicolas Constant | 8509e43df3 | |
Nicolas Constant | f811c5bbb3 | |
Nicolas Constant | 655d120cd9 | |
Nicolas Constant | 62baf8474d | |
Nicolas Constant | 41c7137dc7 | |
Nicolas Constant | 1055fe6117 | |
Nicolas Constant | 700c9c890f | |
Nicolas Constant | 6c8ff4cece | |
Nicolas Constant | 0136d71e2b | |
Nicolas Constant | 5dbe3bafa4 | |
Nicolas Constant | c29caf8a58 | |
Nicolas Constant | 95329609a9 | |
Nicolas Constant | e93de3c515 | |
Nicolas Constant | 5da0793cc3 | |
Nicolas Constant | f7045a140c | |
Nicolas Constant | fe8aadd8ff | |
Nicolas Constant | 8a578e2e3b | |
Nicolas Constant | 258fc00914 | |
Nicolas Constant | 4362e87806 | |
Nicolas Constant | 84b0006858 | |
Nicolas Constant | bd2d02f110 | |
Nicolas Constant | 251247e0db | |
Nicolas Constant | 8c5b25f864 | |
Nicolas Constant | 8256da9f4e | |
Nicolas Constant | 94f3a14dab | |
Nicolas Constant | 66037804cc | |
Nicolas Constant | 1e400d4e40 | |
Nicolas Constant | d163426dff | |
Nicolas Constant | 90effaaf2e | |
Nicolas Constant | c3dabb7352 | |
Nicolas Constant | efbf14b466 | |
Nicolas Constant | 3f5208087e | |
Nicolas Constant | 4e9b2b34f0 | |
Nicolas Constant | 158a2f4927 | |
Nicolas Constant | c56a055172 | |
Nicolas Constant | 4569352f58 | |
Nicolas Constant | 310d2ae58f | |
Nicolas Constant | f654ebd4c8 | |
Nicolas Constant | 63471bc8fd | |
Nicolas Constant | ebdff4e861 | |
Nicolas Constant | 22427779ec | |
Nicolas Constant | ae689f9730 | |
Nicolas Constant | 5181fd2d3d | |
Nicolas Constant | 6a238f265b | |
Nicolas Constant | 74e1a31031 | |
Nicolas Constant | b0cd7721a7 | |
Nicolas Constant | 3e76605d47 | |
Nicolas Constant | 106a0191c5 | |
Nicolas Constant | 8ecbd53b5b | |
Nicolas Constant | ba2479915a | |
Nicolas Constant | bc99ce15da | |
Nicolas Constant | 45fff1b064 | |
Nicolas Constant | fe897318d7 | |
Nicolas Constant | cef07f0bf8 | |
Nicolas Constant | 43f866f02e | |
Nicolas Constant | 3b6ccbf913 | |
Nicolas Constant | 6ac2c45691 |
|
@ -0,0 +1,7 @@
|
|||
.git
|
||||
.gitignore
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
.vscode
|
||||
node_modules
|
||||
dist
|
|
@ -0,0 +1 @@
|
|||
patreon: nicolasconstant
|
47
.travis.yml
|
@ -1,47 +0,0 @@
|
|||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
language: c
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
- os: linux
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1
|
||||
compiler: clang
|
||||
|
||||
node_js:
|
||||
- 10.9.0
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- icnsutils
|
||||
- graphicsmagick
|
||||
- libgnome-keyring-dev
|
||||
- xz-utils
|
||||
- xorriso
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- nvm install 10.9.0
|
||||
- npm install electron-builder@next
|
||||
- npm install
|
||||
- npm rebuild node-sass
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start &
|
||||
- sleep 3
|
||||
|
||||
script:
|
||||
- npm run dist
|
|
@ -20,5 +20,5 @@ For example:
|
|||
|
||||
|
||||
## Pull Requests
|
||||
Pull Requests are maybe a bit early right now, since the project and code can change a lot, so it's not really adviced to open PR today.
|
||||
I will notify explicitely when I'll be more opened to external contributions.
|
||||
|
||||
Please open first an [issue](https://github.com/NicolasConstant/sengi/issues/new) before working on a new functionality you would like to submit to this repository.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Credits
|
||||
|
||||
## Sounds
|
||||
* All eyes on me, Exquisite and Appointed are from [Notification Sounds](https://notificationsounds.com/)
|
||||
* Mastodon Boop is from the [Mastodon Project](https://github.com/tootsuite/mastodon) and made by [@jk@mastodon.social](https://mastodon.social/@jk)
|
|
@ -0,0 +1,28 @@
|
|||
# Sengi's Docker documentation
|
||||
|
||||
Here is some more detailed informations for Sengi's Docker users.
|
||||
|
||||
## Deploy Sengi's
|
||||
|
||||
Execute:
|
||||
|
||||
```
|
||||
docker run -d -p 80:80 nicolasconstant/sengi
|
||||
```
|
||||
|
||||
Sengi will then be available on port 80
|
||||
|
||||
## Landing page
|
||||
|
||||
Sengi's docker contains a landing page so that you can open a pop-up easily.<br />
|
||||
It's available in ```https://your-host/start/index.html```
|
||||
|
||||
## Personalize the Privacy Statement
|
||||
|
||||
You can personalize the privacy statement by linking it as follow:
|
||||
|
||||
```
|
||||
docker run -d -p 80:80 -v /Path/privacy.html:/app/assets/docs/privacy.html nicolasconstant/sengi
|
||||
```
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
FROM node:10-buster-slim AS build
|
||||
|
||||
WORKDIR /build
|
||||
ADD . /build
|
||||
|
||||
RUN apt update && apt install --yes git binutils
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --update --no-cache lighttpd
|
||||
|
||||
ADD lighttpd.conf /etc/lighttpd/lighttpd.conf
|
||||
COPY --from=build /build/dist /app
|
||||
COPY --from=build /build/assets/docker_init /app/start
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["lighttpd", "-D"]
|
||||
CMD ["-f", "/etc/lighttpd/lighttpd.conf"]
|
64
README.md
|
@ -4,45 +4,69 @@
|
|||
|
||||
Sengi is a **Mastodon** and **Pleroma** desktop focused client. It takes inspiration from the old Tweetdeck [client](https://static.makeuseof.com/wp-content/uploads/2012/02/muo-tweetdeck2b.png), the new Tweetdeck webapp and Mastodon UI.
|
||||
|
||||
Focus will be made on the following points:
|
||||
It is strongly focused on the following points:
|
||||
|
||||
* Heavily oriented on multi-accounts usage
|
||||
* Desktop based interactions (right clic, left clic, etc)
|
||||
* One column at a time display (leaves it on the side of your screen, and keep an eye on it while doing your other stuff)
|
||||
* Desktop based interactions (right click, left click, etc)
|
||||
* One column at a time display (leave it on the side of your screen, and keep an eye on it while doing your other stuff)
|
||||
|
||||
It will be released as a **browser webapp** and also packaged as an **cross-platform desktop application** (Mac, Windows, and Linux).
|
||||
It is released as a **browser webapp** and also packaged as an **cross-platform desktop application** (Mac, Windows, and Linux).
|
||||
|
||||
The Electron code isn't hosted here anymore, and you'll find it [here](https://github.com/NicolasConstant/sengi-electron).
|
||||
|
||||
## Official project page
|
||||
|
||||
[Discover Sengi](https://nicolasconstant.github.io/sengi/)
|
||||
|
||||
## State of development
|
||||
|
||||
Sengi is at a very early development stage, a lot has to be done before a first pre-release.
|
||||
The first major stable release has been published (1.0.0), the project is open to external contributions.
|
||||
|
||||
## Screens
|
||||
|
||||
soon™
|
||||
![https://raw.githubusercontent.com/NicolasConstant/sengi/master/docs/images/presentation_small.gif](https://raw.githubusercontent.com/NicolasConstant/sengi/master/docs/images/presentation_small.gif)
|
||||
|
||||
## Docker
|
||||
|
||||
A docker image is available for auto-hosting your own Sengi webapp!
|
||||
|
||||
```
|
||||
docker run -d -p 80:80 nicolasconstant/sengi
|
||||
```
|
||||
|
||||
Find more informations [here](https://github.com/NicolasConstant/sengi/blob/master/DOCKER.md).
|
||||
|
||||
The docker image also provide a landing page so that you can open a pop-up really easily. <br />
|
||||
It's available in ```https://your-host/start/index.html```
|
||||
|
||||
## Contact
|
||||
|
||||
* [Official Sengi Account](https://mastodon.social/@sengi_app)
|
||||
|
||||
## Contribute
|
||||
|
||||
Please see the [contributing guidelines](https://github.com/NicolasConstant/sengi/blob/master/CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPLv3 License - see [LICENSE](https://github.com/NicolasConstant/sengi/blob/master/LICENSE) for details
|
||||
|
||||
## Credits
|
||||
|
||||
See [credits](https://github.com/NicolasConstant/sengi/blob/master/CREDITS.md)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* [Angular 7](https://github.com/angular/angular)
|
||||
* [NGXS](https://github.com/ngxs/store)
|
||||
* [SASS](https://github.com/sass/dart-sass)
|
||||
* [Electron 10](https://github.com/electron/electron)
|
||||
|
||||
## What's a sengi?!
|
||||
|
||||
It's a little [elephant shrew](https://en.wikipedia.org/wiki/Elephant_shrew) from Africa:
|
||||
|
||||
![Rhynchocyon petersi](https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Rhynchocyon_petersi_from_side.jpg/400px-Rhynchocyon_petersi_from_side.jpg)
|
||||
|
||||
## Contribute
|
||||
|
||||
Please see the [contributing guidelines](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPLv3 License - see [LICENSE](LICENSE) for details
|
||||
|
||||
## Dependencies
|
||||
|
||||
* [Angular 7](https://github.com/angular/angular)
|
||||
* [NGXS](https://github.com/ngxs/store)
|
||||
* [SASS](https://github.com/sass/dart-sass)
|
||||
* [Electron 4](https://github.com/electron/electron)
|
||||
|
||||
|
||||
|
|
271
angular.json
|
@ -1,144 +1,149 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"sengi": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico"
|
||||
],
|
||||
"styles": [
|
||||
"src/sass/styles.scss"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"./src/sass",
|
||||
"./node_modules/bootstrap/scss"
|
||||
]
|
||||
},
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "sengi:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "sengi:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "sengi:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"karmaConfig": "./karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/sass/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"./src/sass",
|
||||
"./node_modules/bootstrap/scss"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"sengi": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/manifest.json"
|
||||
],
|
||||
"styles": [
|
||||
"src/sass/styles.scss",
|
||||
"node_modules/@ctrl/ngx-emoji-mart/picker.css"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"./src/sass",
|
||||
"./node_modules/bootstrap/scss"
|
||||
]
|
||||
},
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "src/ngsw-config.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sengi-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "sengi:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "sengi:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "sengi:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "sengi:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"karmaConfig": "./karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/sass/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/manifest.json"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"./src/sass",
|
||||
"./node_modules/bootstrap/scss"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "sengi",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "scss"
|
||||
"sengi-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "sengi:serve"
|
||||
}
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "sengi",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
}
|
||||
}
|
||||
}
|
11
appveyor.yml
|
@ -1,15 +1,15 @@
|
|||
os: unstable
|
||||
cache:
|
||||
- node_modules
|
||||
#- node_modules
|
||||
environment:
|
||||
GH_TOKEN:
|
||||
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
|
||||
secure: eXSiJiDFgLi4vixO5GS93lgrqZ+BzQNy7PKPCQCErHjCQD9mWiEtVQQnhvmUq1FPLUc3fNLmOFQu2nIWA9bnkHg5Yw9WiG2m7QSCPRB+xCnvSY6JbLqpzURZp5x5OLj6
|
||||
matrix:
|
||||
- nodejs_version: 10.9.0
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- set CI=true
|
||||
- npm install -g npm@latest
|
||||
- npm install -g npm@6.9.0
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm install
|
||||
matrix:
|
||||
|
@ -30,6 +30,8 @@ test_script:
|
|||
$wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $_))
|
||||
}
|
||||
- npm run dist
|
||||
- ps: >-
|
||||
Remove-Item 'C:\projects\sengi\dist\assets\emoji' -Recurse
|
||||
artifacts:
|
||||
- path: dist
|
||||
deploy:
|
||||
|
@ -42,4 +44,5 @@ deploy:
|
|||
folder: /
|
||||
application: dist.zip
|
||||
on:
|
||||
branch: master
|
||||
branch: master
|
||||
# APPVEYOR_REPO_TAG: true
|
||||
|
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<link rel="shortcut icon" type="image/png" href="favicon.png">
|
||||
|
||||
<title>Sengi Launcher</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="launcher-wrapper">
|
||||
<div class="launcher">
|
||||
<a href="#" class="button" title="launch sengi in popup"
|
||||
onClick="window.open('/../'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;">
|
||||
<span class="download-button__web--label">Launch Sengi Popup</span>
|
||||
</a><br />
|
||||
|
||||
<a href="/../" class="button" title="launch sengi">
|
||||
<span class="download-button__web--label">Open Sengi</span>
|
||||
</a><br />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,45 @@
|
|||
*, *::after, *::before {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
background-color: #141824;
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.launcher-wrapper{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.launcher {
|
||||
height: 15rem;
|
||||
width: 30rem;
|
||||
margin: 35vh auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #090b10;
|
||||
display: block;
|
||||
width: 30rem;
|
||||
padding: 1.5rem 2rem 1.75rem 2rem;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
font-size: 1.8rem;
|
||||
font-weight: lighter;
|
||||
text-decoration: none;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #1e2433;
|
||||
}
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 688 B |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 353 KiB |
|
@ -94,6 +94,17 @@ body {
|
|||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
margin-bottom: .5rem; }
|
||||
.header__download-box--buttons {
|
||||
margin-bottom: 5px; }
|
||||
.header__old-releases {
|
||||
transition: all .2s;
|
||||
color: #7a7a7a;
|
||||
float: left;
|
||||
font-size: 1.4rem;
|
||||
text-decoration: none; }
|
||||
.header__old-releases:hover {
|
||||
color: #faa424;
|
||||
text-decoration: underline; }
|
||||
|
||||
.download-button {
|
||||
transition: all .2s;
|
||||
|
@ -112,16 +123,10 @@ body {
|
|||
color: #faa424; }
|
||||
.download-button__web {
|
||||
font-size: 25px;
|
||||
background-color: #04d431;
|
||||
background-color: #faa424;
|
||||
background-color: #fd9d0d;
|
||||
background-color: #3f3f3f;
|
||||
color: white;
|
||||
color: #202020;
|
||||
color: white;
|
||||
padding: 10px 20px 10px 15px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ffffff;
|
||||
border: 1px solid #e7e7e7; }
|
||||
.download-button__web--label {
|
||||
font-size: 25px;
|
||||
|
@ -129,7 +134,6 @@ body {
|
|||
padding-left: 10px; }
|
||||
.download-button__web:hover {
|
||||
color: #3d3d3d;
|
||||
background-color: #ffe5be;
|
||||
background-color: #faa424; }
|
||||
|
||||
.fa-apple {
|
||||
|
@ -143,9 +147,113 @@ body {
|
|||
width: 6rem;
|
||||
height: 6rem; }
|
||||
|
||||
.section {
|
||||
font-family: 'Open Sans', sans-serif; }
|
||||
.section-about {
|
||||
min-height: 10rem;
|
||||
background-color: #141414;
|
||||
color: whitesmoke;
|
||||
padding: 7rem 0; }
|
||||
.section-about__about {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin: auto;
|
||||
padding: 0 3rem;
|
||||
text-align: center;
|
||||
font-size: 2.5rem; }
|
||||
.section-clear {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
min-height: 20rem;
|
||||
background-color: whitesmoke;
|
||||
padding: 1rem; }
|
||||
.section-clear__big-title {
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
text-align: center; }
|
||||
.section-clear__title {
|
||||
font-weight: 400;
|
||||
font-size: 2.5rem;
|
||||
color: #141414;
|
||||
margin-top: 5rem;
|
||||
margin-left: 15vw; }
|
||||
@media (max-width: 56.25em) {
|
||||
.section-clear__title {
|
||||
margin: 3rem auto 0 auto;
|
||||
text-align: center; } }
|
||||
.section-clear__subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
color: #141414;
|
||||
margin-left: 1rem;
|
||||
font-style: italic; }
|
||||
.section-separator {
|
||||
height: .5rem;
|
||||
background-color: #141414;
|
||||
background-color: white; }
|
||||
|
||||
.quick-overview__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
width: 800;
|
||||
height: 492; }
|
||||
@media (max-width: 56.25em) {
|
||||
.quick-overview__video {
|
||||
width: 100%;
|
||||
height: 492; } }
|
||||
.quick-overview__video:focus {
|
||||
background: none;
|
||||
border: 0px;
|
||||
outline: none; }
|
||||
|
||||
.link {
|
||||
color: black; }
|
||||
.link:visited, .link:focus {
|
||||
color: black; }
|
||||
.link:hover {
|
||||
color: grey; }
|
||||
|
||||
.functionalities__row {
|
||||
max-width: 100rem; }
|
||||
|
||||
.functionalities__text {
|
||||
display: block;
|
||||
margin: auto;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
padding: 7rem 5rem 0 5rem;
|
||||
max-width: 50rem; }
|
||||
@media (max-width: 56.25em) {
|
||||
.functionalities__text {
|
||||
padding: 3rem 5rem 0 5rem; } }
|
||||
|
||||
.functionalities__conclusion {
|
||||
max-width: 60rem;
|
||||
padding: 2rem 5rem 5rem 5rem; }
|
||||
|
||||
.functionalities__strong {
|
||||
font-weight: 400;
|
||||
font-weight: bold; }
|
||||
|
||||
.functionalities__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
width: 326px;
|
||||
height: 260px; }
|
||||
.functionalities__video:focus {
|
||||
border: none;
|
||||
outline: none; }
|
||||
@media (max-width: 56.25em) {
|
||||
.functionalities__video {
|
||||
width: 100%;
|
||||
max-width: 326px;
|
||||
height: 60%; } }
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
background-color: #141414;
|
||||
background-color: #141414;
|
||||
color: white;
|
||||
height: 18rem;
|
||||
padding-top: 4em; }
|
||||
|
|
|
@ -25,4 +25,8 @@ $default-font-size: 1.6rem;
|
|||
$grid-width: 114rem;
|
||||
$gutter-vertical: 1rem;
|
||||
$gutter-vertical-small: 1rem;
|
||||
$gutter-horizontal: 1rem;
|
||||
$gutter-horizontal: 1rem;
|
||||
|
||||
|
||||
//COLOR
|
||||
$dark-background: rgb(20, 20, 20);
|
|
@ -1,6 +1,7 @@
|
|||
.footer {
|
||||
text-align: center;
|
||||
background-color: rgb(20, 20, 20);
|
||||
background-color: $dark-background;
|
||||
color: white;
|
||||
height: 18rem;
|
||||
padding-top: 4em;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$link-hover-color: rgb(250, 164, 36);;
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
background-color: rgb(247, 247, 247);
|
||||
|
@ -52,6 +54,22 @@
|
|||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
&--buttons {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__old-releases {
|
||||
transition: all .2s;
|
||||
color: rgb(122, 122, 122);
|
||||
float: left;
|
||||
font-size: 1.4rem;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,26 +95,17 @@
|
|||
margin-bottom: 1rem;
|
||||
|
||||
&:hover {
|
||||
color: rgb(250, 164, 36);
|
||||
color: $link-hover-color;
|
||||
|
||||
}
|
||||
|
||||
&__web {
|
||||
font-size: 25px;
|
||||
background-color: rgb(4, 212, 49);
|
||||
background-color: rgb(250, 164, 36);
|
||||
background-color: rgb(253, 157, 13);
|
||||
background-color: rgb(63, 63, 63);;
|
||||
color: white;
|
||||
color: rgb(32, 32, 32);
|
||||
color: white;
|
||||
|
||||
|
||||
padding: 10px 20px 10px 15px;
|
||||
border-radius: 3px;
|
||||
|
||||
// border: 1px solid #ececec;
|
||||
border: 1px solid #ffffff;
|
||||
border: 1px solid #e7e7e7;
|
||||
|
||||
&--label {
|
||||
|
@ -107,8 +116,7 @@
|
|||
|
||||
&:hover {
|
||||
color: rgb(61, 61, 61);
|
||||
background-color: rgb(255, 229, 190);
|
||||
background-color: rgb(250, 164, 36);
|
||||
background-color: $link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
.section {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
&-about {
|
||||
min-height: 10rem;
|
||||
background-color: $dark-background;
|
||||
color: whitesmoke;
|
||||
padding: 7rem 0;
|
||||
|
||||
&__about {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin: auto;
|
||||
padding: 0 3rem;
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-clear {
|
||||
font-weight: 300;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
min-height: 20rem;
|
||||
background-color: whitesmoke;
|
||||
padding: 1rem;
|
||||
|
||||
&__big-title {
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 400;
|
||||
font-size: 2.5rem;
|
||||
color: $dark-background;
|
||||
margin-top: 5rem;
|
||||
margin-left: 15vw;
|
||||
|
||||
@include respond(tab-port) {
|
||||
margin: 3rem auto 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
color: $dark-background;
|
||||
margin-left: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-separator {
|
||||
height: .5rem;
|
||||
background-color: $dark-background;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-overview {
|
||||
&__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
width: 800;
|
||||
height: 492;
|
||||
|
||||
@include respond(tab-port) {
|
||||
width: 100%;
|
||||
height: 492;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: none;
|
||||
border: 0px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: black;
|
||||
|
||||
&:visited, &:focus {
|
||||
color: black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.functionalities {
|
||||
|
||||
&__row {
|
||||
max-width: 100rem;
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
margin: auto;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
|
||||
text-align: center;
|
||||
padding: 7rem 5rem 0 5rem;
|
||||
max-width: 50rem;
|
||||
|
||||
@include respond(tab-port) {
|
||||
padding: 3rem 5rem 0 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__conclusion {
|
||||
max-width: 60rem;
|
||||
padding: 2rem 5rem 5rem 5rem;
|
||||
}
|
||||
|
||||
&__strong {
|
||||
font-weight: 400;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__video {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
width: 326px;
|
||||
height: 260px;
|
||||
|
||||
@include respond(tab-port) {
|
||||
width: 100%;
|
||||
max-width: 326px;
|
||||
height: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,4 +5,5 @@
|
|||
|
||||
@import "./layout/grid";
|
||||
@import "./layout/header";
|
||||
@import "./layout/section";
|
||||
@import "./layout/footer";
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 701 B |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 286 KiB |
After Width: | Height: | Size: 287 KiB |
After Width: | Height: | Size: 134 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 7.2 KiB |
248
docs/index.html
|
@ -20,7 +20,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-1-of-2">
|
||||
<img class="header__image" src="images/sengi_image.png" />
|
||||
<img id="main-illustration" class="header__image" src="images/sengi_image.png" />
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<div class="header__download-box">
|
||||
|
@ -28,56 +28,254 @@
|
|||
|
||||
<div class="header__download-box--description">
|
||||
A FLOSS multi-account Mastodon and Pleroma desktop client<br />
|
||||
Now available in Beta (v0.6.0)<br />
|
||||
<span id="sengi-version"></span> <br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="header__download-box--buttons">
|
||||
<p>
|
||||
<h4 class="header__download-box--subtitle">Try it in your browser!</h4>
|
||||
<a href="#" class="download-button download-button__web" title="what are you waiting for? click!"
|
||||
onClick="window.open('http://sengi.nicolas-constant.com'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;"
|
||||
<h4 class="header__download-box--subtitle">Use it in your browser!</h4>
|
||||
<a href="#" class="download-button download-button__web"
|
||||
title="what are you waiting for? click!"
|
||||
onClick="window.open('https://sengi.nicolas-constant.com'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;"
|
||||
class="button"><i class="fas fa-globe"></i><span
|
||||
class="download-button__web--label">launch!</span></a><br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h4 class="header__download-box--subtitle">Or download the desktop client:</h4>
|
||||
<a href="#" class="download-button" title="download client for windows"><i class="fab fa-windows"></i></a>
|
||||
<a href="#" class="download-button" title="download client for mac"><i class="fab fa-apple"></i></a>
|
||||
<a href="#" class="download-button" title="download client for debian-based distrib"><i class="fab fa-ubuntu"></i></a>
|
||||
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux"><img src="images/snap-store-white.png" /></a>
|
||||
<h4 class="header__download-box--subtitle">Or download the desktop client <span id="electron-version"></span>:</h4>
|
||||
<div id="download-buttons" style="display: none;">
|
||||
<a id="windows" href class="download-button" title="download client for windows">
|
||||
<i class="fab fa-windows"></i>
|
||||
</a>
|
||||
<a id="mac" href class="download-button" title="download client for mac">
|
||||
<i class="fab fa-apple"></i>
|
||||
</a>
|
||||
<a id="deb" href class="download-button"
|
||||
title="download client for debian-based distrib">
|
||||
<i class="fab fa-ubuntu"></i>
|
||||
</a>
|
||||
<a id="appimage" href class="download-button"
|
||||
title="download client for linux (AppImage)">
|
||||
<i class="fab fa-linux"></i>
|
||||
</a>
|
||||
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux">
|
||||
<img src="images/snap-store-white.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div id="download-buttons-nojs">
|
||||
<a href="https://github.com/NicolasConstant/sengi/releases/" class="download-button"
|
||||
title="latest releases">
|
||||
<i class="fab fa-github"></i></a>
|
||||
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux">
|
||||
<img src="images/snap-store-white.png" />
|
||||
</a>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi-electron/releases/"
|
||||
title="browse previous releases">browse previous releases</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header__app-image-box">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="header__app-image-box"></div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="section-about">
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section-about">
|
||||
<div class="section-about__about">
|
||||
<p>
|
||||
Sengi will let you use all your accounts<br /> easily and seamlessly<br />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section-separator"></section>
|
||||
|
||||
<section class="section-clear">
|
||||
<h2 class="section-clear__big-title">Quick Overview</h2>
|
||||
|
||||
<video class="quick-overview__video" controls>
|
||||
<source src="videos/Quick_overview.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</section>
|
||||
|
||||
<section class="section-separator"></section>
|
||||
|
||||
<section class="section-clear">
|
||||
<h2 class="section-clear__big-title">Main Functionalities</h2>
|
||||
|
||||
<h4 class="section-clear__title">Seamless account switch</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Just click on the account's avatar, <br />
|
||||
and all your next actions will be performed by it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<video width="326" height="260" controls class="functionalities__video">
|
||||
<source src="videos/Clip_account_switch.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">All instances timelines in one place</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Add timelines and lists from all your accounts in the same
|
||||
interface.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<img src="images/timelines.png" class="functionalities__video" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">Don't lose your focus</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Opening a profile, thread, hashtag or even just replying to someone will always take place in the
|
||||
current Timeline.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<video width="326" height="260" controls class="functionalities__video">
|
||||
<source src="videos/Clip_timelines.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">Labels</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Get a quick insight if a status is part of a thread, has replies, is from a bot, is old, is
|
||||
cross-posted (limited to local TL) or is remotely fetched.<br/>
|
||||
<a href="https://github.com/NicolasConstant/sengi/wiki/Labels" class="link">more details</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<img src="images/labels.png" class="functionalities__video" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">Auto-remove Thread's Content-Warnings</h4>
|
||||
<div class="row functionalities__row">
|
||||
<div class="col-1-of-2">
|
||||
<p class="functionalities__text">
|
||||
Easily remove all CW from a thread<br />
|
||||
with one single click!
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-1-of-2">
|
||||
<video width="326" height="260" controls class="functionalities__video">
|
||||
<source src="videos/Clip_cw_button.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-clear__title">And many more!</h4>
|
||||
|
||||
<div class="row functionalities__row">
|
||||
<p class="functionalities__text functionalities__conclusion">
|
||||
There is a lot more things to discover<br/> and more to come too!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<section class="section-separator"></section>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<h3 class="footer__title">Let's keep in touch!</h3>
|
||||
<h3 class="footer__title">Let's keep in touch!</h3>
|
||||
|
||||
<div class="footer__buttons">
|
||||
<a href="https://mastodon.social/@sengi_app" class="footer__buttons--button" title="open pleroma-compatible account"><i class="fab fa-mastodon"></i></a>
|
||||
<a href="https://github.com/NicolasConstant/sengi" class="footer__buttons--button" title="open microsoft github repository"><i class="fab fa-github"></i></a>
|
||||
<a href="https://mastodon.social/@sengi_app" rel="me" class="footer__buttons--button"
|
||||
title="open pleroma-compatible account"><i class="fab fa-mastodon"></i></a>
|
||||
<a href="https://github.com/NicolasConstant/sengi" class="footer__buttons--button"
|
||||
title="open microsoft github repository"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script type="text/javascript" language="javascript">
|
||||
const getLastRelease = async () => {
|
||||
const response = await fetch('https://api.github.com/repos/NicolasConstant/sengi/releases/latest');
|
||||
const myJson = await response.json();
|
||||
return myJson;
|
||||
}
|
||||
|
||||
const getLastElectronRelease = async () => {
|
||||
const response = await fetch('https://api.github.com/repos/NicolasConstant/sengi-electron/releases/latest');
|
||||
const myJson = await response.json();
|
||||
return myJson;
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
var userAgent = window.navigator.userAgent,
|
||||
platform = window.navigator.platform,
|
||||
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
|
||||
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
|
||||
iosPlatforms = ['iPhone', 'iPad', 'iPod'],
|
||||
os = null;
|
||||
|
||||
if (macosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'Mac OS';
|
||||
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'iOS';
|
||||
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'Windows';
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
os = 'Android';
|
||||
} else if (!os && /Linux/.test(platform)) {
|
||||
os = 'Linux';
|
||||
}
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
let lastRelease = await getLastRelease();
|
||||
let version = lastRelease.tag_name;
|
||||
|
||||
let lastElectronRelease = await getLastElectronRelease();
|
||||
let electronVersion = lastElectronRelease.tag_name;
|
||||
|
||||
var downloadButtons = document.getElementById('download-buttons');
|
||||
downloadButtons.style.display = 'block';
|
||||
|
||||
var downloadButtonsNojs = document.getElementById('download-buttons-nojs');
|
||||
downloadButtonsNojs.style.display = 'none';
|
||||
|
||||
var sengiVersion = document.getElementById('sengi-version');
|
||||
sengiVersion.textContent = `Current version: v${version}`;
|
||||
|
||||
var htmlElectronVersion = document.getElementById('electron-version');
|
||||
htmlElectronVersion.textContent = `(${electronVersion})`;
|
||||
|
||||
document.getElementById('windows').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-win.exe`;
|
||||
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-mac.dmg`;
|
||||
document.getElementById('deb').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-linux.deb`;
|
||||
document.getElementById('appimage').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-linux.AppImage`;
|
||||
|
||||
|
||||
let userOs = getOS();
|
||||
if(userOs === 'Linux'){
|
||||
var illustration = document.getElementById('main-illustration');
|
||||
illustration.src = 'images/sengi_image_ubuntu.png';
|
||||
}
|
||||
}, false);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -9,11 +9,10 @@
|
|||
"author": "Nicolas Constant",
|
||||
"license": "WTFPL",
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"gulp": "~3.9.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-run": "^1.7.1",
|
||||
"gulp-sass": "^4.0.1"
|
||||
}
|
||||
"gulp-sass": "^4.0.1",
|
||||
"node-sass": "^4.13.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
server.port = 80
|
||||
server.document-root = "/app"
|
||||
server.errorlog = "/dev/stdout"
|
||||
accesslog.filename = "/dev/stdout"
|
||||
dir-listing.activate = "disable"
|
||||
server.modules = (
|
||||
"mod_access",
|
||||
"mod_accesslog",
|
||||
)
|
||||
include "mime-types.conf"
|
||||
server.pid-file = "/run/lighttpd.pid"
|
||||
index-file.names = ( "index.html", "index.htm" )
|
||||
#url.rewrite-once = ( "^sengi/(.*)" => "/sengi/index.html" )
|
||||
server.error-handler-404 = "/index.html"
|
123
main-electron.js
|
@ -1,123 +0,0 @@
|
|||
const { app, Menu, server, BrowserWindow, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({ width: 377, height: 800, title: "Sengi", backgroundColor: '#FFF', 'useContentSize': true });
|
||||
|
||||
var server = http.createServer(requestHandler).listen(9527);
|
||||
win.loadURL('http://localhost:9527');
|
||||
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forcereload' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' }
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{ role: 'toggledevtools' },
|
||||
{
|
||||
label: 'Open GitHub project',
|
||||
click() { require('electron').shell.openExternal('https://github.com/NicolasConstant/sengi') }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
win.setMenu(menu);
|
||||
|
||||
// Open the DevTools.
|
||||
// win.webContents.openDevTools()
|
||||
|
||||
//open external links to browser
|
||||
win.webContents.on('new-window', function (event, url) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
// Emitted when the window is closed.
|
||||
win.on('closed', () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
win = null
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
function requestHandler(req, res) {
|
||||
var file = req.url == '/' ? '/index.html' : req.url,
|
||||
root = __dirname + '/dist',
|
||||
page404 = root + '/404.html';
|
||||
|
||||
if (file.includes('register') || file.includes('home')) file = '/index.html';
|
||||
|
||||
getFile((root + file), res, page404);
|
||||
};
|
||||
|
||||
function getFile(filePath, res, page404) {
|
||||
console.warn(`filePath: ${filePath}`)
|
||||
fs.exists(filePath, function (exists) {
|
||||
if (exists) {
|
||||
fs.readFile(filePath, function (err, contents) {
|
||||
if (!err) {
|
||||
res.end(contents);
|
||||
} else {
|
||||
console.dir(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fs.readFile(page404, function (err, contents) {
|
||||
if (!err) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end(contents);
|
||||
} else {
|
||||
console.dir(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
app.commandLine.appendSwitch('force-color-profile', 'srgb');
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow)
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (win === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
33
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "0.6.0",
|
||||
"version": "1.7.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
|
@ -15,37 +15,46 @@
|
|||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start-mem": "node --max_old_space_size=5048 ./node_modules/@angular/cli/bin/ng serve",
|
||||
"build": "ng build --prod",
|
||||
"test": "ng test",
|
||||
"test-nowatch": "ng test --watch=false",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"electron": "ng build --prod && electron .",
|
||||
"electron-debug": "ng build && electron .",
|
||||
"dist": "npm run build && build --publish onTagOrDraft"
|
||||
"dist": "npm run build"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.2.7",
|
||||
"@angular/animations": "^7.2.16",
|
||||
"@angular/cdk": "^7.3.7",
|
||||
"@angular/common": "^7.2.7",
|
||||
"@angular/compiler": "^7.2.7",
|
||||
"@angular/core": "^7.2.7",
|
||||
"@angular/forms": "^7.2.7",
|
||||
"@angular/http": "^7.2.7",
|
||||
"@angular/material": "^16.2.1",
|
||||
"@angular/platform-browser": "^7.2.7",
|
||||
"@angular/platform-browser-dynamic": "^7.2.7",
|
||||
"@angular/pwa": "^0.12.4",
|
||||
"@angular/router": "^7.2.7",
|
||||
"@angular/service-worker": "^7.2.7",
|
||||
"@ctrl/ngx-emoji-mart": "^0.17.0",
|
||||
"@fortawesome/angular-fontawesome": "^0.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.13",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.7.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.7.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.7.0",
|
||||
"@ngxs/storage-plugin": "^3.2.0",
|
||||
"@ngxs/store": "^3.2.0",
|
||||
"@ngxs/storage-plugin": "~3.2.0",
|
||||
"@ngxs/store": "~3.2.0",
|
||||
"angular2-hotkeys": "~2.1.5",
|
||||
"bootstrap": "^4.1.3",
|
||||
"core-js": "^2.5.4",
|
||||
"ionicons": "^4.4.3",
|
||||
"emojione": "~4.5.0",
|
||||
"howler": "^2.1.2",
|
||||
"ng-pick-datetime": "^7.0.0",
|
||||
"ngx-contextmenu": "^5.2.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"smooth-scroll-into-view-if-needed": "^1.1.23",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "^0.8.29"
|
||||
},
|
||||
|
@ -58,8 +67,6 @@
|
|||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.2.1",
|
||||
"electron": "^4.0.6",
|
||||
"electron-builder": "^20.39.0",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~1.7.1",
|
||||
|
@ -81,6 +88,7 @@
|
|||
"productName": "Sengi",
|
||||
"appId": "org.sengi.desktop",
|
||||
"artifactName": "${productName}-${version}-${os}.${ext}",
|
||||
"npmRebuild": false,
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
|
@ -123,6 +131,11 @@
|
|||
"snap"
|
||||
],
|
||||
"category": "Network"
|
||||
},
|
||||
"snap": {
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="drag-and-drop" *ngIf="drag" (dragover)="dragover($event)" (drop)="drop($event)"
|
||||
<div class="drag-and-drop" *ngIf="drag" (dragover)="dragover($event)" (drop)="drop($event)"
|
||||
[ngClass]="{'drag-and-drop__on-drag': drag2 === true }">
|
||||
<!-- (dragleave)="dragleave($event)" -->
|
||||
<div class="drag-and-drop__card">
|
||||
|
@ -10,6 +10,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="showRestartNotification" class="auto-update" [class.auto-update__activated]="restartNotificationAvailable">
|
||||
<div class="auto-update__display">
|
||||
<div class="auto-update__display--text">{{restartNotificationLabel}}</div> <a href class="auto-update__display--reload" (click)="loadNewVersion()" title="reload">reload</a> <a href class="auto-update__display--close" (click)="closeRestartNotification()" title="close"><fa-icon [icon]="faTimes"></fa-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="enhancedTutorialActive" class="enhanced-tutorial"
|
||||
[class.enhanced-tutorial__visible]="enhancedTutorialVisible">
|
||||
<app-tutorial-enhanced class="enhanced-tutorial__content" (closeEvent)="closeTutorial()"></app-tutorial-enhanced>
|
||||
</div>
|
||||
|
||||
<app-media-viewer id="media-viewer" *ngIf="openedMediaEvent" [openedMediaEvent]="openedMediaEvent"
|
||||
(closeSubject)="closeMedia()" (dragenter)="dragenter($event)"></app-media-viewer>
|
||||
|
||||
|
|
|
@ -97,4 +97,101 @@ app-streams-selection-footer {
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.auto-update {
|
||||
transition: all .2s;
|
||||
transition-timing-function: ease-in;
|
||||
|
||||
position: absolute;
|
||||
height: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
//bottom: -80px;
|
||||
opacity: 0;
|
||||
z-index: 999999999;
|
||||
|
||||
&__activated {
|
||||
// opacity: 1;
|
||||
transition: all .25s;
|
||||
transition-timing-function: ease-out;
|
||||
opacity: 1;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
&__display {
|
||||
position: relative;
|
||||
// height: 30px;
|
||||
width: 300px;
|
||||
// margin: 0 auto 30px auto;
|
||||
margin: auto;
|
||||
border-radius: 2px;
|
||||
color: rgba(rgb(0, 4, 24), 1);
|
||||
background: #ffffff;
|
||||
|
||||
box-shadow: 0px 0px 10px rgb(0, 0, 0);
|
||||
|
||||
&--text {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
&--reload {
|
||||
transition: all .2s;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
background-color: #3e455f;
|
||||
|
||||
&:hover {
|
||||
background-color: #1d202c;
|
||||
}
|
||||
}
|
||||
|
||||
&--close {
|
||||
transition: all .2s;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
color: #ffffff;
|
||||
background-color: #3e455f;
|
||||
|
||||
&:hover {
|
||||
background-color: #1d202c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enhanced-tutorial {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
z-index: 9999999;
|
||||
opacity: 0;
|
||||
transition: all .4s;
|
||||
|
||||
&__visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: block;
|
||||
padding: 25px;
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,25 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subscription, Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, map } from 'rxjs/operators';
|
||||
import { Select } from '@ngxs/store';
|
||||
// import { ElectronService } from 'ngx-electron';
|
||||
import { Select, Store } from '@ngxs/store';
|
||||
|
||||
import { NavigationService, LeftPanelType } from './services/navigation.service';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { NavigationService, LeftPanelType, OpenLeftPanelEvent } from './services/navigation.service';
|
||||
import { StreamElement } from './states/streams.state';
|
||||
import { AccountInfo, AddAccount } from "./states/accounts.state";
|
||||
import { OpenMediaEvent } from './models/common.model';
|
||||
import { ToolsService } from './services/tools.service';
|
||||
import { MediaService } from './services/media.service';
|
||||
import { ServiceWorkerService } from './services/service-worker.service';
|
||||
import { AuthService, CurrentAuthProcess } from './services/auth.service';
|
||||
|
||||
import { MastodonWrapperService } from './services/mastodon-wrapper.service';
|
||||
import { TokenData, Account } from './services/models/mastodon.interfaces';
|
||||
import { NotificationService } from './services/notification.service';
|
||||
import { AppInfo, RegisteredAppsStateModel } from './states/registered-apps.state';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -16,26 +27,95 @@ import { MediaService } from './services/media.service';
|
|||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
faTimes = faTimes;
|
||||
title = 'Sengi';
|
||||
floatingColumnActive: boolean;
|
||||
tutorialActive: boolean;
|
||||
// mediaViewerActive: boolean = false;
|
||||
openedMediaEvent: OpenMediaEvent
|
||||
|
||||
restartNotificationLabel: string;
|
||||
restartNotificationAvailable: boolean;
|
||||
showRestartNotification: boolean;
|
||||
|
||||
private authStorageKey: string = 'tempAuth';
|
||||
|
||||
private columnEditorSub: Subscription;
|
||||
private openMediaSub: Subscription;
|
||||
private streamSub: Subscription;
|
||||
private dragoverSub: Subscription;
|
||||
private paramsSub: Subscription;
|
||||
private restartNotificationSub: Subscription;
|
||||
|
||||
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly store: Store,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly activatedRoute: ActivatedRoute,
|
||||
private readonly serviceWorkerService: ServiceWorkerService, // Ensure update checks
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mediaService: MediaService,
|
||||
private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.paramsSub = this.activatedRoute.queryParams.subscribe(params => {
|
||||
const code = params['code'];
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appDataWrapper = <CurrentAuthProcess>JSON.parse(localStorage.getItem(this.authStorageKey));
|
||||
if (!appDataWrapper) {
|
||||
this.notificationService.notify('', 400, 'Something when wrong in the authentication process. Please retry.', true);
|
||||
this.router.navigate(['/']);
|
||||
return;
|
||||
}
|
||||
|
||||
const appInfo = this.getAllSavedApps().filter(x => x.instance === appDataWrapper.instance)[0];
|
||||
let usedTokenData: TokenData;
|
||||
this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri)
|
||||
.then((tokenData: TokenData) => {
|
||||
|
||||
if (tokenData.refresh_token && !tokenData.created_at) {
|
||||
const nowEpoch = Date.now() / 1000 | 0;
|
||||
tokenData.created_at = nowEpoch;
|
||||
}
|
||||
|
||||
usedTokenData = tokenData;
|
||||
|
||||
return this.mastodonService.retrieveAccountDetails({ 'instance': appDataWrapper.instance, 'id': '', 'username': '', 'order': 0, 'isSelected': true, 'token': tokenData });
|
||||
})
|
||||
.then((account: Account) => {
|
||||
var username = account.username.toLowerCase();
|
||||
var instance = appDataWrapper.instance.toLowerCase();
|
||||
|
||||
if (this.isAccountAlreadyPresent(username, instance)) {
|
||||
this.notificationService.notify(null, null, `Account @${username}@${instance} is already registered`, true);
|
||||
this.router.navigate(['/']);
|
||||
return;
|
||||
}
|
||||
|
||||
const accountInfo = new AccountInfo();
|
||||
accountInfo.username = username;
|
||||
accountInfo.instance = instance;
|
||||
accountInfo.token = usedTokenData;
|
||||
|
||||
this.store.dispatch([new AddAccount(accountInfo)])
|
||||
.subscribe(() => {
|
||||
localStorage.removeItem(this.authStorageKey);
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err, null);
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
});
|
||||
|
||||
this.streamSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
|
||||
if (streams && streams.length === 0) {
|
||||
this.tutorialActive = true;
|
||||
|
@ -44,9 +124,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
|
||||
if (type === LeftPanelType.Closed) {
|
||||
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((event: OpenLeftPanelEvent) => {
|
||||
if (event.type === LeftPanelType.Closed) {
|
||||
this.floatingColumnActive = false;
|
||||
|
||||
this.checkEnhancedTutorial();
|
||||
} else {
|
||||
this.floatingColumnActive = true;
|
||||
}
|
||||
|
@ -60,14 +142,42 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
this.dragoverSub = this.dragoverSubject
|
||||
.pipe(
|
||||
debounceTime(1500)
|
||||
)
|
||||
.subscribe(() => {
|
||||
.subscribe(() => {
|
||||
this.drag = false;
|
||||
})
|
||||
});
|
||||
|
||||
this.restartNotificationSub = this.notificationService.restartNotificationStream.subscribe((label: string) => {
|
||||
if (label) {
|
||||
this.displayRestartNotification(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enhancedTutorialActive: boolean;
|
||||
enhancedTutorialVisible: boolean;
|
||||
private checkEnhancedTutorial() {
|
||||
let enhancedTutorialDesactivated = JSON.parse(localStorage.getItem('tutorial'));
|
||||
if (!this.floatingColumnActive && !this.tutorialActive && !enhancedTutorialDesactivated) {
|
||||
setTimeout(() => {
|
||||
this.enhancedTutorialActive = true;
|
||||
setTimeout(() => {
|
||||
this.enhancedTutorialVisible = true;
|
||||
}, 100);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
closeTutorial(){
|
||||
localStorage.setItem('tutorial', JSON.stringify(true));
|
||||
|
||||
this.enhancedTutorialVisible = false;
|
||||
setTimeout(() => {
|
||||
this.enhancedTutorialActive = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@ -75,6 +185,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
this.columnEditorSub.unsubscribe();
|
||||
this.openMediaSub.unsubscribe();
|
||||
this.dragoverSub.unsubscribe();
|
||||
this.paramsSub.unsubscribe();
|
||||
this.restartNotificationSub.unsubscribe();
|
||||
}
|
||||
|
||||
closeMedia() {
|
||||
|
@ -96,7 +208,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
dragover(event): boolean {
|
||||
// console.warn('dragover');
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.dragoverSubject.next(true);
|
||||
|
@ -112,4 +223,44 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
this.mediaService.uploadMedia(selectedAccount, files);
|
||||
return false;
|
||||
}
|
||||
|
||||
loadNewVersion(): boolean {
|
||||
document.location.reload();
|
||||
// this.serviceWorkerService.loadNewAppVersion();
|
||||
return false;
|
||||
}
|
||||
|
||||
displayRestartNotification(label: string): boolean {
|
||||
this.restartNotificationLabel = label;
|
||||
this.showRestartNotification = true;
|
||||
setTimeout(() => {
|
||||
this.restartNotificationAvailable = true;
|
||||
}, 200);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
closeRestartNotification(): boolean {
|
||||
this.restartNotificationAvailable = false;
|
||||
setTimeout(() => {
|
||||
this.showRestartNotification = false;
|
||||
}, 250);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isAccountAlreadyPresent(username: string, instance: string): boolean {
|
||||
const accounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
for (let acc of accounts) {
|
||||
if (acc.instance === instance && acc.username == username) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getAllSavedApps(): AppInfo[] {
|
||||
const snapshot = <RegisteredAppsStateModel>this.store.snapshot().registeredapps;
|
||||
return snapshot.apps;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { HttpModule } from "@angular/http";
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
// import { NgxElectronModule } from "ngx-electron";
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
// import { NgxElectronModule } from 'ngx-electron';
|
||||
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { ContextMenuModule } from 'ngx-contextmenu';
|
||||
import { PickerModule } from '@ctrl/ngx-emoji-mart';
|
||||
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime';
|
||||
import { HotkeyModule } from 'angular2-hotkeys';
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.component";
|
||||
import { StreamsMainDisplayComponent } from "./pages/streams-main-display/streams-main-display.component";
|
||||
import { StreamComponent } from "./components/stream/stream.component";
|
||||
import { StreamsSelectionFooterComponent } from "./components/streams-selection-footer/streams-selection-footer.component";
|
||||
import { RegisterNewAccountComponent } from "./pages/register-new-account/register-new-account.component";
|
||||
// import { RegisterNewAccountComponent } from "./pages/register-new-account/register-new-account.component";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
import { StreamingService } from "./services/streaming.service";
|
||||
import { RegisteredAppsState } from "./states/registered-apps.state";
|
||||
|
@ -28,6 +34,7 @@ import { FloatingColumnComponent } from './components/floating-column/floating-c
|
|||
import { StreamsState } from "./states/streams.state";
|
||||
import { StatusComponent } from "./components/stream/status/status.component";
|
||||
import { MastodonService } from "./services/mastodon.service";
|
||||
import { MastodonWrapperService } from "./services/mastodon-wrapper.service";
|
||||
import { AttachementsComponent } from './components/stream/status/attachements/attachements.component';
|
||||
import { SettingsComponent } from './components/floating-column/settings/settings.component';
|
||||
import { AddNewAccountComponent } from './components/floating-column/add-new-account/add-new-account.component';
|
||||
|
@ -56,70 +63,139 @@ import { DirectMessagesComponent } from './components/floating-column/manage-acc
|
|||
import { MentionsComponent } from './components/floating-column/manage-account/mentions/mentions.component';
|
||||
import { NotificationsComponent } from './components/floating-column/manage-account/notifications/notifications.component';
|
||||
import { SettingsState } from './states/settings.state';
|
||||
import { AccountEmojiPipe } from './pipes/account-emoji.pipe';
|
||||
import { CardComponent } from './components/stream/status/card/card.component';
|
||||
import { ListEditorComponent } from './components/floating-column/manage-account/my-account/list-editor/list-editor.component';
|
||||
import { ListAccountComponent } from './components/floating-column/manage-account/my-account/list-editor/list-account/list-account.component';
|
||||
import { PollComponent } from './components/stream/status/poll/poll.component';
|
||||
import { TimeLeftPipe } from './pipes/time-left.pipe';
|
||||
import { AutosuggestComponent } from './components/create-status/autosuggest/autosuggest.component';
|
||||
import { EmojiPickerComponent } from './components/create-status/emoji-picker/emoji-picker.component';
|
||||
import { StatusUserContextMenuComponent } from './components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component';
|
||||
import { StatusSchedulerComponent } from './components/create-status/status-scheduler/status-scheduler.component';
|
||||
import { PollEditorComponent } from './components/create-status/poll-editor/poll-editor.component';
|
||||
import { PollEntryComponent } from './components/create-status/poll-editor/poll-entry/poll-entry.component';
|
||||
import { ScheduledStatusesComponent } from './components/floating-column/scheduled-statuses/scheduled-statuses.component';
|
||||
import { ScheduledStatusComponent } from './components/floating-column/scheduled-statuses/scheduled-status/scheduled-status.component';
|
||||
import { StreamNotificationsComponent } from './components/stream/stream-notifications/stream-notifications.component';
|
||||
import { NotificationComponent } from './components/floating-column/manage-account/notifications/notification/notification.component';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { BookmarksComponent } from './components/floating-column/manage-account/bookmarks/bookmarks.component';
|
||||
import { AttachementImageComponent } from './components/stream/status/attachements/attachement-image/attachement-image.component';
|
||||
import { EnsureHttpsPipe } from './pipes/ensure-https.pipe';
|
||||
import { UserFollowsComponent } from './components/stream/user-follows/user-follows.component';
|
||||
import { AccountComponent } from './components/common/account/account.component';
|
||||
import { TutorialEnhancedComponent } from './components/tutorial-enhanced/tutorial-enhanced.component';
|
||||
import { NotificationsTutorialComponent } from './components/tutorial-enhanced/notifications-tutorial/notifications-tutorial.component';
|
||||
import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-tutorial.component';
|
||||
import { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component';
|
||||
import { StatusTranslateComponent } from './components/stream/status/status-translate/status-translate.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
{ path: "home", component: StreamsMainDisplayComponent },
|
||||
{ path: "register", component: RegisterNewAccountComponent},
|
||||
{ path: "**", redirectTo: "home" }
|
||||
{ path: "", component: StreamsMainDisplayComponent },
|
||||
// { path: "home", component: StreamsMainDisplayComponent },
|
||||
// { path: "register", component: RegisterNewAccountComponent },
|
||||
{ path: "**", redirectTo: "" }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
LeftSideBarComponent,
|
||||
StreamsMainDisplayComponent,
|
||||
StreamComponent,
|
||||
StreamsSelectionFooterComponent,
|
||||
StatusComponent,
|
||||
RegisterNewAccountComponent,
|
||||
AccountIconComponent,
|
||||
FloatingColumnComponent,
|
||||
ManageAccountComponent,
|
||||
AddNewStatusComponent,
|
||||
AttachementsComponent,
|
||||
SettingsComponent,
|
||||
AddNewAccountComponent,
|
||||
SearchComponent,
|
||||
ActionBarComponent,
|
||||
WaitingAnimationComponent,
|
||||
UserProfileComponent,
|
||||
ThreadComponent,
|
||||
HashtagComponent,
|
||||
StreamOverlayComponent,
|
||||
DatabindedTextComponent,
|
||||
TimeAgoPipe,
|
||||
StreamStatusesComponent,
|
||||
StreamEditionComponent,
|
||||
TutorialComponent,
|
||||
NotificationHubComponent,
|
||||
MediaViewerComponent,
|
||||
CreateStatusComponent,
|
||||
MediaComponent,
|
||||
MyAccountComponent,
|
||||
FavoritesComponent,
|
||||
DirectMessagesComponent,
|
||||
MentionsComponent,
|
||||
NotificationsComponent
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeModule,
|
||||
BrowserModule,
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
RouterModule.forRoot(routes),
|
||||
declarations: [
|
||||
AppComponent,
|
||||
LeftSideBarComponent,
|
||||
StreamsMainDisplayComponent,
|
||||
StreamComponent,
|
||||
StreamsSelectionFooterComponent,
|
||||
StatusComponent,
|
||||
// RegisterNewAccountComponent,
|
||||
AccountIconComponent,
|
||||
FloatingColumnComponent,
|
||||
ManageAccountComponent,
|
||||
AddNewStatusComponent,
|
||||
AttachementsComponent,
|
||||
SettingsComponent,
|
||||
AddNewAccountComponent,
|
||||
SearchComponent,
|
||||
ActionBarComponent,
|
||||
WaitingAnimationComponent,
|
||||
UserProfileComponent,
|
||||
ThreadComponent,
|
||||
HashtagComponent,
|
||||
StreamOverlayComponent,
|
||||
DatabindedTextComponent,
|
||||
TimeAgoPipe,
|
||||
StreamStatusesComponent,
|
||||
StreamEditionComponent,
|
||||
TutorialComponent,
|
||||
NotificationHubComponent,
|
||||
MediaViewerComponent,
|
||||
CreateStatusComponent,
|
||||
MediaComponent,
|
||||
MyAccountComponent,
|
||||
FavoritesComponent,
|
||||
DirectMessagesComponent,
|
||||
MentionsComponent,
|
||||
NotificationsComponent,
|
||||
AccountEmojiPipe,
|
||||
CardComponent,
|
||||
ListEditorComponent,
|
||||
ListAccountComponent,
|
||||
PollComponent,
|
||||
TimeLeftPipe,
|
||||
AutosuggestComponent,
|
||||
EmojiPickerComponent,
|
||||
StatusUserContextMenuComponent,
|
||||
StatusSchedulerComponent,
|
||||
PollEditorComponent,
|
||||
PollEntryComponent,
|
||||
ScheduledStatusesComponent,
|
||||
ScheduledStatusComponent,
|
||||
StreamNotificationsComponent,
|
||||
NotificationComponent,
|
||||
BookmarksComponent,
|
||||
AttachementImageComponent,
|
||||
EnsureHttpsPipe,
|
||||
UserFollowsComponent,
|
||||
AccountComponent,
|
||||
TutorialEnhancedComponent,
|
||||
NotificationsTutorialComponent,
|
||||
LabelsTutorialComponent,
|
||||
ThankyouTutorialComponent,
|
||||
StatusTranslateComponent
|
||||
],
|
||||
entryComponents: [
|
||||
EmojiPickerComponent
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PickerModule,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule,
|
||||
OverlayModule,
|
||||
DragDropModule,
|
||||
// NgxElectronModule,
|
||||
RouterModule.forRoot(routes),
|
||||
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
NgxsStoragePluginModule.forRoot()
|
||||
],
|
||||
providers: [AuthService, NavigationService, NotificationService, MastodonService, StreamingService],
|
||||
bootstrap: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
//], { developmentMode: !environment.production }),
|
||||
NgxsStoragePluginModule.forRoot(),
|
||||
ContextMenuModule.forRoot(),
|
||||
HotkeyModule.forRoot(),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
],
|
||||
providers: [AuthService, NavigationService, NotificationService, MastodonWrapperService, MastodonService, StreamingService],
|
||||
bootstrap: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<a href class="account" title="open account" (click)="selected()" (auxclick)="openAccount()">
|
||||
<img src="{{account.avatar}}" class="account__avatar" />
|
||||
<div class="account__name" innerHTML="{{ account | accountEmoji }}"></div>
|
||||
<div class="account__fullhandle">@{{ account.acct }}</div>
|
||||
</a>
|
|
@ -0,0 +1,48 @@
|
|||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
.account {
|
||||
font-size: $small-font-size;
|
||||
|
||||
display: block;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
transition: all .3s;
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 40px;
|
||||
margin: 5px 10px 5px 5px;
|
||||
float: left;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin: 7px 0 0 0;
|
||||
}
|
||||
|
||||
&__fullhandle {
|
||||
margin: 0 0 5px 0;
|
||||
color: $status-secondary-color;
|
||||
transition: all .3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover &__fullhandle {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
@include clearfix;
|
||||
}
|
|
@ -1,20 +1,20 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegisterNewAccountComponent } from './register-new-account.component';
|
||||
import { AccountComponent } from './account.component';
|
||||
|
||||
xdescribe('RegisterNewAccountComponent', () => {
|
||||
let component: RegisterNewAccountComponent;
|
||||
let fixture: ComponentFixture<RegisterNewAccountComponent>;
|
||||
xdescribe('AccountComponent', () => {
|
||||
let component: AccountComponent;
|
||||
let fixture: ComponentFixture<AccountComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RegisterNewAccountComponent ]
|
||||
declarations: [ AccountComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RegisterNewAccountComponent);
|
||||
fixture = TestBed.createComponent(AccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
|
||||
import { Account } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: ['./account.component.scss']
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
|
||||
@Input() account: Account;
|
||||
@Output() accountSelected = new EventEmitter<Account>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
selected(): boolean{
|
||||
this.accountSelected.next(this.account);
|
||||
return false;
|
||||
}
|
||||
|
||||
openAccount(): boolean {
|
||||
window.open(this.account.url, '_blank');
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
|
||||
|
||||
import { OpenThreadEvent } from '../../services/tools.service';
|
||||
|
||||
export abstract class BrowseBase implements OnInit, OnDestroy {
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
abstract ngOnInit();
|
||||
abstract ngOnDestroy();
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import { OnInit, Input, OnDestroy, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
|
||||
import { AccountInfo } from '../../states/accounts.state';
|
||||
import { StreamingWrapper } from '../../services/streaming.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { ToolsService, OpenThreadEvent } from '../../services/tools.service';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
import { Status } from '../../services/models/mastodon.interfaces';
|
||||
import { TimeLineModeEnum } from '../../states/settings.state';
|
||||
import { BrowseBase } from './browse-base';
|
||||
|
||||
export abstract class TimelineBase extends BrowseBase {
|
||||
isLoading = true;
|
||||
protected maxReached = false;
|
||||
protected lastCallReachedMax = false;
|
||||
isThread = false;
|
||||
displayError: string;
|
||||
hasContentWarnings = false;
|
||||
|
||||
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
|
||||
|
||||
protected account: AccountInfo;
|
||||
protected websocketStreaming: StreamingWrapper;
|
||||
|
||||
statuses: StatusWrapper[] = [];
|
||||
bufferStream: Status[] = [];
|
||||
protected bufferWasCleared: boolean;
|
||||
numNewItems: number;
|
||||
streamPositionnedAtTop: boolean = true;
|
||||
protected isProcessingInfiniteScroll: boolean;
|
||||
|
||||
protected hideBoosts: boolean;
|
||||
protected hideReplies: boolean;
|
||||
protected hideBots: boolean;
|
||||
|
||||
@Input() goToTop: Observable<void>;
|
||||
|
||||
@Input() userLocked = true;
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
constructor(
|
||||
protected readonly toolsService: ToolsService,
|
||||
protected readonly notificationService: NotificationService,
|
||||
protected readonly mastodonService: MastodonWrapperService) {
|
||||
super();
|
||||
}
|
||||
|
||||
abstract ngOnInit();
|
||||
abstract ngOnDestroy();
|
||||
protected abstract scrolledToTop();
|
||||
protected abstract statusProcessOnGoToTop();
|
||||
protected abstract getNextStatuses(): Promise<Status[]>;
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
const atTop = element.scrollTop === 0;
|
||||
|
||||
this.streamPositionnedAtTop = false;
|
||||
if (atBottom && !this.isProcessingInfiniteScroll) {
|
||||
this.scrolledToBottom();
|
||||
} else if (atTop) {
|
||||
this.scrolledToTop();
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledErrorOccured = false;
|
||||
protected scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached || this.scrolledErrorOccured) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.isProcessingInfiniteScroll = true;
|
||||
|
||||
this.getNextStatuses()
|
||||
.then((status: Status[]) => {
|
||||
if (!status || status.length === 0 || this.maxReached) {
|
||||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
for (const s of status) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s);
|
||||
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.lastCallReachedMax){
|
||||
this.maxReached = true;
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.scrolledErrorOccured = true;
|
||||
setTimeout(() => {
|
||||
this.scrolledErrorOccured = false;
|
||||
}, 5000);
|
||||
|
||||
this.notificationService.notifyHttpError(err, this.account);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
this.isProcessingInfiniteScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
applyGoToTop(): boolean {
|
||||
this.statusProcessOnGoToTop();
|
||||
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<div class="autosuggest" *ngIf="accounts.length > 0 || hashtags.length > 0">
|
||||
<a href *ngFor="let a of accounts"
|
||||
title="@{{a.account.acct}}"
|
||||
(click)="accountSelected(a)"
|
||||
class="autosuggest__entry autosuggest__account"
|
||||
[class.autosuggest__entry--selected]="a.selected">
|
||||
<img class="autosuggest__account--avatar" src="{{ a.account.avatar }}" /> <span class="autosuggest__account--text"><span class="autosuggest__account--handle">{{ a.account.username }}</span> @{{ a.account.acct }}</span>
|
||||
</a>
|
||||
|
||||
<a href *ngFor="let h of hashtags"
|
||||
title="#{{h.hashtag}}"
|
||||
(click)="hashtagSelected(h)"
|
||||
class="autosuggest__entry"
|
||||
[class.autosuggest__entry--selected]="h.selected">
|
||||
<span class="autosuggest__account--handle">#{{ h.hashtag }}</span>
|
||||
</a>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
@import "variables";
|
||||
|
||||
|
||||
.autosuggest {
|
||||
background-color: $autosuggest-background;
|
||||
// border: solid $autosuggest-background;
|
||||
// border-width: 0 1px 1px 1px;
|
||||
|
||||
&__entry {
|
||||
display: block;
|
||||
padding: 1px 5px;
|
||||
color: $autosuggest-entry-color;
|
||||
background-color: $autosuggest-entry-background;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: calc(100%);
|
||||
|
||||
&:hover, &--selected {
|
||||
color: $autosuggest-entry-color-hover;
|
||||
text-decoration: none;
|
||||
background-color: $autosuggest-entry-background-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__account {
|
||||
padding: 5px 5px 5px 5px;
|
||||
|
||||
&:last-child{
|
||||
padding: 5px 5px 5px 5px;
|
||||
}
|
||||
|
||||
&--avatar {
|
||||
width: 25px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
&--text {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&--handle {
|
||||
color: $autosuggest-entry-handle-color;
|
||||
}
|
||||
// &--acct {
|
||||
// color: $autosuggest-entry-color;
|
||||
// }
|
||||
}
|
||||
|
||||
&__entry:hover &__account--handle, &__entry--selected &__account--handle {
|
||||
color: $autosuggest-entry-handle-color-hover;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AutosuggestComponent } from './autosuggest.component';
|
||||
|
||||
xdescribe('AutosuggestComponent', () => {
|
||||
let component: AutosuggestComponent;
|
||||
let fixture: ComponentFixture<AutosuggestComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AutosuggestComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AutosuggestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,190 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core';
|
||||
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { Results, Account } from '../../../services/models/mastodon.interfaces';
|
||||
import { Actions } from '@ngxs/store';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-autosuggest',
|
||||
templateUrl: './autosuggest.component.html',
|
||||
styleUrls: ['./autosuggest.component.scss']
|
||||
})
|
||||
export class AutosuggestComponent implements OnInit, OnDestroy {
|
||||
|
||||
private lastPatternUsed: string;
|
||||
private lastPatternUsedWtType: string;
|
||||
accounts: SelectableAccount[] = [];
|
||||
hashtags: SelectableHashtag[] = [];
|
||||
|
||||
@Output() suggestionSelectedEvent = new EventEmitter<AutosuggestSelection>();
|
||||
@Output() hasSuggestionsEvent = new EventEmitter<boolean>();
|
||||
|
||||
private _pattern: string;
|
||||
@Input('pattern')
|
||||
set pattern(value: string) {
|
||||
if (value) {
|
||||
this._pattern = value;
|
||||
this.analysePattern(value);
|
||||
} else {
|
||||
this._pattern = null;
|
||||
this.accounts.length = 0;
|
||||
this.hashtags.length = 0;
|
||||
}
|
||||
}
|
||||
get pattern(): string {
|
||||
return this._pattern;
|
||||
}
|
||||
|
||||
@Input() autoSuggestUserActionsStream: EventEmitter<AutosuggestUserActionEnum>;
|
||||
private autoSuggestUserActionsSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.autoSuggestUserActionsStream) {
|
||||
this.autoSuggestUserActionsSub = this.autoSuggestUserActionsStream.subscribe((action: AutosuggestUserActionEnum) => {
|
||||
this.processUserInput(action);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.autoSuggestUserActionsSub) this.autoSuggestUserActionsSub.unsubscribe();
|
||||
}
|
||||
|
||||
private analysePattern(value: string) {
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
const isAccount = value[0] === '@';
|
||||
const pattern = value.substring(1);
|
||||
this.lastPatternUsed = pattern;
|
||||
this.lastPatternUsedWtType = value;
|
||||
|
||||
this.toolsService.getInstanceInfo(selectedAccount)
|
||||
.then(instance => {
|
||||
let version: 'v1' | 'v2' = 'v1';
|
||||
if(instance.major >= 3) version = 'v2';
|
||||
return this.mastodonService.search(selectedAccount, pattern, version, false);
|
||||
})
|
||||
.then((results: Results) => {
|
||||
if (this.lastPatternUsed !== pattern) return;
|
||||
|
||||
this.accounts.length = 0;
|
||||
this.hashtags.length = 0;
|
||||
|
||||
if (isAccount) {
|
||||
for (let account of results.accounts) {
|
||||
//if (account.acct != this.lastPatternUsed) {
|
||||
this.accounts.push(new SelectableAccount(account));
|
||||
this.accounts[0].selected = true;
|
||||
if (this.accounts.length > 7) return;
|
||||
//}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let hashtag of results.hashtags) {
|
||||
//if (hashtag !== this.lastPatternUsed) {
|
||||
//if (hashtag.includes(this.lastPatternUsed.toLocaleLowerCase()) && hashtag !== this.lastPatternUsed) {
|
||||
//if (hashtag.includes(this.lastPatternUsed) && hashtag !== this.lastPatternUsed) {
|
||||
this.hashtags.push(new SelectableHashtag(hashtag));
|
||||
this.hashtags[0].selected = true;
|
||||
if (this.hashtags.length > 7) return;
|
||||
//}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (this.hashtags.length > 0 || this.accounts.length > 0) {
|
||||
this.hasSuggestionsEvent.next(true);
|
||||
} else {
|
||||
this.hasSuggestionsEvent.next(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, selectedAccount);
|
||||
});
|
||||
}
|
||||
|
||||
private processUserInput(action: AutosuggestUserActionEnum) {
|
||||
const isAutosuggestingHashtag = this.hashtags.length > 0;
|
||||
|
||||
switch (action) {
|
||||
case AutosuggestUserActionEnum.Validate:
|
||||
if (isAutosuggestingHashtag) {
|
||||
let selection = this.hashtags.find(x => x.selected);
|
||||
this.hashtagSelected(selection);
|
||||
} else {
|
||||
let selection = this.accounts.find(x => x.selected);
|
||||
this.accountSelected(selection);
|
||||
}
|
||||
break;
|
||||
case AutosuggestUserActionEnum.MoveDown:
|
||||
if (isAutosuggestingHashtag) {
|
||||
let selectionIndex = this.hashtags.findIndex(x => x.selected);
|
||||
if (selectionIndex < (this.hashtags.length - 1)) {
|
||||
this.hashtags[selectionIndex].selected = false;
|
||||
this.hashtags[selectionIndex + 1].selected = true;
|
||||
}
|
||||
} else {
|
||||
let selectionIndex = this.accounts.findIndex(x => x.selected);
|
||||
if (selectionIndex < (this.accounts.length - 1)) {
|
||||
this.accounts[selectionIndex].selected = false;
|
||||
this.accounts[selectionIndex + 1].selected = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AutosuggestUserActionEnum.MoveUp:
|
||||
if (isAutosuggestingHashtag) {
|
||||
let selectionIndex = this.hashtags.findIndex(x => x.selected);
|
||||
if (selectionIndex > 0) {
|
||||
this.hashtags[selectionIndex].selected = false;
|
||||
this.hashtags[selectionIndex - 1].selected = true;
|
||||
}
|
||||
} else {
|
||||
let selectionIndex = this.accounts.findIndex(x => x.selected);
|
||||
if (selectionIndex > 0) {
|
||||
this.accounts[selectionIndex].selected = false;
|
||||
this.accounts[selectionIndex - 1].selected = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
accountSelected(selAccount: SelectableAccount): boolean {
|
||||
const fullHandle = this.toolsService.getAccountFullHandle(selAccount.account);
|
||||
this.suggestionSelectedEvent.next(new AutosuggestSelection(this.lastPatternUsedWtType, fullHandle));
|
||||
return false;
|
||||
}
|
||||
|
||||
hashtagSelected(selHashtag: SelectableHashtag): boolean {
|
||||
this.suggestionSelectedEvent.next(new AutosuggestSelection(this.lastPatternUsedWtType, `#${selHashtag.hashtag}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableAccount {
|
||||
constructor(public account: Account, public selected: boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableHashtag {
|
||||
constructor(public hashtag: string, public selected: boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
export class AutosuggestSelection {
|
||||
constructor(public pattern: string, public autosuggest: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export enum AutosuggestUserActionEnum {
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Validate
|
||||
}
|
|
@ -1,28 +1,105 @@
|
|||
<form class="status-form" (ngSubmit)="onSubmit()">
|
||||
<div class="status-form__sending" *ngIf="isSending">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<form class="status-editor" (ngSubmit)="onSubmit()">
|
||||
<input #mytitle [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title"
|
||||
name="title" autocomplete="off" placeholder="Title, Content Warning (optional)"
|
||||
title="title, content warning (optional)" dir="auto"
|
||||
(keydown.escape)="mytitle.blur()" />
|
||||
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm" name="title" autocomplete="off"
|
||||
placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" />
|
||||
<a class="status-editor__emoji" title="Insert Emoji"
|
||||
#emojiButton href (click)="openEmojiPicker($event)">
|
||||
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
|
||||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status"
|
||||
class="form-control form-control-sm status-form__status flexcroll" rows="5" required title="content"
|
||||
placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
|
||||
<a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
|
||||
{{ selectedLanguage.iso639 }}
|
||||
</a>
|
||||
|
||||
<div class="status-form__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to the
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's on your mind?"
|
||||
(keydown.control.enter)="onCtrlEnter()"
|
||||
(keydown.meta.enter)="onCtrlEnter()"
|
||||
(keydown.escape)="reply.blur()"
|
||||
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||
</textarea>
|
||||
|
||||
<div class="status-editor__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to
|
||||
the
|
||||
start in order to use multiposting.</div>
|
||||
|
||||
<select class="form-control form-control-sm form-control--privacy" id="privacy" name="privacy"
|
||||
[(ngModel)]="selectedPrivacy">
|
||||
<option *ngFor="let p of privacyList" [ngValue]="p">{{p}}</option>
|
||||
</select>
|
||||
<div class="status-form__counter">
|
||||
<span class="status-form__counter--count">{{charCountLeft}}</span> <span
|
||||
class="status-form__counter--posts">{{postCounts - 1}}/{{postCounts}}</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary" *ngIf="statusReplyingToWrapper">REPLY!</button>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary" *ngIf="!statusReplyingToWrapper">POST!</button>
|
||||
<app-autosuggest class="status-editor__autosuggest" *ngIf="autosuggestData" [pattern]="autosuggestData"
|
||||
[autoSuggestUserActionsStream]="autoSuggestUserActionsStream"
|
||||
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
||||
</app-autosuggest>
|
||||
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive" [oldPoll]="oldPoll"></app-poll-editor>
|
||||
|
||||
<app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler>
|
||||
|
||||
<div class="status-editor__footer" #footer>
|
||||
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
|
||||
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">REPLY!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
|
||||
<span *ngIf="!isSending && isEditing">EDIT!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
|
||||
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">POST!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
|
||||
<span *ngIf="!isSending && isEditing">EDIT!</span>
|
||||
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
|
||||
</button>
|
||||
<div class="status-editor__footer__counter">
|
||||
<div class="status-editor__footer__counter--posts" title="number of statuses">
|
||||
{{postCounts - 1}}/{{postCounts}}</div>
|
||||
<div class="status-editor__footer__counter--count" title="chars left">{{charCountLeft}}</div>
|
||||
</div>
|
||||
|
||||
<a href class="status-editor__footer--link" title="add media" (click)="addMedia()">
|
||||
<fa-icon [icon]="faPaperclip"></fa-icon>
|
||||
</a>
|
||||
<input #fileInput type="file" id="file" style="display: none;" (change)="handleFileInput($event.target.files)">
|
||||
|
||||
<a href class="status-editor__footer--link" title="{{ selectedPrivacy }}" (click)="onContextMenu($event)">
|
||||
<fa-icon [icon]="faGlobeAmericas" *ngIf="selectedPrivacy === 'Public'"></fa-icon>
|
||||
<fa-icon [icon]="faLockOpen" *ngIf="selectedPrivacy === 'Unlisted'"></fa-icon>
|
||||
<fa-icon [icon]="faLock" *ngIf="selectedPrivacy === 'Follows-only'"></fa-icon>
|
||||
<fa-icon [icon]="faEnvelope" *ngIf="selectedPrivacy === 'DM'"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href *ngIf="instanceSupportsPoll"
|
||||
class="status-editor__footer--link status-editor__footer--add-poll" title="add poll" (click)="addPoll()">
|
||||
<fa-icon [icon]="faPollH"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href *ngIf="instanceSupportsScheduling"
|
||||
class="status-editor__footer--link" title="schedule" (click)="schedule()">
|
||||
<fa-icon [icon]="faClock"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="language-warning" *ngIf="!configuredLanguages || configuredLanguages.length === 0">
|
||||
You haven't set your language(s) yet, please <a href class="language-warning__link" (click)="onNavigateToSettings()">go in the settings</a> to provide it.
|
||||
</div>
|
||||
|
||||
<context-menu #contextMenu>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
|
||||
<fa-icon [icon]="faGlobeAmericas" class="context-menu-icon"></fa-icon> Public
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Unlisted')">
|
||||
<fa-icon [icon]="faLockOpen" class="context-menu-icon"></fa-icon> Unlisted
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('Follows-only')">
|
||||
<fa-icon [icon]="faLock" class="context-menu-icon"></fa-icon> Followers-only
|
||||
</ng-template>
|
||||
<ng-template contextMenuItem (execute)="changePrivacy('DM')">
|
||||
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
|
||||
<context-menu #langContextMenu>
|
||||
<ng-template contextMenuItem (execute)="setLanguage(l)" *ngFor="let l of configuredLanguages">
|
||||
{{ l.name }}
|
||||
</ng-template>
|
||||
</context-menu>
|
||||
|
||||
<app-media></app-media>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
@ -2,96 +2,256 @@
|
|||
@import "commons";
|
||||
@import "panel";
|
||||
@import "buttons";
|
||||
@import "mixins";
|
||||
$btn-send-status-width: 60px;
|
||||
$counter-width: 90px;
|
||||
|
||||
// @import "~@ctrl/ngx-emoji-mart/picker";
|
||||
|
||||
.form-control {
|
||||
margin: 0 0 5px 5px;
|
||||
width: calc(100% - 10px);
|
||||
background-color: $column-color;
|
||||
background-color: $status-editor-background;
|
||||
border-color: $status-secondary-color;
|
||||
color: #fff;
|
||||
color: $status-editor-color;
|
||||
font-size: $default-font-size;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&--privacy {
|
||||
display: inline-block;
|
||||
width: calc(100% - 15px - #{$btn-send-status-width} - #{$counter-width});
|
||||
}
|
||||
}
|
||||
|
||||
.status-editor {
|
||||
position: relative;
|
||||
font-size: $default-font-size;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&__title {
|
||||
background-color: $status-editor-title-background;
|
||||
color: $status-editor-color;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
border-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__emoji {
|
||||
position: absolute;
|
||||
top: 37px;
|
||||
right: 10px;
|
||||
|
||||
&--image {
|
||||
transition: all .2s;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
-webkit-filter: grayscale(100%);
|
||||
-moz-filter: grayscale(100%);
|
||||
-ms-filter: grayscale(100%);
|
||||
-o-filter: grayscale(100%);
|
||||
filter: gray;
|
||||
opacity: .7;
|
||||
|
||||
&:hover {
|
||||
filter: none;
|
||||
-webkit-filter: grayscale(0%);
|
||||
-moz-filter: grayscale(0%);
|
||||
-ms-filter: grayscale(0%);
|
||||
-o-filter: grayscale(0%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__lang {
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
|
||||
font-weight: bolder;
|
||||
font-size: 12px;
|
||||
color: #a5a5a5;
|
||||
text-decoration: none;
|
||||
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 19px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
|
||||
padding: 1px 0 0 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color:black;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
border-width: 0;
|
||||
background-color: $status-editor-background;
|
||||
color: $status-editor-color;
|
||||
margin-bottom: 0;
|
||||
|
||||
resize: none;
|
||||
border: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
|
||||
min-height: 110px;
|
||||
height: 110px;
|
||||
|
||||
padding-bottom: 10px;
|
||||
padding-right: 30px;
|
||||
//border-bottom: 1px solid black;
|
||||
|
||||
&::-webkit-resizer {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__mention-error {
|
||||
background-color: $status-editor-background;
|
||||
color: rgb(255, 34, 34);
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__autosuggest {
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
height: 34px;
|
||||
margin: 0 5px;
|
||||
border-width: 0;
|
||||
|
||||
background-color: $status-editor-footer-background;
|
||||
|
||||
&--link {
|
||||
color: $status-editor-footer-link-color;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin: 2px 0 0 5px;
|
||||
}
|
||||
|
||||
&--add-poll {
|
||||
font-size: 16px;
|
||||
margin: 0 0 0 5px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
&--send-button {
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
float: right;
|
||||
padding: 0 15px 0 15px;
|
||||
height: 34px;
|
||||
background-color: $status-editor-footer-background;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($status-editor-footer-background, 20%);
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
outline: inherit;
|
||||
&:focus {
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
& span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__counter {
|
||||
float: right;
|
||||
height: 34px;
|
||||
padding: 6px 7px 0 7px;
|
||||
vertical-align: center;
|
||||
|
||||
&--count {
|
||||
display: block;
|
||||
margin-right: 40px
|
||||
}
|
||||
|
||||
&--posts {
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-custom-primary {
|
||||
display: inline-block;
|
||||
width: $btn-send-status-width;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 5px; // background-color: orange;
|
||||
// border-color: orange;
|
||||
// color: black;
|
||||
font-weight: 500; // &:hover {
|
||||
// }
|
||||
// &:focus {
|
||||
// border-color: darkblue;
|
||||
// }
|
||||
left: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-form {
|
||||
.context-menu-icon {
|
||||
position: relative;
|
||||
font-size: $default-font-size;
|
||||
&__sending {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
background-color: rgba($column-color, .75);
|
||||
z-index: 2;
|
||||
&--waiting {
|
||||
margin-top: calc(25%);
|
||||
}
|
||||
}
|
||||
&__counter {
|
||||
display: inline-block;
|
||||
border: 1px solid $status-secondary-color;
|
||||
margin-left: 5px;
|
||||
width: calc(#{$counter-width} - 5px);
|
||||
height: 32px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
padding: 4px 7px 0 7px; // color: lighten($font-link-primary-hover, 10);
|
||||
// position: relative;
|
||||
// overflow: hidden;
|
||||
&--count {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// overflow: hidden;
|
||||
// outline: 1px solid greenyellow;
|
||||
}
|
||||
&--posts {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
&__status {
|
||||
&::-webkit-resizer {
|
||||
// border: 2px solid black;
|
||||
background: $font-link-primary-hover;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
// box-shadow: 0 0 5px 5px blue;
|
||||
// outline: 2px solid yellow;
|
||||
}
|
||||
left: -3px;
|
||||
font-size: 12px;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
.emojipicker {
|
||||
font-size: $default-font-size !important;
|
||||
}
|
||||
|
||||
.scheduler {
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.language-warning {
|
||||
padding: 5px 10px;
|
||||
color: orange;
|
||||
|
||||
&__link {
|
||||
text-decoration: underline;
|
||||
color: #f0d124;
|
||||
|
||||
&:hover {
|
||||
color: #d18800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__mention-error {
|
||||
border: 2px dashed red;
|
||||
padding: 5px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||
// ::ng-deep .cdk-overlay-backdrop {
|
||||
// // width: 100%;
|
||||
// // height: 100%;
|
||||
// border: 3px solid greenyellow;
|
||||
// background-color: black;
|
||||
// min-height: 20px;
|
||||
// }
|
|
@ -2,6 +2,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
import { FormsModule } from '@angular/forms';
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ContextMenuModule } from 'ngx-contextmenu';
|
||||
|
||||
import { CreateStatusComponent } from './create-status.component';
|
||||
import { WaitingAnimationComponent } from '../waiting-animation/waiting-animation.component';
|
||||
|
@ -12,7 +14,8 @@ import { StreamsState } from '../../states/streams.state';
|
|||
import { NavigationService } from '../../services/navigation.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { MastodonService } from '../../services/mastodon.service';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { SettingsState } from '../../states/settings.state';
|
||||
|
||||
describe('CreateStatusComponent', () => {
|
||||
let component: CreateStatusComponent;
|
||||
|
@ -26,13 +29,15 @@ describe('CreateStatusComponent', () => {
|
|||
imports: [
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
ContextMenuModule.forRoot(),
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
],
|
||||
providers: [NavigationService, NotificationService, MastodonService],
|
||||
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
||||
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
@ -47,6 +52,57 @@ describe('CreateStatusComponent', () => {
|
|||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not count emoji as multiple chars', () => {
|
||||
const status = '😃 😍 👌 👇 😱 😶 status with 😱 😶 emojis 😏 👍 ';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(461);
|
||||
});
|
||||
|
||||
it('should not count emoji in CW as multiple chars', () => {
|
||||
const status = 'test';
|
||||
(<any>component).title = '🙂 test';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(490);
|
||||
});
|
||||
|
||||
it('should not count domain chars in username', () => {
|
||||
const status = 'dsqdqs @NicolasConstant@mastodon.partipirate.org dsqdqsdqsd';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(466);
|
||||
});
|
||||
|
||||
it('should not count https link more than the minimum', () => {
|
||||
const status = "https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(477);
|
||||
});
|
||||
|
||||
it('should not count http link more than the minimum', () => {
|
||||
const status = "http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(477);
|
||||
});
|
||||
|
||||
it('should not count links more than the minimum', () => {
|
||||
const status = "http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(429);
|
||||
});
|
||||
|
||||
it('should count correctly complex status', () => {
|
||||
const status = 'dsqdqs @NicolasConstant@mastodon.partipirate.org dsqdqs👇😱 😶 status https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ #Pleroma with 😱 😶 emojis 😏 👍 #Mastodon @ddqsdqs @dsqdsq@dqsdsqqdsq';
|
||||
(<any>component).title = '🙂 test';
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(373);
|
||||
});
|
||||
|
||||
it('should not parse small status', () => {
|
||||
const status = 'this is a cool status';
|
||||
(<any>component).maxCharLength = 500;
|
||||
|
@ -109,6 +165,41 @@ describe('CreateStatusComponent', () => {
|
|||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should cound URL correctly', () => {
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s`;
|
||||
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
});
|
||||
|
||||
it('should cound URL correctly - new lines', () => {
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd\nhttps://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs\ndsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s`;
|
||||
|
||||
(<any>component).maxCharLength = 500;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
});
|
||||
|
||||
it('should cound URL correctly - dual post', () => {
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s`;
|
||||
|
||||
(<any>component).maxCharLength = 512;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
expect((<any>component).postCounts).toBe(2);
|
||||
});
|
||||
|
||||
it('should cound URL correctly - triple post', () => {
|
||||
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qs qsd qsd qsd qsd sqd qsd qsd sqd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd sd`;
|
||||
|
||||
(<any>component).maxCharLength = 512;
|
||||
(<any>component).countStatusChar(status);
|
||||
expect((<any>component).charCountLeft).toBe(0);
|
||||
expect((<any>component).postCounts).toBe(3);
|
||||
});
|
||||
|
||||
it('should add alias in multiposting replies', () => {
|
||||
const status = '@Lorem@ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictu0';
|
||||
(<any>component).maxCharLength = 500;
|
||||
|
@ -131,4 +222,116 @@ describe('CreateStatusComponent', () => {
|
|||
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
||||
});
|
||||
|
||||
it('should parse long link properly for multiposting', () => {
|
||||
const status = 'dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd dsq http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].length).toBeLessThanOrEqual(527);
|
||||
expect(result[1].length).toBeLessThanOrEqual(527);
|
||||
expect(result[1]).toBe('http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - mastodon', () => {
|
||||
let mastodonMention = '<p>test <span class="h-card"><a href="https://mastodon.social/@sengi_app" class="u-url mention">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a href="https://mastodon.social/@test" class="u-url mention">@<span>test</span></a></span> <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(mastodonMention);
|
||||
expect(result).toBe('<p>test @sengi_app@mastodon.social qsdqds qsd qsd qsd q @test@mastodon.social <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - mastodon 2', () => {
|
||||
let mastodonMention = '<p>test <span class="h-card"><a href="https://pleroma.site/users/sengi_app" class="u-url mention">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a href="https://pleroma.site/users/test" class="u-url mention">@<span>test</span></a></span> <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(mastodonMention);
|
||||
expect(result).toBe('<p>test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - pleroma', () => {
|
||||
let pleromaMention = '<p>test <span class="h-card"><a data-user="50504" class="u-url mention" href="https://mastodon.social/@sengi_app" rel="ugc">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a data-user="50504" class="u-url mention" href="https://mastodon.social/@test" rel="ugc">@<span>test</span></a></span> <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(pleromaMention);
|
||||
expect(result).toBe('<p>test @sengi_app@mastodon.social qsdqds qsd qsd qsd q @test@mastodon.social <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - pleroma 2', () => {
|
||||
let pleromaMention = '<p>test <span class="h-card"><a data-user="50504" class="u-url mention" href="https://pleroma.site/users/sengi_app" rel="ugc">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a data-user="50504" class="u-url mention" href="https://pleroma.site/users/test" rel="ugc">@<span>test</span></a></span> <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(pleromaMention);
|
||||
expect(result).toBe('<p>test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should autocomplete - at the end', () => {
|
||||
let text = 'data @sengi';
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe('data @sengi@mastodon.social ');
|
||||
});
|
||||
|
||||
it('should autocomplete - at the start', () => {
|
||||
let text = '@sengi data';
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe('@sengi@mastodon.social data');
|
||||
});
|
||||
|
||||
it('should autocomplete - at the middle', () => {
|
||||
let text = 'data @sengi data';
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe('data @sengi@mastodon.social data');
|
||||
});
|
||||
|
||||
it('should autocomplete - duplicate', () => {
|
||||
let text = 'data @sengi @sengi2 data';
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe('data @sengi@mastodon.social @sengi2 data');
|
||||
});
|
||||
|
||||
it('should autocomplete - duplicate 2', () => {
|
||||
let text = 'data @sengi2 @sengi data';
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe('data @sengi2 @sengi@mastodon.social data');
|
||||
});
|
||||
|
||||
it('should autocomplete - new lines', () => {
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
let text = `@sengi${newLine}${newLine}data`;
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe(`@sengi@mastodon.social${newLine}${newLine}data`);
|
||||
});
|
||||
|
||||
it('should autocomplete - new lines 2', () => {
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
let text = `@nicolasconstant\n\ndata`;
|
||||
let pattern = '@nicolasconstant';
|
||||
let autosuggest = '@nicolasconstant@social.nicolas-constant.com';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe(`@nicolasconstant@social.nicolas-constant.com${newLine}${newLine}data`);
|
||||
});
|
||||
|
||||
it('should autocomplete - complex', () => {
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
let text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ullamcorper nulla eu metus euismod, non lobortis${newLine}quam congue. @sengi Ut hendrerit, nulla vel feugiat lobortis, diam ligula congue lacus, sed facilisis nisl dui at mauris.${newLine}Cras non hendrerit tellus. Donec eleifend metus quis nibh commodo${newLine}${newLine}data`;
|
||||
let pattern = '@sengi';
|
||||
let autosuggest = '@sengi@mastodon.social';
|
||||
|
||||
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
|
||||
expect(result).toBe(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ullamcorper nulla eu metus euismod, non lobortis${newLine}quam congue. @sengi@mastodon.social Ut hendrerit, nulla vel feugiat lobortis, diam ligula congue lacus, sed facilisis nisl dui at mauris.${newLine}Cras non hendrerit tellus. Donec eleifend metus quis nibh commodo${newLine}${newLine}data`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
<emoji-mart
|
||||
*ngIf="loaded"
|
||||
[showPreview]="false" [perLine]="7" [isNative]="true" [sheetSize]="16" [emojiTooltip]="true"
|
||||
[custom]="customEmojis" (emojiSelect)="emojiSelected($event)" class="emojipicker" title="Pick your emoji…"
|
||||
emoji="point_up"></emoji-mart>
|
|
@ -0,0 +1,22 @@
|
|||
::ng-deep .emoji-mart {
|
||||
border-radius: 0 !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .emoji-mart-emoji-native {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
::ng-deep .emoji-mart-emoji-native span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: -1px;
|
||||
font-size: 19px !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
::ng-deep .emoji-mart-emoji-custom span {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EmojiPickerComponent } from './emoji-picker.component';
|
||||
|
||||
xdescribe('EmojiPickerComponent', () => {
|
||||
let component: EmojiPickerComponent;
|
||||
let fixture: ComponentFixture<EmojiPickerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EmojiPickerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EmojiPickerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { Component, OnInit, HostListener, ElementRef, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { Emoji } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-emoji-picker',
|
||||
templateUrl: './emoji-picker.component.html',
|
||||
styleUrls: ['./emoji-picker.component.scss']
|
||||
})
|
||||
export class EmojiPickerComponent implements OnInit {
|
||||
private init = false;
|
||||
|
||||
@Output('closed') public closedEvent = new EventEmitter();
|
||||
@Output('emojiSelected') public emojiSelectedEvent = new EventEmitter<string>();
|
||||
|
||||
customEmojis: PickerCustomEmoji[] = [];
|
||||
loaded: boolean;
|
||||
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
private toolsService: ToolsService,
|
||||
private eRef: ElementRef) { }
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
clickout(event) {
|
||||
if (!this.init) return;
|
||||
|
||||
if (!this.eRef.nativeElement.contains(event.target)) {
|
||||
this.closedEvent.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let currentAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.toolsService.getCustomEmojis(currentAccount)
|
||||
.then(emojis => {
|
||||
this.customEmojis = emojis.map(x => this.convertEmoji(x));
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, currentAccount);
|
||||
})
|
||||
.then(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.init = true;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private convertEmoji(emoji: Emoji): PickerCustomEmoji {
|
||||
return new PickerCustomEmoji(emoji.shortcode, [emoji.shortcode], emoji.shortcode, [emoji.shortcode], emoji.url);
|
||||
}
|
||||
|
||||
emojiSelected(select: any): boolean {
|
||||
if (select.emoji.custom) {
|
||||
this.emojiSelectedEvent.next(select.emoji.colons);
|
||||
} else {
|
||||
this.emojiSelectedEvent.next(select.emoji.native);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class PickerCustomEmoji {
|
||||
constructor(
|
||||
public name: string,
|
||||
public shortNames: string[],
|
||||
public text: string,
|
||||
public keywords: string[],
|
||||
public imageUrl: string) {
|
||||
}
|
||||
}
|
|
@ -1,19 +1,34 @@
|
|||
<div *ngFor="let m of media" class="media">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{m.file.name}}">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{getName(m)}}">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null" class="media__loaded" title="{{m.file.name}}"
|
||||
(mouseleave) ="updateMedia(m)">
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{getName(m)}}"
|
||||
(mouseleave)="updateMedia(m)">
|
||||
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div class="media__loaded--hover">
|
||||
<button class="media__loaded--button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</button>
|
||||
<input class="media__loaded--description" [(ngModel)]="m.description"
|
||||
autocomplete="off" placeholder="Describe for the visually impaired"/>
|
||||
<a href class="media__loaded--button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<input class="media__loaded--description" [(ngModel)]="m.description" autocomplete="off"
|
||||
placeholder="Describe for the visually impaired" />
|
||||
</div>
|
||||
<img class="media__loaded--preview" src="{{m.attachment.preview_url}}" />
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null && m.attachment.type === 'audio'" class="audio">
|
||||
<a href class="audio__button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
|
||||
<div *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
|
||||
<audio *ngIf="m.audioType && !m.isMigrating" controls class="audio__player">
|
||||
<source src="{{ m.attachment.url }}" type="{{ m.audioType }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
.media {
|
||||
width: calc(100%);
|
||||
padding: 0 5px 5px 5px;
|
||||
padding: 5px 5px 0px 5px;
|
||||
|
||||
&__loading{
|
||||
width: calc(100%);
|
||||
border: 1px solid $status-secondary-color;
|
||||
//border: 1px solid $status-secondary-color;
|
||||
// background: rgb(0, 96, 134);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
@ -16,7 +16,7 @@
|
|||
&__loaded{
|
||||
width: calc(100%);
|
||||
height: 75px;
|
||||
border: 1px solid $status-secondary-color;
|
||||
//border: 1px solid $status-secondary-color;
|
||||
position: relative;
|
||||
transition: all .2s;
|
||||
|
||||
|
@ -46,12 +46,11 @@
|
|||
opacity: 100;
|
||||
}
|
||||
|
||||
&--button {
|
||||
@include clearButton;
|
||||
&--button {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
top:5px;
|
||||
top:0px;
|
||||
right:8px;
|
||||
color: white;
|
||||
}
|
||||
|
@ -61,20 +60,34 @@
|
|||
bottom:5px;
|
||||
left: 5px;
|
||||
width: calc(100% - 10px);
|
||||
// background: black;
|
||||
// color: white;
|
||||
}
|
||||
|
||||
&--preview {
|
||||
// display: block;
|
||||
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio {
|
||||
width: calc(100%);
|
||||
height: 30px;
|
||||
|
||||
&__player {
|
||||
width: calc(100% - 20px);
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: white;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
}
|
|
@ -50,8 +50,19 @@ export class MediaComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
updateMedia(media: MediaWrapper): boolean {
|
||||
if(media.description === null || media.description === undefined) return false;
|
||||
|
||||
const account = this.toolsService.getSelectedAccounts()[0];
|
||||
this.mediaService.update(account, media);
|
||||
return false;
|
||||
}
|
||||
|
||||
getName(media: MediaWrapper): string {
|
||||
if(media && media.file && media.file.name){
|
||||
return media.file.name;
|
||||
}
|
||||
if(media.attachment && media.attachment.description){
|
||||
return media.attachment.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<div class="poll-editor">
|
||||
<div class="poll-editor__entries">
|
||||
<div *ngFor="let e of entries">
|
||||
<app-poll-entry class="poll-editor__entry" [entry]="e" (removeEvent)="removeElement(e)"
|
||||
(toogleMultiEvent)="toogleMulti()"></app-poll-entry>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="poll-editor__footer">
|
||||
<select [(ngModel)]="selectedId" class="poll-editor__footer--select-duration">
|
||||
<option *ngFor="let d of delayChoice" [ngValue]="d.id">{{d.label}}</option>
|
||||
</select>
|
||||
|
||||
<a href (click)="addEntry()" class="poll-editor__footer--add-choice">
|
||||
<fa-icon [icon]="faPlus"></fa-icon> Add a choice
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,37 @@
|
|||
@import "variables";
|
||||
|
||||
.poll-editor {
|
||||
background-color: $poll-editor-background;
|
||||
border-top: 1px solid $poll-editor-separator;
|
||||
min-height: 30px;
|
||||
margin: 0 5px;
|
||||
|
||||
&__entries {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
transition: all .2s;
|
||||
border-top: 1px solid $poll-editor-separator;
|
||||
min-height: 30px;
|
||||
padding: 5px;
|
||||
|
||||
&--add-choice {
|
||||
color: rgb(49, 49, 49);
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: rgb(122, 122, 122);
|
||||
}
|
||||
}
|
||||
|
||||
&--select-duration {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PollEditorComponent } from './poll-editor.component';
|
||||
|
||||
xdescribe('PollEditorComponent', () => {
|
||||
let component: PollEditorComponent;
|
||||
let fixture: ComponentFixture<PollEditorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PollEditorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PollEditorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
import { Component, Input, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { PollEntry } from './poll-entry/poll-entry.component';
|
||||
import { PollParameters } from '../../../services/mastodon.service';
|
||||
import { Poll } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-editor',
|
||||
templateUrl: './poll-editor.component.html',
|
||||
styleUrls: ['./poll-editor.component.scss']
|
||||
})
|
||||
export class PollEditorComponent implements OnInit {
|
||||
faPlus = faPlus;
|
||||
|
||||
private entryUuid: number = 0;
|
||||
entries: PollEntry[] = [];
|
||||
delayChoice: Delay[] = [];
|
||||
selectedId: string;
|
||||
private multiSelected: boolean;
|
||||
|
||||
@Input() oldPoll: Poll;
|
||||
|
||||
constructor() {
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
|
||||
this.delayChoice.push(new Delay(60 * 5, "5 minutes"));
|
||||
this.delayChoice.push(new Delay(60 * 30, "30 minutes"));
|
||||
this.delayChoice.push(new Delay(60 * 60, "1 hour"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 6, "6 hours"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24, "1 day"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 3, "3 days"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 7, "7 days"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 15, "15 days"));
|
||||
this.delayChoice.push(new Delay(60 * 60 * 24 * 30, "30 days"));
|
||||
|
||||
this.selectedId = this.delayChoice[4].id;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['oldPoll']) {
|
||||
this.loadPollParameters(this.oldPoll);
|
||||
}
|
||||
}
|
||||
|
||||
private getEntryUuid(): number {
|
||||
this.entryUuid++;
|
||||
return this.entryUuid;
|
||||
}
|
||||
|
||||
addEntry(): boolean {
|
||||
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
|
||||
return false;
|
||||
}
|
||||
|
||||
removeElement(entry: PollEntry) {
|
||||
this.entries = this.entries.filter(x => x.id != entry.id);
|
||||
}
|
||||
|
||||
toogleMulti() {
|
||||
this.multiSelected = !this.multiSelected;
|
||||
this.entries.forEach((e: PollEntry) => {
|
||||
e.isMulti = this.multiSelected;
|
||||
});
|
||||
}
|
||||
|
||||
getPollParameters(): PollParameters {
|
||||
let params = new PollParameters();
|
||||
params.expires_in = this.delayChoice.find(x => x.id === this.selectedId).delayInSeconds;
|
||||
params.multiple = this.multiSelected;
|
||||
params.options = this.entries.map(x => x.label);
|
||||
params.hide_totals = false;
|
||||
return params;
|
||||
}
|
||||
|
||||
private loadPollParameters(poll: Poll) {
|
||||
if(!this.oldPoll) return;
|
||||
|
||||
const isMulti = poll.multiple;
|
||||
|
||||
this.entries.length = 0;
|
||||
for (let o of poll.options) {
|
||||
const entry = new PollEntry(this.getEntryUuid(), isMulti);
|
||||
entry.label = o.title;
|
||||
this.entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Delay {
|
||||
constructor(public delayInSeconds: number, public label: string) {
|
||||
this.id = delayInSeconds.toString();
|
||||
}
|
||||
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<div class="poll-entry">
|
||||
<div class="poll-entry__remove">
|
||||
<a href (click)="remove()" title="remove" class="poll-entry__remove--link">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="poll-entry__multi">
|
||||
<a href (click)="toogleMulti()" class="poll-entry__multi--link">
|
||||
<span class="check-mark" [class.check-mark__round]="!entry.isMulti" [class.check-mark__box]="entry.isMulti">
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="poll-entry__label">
|
||||
<input type="text" [(ngModel)]="entry.label" class="poll-entry__label--input" [(ngModel)]="entry.label"/>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,66 @@
|
|||
@import "variables";
|
||||
|
||||
$selector-size: 20px;
|
||||
$selector-padding: 5px;
|
||||
|
||||
.poll-entry {
|
||||
position: relative;
|
||||
|
||||
&__multi {
|
||||
&--link {
|
||||
display: block;
|
||||
padding: $selector-padding;
|
||||
width: calc(#{$selector-size} + 2 * #{$selector-padding});
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
height: calc(#{$selector-size} + 2 * #{$selector-padding});
|
||||
padding-top: 3px;
|
||||
|
||||
&--input {
|
||||
width: calc(100% - #{$selector-size} - 2 * #{$selector-padding} - 30px);
|
||||
border:1px solid $poll-editor-input-border;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border:1px solid $poll-editor-input-border-focus;
|
||||
box-shadow: 0 0 0 #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__remove {
|
||||
float: right;
|
||||
width: 25px;
|
||||
height: 30px;
|
||||
|
||||
&--link {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 5px;
|
||||
color: rgb(139, 139, 139);
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
display: block;
|
||||
border: 1px solid rgb(100, 100, 100);
|
||||
width: $selector-size;
|
||||
height: $selector-size;
|
||||
|
||||
&__round {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
&__box {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PollEntryComponent } from './poll-entry.component';
|
||||
|
||||
xdescribe('PollEntryComponent', () => {
|
||||
let component: PollEntryComponent;
|
||||
let fixture: ComponentFixture<PollEntryComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PollEntryComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PollEntryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-entry',
|
||||
templateUrl: './poll-entry.component.html',
|
||||
styleUrls: ['./poll-entry.component.scss']
|
||||
})
|
||||
export class PollEntryComponent implements OnInit {
|
||||
faTimes = faTimes;
|
||||
|
||||
@Input() entry: PollEntry;
|
||||
|
||||
@Output() removeEvent = new EventEmitter();
|
||||
@Output() toogleMultiEvent = new EventEmitter();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
remove(): boolean {
|
||||
this.removeEvent.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
toogleMulti(): boolean {
|
||||
this.toogleMultiEvent.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PollEntry {
|
||||
constructor(public id: number, public isMulti: boolean) {
|
||||
}
|
||||
|
||||
public label: string;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<div class="scheduler">
|
||||
<input class="scheduler__input" [owlDateTime]="dt2" [owlDateTimeTrigger]="dt2" placeholder="" [min]="min" [(ngModel)]="scheduledDate">
|
||||
<a class="scheduler__icon" href (click)="openScheduler()" [owlDateTimeTrigger]="dt2" title="open datetime picker"><fa-icon [icon]="faCalendarAlt"></fa-icon></a>
|
||||
<owl-date-time #dt2></owl-date-time>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
@import "variables";
|
||||
|
||||
.scheduler {
|
||||
background-color: $scheduler-background;
|
||||
|
||||
&__input {
|
||||
color: whitesmoke;
|
||||
padding: 3px;
|
||||
width: calc(100% - 25px);
|
||||
border: 1px solid $scheduler-background;
|
||||
outline: 0;
|
||||
background-color: $scheduler-background;
|
||||
&:focus{
|
||||
border: 1px solid $scheduler-background;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: whitesmoke;
|
||||
&:hover {
|
||||
color:rgb(204, 204, 204);
|
||||
}
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StatusSchedulerComponent } from './status-scheduler.component';
|
||||
|
||||
xdescribe('StatusSchedulerComponent', () => {
|
||||
let component: StatusSchedulerComponent;
|
||||
let fixture: ComponentFixture<StatusSchedulerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StatusSchedulerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StatusSchedulerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-scheduler',
|
||||
templateUrl: './status-scheduler.component.html',
|
||||
styleUrls: ['./status-scheduler.component.scss']
|
||||
})
|
||||
export class StatusSchedulerComponent implements OnInit {
|
||||
faCalendarAlt = faCalendarAlt;
|
||||
min = new Date();
|
||||
// scheduledDate: string;
|
||||
|
||||
@Input() scheduledDate: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
openScheduler(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getScheduledDate(): string {
|
||||
try {
|
||||
return new Date(this.scheduledDate).toISOString();
|
||||
} catch(err){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,42 @@
|
|||
<div class="panel">
|
||||
<h3 class="panel__title">Add new account</h3>
|
||||
<div class="panel" [class.comrade__background]="isComrade">
|
||||
<h3 class="panel__title" [class.comrade__text]="isComrade">Add new account</h3>
|
||||
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<label>Please provide your account:</label>
|
||||
<input type="text" class="form-control form-control-sm" [(ngModel)]="mastodonFullHandle" name="mastodonFullHandle"
|
||||
placeholder="@nickname@mastodon.social" />
|
||||
<br />
|
||||
<button type="submit" class="btn btn-success btn-sm">Submit</button>
|
||||
</form>
|
||||
<div class="panel__content">
|
||||
<h2 class="comrade__title" *ngIf="isComrade">Welcome Comrade!</h2>
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<label [class.comrade__text]="isComrade">Please provide your <span *ngIf="isComrade">comrade</span>
|
||||
instance:</label>
|
||||
|
||||
<div>
|
||||
<input type="text" class="form-control form-control-sm form-with-button"
|
||||
[(ngModel)]="setInstance" name="instance" [class.comrade__input]="isComrade"
|
||||
placeholder="mastodon.social" />
|
||||
|
||||
<button type="submit" class="form-button"
|
||||
title="add account"
|
||||
[class.comrade__button]="isComrade">
|
||||
|
||||
<span *ngIf="!isLoading && !this.isInstanceMultiAccountLoading">Submit</span>
|
||||
<span *ngIf="!isLoading && this.isInstanceMultiAccountLoading" class="faq__warning">See FAQ</span>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div *ngIf="isComrade" class="comrade__video">
|
||||
<iframe width="300" height="170"
|
||||
src="https://www.youtube.com/embed/NzBjnoRG7Mo?feature=oembed&autoplay=1&auto_play=1" frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div class="faq" *ngIf="isInstanceMultiAccount">
|
||||
<p>
|
||||
FAQ<br/>
|
||||
<a href="https://github.com/NicolasConstant/sengi/wiki/How-to-add-multiple-accounts-from-the-same-instance" target="_blank">How to add multiple accounts from the same instance?</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,2 +1,129 @@
|
|||
@import "variables";
|
||||
@import "panel";
|
||||
@import "mixins";
|
||||
@import "panel";
|
||||
|
||||
$button-size: 70px;
|
||||
|
||||
.panel {
|
||||
padding-left: 0px;
|
||||
// padding-right: 0px;
|
||||
background-position: 0 100%;
|
||||
|
||||
&__content {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-with-button {
|
||||
width: calc(100% - #{$button-size});
|
||||
float: left;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
font-size: $default-font-size;
|
||||
height: 29px;
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
background-color: $status-editor-title-background;
|
||||
color: $status-editor-color;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-width: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
// background-color: $column-color;
|
||||
// border-color: $button-border-color;
|
||||
// color: #fff;
|
||||
// font-size: $default-font-size;
|
||||
// &:focus {
|
||||
// box-shadow: none;
|
||||
// }
|
||||
// height: 29px;
|
||||
// padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.waiting-icon {
|
||||
position: relative;
|
||||
top:1px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.form-button {
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
background-color: $status-editor-footer-background;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($status-editor-footer-background, 20%);
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
outline: inherit;
|
||||
&:focus {
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
$comrade_yellow: #ffcc00;
|
||||
$comrade_red: #a50000;
|
||||
.comrade {
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
margin-left: 5px;
|
||||
color: $comrade_yellow;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $comrade_yellow;
|
||||
}
|
||||
|
||||
&__button {
|
||||
background: $comrade_yellow;
|
||||
border-color: $comrade_yellow;
|
||||
color: $comrade_red;
|
||||
}
|
||||
|
||||
&__video {
|
||||
width: 300px;
|
||||
padding-top: 20px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&__input {
|
||||
color: $comrade_red;
|
||||
border-color: $comrade_yellow;
|
||||
background: $comrade_yellow;
|
||||
}
|
||||
|
||||
&__background {
|
||||
transition: all 3s;
|
||||
background-image: url("assets/img/juche-background.jpg");
|
||||
background-color: $comrade_red;
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.faq {
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
& a {
|
||||
color: #ffcc00;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: #ffe88a;
|
||||
}
|
||||
}
|
||||
|
||||
&__warning {
|
||||
color: #ffdc52;
|
||||
}
|
||||
}
|
|
@ -6,16 +6,33 @@ import { RegisteredAppsStateModel, AppInfo, AddRegisteredApp } from '../../../st
|
|||
import { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
|
||||
import { AppData } from '../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-account',
|
||||
templateUrl: './add-new-account.component.html',
|
||||
styleUrls: ['./add-new-account.component.scss']
|
||||
})
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
@Input() mastodonFullHandle: string;
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||
private comradeList = ['juche.town'];
|
||||
|
||||
isComrade: boolean;
|
||||
isLoading: boolean;
|
||||
|
||||
private instance: string;
|
||||
@Input()
|
||||
set setInstance(value: string) {
|
||||
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
|
||||
this.checkComrad();
|
||||
this.checkInstanceMultiAccount(value);
|
||||
}
|
||||
get setInstance(): string {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly store: Store) { }
|
||||
|
@ -23,29 +40,83 @@ export class AddNewAccountComponent implements OnInit {
|
|||
ngOnInit() {
|
||||
}
|
||||
|
||||
checkComrad(): any {
|
||||
if (this.instance) {
|
||||
let cleanInstance = this.instance.replace('http://', '').replace('https://', '').toLowerCase();
|
||||
for (let b of this.comradeList) {
|
||||
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
|
||||
this.isComrade = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isComrade = false;
|
||||
}
|
||||
|
||||
isInstanceMultiAccount: boolean;
|
||||
isInstanceMultiAccountLoading: boolean;
|
||||
checkInstanceMultiAccount(value: string) {
|
||||
if(value) {
|
||||
const instances: string[] = this.toolsService.getAllAccounts().map(x => x.instance);
|
||||
if(instances && instances.indexOf(value) > -1){
|
||||
this.isInstanceMultiAccount = true;
|
||||
this.isInstanceMultiAccountLoading = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
this.isInstanceMultiAccount = false;
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
let fullHandle = this.mastodonFullHandle.split('@').filter(x => x != null && x !== '');
|
||||
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
|
||||
|
||||
const username = fullHandle[0];
|
||||
const instance = fullHandle[1];
|
||||
this.isLoading = true;
|
||||
|
||||
this.checkAndCreateApplication(instance)
|
||||
this.checkBlockList(this.instance);
|
||||
|
||||
this.checkAndCreateApplication(this.instance)
|
||||
.then((appData: AppData) => {
|
||||
this.redirectToInstanceAuthPage(username, instance, appData);
|
||||
this.redirectToInstanceAuthPage(this.instance, appData);
|
||||
})
|
||||
.then(x => {
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
}, 1000);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
} else if ((<Error>err).message === 'CORS'){
|
||||
this.notificationService.notify('Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
|
||||
this.notificationService.notifyHttpError(err, null);
|
||||
} else if ((<Error>err).message === 'CORS') {
|
||||
this.notificationService.notify(null, null, 'Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
|
||||
} else {
|
||||
this.notificationService.notify('Unkown error', true);
|
||||
this.notificationService.notify(null, null, 'Unkown error', true);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private checkBlockList(instance: string) {
|
||||
let cleanInstance = instance.replace('http://', '').replace('https://', '').toLowerCase();
|
||||
for (let b of this.blockList) {
|
||||
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
|
||||
let content = '<div style="width:100%; height:100%; background-color: black;"><iframe style="pointer-events: none;" width="100%" height="100%" src="https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0&autoplay=1&showinfo=0&controls=0" allow="autoplay; fullscreen"></div>';
|
||||
|
||||
document.open();
|
||||
document.write(content);
|
||||
document.close();
|
||||
throw Error('Oh Noz!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkAndCreateApplication(instance: string): Promise<AppData> {
|
||||
const alreadyRegisteredApps = this.getAllSavedApps();
|
||||
const instanceApps = alreadyRegisteredApps.filter(x => x.instance === instance);
|
||||
|
@ -53,11 +124,21 @@ export class AddNewAccountComponent implements OnInit {
|
|||
if (instanceApps.length !== 0) {
|
||||
return Promise.resolve(instanceApps[0].app);
|
||||
} else {
|
||||
const redirect_uri = this.getLocalHostname() + '/register';
|
||||
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://github.com/NicolasConstant/sengi')
|
||||
let redirect_uri = this.getLocalHostname();
|
||||
|
||||
// let userAgent = navigator.userAgent.toLowerCase();
|
||||
// console.log(`userAgent ${userAgent}`);
|
||||
|
||||
// if (userAgent.includes(' electron/')) {
|
||||
// redirect_uri += '/register';
|
||||
// }
|
||||
|
||||
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://nicolasconstant.github.io/sengi/')
|
||||
.then((appData: AppData) => {
|
||||
return this.saveNewApp(instance, appData)
|
||||
.then(() => { return appData; });
|
||||
.then(() => {
|
||||
return new Promise<AppData>(resolve => setTimeout(resolve, 1000, appData));
|
||||
});
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
if (err.status === 0) {
|
||||
|
@ -74,8 +155,8 @@ export class AddNewAccountComponent implements OnInit {
|
|||
return snapshot.apps;
|
||||
}
|
||||
|
||||
private redirectToInstanceAuthPage(username: string, instance: string, app: AppData) {
|
||||
const appDataTemp = new CurrentAuthProcess(username, instance);
|
||||
private redirectToInstanceAuthPage(instance: string, app: AppData) {
|
||||
const appDataTemp = new CurrentAuthProcess(instance);
|
||||
localStorage.setItem('tempAuth', JSON.stringify(appDataTemp));
|
||||
|
||||
let instanceUrl = this.authService.getInstanceLoginUrl(instance, app.client_id, app.redirect_uri);
|
||||
|
@ -84,8 +165,12 @@ export class AddNewAccountComponent implements OnInit {
|
|||
}
|
||||
|
||||
private getLocalHostname(): string {
|
||||
let localHostname = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
|
||||
return localHostname;
|
||||
let href = window.location.href;
|
||||
if(href.includes('/home')){
|
||||
return href.split('/home')[0];
|
||||
} else {
|
||||
return location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
|
||||
}
|
||||
}
|
||||
|
||||
private saveNewApp(instance: string, app: AppData): Promise<any> {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<div class="panel">
|
||||
<h3 class="panel__title">new message</h3>
|
||||
|
||||
<app-create-status (onClose)="closeColumn()"></app-create-status>
|
||||
<div class=" new-message-body flexcroll">
|
||||
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
|
||||
[replyingUserHandle]="userHandle" [statusToEdit]="statusToEdit" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
</div>
|
||||
</div>
|
|
@ -1,6 +1,7 @@
|
|||
@import "variables";
|
||||
@import "panel";
|
||||
@import "buttons";
|
||||
@import "commons";
|
||||
|
||||
$btn-send-status-width: 60px;
|
||||
|
||||
|
@ -19,17 +20,18 @@ $btn-send-status-width: 60px;
|
|||
position: relative;
|
||||
top: -1px;
|
||||
left: 5px;
|
||||
// background-color: orange;
|
||||
// border-color: orange;
|
||||
// color: black;
|
||||
font-weight: 500;
|
||||
|
||||
// &:hover {
|
||||
|
||||
// }
|
||||
|
||||
// &:focus {
|
||||
// border-color: darkblue;
|
||||
// }
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.new-message-body {
|
||||
overflow: auto;
|
||||
height: calc(100% - 30px);
|
||||
padding-right: 5px;
|
||||
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-status',
|
||||
|
@ -8,10 +9,16 @@ import { NavigationService } from '../../../services/navigation.service';
|
|||
styleUrls: ['./add-new-status.component.scss']
|
||||
})
|
||||
export class AddNewStatusComponent implements OnInit {
|
||||
constructor(
|
||||
private readonly navigationService: NavigationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
@Input() isDirectMention: boolean;
|
||||
@Input() userHandle: string;
|
||||
@Input() redraftedStatus: StatusWrapper;
|
||||
@Input() statusToEdit: StatusWrapper;
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
closeColumn() {
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
<div class="floating-column">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive" (closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
<div class="floating-column__inner">
|
||||
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
(closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<div class="floating-column__inner--left">
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon class="close-button__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'" [isDirectMention]="isDirectMention"
|
||||
[userHandle]="userHandle"
|
||||
[redraftedStatus]="redraftedStatus"
|
||||
[statusToEdit]="statusToEdit"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)">
|
||||
</app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
<app-scheduled-statuses *ngIf="openPanel === 'scheduledStatuses'"></app-scheduled-statuses>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'"
|
||||
[account]="userAccountUsed"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
</div>
|
|
@ -1,11 +1,8 @@
|
|||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "panel";
|
||||
|
||||
|
||||
.floating-column {
|
||||
width: calc(100%);
|
||||
width: $floating-column-size;
|
||||
|
||||
.floating-column {
|
||||
background-color: $color-secondary;
|
||||
overflow: hidden;
|
||||
z-index: 200;
|
||||
|
@ -16,45 +13,36 @@
|
|||
|
||||
white-space: normal;
|
||||
|
||||
// &__header {
|
||||
|
||||
// }
|
||||
}
|
||||
&__inner {
|
||||
position: relative;
|
||||
width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
|
||||
.stream-overlay {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
width: $floating-column-size;
|
||||
height: calc(100%);
|
||||
margin: 0 0 0 $stream-column-separator;
|
||||
overflow: hidden;
|
||||
|
||||
&--left {
|
||||
width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
// outline: 1px dotted orange;
|
||||
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin: 10px 16px 0 0;
|
||||
margin: 5px 5px 0 0;
|
||||
|
||||
// display: inline-block;
|
||||
// background-color: $color-primary;
|
||||
// color: darken(white, 30);
|
||||
// border-radius: 999px;
|
||||
// width: 26px;
|
||||
// height: 26px;
|
||||
// text-align: center;
|
||||
// text-decoration: none;
|
||||
// padding: 1px;
|
||||
width: 40px;
|
||||
height: 34px;
|
||||
|
||||
// z-index: 9999;
|
||||
// float: right;
|
||||
// margin: 10px;
|
||||
|
||||
// transition: all .2s;
|
||||
|
||||
// &:hover {
|
||||
// background-color: lighten($color-primary, 20);
|
||||
// color: white;
|
||||
// // transform: scale(1.2);
|
||||
// }
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
left: 17px;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { NavigationService, LeftPanelType } from '../../services/navigation.service';
|
||||
import { NavigationService, LeftPanelType, OpenLeftPanelEvent, LeftPanelAction } from '../../services/navigation.service';
|
||||
import { AccountWrapper } from '../../models/account.models';
|
||||
import { OpenThreadEvent } from '../../services/tools.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-floating-column',
|
||||
|
@ -12,7 +13,7 @@ import { Subscription } from 'rxjs';
|
|||
styleUrls: ['./floating-column.component.scss']
|
||||
})
|
||||
export class FloatingColumnComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
||||
faTimes = faTimes;
|
||||
overlayActive: boolean;
|
||||
overlayAccountToBrowse: string;
|
||||
|
@ -21,6 +22,11 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
|
||||
userAccountUsed: AccountWrapper;
|
||||
|
||||
isDirectMention: boolean;
|
||||
userHandle: string;
|
||||
redraftedStatus: StatusWrapper;
|
||||
statusToEdit: StatusWrapper;
|
||||
|
||||
openPanel: string = '';
|
||||
|
||||
private activatedPanelSub: Subscription;
|
||||
|
@ -28,9 +34,11 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
constructor(private readonly navigationService: NavigationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedPanelSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
|
||||
this.activatedPanelSub = this.navigationService.activatedPanelSubject.subscribe((event: OpenLeftPanelEvent) => {
|
||||
this.isDirectMention = false;
|
||||
this.userHandle = null;
|
||||
this.overlayActive = false;
|
||||
switch (type) {
|
||||
switch (event.type) {
|
||||
case LeftPanelType.Closed:
|
||||
this.openPanel = '';
|
||||
break;
|
||||
|
@ -42,9 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
break;
|
||||
case LeftPanelType.CreateNewStatus:
|
||||
if (this.openPanel === 'createNewStatus') {
|
||||
case LeftPanelType.EditStatus:
|
||||
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.isDirectMention = event.action === LeftPanelAction.DM;
|
||||
this.userHandle = event.userHandle;
|
||||
|
||||
if(event.type === LeftPanelType.CreateNewStatus){
|
||||
this.redraftedStatus = event.status;
|
||||
this.statusToEdit = null;
|
||||
} else {
|
||||
this.redraftedStatus = null;
|
||||
this.statusToEdit = event.status;
|
||||
}
|
||||
|
||||
this.openPanel = 'createNewStatus';
|
||||
}
|
||||
break;
|
||||
|
@ -75,6 +95,13 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
this.openPanel = 'settings';
|
||||
}
|
||||
break;
|
||||
case LeftPanelType.ScheduledStatuses:
|
||||
if (this.openPanel === 'scheduledStatuses') {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.openPanel = 'scheduledStatuses';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.openPanel = '';
|
||||
}
|
||||
|
@ -82,7 +109,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.activatedPanelSub) {
|
||||
if (this.activatedPanelSub) {
|
||||
this.activatedPanelSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@import "variables";
|
||||
|
||||
.stream-toots {
|
||||
background-color: $column-background;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BookmarksComponent } from './bookmarks.component';
|
||||
|
||||
xdescribe('BookmarksComponent', () => {
|
||||
let component: BookmarksComponent;
|
||||
let fixture: ComponentFixture<BookmarksComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BookmarksComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BookmarksComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
|
||||
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { BookmarkResult } from '../../../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { Status } from '../../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { TimelineBase } from '../../../../components/common/timeline-base';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookmarks',
|
||||
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
|
||||
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './bookmarks.component.scss']
|
||||
})
|
||||
export class BookmarksComponent extends TimelineBase {
|
||||
private maxId: string;
|
||||
private _accountWrapper: AccountWrapper;
|
||||
|
||||
@Input('account')
|
||||
set accountWrapper(acc: AccountWrapper) {
|
||||
this._accountWrapper = acc;
|
||||
this.account = acc.info;
|
||||
this.getBookmarks();
|
||||
}
|
||||
get accountWrapper(): AccountWrapper {
|
||||
return this._accountWrapper;
|
||||
}
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
constructor(
|
||||
protected readonly toolsService: ToolsService,
|
||||
protected readonly notificationService: NotificationService,
|
||||
protected readonly mastodonService: MastodonWrapperService) {
|
||||
|
||||
super(toolsService, notificationService, mastodonService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.isLoading = true;
|
||||
this.statuses.length = 0;
|
||||
this.maxReached = false;
|
||||
this.lastCallReachedMax = false;
|
||||
this.maxId = null;
|
||||
}
|
||||
|
||||
private getBookmarks() {
|
||||
this.reset();
|
||||
|
||||
this.mastodonService.getBookmarks(this.account)
|
||||
.then((result: BookmarkResult) => {
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if(!this.maxId){
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
for (const s of result.bookmarked) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s);
|
||||
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
protected getNextStatuses(): Promise<Status[]> {
|
||||
if(this.lastCallReachedMax) return Promise.resolve([]);
|
||||
|
||||
return this.mastodonService.getBookmarks(this.account, this.maxId)
|
||||
.then((result: BookmarkResult) => {
|
||||
const statuses = result.bookmarked;
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if(!this.maxId){
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
return statuses;
|
||||
});
|
||||
}
|
||||
|
||||
protected scrolledToTop() {}
|
||||
|
||||
protected statusProcessOnGoToTop(){}
|
||||
}
|