Compare commits
558 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 |
|
@ -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,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
|
||||
```
|
||||
|
||||
|
|
@ -12,8 +12,8 @@ FROM alpine:latest
|
|||
RUN apk add --update --no-cache lighttpd
|
||||
|
||||
ADD lighttpd.conf /etc/lighttpd/lighttpd.conf
|
||||
COPY --from=build /build/dist /app/sengi
|
||||
COPY --from=build /build/assets/docker_init /app
|
||||
COPY --from=build /build/dist /app
|
||||
COPY --from=build /build/assets/docker_init /app/start
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
|
|
60
README.md
|
@ -7,50 +7,66 @@ Sengi is a **Mastodon** and **Pleroma** desktop focused client. It takes inspira
|
|||
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 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 already supporting all the basics functionalities, but many minors enhancements are still needed before a 1.0.0 release.
|
||||
The first major stable release has been published (1.0.0), the project is open to external contributions.
|
||||
|
||||
## Screens
|
||||
|
||||
![/docs/images/presentation_small.gif](/docs/images/presentation_small.gif)
|
||||
![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
|
||||
|
||||
## Credits
|
||||
|
||||
See [credits](CREDITS.md)
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
|
|
272
angular.json
|
@ -1,145 +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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -15,11 +15,11 @@
|
|||
<div class="launcher-wrapper">
|
||||
<div class="launcher">
|
||||
<a href="#" class="button" title="launch sengi in popup"
|
||||
onClick="window.open('/sengi/'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;">
|
||||
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="/sengi/" class="button" title="launch sengi">
|
||||
<a href="/../" class="button" title="launch sengi">
|
||||
<span class="download-button__web--label">Open Sengi</span>
|
||||
</a><br />
|
||||
</div>
|
||||
|
|
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 |
|
@ -201,6 +201,17 @@ body {
|
|||
.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; }
|
||||
|
|
|
@ -71,6 +71,24 @@
|
|||
width: 100%;
|
||||
height: 492;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: none;
|
||||
border: 0px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: black;
|
||||
|
||||
&:visited, &:focus {
|
||||
color: black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +102,7 @@
|
|||
display: block;
|
||||
margin: auto;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
font-size: 2rem;
|
||||
|
||||
text-align: center;
|
||||
padding: 7rem 5rem 0 5rem;
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 701 B |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 1.6 MiB 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 |
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 286 KiB |
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 287 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 7.2 KiB |
|
@ -28,13 +28,13 @@
|
|||
|
||||
<div class="header__download-box--description">
|
||||
A FLOSS multi-account Mastodon and Pleroma desktop client<br />
|
||||
Now available in Beta <span id="sengi-version"></span> <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>
|
||||
<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;"
|
||||
|
@ -43,7 +43,7 @@
|
|||
<br />
|
||||
<br />
|
||||
|
||||
<h4 class="header__download-box--subtitle">Or download the desktop client:</h4>
|
||||
<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>
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
</div>
|
||||
<div>
|
||||
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi/releases/"
|
||||
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi-electron/releases/"
|
||||
title="browse previous releases">browse previous releases</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -159,8 +159,9 @@
|
|||
<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 or was
|
||||
cross-posted (limited to local TL).
|
||||
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">
|
||||
|
@ -214,6 +215,12 @@
|
|||
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,
|
||||
|
@ -241,6 +248,9 @@
|
|||
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';
|
||||
|
||||
|
@ -248,12 +258,15 @@
|
|||
downloadButtonsNojs.style.display = 'none';
|
||||
|
||||
var sengiVersion = document.getElementById('sengi-version');
|
||||
sengiVersion.textContent = `(${version})`;
|
||||
sengiVersion.textContent = `Current version: v${version}`;
|
||||
|
||||
document.getElementById('windows').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-win.exe`;
|
||||
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-mac.dmg`;
|
||||
document.getElementById('deb').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-linux.deb`;
|
||||
document.getElementById('appimage').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-linux.AppImage`;
|
||||
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();
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
"author": "Nicolas Constant",
|
||||
"license": "WTFPL",
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.13.0",
|
||||
"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": {}
|
||||
}
|
||||
|
|
|
@ -11,4 +11,4 @@ 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 = "/sengi/index.html"
|
||||
server.error-handler-404 = "/index.html"
|
234
main-electron.js
|
@ -1,234 +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
|
||||
});
|
||||
|
||||
win.setAutoHideMenuBar(true);
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
var server = http.createServer(requestHandler).listen(9527);
|
||||
const sengiUrl = "http://localhost:9527";
|
||||
win.loadURL(sengiUrl);
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{
|
||||
label: "Return on Sengi",
|
||||
click() {
|
||||
win.loadURL(sengiUrl);
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "forcereload" },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
{ type: "separator" },
|
||||
{ role: "close" },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
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);
|
||||
|
||||
// Check if we are on a MAC
|
||||
if (process.platform === "darwin") {
|
||||
// Create our menu entries so that we can use MAC shortcuts
|
||||
Menu.setApplicationMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Sengi",
|
||||
submenu: [
|
||||
{ role: "close" },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// label: "File",
|
||||
// submenu: [
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "pasteandmatchstyle" },
|
||||
{ role: "delete" },
|
||||
{ role: "selectall" }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// label: "Format",
|
||||
// submenu: [
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{
|
||||
label: "Return on Sengi",
|
||||
click() {
|
||||
win.loadURL(sengiUrl);
|
||||
}
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "forcereload" },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// label: "Window",
|
||||
// submenu: [
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
role: "Help",
|
||||
submenu: [
|
||||
{ role: "toggledevtools" },
|
||||
{
|
||||
label: "Open GitHub project",
|
||||
click() {
|
||||
require("electron").shell.openExternal(
|
||||
"https://github.com/NicolasConstant/sengi"
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (win) {
|
||||
if (win.isMinimized()) win.restore()
|
||||
win.focus()
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
24
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "0.19.3",
|
||||
"version": "1.7.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
|
@ -12,7 +12,7 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/NicolasConstant/sengi.git"
|
||||
},
|
||||
"scripts": {
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start-mem": "node --max_old_space_size=5048 ./node_modules/@angular/cli/bin/ng serve",
|
||||
|
@ -21,31 +21,32 @@
|
|||
"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 && electron-builder --publish onTagOrDraft"
|
||||
"dist": "npm run build"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.2.7",
|
||||
"@angular/cdk": "^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",
|
||||
"angular2-hotkeys": "^2.1.5",
|
||||
"@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",
|
||||
"emojione": "~4.5.0",
|
||||
|
@ -66,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",
|
||||
|
@ -89,6 +88,7 @@
|
|||
"productName": "Sengi",
|
||||
"appId": "org.sengi.desktop",
|
||||
"artifactName": "${productName}-${version}-${os}.${ext}",
|
||||
"npmRebuild": false,
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
|
|
|
@ -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 { 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;
|
||||
|
@ -47,6 +127,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ 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';
|
||||
|
@ -23,7 +23,7 @@ import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.c
|
|||
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";
|
||||
|
@ -79,13 +79,24 @@ import { ScheduledStatusesComponent } from './components/floating-column/schedul
|
|||
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({
|
||||
|
@ -96,7 +107,7 @@ const routes: Routes = [
|
|||
StreamComponent,
|
||||
StreamsSelectionFooterComponent,
|
||||
StatusComponent,
|
||||
RegisterNewAccountComponent,
|
||||
// RegisterNewAccountComponent,
|
||||
AccountIconComponent,
|
||||
FloatingColumnComponent,
|
||||
ManageAccountComponent,
|
||||
|
@ -140,7 +151,17 @@ const routes: Routes = [
|
|||
ScheduledStatusesComponent,
|
||||
ScheduledStatusComponent,
|
||||
StreamNotificationsComponent,
|
||||
NotificationComponent
|
||||
NotificationComponent,
|
||||
BookmarksComponent,
|
||||
AttachementImageComponent,
|
||||
EnsureHttpsPipe,
|
||||
UserFollowsComponent,
|
||||
AccountComponent,
|
||||
TutorialEnhancedComponent,
|
||||
NotificationsTutorialComponent,
|
||||
LabelsTutorialComponent,
|
||||
ThankyouTutorialComponent,
|
||||
StatusTranslateComponent
|
||||
],
|
||||
entryComponents: [
|
||||
EmojiPickerComponent
|
||||
|
@ -154,9 +175,11 @@ const routes: Routes = [
|
|||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PickerModule,
|
||||
OwlDateTimeModule,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule,
|
||||
OverlayModule,
|
||||
DragDropModule,
|
||||
// NgxElectronModule,
|
||||
RouterModule.forRoot(routes),
|
||||
|
||||
NgxsModule.forRoot([
|
||||
|
@ -165,9 +188,11 @@ const routes: Routes = [
|
|||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
//], { developmentMode: !environment.production }),
|
||||
NgxsStoragePluginModule.forRoot(),
|
||||
ContextMenuModule.forRoot(),
|
||||
HotkeyModule.forRoot()
|
||||
HotkeyModule.forRoot(),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
],
|
||||
providers: [AuthService, NavigationService, NotificationService, MastodonWrapperService, MastodonService, StreamingService],
|
||||
bootstrap: [AppComponent],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,23 @@
|
|||
<form class="status-editor" (ngSubmit)="onSubmit()">
|
||||
<input [(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" />
|
||||
<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()" />
|
||||
|
||||
<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-editor__content"
|
||||
rows="5" required title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
<a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
|
||||
{{ selectedLanguage.iso639 }}
|
||||
</a>
|
||||
|
||||
<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>
|
||||
|
||||
|
@ -21,19 +30,21 @@
|
|||
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
|
||||
</app-autosuggest>
|
||||
|
||||
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive"></app-poll-editor>
|
||||
<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">REPLY!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
|
||||
<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">POST!</span>
|
||||
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
|
||||
<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">
|
||||
|
@ -64,6 +75,10 @@
|
|||
<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')">
|
||||
|
@ -79,5 +94,12 @@
|
|||
<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>
|
||||
|
|
|
@ -70,6 +70,32 @@ $counter-width: 90px;
|
|||
}
|
||||
}
|
||||
|
||||
&__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;
|
||||
|
@ -154,6 +180,9 @@ $counter-width: 90px;
|
|||
}
|
||||
|
||||
& span {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -204,6 +233,20 @@ $counter-width: 90px;
|
|||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.language-warning {
|
||||
padding: 5px 10px;
|
||||
color: orange;
|
||||
|
||||
&__link {
|
||||
text-decoration: underline;
|
||||
color: #f0d124;
|
||||
|
||||
&:hover {
|
||||
color: #d18800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import '~@angular/cdk/overlay-prebuilt.css';
|
||||
// ::ng-deep .cdk-overlay-backdrop {
|
||||
// // width: 100%;
|
||||
|
|
|
@ -15,7 +15,7 @@ import { NavigationService } from '../../services/navigation.service';
|
|||
import { NotificationService } from '../../services/notification.service';
|
||||
import { MastodonService } from '../../services/mastodon.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
import { SettingsState } from '../../states/settings.state';
|
||||
|
||||
describe('CreateStatusComponent', () => {
|
||||
let component: CreateStatusComponent;
|
||||
|
@ -33,7 +33,8 @@ describe('CreateStatusComponent', () => {
|
|||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
],
|
||||
providers: [NavigationService, NotificationService, MastodonService, AuthService],
|
||||
|
@ -164,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;
|
||||
|
@ -195,4 +231,107 @@ describe('CreateStatusComponent', () => {
|
|||
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`);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild, ViewContainerRef, ComponentRef, HostListener } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { UP_ARROW, DOWN_ARROW, ENTER, ESCAPE } from '@angular/cdk/keycodes';
|
||||
|
@ -9,20 +11,23 @@ import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
|
|||
|
||||
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
|
||||
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
|
||||
import { Status, Attachment } from '../../services/models/mastodon.interfaces';
|
||||
import { Status, Attachment, Poll } from '../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
import { AccountInfo } from '../../states/accounts.state';
|
||||
import { InstancesInfoService } from '../../services/instances-info.service';
|
||||
import { MediaService } from '../../services/media.service';
|
||||
import { MediaService, MediaWrapper } from '../../services/media.service';
|
||||
import { AutosuggestSelection, AutosuggestUserActionEnum } from './autosuggest/autosuggest.component';
|
||||
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
|
||||
import { EmojiPickerComponent } from './emoji-picker/emoji-picker.component';
|
||||
import { PollEditorComponent } from './poll-editor/poll-editor.component';
|
||||
import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.component';
|
||||
import { ScheduledStatusService } from '../../services/scheduled-status.service';
|
||||
import { StatusesStateService } from '../../services/statuses-state.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { ILanguage } from '../../states/settings.state';
|
||||
import { LeftPanelType, NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-status',
|
||||
|
@ -40,6 +45,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
faClock = faClock;
|
||||
|
||||
autoSuggestUserActionsStream = new EventEmitter<AutosuggestUserActionEnum>();
|
||||
private isRedrafting: boolean;
|
||||
|
||||
private _title: string;
|
||||
set title(value: string) {
|
||||
|
@ -53,10 +59,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private _status: string = '';
|
||||
@Input('status')
|
||||
set status(value: string) {
|
||||
if (this.isRedrafting) {
|
||||
this.statusStateService.setStatusContent(value, null);
|
||||
} else {
|
||||
this.statusStateService.setStatusContent(value, this.statusReplyingToWrapper);
|
||||
}
|
||||
this.countStatusChar(value);
|
||||
this.detectAutosuggestion(value);
|
||||
this._status = value;
|
||||
|
||||
this.languageService.autoDetectLang(value);
|
||||
|
||||
setTimeout(() => {
|
||||
this.autoGrow();
|
||||
}, 0);
|
||||
|
@ -65,14 +78,62 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return this._status;
|
||||
}
|
||||
|
||||
private trim(s, mask) {
|
||||
while (~mask.indexOf(s[0])) {
|
||||
s = s.slice(1);
|
||||
}
|
||||
while (~mask.indexOf(s[s.length - 1])) {
|
||||
s = s.slice(0, -1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@Input('statusToEdit')
|
||||
set statusToEdit(value: StatusWrapper) {
|
||||
if (value) {
|
||||
this.isEditing = true;
|
||||
this.editingStatusId = value.status.id;
|
||||
this.redraftedStatus = value;
|
||||
this.mediaService.loadMedia(value.status.media_attachments);
|
||||
}
|
||||
}
|
||||
|
||||
@Input('redraftedStatus')
|
||||
set redraftedStatus(value: StatusWrapper) {
|
||||
if (value) {
|
||||
this.isRedrafting = true;
|
||||
this.statusLoaded = false;
|
||||
|
||||
if (value.status && value.status.media_attachments) {
|
||||
for (const m of value.status.media_attachments) {
|
||||
this.mediaService.addExistingMedia(new MediaWrapper(m.id, null, m));
|
||||
}
|
||||
}
|
||||
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
let content = value.status.content;
|
||||
|
||||
content = this.tranformHtmlRepliesToReplies(content);
|
||||
|
||||
while (content.includes('<p>') || content.includes('</p>') || content.includes('<br>') || content.includes('<br/>') || content.includes('<br />')) {
|
||||
content = content.replace('<p>', '').replace('</p>', newLine + newLine).replace('<br />', newLine).replace('<br/>', newLine).replace('<br>', newLine);
|
||||
}
|
||||
|
||||
content = this.trim(content, newLine);
|
||||
|
||||
let parser = new DOMParser();
|
||||
var dom = parser.parseFromString(value.status.content, 'text/html')
|
||||
var dom = parser.parseFromString(content, 'text/html')
|
||||
this.status = dom.body.textContent;
|
||||
|
||||
// this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
|
||||
|
||||
// Retrieve mentions
|
||||
for(let mention of value.status.mentions){
|
||||
if(this.status){
|
||||
this.status = this.status.replace(`@${mention.username}`, `@${mention.acct}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.setVisibilityFromStatus(value.status);
|
||||
this.title = value.status.spoiler_text;
|
||||
this.statusLoaded = true;
|
||||
|
@ -81,18 +142,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.isSending = true;
|
||||
this.mastodonService.getStatus(value.provider, value.status.in_reply_to_id)
|
||||
.then((status: Status) => {
|
||||
this.statusReplyingToWrapper = new StatusWrapper(status, value.provider);
|
||||
|
||||
const mentions = this.getMentions(this.statusReplyingToWrapper.status, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of mentions) {
|
||||
const name = `@${mention.split('@')[0]}`;
|
||||
if (this.status.includes(name)) {
|
||||
this.status = this.status.replace(name, `@${mention}`);
|
||||
} else {
|
||||
this.status = `@${mention} ` + this.status;
|
||||
}
|
||||
}
|
||||
|
||||
let cwResult = this.toolsService.checkContentWarning(status);
|
||||
this.statusReplyingToWrapper = new StatusWrapper(cwResult.status, value.provider, cwResult.applyCw, cwResult.hide);
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, value.provider);
|
||||
|
@ -101,9 +152,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.isSending = false;
|
||||
});
|
||||
}
|
||||
|
||||
if(value.status.poll){
|
||||
this.pollIsActive = true;
|
||||
this.oldPoll = value.status.poll;
|
||||
// setTimeout(() => {
|
||||
// if(this.pollEditor) this.pollEditor.loadPollParameters(value.status.poll);
|
||||
// }, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldPoll: Poll;
|
||||
|
||||
private maxCharLength: number;
|
||||
charCountLeft: number;
|
||||
postCounts: number = 1;
|
||||
|
@ -112,6 +173,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
autosuggestData: string = null;
|
||||
instanceSupportsPoll = true;
|
||||
instanceSupportsScheduling = true;
|
||||
isEditing: boolean;
|
||||
editingStatusId: string;
|
||||
configuredLanguages: ILanguage[] = [];
|
||||
selectedLanguage: ILanguage;
|
||||
private statusLoaded: boolean;
|
||||
private hasSuggestions: boolean;
|
||||
|
||||
|
@ -121,6 +186,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
@ViewChild('fileInput') fileInputElement: ElementRef;
|
||||
@ViewChild('footer') footerElement: ElementRef;
|
||||
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
|
||||
@ViewChild('langContextMenu') public langContextMenu: ContextMenuComponent;
|
||||
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
|
||||
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
|
||||
|
||||
|
@ -151,13 +217,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private statusReplyingTo: Status;
|
||||
|
||||
selectedPrivacy = 'Public';
|
||||
// privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
private selectedPrivacySetByRedraft = false;
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
private langSub: Subscription;
|
||||
private selectLangSub: Subscription;
|
||||
private selectedAccount: AccountInfo;
|
||||
|
||||
constructor(
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly statusStateService: StatusesStateService,
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly contextMenuService: ContextMenuService,
|
||||
private readonly store: Store,
|
||||
|
@ -167,11 +239,45 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private readonly instancesInfoService: InstancesInfoService,
|
||||
private readonly mediaService: MediaService,
|
||||
private readonly overlay: Overlay,
|
||||
public viewContainerRef: ViewContainerRef) {
|
||||
public viewContainerRef: ViewContainerRef,
|
||||
private readonly statusesStateService: StatusesStateService) {
|
||||
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
private initLanguages(){
|
||||
this.configuredLanguages = this.languageService.getConfiguredLanguages();
|
||||
this.selectedLanguage = this.languageService.getSelectedLanguage();
|
||||
this.langSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||
this.configuredLanguages = l;
|
||||
// if(this.configuredLanguages.length > 0
|
||||
// && this.selectedLanguage
|
||||
// && this.configuredLanguages.findIndex(x => x.iso639 === this.selectedLanguage.iso639)){
|
||||
// this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
|
||||
// }
|
||||
});
|
||||
this.selectLangSub = this.languageService.selectedLanguageChanged.subscribe(l => {
|
||||
this.selectedLanguage = l;
|
||||
});
|
||||
if(!this.selectedLanguage && this.configuredLanguages.length > 0){
|
||||
this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
|
||||
}
|
||||
}
|
||||
|
||||
setLanguage(lang: ILanguage): boolean {
|
||||
if(lang){
|
||||
this.languageService.setSelectedLanguage(lang);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initLanguages();
|
||||
|
||||
if (!this.isRedrafting) {
|
||||
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
||||
}
|
||||
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
this.accountChanged(accounts);
|
||||
});
|
||||
|
@ -184,9 +290,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.statusReplyingTo = this.statusReplyingToWrapper.status;
|
||||
}
|
||||
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
// let state = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
||||
// if (state && state !== '') {
|
||||
// this.status = state;
|
||||
// } else {
|
||||
if (!this.status || this.status === '') {
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
}
|
||||
}
|
||||
|
||||
this.setVisibilityFromStatus(this.statusReplyingTo);
|
||||
|
@ -203,7 +315,30 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.isRedrafting) {
|
||||
this.statusStateService.resetStatusContent(null);
|
||||
}
|
||||
|
||||
this.accountSub.unsubscribe();
|
||||
this.langSub.unsubscribe();
|
||||
this.selectLangSub.unsubscribe();
|
||||
}
|
||||
|
||||
onNavigateToSettings(): boolean {
|
||||
this.navigationService.openPanel(LeftPanelType.Settings);
|
||||
return false;
|
||||
}
|
||||
|
||||
onPaste(e: any) {
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
let blobs: File[] = [];
|
||||
for (const item of items) {
|
||||
if (item.type.indexOf('image') === 0) {
|
||||
let blob = item.getAsFile();
|
||||
blobs.push(blob);
|
||||
}
|
||||
}
|
||||
this.handleFileInput(blobs);
|
||||
}
|
||||
|
||||
changePrivacy(value: string): boolean {
|
||||
|
@ -224,8 +359,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
private detectAutosuggestion(status: string) {
|
||||
if (!this.statusLoaded) return;
|
||||
|
||||
if(!status.includes('@') && !status.includes('#')){
|
||||
|
||||
if (!status.includes('@') && !status.includes('#')) {
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
return;
|
||||
|
@ -250,7 +385,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
const word = this.getWordByPos(currentSection, caretPosition - offset);
|
||||
if (!lastCharIsSpace && word && word.length > 0 && (word.startsWith('@') || word.startsWith('#'))) {
|
||||
if (!lastCharIsSpace && word && word.length > 1 && (word.startsWith('@') || word.startsWith('#'))) {
|
||||
this.autosuggestData = word;
|
||||
return;
|
||||
}
|
||||
|
@ -314,7 +449,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
if (accounts && accounts.length > 0) {
|
||||
this.selectedAccount = accounts.filter(x => x.isSelected)[0];
|
||||
|
||||
const settings = this.toolsService.getAccountSettings(this.selectedAccount);
|
||||
const settings = this.settingsService.getAccountSettings(this.selectedAccount);
|
||||
if (settings.customStatusCharLengthEnabled) {
|
||||
this.maxCharLength = settings.customStatusCharLength;
|
||||
this.countStatusChar(this.status);
|
||||
|
@ -373,9 +508,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.setVisibility(VisibilityEnum.Direct);
|
||||
break;
|
||||
}
|
||||
|
||||
this.selectedPrivacySetByRedraft = true;
|
||||
}
|
||||
|
||||
private setVisibility(defaultPrivacy: VisibilityEnum) {
|
||||
if (this.selectedPrivacySetByRedraft) return;
|
||||
|
||||
switch (defaultPrivacy) {
|
||||
case VisibilityEnum.Public:
|
||||
this.selectedPrivacy = 'Public';
|
||||
|
@ -414,8 +553,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
const currentStatus = parseStatus[parseStatus.length - 1];
|
||||
const statusExtraChars = this.getMentionExtraChars(status);
|
||||
const linksExtraChars = this.getLinksExtraChars(status);
|
||||
const statusExtraChars = this.getMentionExtraChars(currentStatus);
|
||||
const linksExtraChars = this.getLinksExtraChars(currentStatus);
|
||||
|
||||
const statusLength = [...currentStatus].length - statusExtraChars - linksExtraChars;
|
||||
this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
|
||||
|
@ -430,8 +569,20 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return cwLength;
|
||||
}
|
||||
|
||||
private getMentions(status: Status, providerInfo: AccountInfo): string[] {
|
||||
const mentions = [status.account.acct, ...status.mentions.map(x => x.acct)];
|
||||
private getMentions(status: Status): string[] {
|
||||
let acct = status.account.acct;
|
||||
if (!acct.includes('@')) {
|
||||
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
|
||||
}
|
||||
|
||||
const mentions = [acct];
|
||||
status.mentions.forEach(m => {
|
||||
let mentionAcct = m.acct;
|
||||
if (!mentionAcct.includes('@')) {
|
||||
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
|
||||
}
|
||||
mentions.push(mentionAcct);
|
||||
});
|
||||
|
||||
let uniqueMentions = [];
|
||||
for (let mention of mentions) {
|
||||
|
@ -440,18 +591,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
let globalUniqueMentions = [];
|
||||
for (let mention of uniqueMentions) {
|
||||
if (!mention.includes('@')) {
|
||||
mention += `@${providerInfo.instance}`;
|
||||
}
|
||||
globalUniqueMentions.push(mention);
|
||||
}
|
||||
|
||||
const selectedUser = this.toolsService.getSelectedAccounts()[0];
|
||||
globalUniqueMentions = globalUniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
|
||||
uniqueMentions = uniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
|
||||
|
||||
return globalUniqueMentions;
|
||||
return uniqueMentions;
|
||||
}
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
|
@ -459,7 +602,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
async onSubmit(): Promise<boolean> {
|
||||
if (this.isSending || this.mentionTooFarAwayError) return false;
|
||||
|
||||
this.isSending = true;
|
||||
|
@ -480,9 +623,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
break;
|
||||
}
|
||||
|
||||
const mediaAttachments = this.mediaService.mediaSubject.value.map(x => x.attachment);
|
||||
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
const mediaAttachments = (await this.mediaService.retrieveUpToDateMedia(acc)).map(x => x.attachment);
|
||||
|
||||
let usableStatus: Promise<Status>;
|
||||
if (this.statusReplyingToWrapper) {
|
||||
usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
|
||||
|
@ -506,7 +650,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime);
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime, this.editingStatusId);
|
||||
})
|
||||
.then((res: Status) => {
|
||||
this.title = '';
|
||||
|
@ -516,6 +660,12 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
if (this.scheduleIsActive) {
|
||||
this.scheduledStatusService.statusAdded(acc);
|
||||
}
|
||||
|
||||
if (this.isRedrafting) {
|
||||
this.statusStateService.resetStatusContent(null);
|
||||
} else {
|
||||
this.statusStateService.resetStatusContent(this.statusReplyingToWrapper);
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
|
@ -527,7 +677,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise<Status> {
|
||||
private currentLang(): string {
|
||||
if(this.selectedLanguage){
|
||||
return this.selectedLanguage.iso639;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string, editingStatusId: string): Promise<Status> {
|
||||
let parsedStatus = this.parseStatus(status);
|
||||
let resultPromise = Promise.resolve(previousStatus);
|
||||
|
||||
|
@ -541,18 +699,41 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (i === 0) {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt)
|
||||
let postPromise: Promise<Status>;
|
||||
|
||||
if (this.isEditing) {
|
||||
postPromise = this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, attachments, poll, scheduledAt, this.currentLang());
|
||||
} else {
|
||||
postPromise = this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt, this.currentLang());
|
||||
}
|
||||
|
||||
return postPromise
|
||||
.then((status: Status) => {
|
||||
this.mediaService.clearMedia();
|
||||
return status;
|
||||
});
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
|
||||
if (this.isEditing) {
|
||||
return this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.statusReplyingToWrapper) {
|
||||
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(status, account));
|
||||
let cwPolicy = this.toolsService.checkContentWarning(status);
|
||||
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(cwPolicy.status, account, cwPolicy.applyCw, cwPolicy.hide));
|
||||
}
|
||||
|
||||
return status;
|
||||
})
|
||||
.then((status: Status) => {
|
||||
if (this.isEditing) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(status);
|
||||
let statusWrapper = new StatusWrapper(status, account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
|
||||
this.statusesStateService.statusEditedStatusChanged(status.url, account.id, statusWrapper);
|
||||
}
|
||||
|
||||
return status;
|
||||
|
@ -564,6 +745,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
private parseStatus(status: string): string[] {
|
||||
let mentionExtraChars = this.getMentionExtraChars(status);
|
||||
let urlExtraChar = this.getLinksExtraChars(status);
|
||||
let trucatedStatus = `${status}`;
|
||||
let results = [];
|
||||
|
||||
|
@ -573,13 +755,24 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
aggregateMention += `${x} `;
|
||||
});
|
||||
|
||||
const currentMaxCharLength = this.maxCharLength + mentionExtraChars - this.getCwLength();
|
||||
const maxChars = currentMaxCharLength - 6;
|
||||
let currentMaxCharLength = this.maxCharLength + mentionExtraChars + urlExtraChar - this.getCwLength();
|
||||
let maxChars = currentMaxCharLength - 6;
|
||||
|
||||
while (trucatedStatus.length > currentMaxCharLength) {
|
||||
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
|
||||
|
||||
if (nextIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
results.push(trucatedStatus.substr(0, nextIndex) + ' (...)');
|
||||
trucatedStatus = aggregateMention + trucatedStatus.substr(nextIndex + 1);
|
||||
|
||||
// Refresh max
|
||||
let mentionExtraChars = this.getMentionExtraChars(trucatedStatus);
|
||||
let urlExtraChar = this.getLinksExtraChars(trucatedStatus);
|
||||
currentMaxCharLength = this.maxCharLength + mentionExtraChars + urlExtraChar - this.getCwLength();
|
||||
maxChars = currentMaxCharLength - 6;
|
||||
}
|
||||
results.push(trucatedStatus);
|
||||
return results;
|
||||
|
@ -587,7 +780,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
private getLinksExtraChars(status: string): number {
|
||||
let mentionExtraChars = 0;
|
||||
let links = status.split(' ').filter(x => x.startsWith('http://') || x.startsWith('https://'));
|
||||
let links = status.split(/\s+/).filter(x => x.startsWith('http://') || x.startsWith('https://'));
|
||||
for (let link of links) {
|
||||
if (link.length > 23) {
|
||||
mentionExtraChars += link.length - 23;
|
||||
|
@ -618,15 +811,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
suggestionSelected(selection: AutosuggestSelection) {
|
||||
if (this.status.includes(selection.pattern)) {
|
||||
this.status = this.replacePatternWithAutosuggest(this.status, selection.pattern, selection.autosuggest);
|
||||
|
||||
let transformedStatus = this.status;
|
||||
transformedStatus = transformedStatus.replace(new RegExp(` ${selection.pattern} `), ` ${selection.autosuggest} `).replace(' ', ' ');
|
||||
transformedStatus = transformedStatus.replace(new RegExp(`${selection.pattern} `), `${selection.autosuggest} `).replace(' ', ' ');
|
||||
transformedStatus = transformedStatus.replace(new RegExp(`${selection.pattern}$`), `${selection.autosuggest} `).replace(' ', ' ');
|
||||
this.status = transformedStatus;
|
||||
|
||||
let newCaretPosition = this.status.indexOf(`${selection.autosuggest} `) + selection.autosuggest.length + 1;
|
||||
if (newCaretPosition > this.status.length) newCaretPosition = this.status.length;
|
||||
let cleanStatus = this.status.replace(/\r?\n/g, ' ');
|
||||
let newCaretPosition = cleanStatus.indexOf(`${selection.autosuggest}`) + selection.autosuggest.length;
|
||||
if (newCaretPosition > cleanStatus.length) newCaretPosition = cleanStatus.length;
|
||||
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
|
@ -641,6 +830,57 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private replacePatternWithAutosuggest(status: string, pattern: string, autosuggest: string): string {
|
||||
status = status.replace(/ /g, ' ');
|
||||
|
||||
const newLine = String.fromCharCode(13, 10);
|
||||
// let statusPerLines = status.split(newLine);
|
||||
let statusPerLines = status.split(/\r?\n/);
|
||||
let statusPerLinesPerWords: string[][] = [];
|
||||
let regex = new RegExp(`^${pattern}$`, 'i');
|
||||
|
||||
statusPerLines.forEach(line => {
|
||||
let words = line.split(' ');
|
||||
|
||||
words = words.map(word => {
|
||||
return word.replace(regex, `${autosuggest}`);
|
||||
});
|
||||
|
||||
statusPerLinesPerWords.push(words);
|
||||
});
|
||||
|
||||
let result = '';
|
||||
let nberLines = statusPerLinesPerWords.length;
|
||||
let i = 0;
|
||||
|
||||
statusPerLinesPerWords.forEach(line => {
|
||||
i++;
|
||||
|
||||
let wordCount = line.length;
|
||||
let w = 0;
|
||||
line.forEach(word => {
|
||||
w++;
|
||||
result += `${word}`;
|
||||
|
||||
if (w < wordCount || i === nberLines) {
|
||||
result += ' ';
|
||||
}
|
||||
});
|
||||
if (i < nberLines) {
|
||||
result += newLine;
|
||||
}
|
||||
})
|
||||
|
||||
result = result.replace(' ', ' ');
|
||||
|
||||
let endRegex = new RegExp(`${autosuggest} $`, 'i');
|
||||
if (!result.match(endRegex)) {
|
||||
result = result.substring(0, result.length - 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
suggestionsChanged(hasSuggestions: boolean) {
|
||||
this.hasSuggestions = hasSuggestions;
|
||||
}
|
||||
|
@ -692,7 +932,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
if (isVisible) {
|
||||
setTimeout(() => {
|
||||
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
try {
|
||||
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
} catch (err) {
|
||||
this.footerElement.nativeElement.scrollIntoView({ behavior: 'instant', block: 'end', inline: 'start' });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
@ -715,6 +959,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
public onLangContextMenu($event: MouseEvent): void {
|
||||
this.contextMenuService.show.next({
|
||||
// Optional - if unspecified, all context menu components will open
|
||||
contextMenu: this.langContextMenu,
|
||||
event: $event,
|
||||
item: null
|
||||
});
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
//https://stackblitz.com/edit/overlay-demo
|
||||
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
|
||||
overlayRef: OverlayRef;
|
||||
|
@ -747,7 +1002,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.overlayRef = this.overlay.create(config);
|
||||
// this.overlayRef.backdropClick().subscribe(() => {
|
||||
// console.warn('wut?');
|
||||
// this.overlayRef.dispose();
|
||||
// });
|
||||
|
||||
|
@ -792,4 +1046,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.scheduleIsActive = !this.scheduleIsActive;
|
||||
return false;
|
||||
}
|
||||
|
||||
private tranformHtmlRepliesToReplies(data: string): string {
|
||||
const mastodonMentionRegex = /<span class="h-card"><a href="https:\/\/([a-zA-Z0-9.]{0,255})\/[a-zA-Z0-9_@/-]{0,255}" class="u-url mention">@<span>([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi;
|
||||
const pleromaMentionRegex = /<span class="h-card"><a data-user="[a-zA-Z0-9]{0,255}" class="u-url mention" href="https:\/\/([a-zA-Z0-9.]{0,255})\/[a-zA-Z0-9_@/-]{0,255}" rel="ugc">@<span>([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi;
|
||||
|
||||
while (data.match(mastodonMentionRegex)) {
|
||||
data = data.replace(mastodonMentionRegex, '@$2@$1');
|
||||
}
|
||||
|
||||
while (data.match(pleromaMentionRegex)) {
|
||||
data = data.replace(pleromaMentionRegex, '@$2@$1');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<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 && m.attachment.type !== 'audio'" class="media__loaded" title="{{m.file.name}}"
|
||||
<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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
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 { retry } from 'rxjs/operators';
|
||||
import { Poll } from '../../../services/models/mastodon.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-poll-editor',
|
||||
|
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
|
|||
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));
|
||||
|
@ -40,6 +42,12 @@ export class PollEditorComponent implements OnInit {
|
|||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['oldPoll']) {
|
||||
this.loadPollParameters(this.oldPoll);
|
||||
}
|
||||
}
|
||||
|
||||
private getEntryUuid(): number {
|
||||
this.entryUuid++;
|
||||
return this.entryUuid;
|
||||
|
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
removeElement(entry: PollEntry){
|
||||
removeElement(entry: PollEntry) {
|
||||
this.entries = this.entries.filter(x => x.id != entry.id);
|
||||
}
|
||||
|
||||
|
@ -69,6 +77,19 @@ export class PollEditorComponent implements OnInit {
|
|||
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 {
|
||||
|
|
|
@ -1,19 +1,42 @@
|
|||
<div class="panel" [class.comrade__background]="isComrade">
|
||||
<h3 class="panel__title" [class.comrade__text]="isComrade">Add new account</h3>
|
||||
|
||||
<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> account:</label>
|
||||
<input type="text" class="form-control form-control-sm form-color" [(ngModel)]="mastodonFullHandle" name="mastodonFullHandle" [class.comrade__input]="isComrade"
|
||||
placeholder="@nickname@mastodon.social" />
|
||||
<br />
|
||||
<button *ngIf="!isLoading" type="submit" class="btn btn-success btn-sm" [class.comrade__button]="isComrade">Submit</button>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</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 *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>
|
||||
<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,21 +1,74 @@
|
|||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "panel";
|
||||
|
||||
$button-size: 70px;
|
||||
|
||||
.panel {
|
||||
|
||||
padding-left: 0px;
|
||||
// padding-right: 0px;
|
||||
background-position: 0 100%;
|
||||
|
||||
&__content {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-color {
|
||||
background-color: $column-color;
|
||||
border-color: $button-border-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
.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;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
$comrade_yellow: #ffcc00;
|
||||
|
@ -56,4 +109,21 @@ $comrade_red: #a50000;
|
|||
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,33 +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 {
|
||||
export class AddNewAccountComponent implements OnInit {
|
||||
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||
private comradeList = ['juche.town'];
|
||||
|
||||
private username: string;
|
||||
private instance: string;
|
||||
isComrade: boolean;
|
||||
|
||||
isLoading: boolean;
|
||||
|
||||
private _mastodonFullHandle: string;
|
||||
private instance: string;
|
||||
@Input()
|
||||
set mastodonFullHandle(value: string) {
|
||||
this._mastodonFullHandle = value;
|
||||
set setInstance(value: string) {
|
||||
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
|
||||
this.checkComrad();
|
||||
this.checkInstanceMultiAccount(value);
|
||||
}
|
||||
get mastodonFullHandle(): string {
|
||||
return this._mastodonFullHandle;
|
||||
get setInstance(): string {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly store: Store) { }
|
||||
|
@ -41,11 +41,7 @@ export class AddNewAccountComponent implements OnInit {
|
|||
}
|
||||
|
||||
checkComrad(): any {
|
||||
let fullHandle = this.mastodonFullHandle.split('@').filter(x => x != null && x !== '');
|
||||
this.username = fullHandle[0];
|
||||
this.instance = fullHandle[1];
|
||||
|
||||
if (this.username && this.instance) {
|
||||
if (this.instance) {
|
||||
let cleanInstance = this.instance.replace('http://', '').replace('https://', '').toLowerCase();
|
||||
for (let b of this.comradeList) {
|
||||
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
|
||||
|
@ -58,13 +54,35 @@ export class AddNewAccountComponent implements OnInit {
|
|||
this.isComrade = false;
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
this.checkBlockList(this.instance);
|
||||
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;
|
||||
|
||||
this.isLoading = true;
|
||||
setTimeout(() => {
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
this.isInstanceMultiAccount = false;
|
||||
this.isInstanceMultiAccountLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.checkBlockList(this.instance);
|
||||
|
||||
this.checkAndCreateApplication(this.instance)
|
||||
.then((appData: AppData) => {
|
||||
this.redirectToInstanceAuthPage(this.username, this.instance, appData);
|
||||
this.redirectToInstanceAuthPage(this.instance, appData);
|
||||
})
|
||||
.then(x => {
|
||||
setTimeout(() => {
|
||||
|
@ -108,12 +126,12 @@ export class AddNewAccountComponent implements OnInit {
|
|||
} else {
|
||||
let redirect_uri = this.getLocalHostname();
|
||||
|
||||
let userAgent = navigator.userAgent.toLowerCase();
|
||||
console.log(`userAgent ${userAgent}`);
|
||||
// let userAgent = navigator.userAgent.toLowerCase();
|
||||
// console.log(`userAgent ${userAgent}`);
|
||||
|
||||
if (userAgent.includes(' electron/')) {
|
||||
redirect_uri += '/register';
|
||||
}
|
||||
// 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) => {
|
||||
|
@ -137,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);
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
<div class=" new-message-body flexcroll">
|
||||
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
|
||||
[replyingUserHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
|
||||
[replyingUserHandle]="userHandle" [statusToEdit]="statusToEdit" [redraftedStatus]="redraftedStatus"></app-create-status>
|
||||
</div>
|
||||
</div>
|
|
@ -13,6 +13,7 @@ export class AddNewStatusComponent implements OnInit {
|
|||
@Input() isDirectMention: boolean;
|
||||
@Input() userHandle: string;
|
||||
@Input() redraftedStatus: StatusWrapper;
|
||||
@Input() statusToEdit: StatusWrapper;
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
<div class="floating-column">
|
||||
<div class="floating-column__inner">
|
||||
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
(closeOverlay)="closeOverlay()"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseAccountData]="overlayAccountToBrowse"
|
||||
[browseHashtagData]="overlayHashtagToBrowse"
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
|
||||
<div class="floating-column__inner--left">
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
<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)"
|
||||
(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"></app-add-new-status>
|
||||
[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 *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>
|
||||
|
|
|
@ -29,9 +29,20 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
|
||||
width: 40px;
|
||||
height: 34px;
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
left: 17px;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
isDirectMention: boolean;
|
||||
userHandle: string;
|
||||
redraftedStatus: StatusWrapper;
|
||||
statusToEdit: StatusWrapper;
|
||||
|
||||
openPanel: string = '';
|
||||
|
||||
|
@ -49,12 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
break;
|
||||
case LeftPanelType.CreateNewStatus:
|
||||
case LeftPanelType.EditStatus:
|
||||
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
|
||||
this.closePanel();
|
||||
} else {
|
||||
this.isDirectMention = event.action === LeftPanelAction.DM;
|
||||
this.userHandle = event.userHandle;
|
||||
this.redraftedStatus = event.status;
|
||||
|
||||
if(event.type === LeftPanelType.CreateNewStatus){
|
||||
this.redraftedStatus = event.status;
|
||||
this.statusToEdit = null;
|
||||
} else {
|
||||
this.redraftedStatus = null;
|
||||
this.statusToEdit = event.status;
|
||||
}
|
||||
|
||||
this.openPanel = 'createNewStatus';
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -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(){}
|
||||
}
|
|
@ -2,19 +2,20 @@ import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef }
|
|||
import { faUserFriends } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { Conversation } from '../../../../services/models/mastodon.interfaces';
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { BrowseBase } from '../../../common/browse-base';
|
||||
|
||||
@Component({
|
||||
selector: 'app-direct-messages',
|
||||
templateUrl: './direct-messages.component.html',
|
||||
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './direct-messages.component.scss']
|
||||
})
|
||||
export class DirectMessagesComponent implements OnInit {
|
||||
export class DirectMessagesComponent extends BrowseBase {
|
||||
faUserFriends = faUserFriends;
|
||||
|
||||
conversations: ConversationWrapper[] = [];
|
||||
|
@ -23,9 +24,7 @@ export class DirectMessagesComponent implements OnInit {
|
|||
isThread = false;
|
||||
hasContentWarnings = false;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
private isProcessingInfiniteScroll: boolean;
|
||||
|
||||
private maxReached = false;
|
||||
private _account: AccountWrapper;
|
||||
|
@ -42,12 +41,18 @@ export class DirectMessagesComponent implements OnInit {
|
|||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
private readonly mastodonService: MastodonWrapperService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.isLoading = true;
|
||||
this.conversations.length = 0;
|
||||
|
@ -60,7 +65,8 @@ export class DirectMessagesComponent implements OnInit {
|
|||
this.mastodonService.getConversations(this.account.info)
|
||||
.then((conversations: Conversation[]) => {
|
||||
for (const c of conversations) {
|
||||
const wrapper = new ConversationWrapper(c, this.account.info, this.account.avatar);
|
||||
let cwPolicy = this.toolsService.checkContentWarning(c.last_status);
|
||||
const wrapper = new ConversationWrapper(c, this.account.info, this.account.avatar, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.conversations.push(wrapper);
|
||||
}
|
||||
})
|
||||
|
@ -76,17 +82,19 @@ export class DirectMessagesComponent implements OnInit {
|
|||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
|
||||
if (atBottom) {
|
||||
if (atBottom && !this.isProcessingInfiniteScroll) {
|
||||
this.scrolledToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledErrorOccured = false;
|
||||
private scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached) return;
|
||||
if (this.isLoading || this.maxReached || this.scrolledErrorOccured) return;
|
||||
|
||||
const maxId = this.conversations[this.conversations.length - 1].conversation.last_status.id;
|
||||
|
||||
this.isLoading = true;
|
||||
this.isProcessingInfiniteScroll = true;
|
||||
this.mastodonService.getConversations(this.account.info, maxId)
|
||||
.then((conversations: Conversation[]) => {
|
||||
if (conversations.length === 0) {
|
||||
|
@ -95,39 +103,46 @@ export class DirectMessagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
for (const c of conversations) {
|
||||
const wrapper = new ConversationWrapper(c, this.account.info, this.account.avatar);
|
||||
let cwPolicy = this.toolsService.checkContentWarning(c.last_status);
|
||||
const wrapper = new ConversationWrapper(c, this.account.info, this.account.avatar, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.conversations.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.scrolledErrorOccured = true;
|
||||
setTimeout(() => {
|
||||
this.scrolledErrorOccured = false;
|
||||
}, 5000);
|
||||
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
this.isProcessingInfiniteScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationWrapper {
|
||||
|
||||
constructor(
|
||||
public conversation: Conversation,
|
||||
public provider: AccountInfo,
|
||||
public userAvatar: string
|
||||
public userAvatar: string,
|
||||
applyCw: boolean,
|
||||
hideStatus: boolean
|
||||
) {
|
||||
this.lastStatus = new StatusWrapper(conversation.last_status, provider);
|
||||
this.lastStatus = new StatusWrapper(conversation.last_status, provider, applyCw, hideStatus);
|
||||
}
|
||||
|
||||
lastStatus: StatusWrapper;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
favorites works!
|
||||
</p>
|
|
@ -0,0 +1,5 @@
|
|||
@import "variables";
|
||||
|
||||
.stream-toots {
|
||||
background-color: $column-background;
|
||||
}
|
|
@ -1,72 +1,74 @@
|
|||
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
import { FavoriteResult } 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 { resetCompiledComponents } from '@angular/core/src/render3/jit/module';
|
||||
import { TimelineBase } from '../../../../components/common/timeline-base';
|
||||
|
||||
@Component({
|
||||
selector: 'app-favorites',
|
||||
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
|
||||
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './favorites.component.scss']
|
||||
})
|
||||
export class FavoritesComponent implements OnInit {
|
||||
statuses: StatusWrapper[] = [];
|
||||
displayError: string;
|
||||
isLoading = true;
|
||||
isThread = false;
|
||||
hasContentWarnings = false;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
private maxReached = false;
|
||||
export class FavoritesComponent extends TimelineBase {
|
||||
private maxId: string;
|
||||
private _account: AccountWrapper;
|
||||
|
||||
@Input('account')
|
||||
set account(acc: AccountWrapper) {
|
||||
set accountWrapper(acc: AccountWrapper) {
|
||||
this._account = acc;
|
||||
this.account = acc.info;
|
||||
this.getFavorites();
|
||||
}
|
||||
get account(): AccountWrapper {
|
||||
get accountWrapper(): AccountWrapper {
|
||||
return this._account;
|
||||
}
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
protected readonly toolsService: ToolsService,
|
||||
protected readonly notificationService: NotificationService,
|
||||
protected readonly mastodonService: MastodonWrapperService) {
|
||||
|
||||
super(toolsService, notificationService, mastodonService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
private reset(){
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.isLoading = true;
|
||||
this.statuses.length = 0;
|
||||
this.maxReached = false;
|
||||
this.lastCallReachedMax = false;
|
||||
this.maxId = null;
|
||||
}
|
||||
|
||||
private getFavorites() {
|
||||
this.reset();
|
||||
|
||||
this.mastodonService.getFavorites(this.account.info)
|
||||
this.mastodonService.getFavorites(this.account)
|
||||
.then((result: FavoriteResult) => {
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if (!this.maxId) {
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
for (const s of result.favorites) {
|
||||
const wrapper = new StatusWrapper(s, this.account.info);
|
||||
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.info);
|
||||
this.notificationService.notifyHttpError(err, this.account);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
|
@ -74,51 +76,23 @@ export class FavoritesComponent implements OnInit {
|
|||
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
protected getNextStatuses(): Promise<Status[]> {
|
||||
if (this.lastCallReachedMax) return Promise.resolve([]);
|
||||
|
||||
if (atBottom) {
|
||||
this.scrolledToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.mastodonService.getFavorites(this.account.info, this.maxId)
|
||||
return this.mastodonService.getFavorites(this.account, this.maxId)
|
||||
.then((result: FavoriteResult) => {
|
||||
const statuses = result.favorites;
|
||||
if (statuses.length === 0 || !this.maxId) {
|
||||
this.maxReached = true;
|
||||
return;
|
||||
this.maxId = result.max_id;
|
||||
|
||||
if (!this.maxId) {
|
||||
this.lastCallReachedMax = true;
|
||||
}
|
||||
|
||||
this.maxId = result.max_id;
|
||||
for (const s of statuses) {
|
||||
const wrapper = new StatusWrapper(s, this.account.info);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
return statuses;
|
||||
});
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
protected scrolledToTop() { }
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
protected statusProcessOnGoToTop() { }
|
||||
}
|
||||
|
|
|
@ -3,10 +3,16 @@
|
|||
|
||||
<div class="account__header">
|
||||
<a href (click)="browseLocalAccount()" (auxclick)="openLocalAccount()" title="open {{ account.info.id }}">
|
||||
<img class="account__avatar" src="{{account.avatar}}"/>
|
||||
<img class="account__avatar" src="{{account.avatar}}" />
|
||||
</a>
|
||||
|
||||
<!-- <a href class="account__header--button"><fa-icon [icon]="faUserPlus"></fa-icon></a> -->
|
||||
<a *ngIf="isBookmarksAvailable" href class="account__header--button" title="bookmark"
|
||||
(click)="loadSubPanel('bookmarks')"
|
||||
[ngClass]="{ 'account__header--button--selected': subPanel === 'bookmarks' }">
|
||||
<fa-icon [icon]="faBookmark"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href class="account__header--button" title="favorites" (click)="loadSubPanel('favorites')"
|
||||
[ngClass]="{ 'account__header--button--selected': subPanel === 'favorites' }">
|
||||
<fa-icon [icon]="faStar"></fa-icon>
|
||||
|
@ -29,17 +35,20 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'" [account]="account"
|
||||
<app-bookmarks #bookmarks class="account__body" *ngIf="subPanel === 'bookmarks'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-bookmarks>
|
||||
<app-direct-messages #dm class="account__body" *ngIf="subPanel === 'dm'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-direct-messages>
|
||||
<app-favorites class="account__body" *ngIf="subPanel === 'favorites'" [account]="account"
|
||||
<app-favorites #favorites class="account__body" *ngIf="subPanel === 'favorites'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-favorites>
|
||||
<app-mentions class="account__body" *ngIf="subPanel === 'mentions'" [account]="account"
|
||||
<app-mentions #mentions class="account__body" *ngIf="subPanel === 'mentions'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-mentions>
|
||||
<app-my-account class="account__body" *ngIf="subPanel === 'account'" [account]="account"></app-my-account>
|
||||
<app-notifications class="account__body" *ngIf="subPanel === 'notifications'" [account]="account"
|
||||
<app-notifications #notifications class="account__body" *ngIf="subPanel === 'notifications'" [account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-notifications>
|
||||
</div>
|
|
@ -1,15 +1,22 @@
|
|||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
|
||||
import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBell, faEnvelope, faUser, faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faBell, faEnvelope, faUser, faStar, faBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountWrapper } from '../../../models/account.models';
|
||||
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||
import { OpenThreadEvent, ToolsService, InstanceInfo } from '../../../services/tools.service';
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { BookmarksComponent } from './bookmarks/bookmarks.component';
|
||||
import { NotificationsComponent } from './notifications/notifications.component';
|
||||
import { MentionsComponent } from './mentions/mentions.component';
|
||||
import { DirectMessagesComponent } from './direct-messages/direct-messages.component';
|
||||
import { FavoritesComponent } from './favorites/favorites.component';
|
||||
import { BrowseBase } from '../../common/browse-base';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -17,27 +24,26 @@ import { AccountInfo } from '../../../states/accounts.state';
|
|||
templateUrl: './manage-account.component.html',
|
||||
styleUrls: ['./manage-account.component.scss']
|
||||
})
|
||||
export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
export class ManageAccountComponent extends BrowseBase {
|
||||
faAt = faAt;
|
||||
faBell = faBell;
|
||||
faEnvelope = faEnvelope;
|
||||
faUser = faUser;
|
||||
faStar = faStar;
|
||||
faUserPlus = faUserPlus;
|
||||
faBookmark = faBookmark;
|
||||
|
||||
subPanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' = 'account';
|
||||
subPanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks' = 'account';
|
||||
hasNotifications = false;
|
||||
hasMentions = false;
|
||||
isBookmarksAvailable = false;
|
||||
|
||||
userAccount: Account;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
@Input('account')
|
||||
set account(acc: AccountWrapper) {
|
||||
this._account = acc;
|
||||
this.checkIfBookmarksAreAvailable();
|
||||
this.checkNotifications();
|
||||
this.getUserUrl(acc.info);
|
||||
}
|
||||
|
@ -49,10 +55,13 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
private _account: AccountWrapper;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService) { }
|
||||
private readonly userNotificationService: UserNotificationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
@ -61,6 +70,16 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
private checkIfBookmarksAreAvailable() {
|
||||
this.toolsService.isBookmarksAreAvailable(this.account.info)
|
||||
.then((isAvailable: boolean) => {
|
||||
this.isBookmarksAvailable = isAvailable;
|
||||
})
|
||||
.catch(err => {
|
||||
this.isBookmarksAvailable = false;
|
||||
});
|
||||
}
|
||||
|
||||
private getUserUrl(account: AccountInfo) {
|
||||
this.mastodonService.retrieveAccountDetails(this.account.info)
|
||||
.then((acc: Account) => {
|
||||
|
@ -79,8 +98,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification) {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let accSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
let settings = this.settingsService.getSettings();
|
||||
let accSettings = this.settingsService.getAccountSettings(this.account.info);
|
||||
|
||||
if (!settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
|
||||
this.hasNotifications = userNotification.hasNewNotifications;
|
||||
|
@ -92,8 +111,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
let current = this.userNotificationService.userNotifications.value;
|
||||
const userNotification = current.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification) {
|
||||
let settings = this.toolsService.getSettings();
|
||||
let accSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
let settings = this.settingsService.getSettings();
|
||||
let accSettings = this.settingsService.getAccountSettings(this.account.info);
|
||||
|
||||
if (!settings.disableAutofocus && !settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
|
||||
if (userNotification.hasNewNotifications) {
|
||||
|
@ -105,13 +124,36 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites'): boolean {
|
||||
this.subPanel = subpanel;
|
||||
return false;
|
||||
}
|
||||
@ViewChild('bookmarks') bookmarksComp: BookmarksComponent;
|
||||
@ViewChild('notifications') notificationsComp: NotificationsComponent;
|
||||
@ViewChild('mentions') mentionsComp: MentionsComponent;
|
||||
@ViewChild('dm') dmComp: DirectMessagesComponent;
|
||||
@ViewChild('favorites') favoritesComp: FavoritesComponent;
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
|
||||
if (this.subPanel === subpanel) {
|
||||
switch (subpanel) {
|
||||
case 'bookmarks':
|
||||
this.bookmarksComp.applyGoToTop();
|
||||
break;
|
||||
case 'notifications':
|
||||
this.notificationsComp.applyGoToTop();
|
||||
break;
|
||||
case 'mentions':
|
||||
this.mentionsComp.applyGoToTop();
|
||||
break;
|
||||
case 'dm':
|
||||
this.dmComp.applyGoToTop();
|
||||
break;
|
||||
case 'favorites':
|
||||
this.favoritesComp.applyGoToTop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.subPanel = subpanel;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
browseLocalAccount(): boolean {
|
||||
|
@ -124,12 +166,4 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
|
|||
window.open(this.userAccount.url, '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
mentions works!
|
||||
</p>
|
|
@ -1,6 +1,4 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
@import "mixins";
|
||||
|
||||
.stream-toots {
|
||||
background-color: $column-background;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
|
@ -7,7 +7,8 @@ import { StatusWrapper } from '../../../../models/common.model';
|
|||
import { Status, Notification } from '../../../../services/models/mastodon.interfaces';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { TimelineBase } from '../../../../components/common/timeline-base';
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -15,41 +16,33 @@ import { OpenThreadEvent } from '../../../../services/tools.service';
|
|||
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
|
||||
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './mentions.component.scss']
|
||||
})
|
||||
export class MentionsComponent implements OnInit, OnDestroy {
|
||||
statuses: StatusWrapper[] = [];
|
||||
displayError: string;
|
||||
isLoading = false;
|
||||
isThread = false;
|
||||
hasContentWarnings = false;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
export class MentionsComponent extends TimelineBase {
|
||||
private lastId: string;
|
||||
private _account: AccountWrapper;
|
||||
|
||||
@Input('account')
|
||||
set account(acc: AccountWrapper) {
|
||||
set accountWrapper(acc: AccountWrapper) {
|
||||
this._account = acc;
|
||||
this.account = acc.info;
|
||||
this.loadMentions();
|
||||
}
|
||||
get account(): AccountWrapper {
|
||||
get accountWrapper(): AccountWrapper {
|
||||
return this._account;
|
||||
}
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
private maxReached = false;
|
||||
private _account: AccountWrapper;
|
||||
private userNotificationServiceSub: Subscription;
|
||||
private lastId: string;
|
||||
private userNotificationServiceSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) {
|
||||
protected readonly toolsService: ToolsService,
|
||||
protected readonly notificationService: NotificationService,
|
||||
protected readonly userNotificationService: UserNotificationService,
|
||||
protected readonly mastodonService: MastodonWrapperService) {
|
||||
|
||||
super(toolsService, notificationService, mastodonService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@ -64,76 +57,42 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.statuses.length = 0;
|
||||
this.userNotificationService.markMentionsAsRead(this.account.info);
|
||||
this.userNotificationService.markMentionsAsRead(this.account);
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.processNewMentions(userNotifications);
|
||||
if(this.statuses.length < 20) this.scrolledToBottom();
|
||||
if (this.statuses.length < 20) this.scrolledToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
private processNewMentions(userNotifications: UserNotification[]) {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
private processNewMentions(userNotifications: UserNotification[]) {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.id);
|
||||
if (userNotification && userNotification.mentions) {
|
||||
let orderedMentions = [...userNotification.mentions.map(x => x.status)].reverse();
|
||||
for (let m of orderedMentions) {
|
||||
if (!this.statuses.find(x => x.status.id === m.id)) {
|
||||
const statusWrapper = new StatusWrapper(m, this.account.info);
|
||||
if (m && !this.statuses.find(x => x.status.id === m.id)) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(m);
|
||||
const statusWrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.statuses.unshift(statusWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.lastId = userNotification.lastMentionsId;
|
||||
this.userNotificationService.markMentionsAsRead(this.account.info);
|
||||
this.userNotificationService.markMentionsAsRead(this.account);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
|
||||
if (atBottom) {
|
||||
this.scrolledToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached || this.statuses.length === 0) return;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.mastodonService.getNotifications(this.account.info, ['follow', 'favourite', 'reblog', 'poll'], this.lastId)
|
||||
.then((result: Notification[]) => {
|
||||
|
||||
protected getNextStatuses(): Promise<Status[]> {
|
||||
return this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll', 'move', 'update'], this.lastId)
|
||||
.then((result: Notification[]) => {
|
||||
const statuses = result.map(x => x.status);
|
||||
if (statuses.length === 0) {
|
||||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastId = result[result.length - 1].id;
|
||||
|
||||
return statuses;
|
||||
});
|
||||
}
|
||||
|
||||
protected scrolledToTop() {}
|
||||
|
||||
for (const s of statuses) {
|
||||
const wrapper = new StatusWrapper(s, this.account.info);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
|
||||
this.lastId = result[result.length - 1].id;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
protected statusProcessOnGoToTop(){}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper
|
|||
import { AccountWrapper } from '../../../../../models/account.models';
|
||||
import { NotificationService } from '../../../../../services/notification.service';
|
||||
import { Account, Relationship, Instance } from "../../../../../services/models/mastodon.interfaces";
|
||||
import { of } from 'rxjs';
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-editor',
|
||||
|
@ -25,6 +25,7 @@ export class ListEditorComponent implements OnInit {
|
|||
searchOpen: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
|
@ -69,13 +70,12 @@ export class ListEditorComponent implements OnInit {
|
|||
}
|
||||
|
||||
addEvent(accountWrapper: AccountListWrapper) {
|
||||
console.log(accountWrapper);
|
||||
const settings = this.settingsService.getSettings();
|
||||
|
||||
accountWrapper.isLoading = true;
|
||||
this.mastodonService.getInstance(this.account.info.instance)
|
||||
.then((instance: Instance) => {
|
||||
console.log(instance);
|
||||
if (instance.version.toLowerCase().includes('pleroma')) {
|
||||
if (instance.version.toLowerCase().includes('pleroma') && !settings.autoFollowOnListEnabled) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return this.followAccount(accountWrapper);
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<div class="my-account__body flexcroll">
|
||||
<h4 class="my-account__label">add timeline:</h4>
|
||||
<a class="my-account__link my-account__link--margin-bottom my-account__blue" href
|
||||
*ngFor="let stream of availableStreams" (click)="addStream(stream)"
|
||||
title="{{ stream.isAdded ? '' : 'add timeline'}}" [class.my-account__link--disabled]="stream.isAdded">
|
||||
{{ stream.name }} <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare">
|
||||
</fa-icon>
|
||||
</a>
|
||||
<div class="my-account__link--margin-bottom" *ngFor="let stream of availableStreams">
|
||||
<a href *ngIf="stream.isAdded" class="my-account__list--button" title="remove timeline" (click)="removeStream(stream)">
|
||||
<fa-icon class="my-account__link--icon my-account__link--remove" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<a href class="my-account__link my-account__link--margin-bottom my-account__blue" (click)="addStream(stream)"
|
||||
title="{{ stream.isAdded ? '' : 'add timeline'}}"
|
||||
[class.my-account__link--disabled]="stream.isAdded">
|
||||
{{ stream.name }} <!-- <fa-icon class="my-account__link--icon" *ngIf="stream.isAdded" [icon]="faCheckSquare"> </fa-icon> -->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<h4 class="my-account__label my-account__margin-top">manage list:</h4>
|
||||
<div class="my-account__link--margin-bottom" *ngFor="let list of availableLists">
|
||||
|
@ -26,11 +31,12 @@
|
|||
*ngIf="list.confirmDeletion">
|
||||
<fa-icon class="my-account__link--icon" [icon]="faCheck"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a href *ngIf="list.isAdded" class="my-account__list--button" title="remove list" (click)="removeStream(list)">
|
||||
<fa-icon class="my-account__link--icon my-account__list--remove" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
<a class="my-account__link my-account__list my-account__blue" href (click)="addStream(list)"
|
||||
title="{{ list.isAdded ? '' : 'add list'}}" [class.my-account__link--disabled]="list.isAdded">
|
||||
{{ list.name }} <fa-icon class="my-account__link--icon" *ngIf="list.isAdded" [icon]="faCheckSquare">
|
||||
</fa-icon>
|
||||
title="{{ list.isAdded ? '' : 'add list'}}" [class.my-account__list--disabled]="list.isAdded">
|
||||
{{ list.name }} <!--<fa-icon class="my-account__link--icon" *ngIf="list.isAdded" [icon]="faCheckSquare"> </fa-icon>-->
|
||||
</a>
|
||||
|
||||
<app-list-editor *ngIf="list.editList" [list]="list" [account]="account"></app-list-editor>
|
||||
|
@ -44,7 +50,9 @@
|
|||
<h4 class="my-account__label my-account__margin-top">advanced settings:</h4>
|
||||
<div class="advanced-settings">
|
||||
<input class="advanced-settings__checkbox" [(ngModel)]="avatarNotificationDisabled"
|
||||
(change)="onDisableAvatarNotificationChanged()" type="checkbox" name="avatarNotification" value="avatarNotification" id="avatarNotification"> <label class="noselect advanced-settings__label" for="avatarNotification">disable avatar notifications</label><br>
|
||||
(change)="onDisableAvatarNotificationChanged()" type="checkbox" name="avatarNotification"
|
||||
value="avatarNotification" id="avatarNotification"> <label class="noselect advanced-settings__label"
|
||||
for="avatarNotification">disable avatar notifications</label><br>
|
||||
<input class="advanced-settings__checkbox" [(ngModel)]="customStatusLengthEnabled"
|
||||
(change)="onCustomLengthEnabledChanged()" type="checkbox" name="customCharLength" value="customCharLength"
|
||||
id="customCharLength"> <label class="noselect advanced-settings__label" for="customCharLength">custom char
|
||||
|
@ -57,6 +65,6 @@
|
|||
|
||||
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4>
|
||||
<a class="my-account__link my-account__red" href (click)="removeAccount()">
|
||||
Delete
|
||||
Remove
|
||||
</a>
|
||||
</div>
|
|
@ -1,6 +1,10 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
|
||||
$list-width: 60px;
|
||||
$button-width: $list-width/2;
|
||||
|
||||
.my-account {
|
||||
transition: all .2s;
|
||||
|
||||
|
@ -50,10 +54,19 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
&--remove {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: default;
|
||||
background-color: darken($color-primary, 4);
|
||||
|
||||
width: calc(100% - #{$button-width} - 1px);
|
||||
//outline: 1px solid greenyellow;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($color-primary, 4);
|
||||
}
|
||||
|
@ -67,9 +80,27 @@
|
|||
}
|
||||
|
||||
&__list {
|
||||
$list-width: 60px;
|
||||
|
||||
width: calc(100% - #{$list-width} - 2px);
|
||||
|
||||
&--remove {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: default;
|
||||
background-color: darken($color-primary, 4);
|
||||
|
||||
width: calc(100% - #{$button-width} * 3 - 3px);
|
||||
//outline: 1px solid greenyellow;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($color-primary, 4);
|
||||
}
|
||||
}
|
||||
|
||||
&--button {
|
||||
margin-left: 1px;
|
||||
width: calc(#{$list-width}/2);
|
||||
|
|
|
@ -10,8 +10,8 @@ import { AccountWrapper } from '../../../../models/account.models';
|
|||
import { RemoveAccount } from '../../../../states/accounts.state';
|
||||
import { NavigationService } from '../../../../services/navigation.service';
|
||||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { AccountSettings } from '../../../../states/settings.state';
|
||||
import { SettingsService } from '../../../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-account',
|
||||
|
@ -49,8 +49,8 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
private streamChangedSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService) { }
|
||||
|
@ -68,7 +68,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private loadAccountSettings(){
|
||||
this.accountSettings = this.toolsService.getAccountSettings(this.account.info);
|
||||
this.accountSettings = this.settingsService.getAccountSettings(this.account.info);
|
||||
|
||||
this.customStatusLengthEnabled = this.accountSettings.customStatusCharLengthEnabled;
|
||||
this.customStatusLength = this.accountSettings.customStatusCharLength;
|
||||
|
@ -77,13 +77,13 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
|
||||
onCustomLengthEnabledChanged(): boolean {
|
||||
this.accountSettings.customStatusCharLengthEnabled = this.customStatusLengthEnabled;
|
||||
this.toolsService.saveAccountSettings(this.accountSettings);
|
||||
this.settingsService.saveAccountSettings(this.accountSettings);
|
||||
return false;
|
||||
}
|
||||
|
||||
customStatusLengthChanged(event): boolean{
|
||||
this.accountSettings.customStatusCharLength = this.customStatusLength;
|
||||
this.toolsService.saveAccountSettings(this.accountSettings);
|
||||
this.settingsService.saveAccountSettings(this.accountSettings);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -104,20 +104,35 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
this.availableLists.length = 0;
|
||||
// this.availableLists.length = 0;
|
||||
this.mastodonService.getLists(account.info)
|
||||
.then((streams: StreamElement[]) => {
|
||||
this.availableLists.length = 0;
|
||||
// this.availableLists.length = 0;
|
||||
for (let stream of streams) {
|
||||
let wrappedStream = new StreamWrapper(stream);
|
||||
let wrappedStream = this.availableLists.find(x => x.id === stream.id);
|
||||
if(!wrappedStream){
|
||||
wrappedStream = new StreamWrapper(stream);
|
||||
this.availableLists.push(wrappedStream);
|
||||
}
|
||||
|
||||
if(loadedStreams.find(x => x.id == stream.id)){
|
||||
wrappedStream.isAdded = true;
|
||||
} else {
|
||||
wrappedStream.isAdded = false;
|
||||
}
|
||||
this.availableLists.push(wrappedStream);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(_ => {
|
||||
this.availableLists.sort((a,b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
});
|
||||
|
@ -133,6 +148,16 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
removeStream(stream: StreamWrapper): boolean {
|
||||
if (stream && stream.isAdded) {
|
||||
this.store.dispatch([new RemoveStream(stream.id)]).toPromise()
|
||||
.then(() => {
|
||||
stream.isAdded = false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeAccount(): boolean {
|
||||
const accountId = this.account.info.id;
|
||||
this.store.dispatch([new RemoveAllStreams(accountId), new RemoveAccount(accountId)]);
|
||||
|
@ -189,9 +214,9 @@ export class MyAccountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
onDisableAvatarNotificationChanged() {
|
||||
let settings = this.toolsService.getAccountSettings(this.account.info);
|
||||
let settings = this.settingsService.getAccountSettings(this.account.info);
|
||||
settings.disableAvatarNotifications = this.avatarNotificationDisabled;
|
||||
this.toolsService.saveAccountSettings(settings);
|
||||
this.settingsService.saveAccountSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,35 @@
|
|||
<div class="notification">
|
||||
<div *ngIf="notification.type === 'follow_request' && !followRequestProcessed">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faUserClock"></fa-icon>
|
||||
</div>
|
||||
<div class="stream__notification--label">
|
||||
<a href class="stream__link" title="{{notification.account.acct}}"
|
||||
(click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
|
||||
innerHTML="{{ notification.account | accountEmoji }}"></a>
|
||||
submitted a follow request
|
||||
</div>
|
||||
|
||||
<a href (click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
|
||||
class="follow-account" title="{{notification.account.acct}}">
|
||||
<img class="follow-account__avatar" src="{{ notification.account.avatar }}" />
|
||||
<span class="follow-account__display-name" innerHTML="{{ notification.account | accountEmoji }}"></span>
|
||||
<span class="follow-account__acct">@{{ notification.account.acct }}</span>
|
||||
</a>
|
||||
|
||||
<div class="follow_request">
|
||||
<a href title="Authorize" class="follow_request__link follow_request__link--check"
|
||||
(click)="acceptFollowRequest()">
|
||||
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
|
||||
</a>
|
||||
<a href title="Reject" class="follow_request__link follow_request__link--cross"
|
||||
(click)="refuseFollowRequest()">
|
||||
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="notification.type === 'follow'">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faUserPlus"></fa-icon>
|
||||
|
@ -19,12 +50,49 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type !== 'mention'" class="stream__status" [statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account" [notificationType]="notification.type"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
<div *ngIf="notification.type === 'move'">
|
||||
<div class="stream__notification--icon" title="{{notification.account.acct}}">
|
||||
<fa-icon class="followed" [icon]="faTruckMoving"></fa-icon>
|
||||
</div>
|
||||
<div class="stream__notification--label">
|
||||
<a href class="stream__link" title="{{notification.account.acct}}"
|
||||
(click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
|
||||
innerHTML="{{ notification.account | accountEmoji }}"></a>
|
||||
migrated to
|
||||
</div>
|
||||
|
||||
<a href (click)="openAccount(notification.target)" (auxclick)="openUrl(notification.target.url)"
|
||||
class="follow-account" title="{{notification.target.acct}}">
|
||||
<img class="follow-account__avatar" src="{{ notification.target.avatar }}" />
|
||||
<span class="follow-account__display-name" innerHTML="{{ notification.target | accountEmoji }}"></span>
|
||||
<span class="follow-account__acct">@{{ notification.target.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type === 'update'" class="stream__status"
|
||||
[statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account"
|
||||
[notificationType]="notification.type"
|
||||
[context]="'notifications'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status" [statusWrapper]="notification.status"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status"
|
||||
[statusWrapper]="notification.status"
|
||||
[context]="'notifications'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
||||
<app-status *ngIf="notification.status && notification.type !== 'mention' && notification.type !== 'update'"
|
||||
class="stream__status"
|
||||
[statusWrapper]="notification.status"
|
||||
[notificationAccount]="notification.account"
|
||||
[notificationType]="notification.type"
|
||||
[context]="'notifications'"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
||||
</div>
|
|
@ -46,6 +46,7 @@
|
|||
color: $boost-color;
|
||||
}
|
||||
|
||||
$acccount-info-left: 70px;
|
||||
.follow-account {
|
||||
padding: 5px;
|
||||
height: 60px;
|
||||
|
@ -62,8 +63,7 @@
|
|||
height: 45px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
$acccount-info-left: 70px;
|
||||
|
||||
&__display-name {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
|
@ -81,5 +81,44 @@
|
|||
left: $acccount-info-left;
|
||||
font-size: 13px;
|
||||
color: $status-links-color;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
width: calc(100% - #{$acccount-info-left});
|
||||
}
|
||||
}
|
||||
|
||||
.follow_request {
|
||||
width: calc(100% - #{$acccount-info-left});
|
||||
margin-left: $acccount-info-left;
|
||||
|
||||
&__link {
|
||||
display: inline-block;
|
||||
width: calc(50%);
|
||||
padding: 2px;
|
||||
|
||||
text-align: center;
|
||||
color: rgb(182, 182, 182);
|
||||
transition: all .2s;
|
||||
|
||||
// outline: 1px dotted greenyellow;
|
||||
|
||||
&--check {
|
||||
&:hover {
|
||||
color: greenyellow;
|
||||
}
|
||||
}
|
||||
|
||||
&--cross {
|
||||
&:hover {
|
||||
color: orangered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -1,39 +1,38 @@
|
|||
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
|
||||
import { faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { faUserPlus, faUserClock, faCheck, faTimes, faTruckMoving } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { NotificationWrapper } from '../notifications.component';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../../../services/tools.service';
|
||||
import { ToolsService } from '../../../../../services/tools.service';
|
||||
import { Account } from '../../../../../services/models/mastodon.interfaces';
|
||||
import { BrowseBase } from '../../../../../components/common/browse-base';
|
||||
import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../../services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification',
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrls: ['./notification.component.scss']
|
||||
})
|
||||
export class NotificationComponent implements OnInit {
|
||||
export class NotificationComponent extends BrowseBase {
|
||||
faUserPlus = faUserPlus;
|
||||
faUserClock = faUserClock;
|
||||
faCheck = faCheck;
|
||||
faTimes = faTimes;
|
||||
faTruckMoving = faTruckMoving;
|
||||
|
||||
@Input() notification: NotificationWrapper;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
constructor(private readonly toolsService: ToolsService) { }
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly toolsService: ToolsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
openAccount(account: Account): boolean {
|
||||
|
@ -41,9 +40,47 @@ export class NotificationComponent implements OnInit {
|
|||
this.browseAccountEvent.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
openUrl(url: string): boolean {
|
||||
window.open(url, '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
followRequestWorking: boolean;
|
||||
followRequestProcessed: boolean;
|
||||
acceptFollowRequest(): boolean {
|
||||
if(this.followRequestWorking) return false;
|
||||
this.followRequestWorking = true;
|
||||
|
||||
this.mastodonService.authorizeFollowRequest(this.notification.provider, this.notification.notification.account.id)
|
||||
.then(res => {
|
||||
this.followRequestProcessed = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationsService.notifyHttpError(err, this.notification.provider);
|
||||
})
|
||||
.then(res => {
|
||||
this.followRequestWorking = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
refuseFollowRequest(): boolean {
|
||||
if(this.followRequestWorking) return false;
|
||||
this.followRequestWorking = true;
|
||||
|
||||
this.mastodonService.rejectFollowRequest(this.notification.provider, this.notification.notification.account.id)
|
||||
.then(res => {
|
||||
this.followRequestProcessed = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationsService.notifyHttpError(err, this.notification.provider);
|
||||
})
|
||||
.then(res => {
|
||||
this.followRequestWorking = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountWrapper } from '../../../../models/account.models';
|
||||
|
@ -8,21 +8,19 @@ import { Notification, Account } from '../../../../services/models/mastodon.inte
|
|||
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { BrowseBase } from '../../../../components/common/browse-base';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notifications',
|
||||
templateUrl: './notifications.component.html',
|
||||
styleUrls: ['./notifications.component.scss']
|
||||
})
|
||||
export class NotificationsComponent implements OnInit, OnDestroy {
|
||||
export class NotificationsComponent extends BrowseBase {
|
||||
notifications: NotificationWrapper[] = [];
|
||||
private isProcessingInfiniteScroll: boolean;
|
||||
isLoading = false;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
@Input('account')
|
||||
set account(acc: AccountWrapper) {
|
||||
this._account = acc;
|
||||
|
@ -31,7 +29,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
get account(): AccountWrapper {
|
||||
return this._account;
|
||||
}
|
||||
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
private maxReached = false;
|
||||
|
@ -39,31 +37,34 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
private userNotificationServiceSub: Subscription;
|
||||
private lastId: string;
|
||||
|
||||
constructor(
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
private readonly mastodonService: MastodonWrapperService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.userNotificationServiceSub){
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private loadNotifications(){
|
||||
if(this.userNotificationServiceSub){
|
||||
private loadNotifications() {
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
this.notifications.length = 0;
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.processNewNotifications(userNotifications);
|
||||
if(this.notifications.length < 20) this.scrolledToBottom();
|
||||
if (this.notifications.length < 20) this.scrolledToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -72,8 +73,9 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
if (userNotification && userNotification.notifications) {
|
||||
let orderedNotifications = [...userNotification.notifications].reverse();
|
||||
for (let n of orderedNotifications) {
|
||||
const notificationWrapper = new NotificationWrapper(n, this.account.info);
|
||||
if (!this.notifications.find(x => x.wrapperId === notificationWrapper.wrapperId)) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(n.status);
|
||||
const notificationWrapper = new NotificationWrapper(n, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
|
||||
if (!this.notifications.find(x => x.wrapperId === notificationWrapper.wrapperId)) {
|
||||
this.notifications.unshift(notificationWrapper);
|
||||
}
|
||||
}
|
||||
|
@ -82,20 +84,22 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
}
|
||||
|
||||
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
|
||||
if (atBottom) {
|
||||
if (atBottom && !this.isProcessingInfiniteScroll) {
|
||||
this.scrolledToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledErrorOccured = false;
|
||||
private scrolledToBottom() {
|
||||
if (this.isLoading || this.maxReached || this.notifications.length === 0) return;
|
||||
if (this.isLoading || this.maxReached || this.notifications.length === 0 || this.scrolledErrorOccured) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.isProcessingInfiniteScroll = true;
|
||||
|
||||
this.mastodonService.getNotifications(this.account.info, ['mention'], this.lastId)
|
||||
.then((notifications: Notification[]) => {
|
||||
|
@ -103,54 +107,67 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (const s of notifications) {
|
||||
const wrapper = new NotificationWrapper(s, this.account.info);
|
||||
this.notifications.push(wrapper);
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s.status);
|
||||
const wrapper = new NotificationWrapper(s, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
|
||||
if (!this.notifications.find(x => x.wrapperId === wrapper.wrapperId)) {
|
||||
this.notifications.push(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastId = notifications[notifications.length - 1].id;
|
||||
})
|
||||
.catch(err => {
|
||||
this.scrolledErrorOccured = true;
|
||||
setTimeout(() => {
|
||||
this.scrolledErrorOccured = false;
|
||||
}, 5000);
|
||||
|
||||
this.notificationService.notifyHttpError(err, this.account.info);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
this.isProcessingInfiniteScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
applyGoToTop(): boolean {
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
setTimeout(() => {
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationWrapper {
|
||||
constructor(notification: Notification, provider: AccountInfo) {
|
||||
constructor(notification: Notification, provider: AccountInfo, applyCw: boolean, hideStatus: boolean) {
|
||||
this.type = notification.type;
|
||||
switch(this.type){
|
||||
case 'mention':
|
||||
case 'reblog':
|
||||
switch (this.type) {
|
||||
case 'mention':
|
||||
case 'reblog':
|
||||
case 'favourite':
|
||||
case 'poll':
|
||||
this.status= new StatusWrapper(notification.status, provider);
|
||||
break;
|
||||
}
|
||||
case 'update':
|
||||
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
|
||||
break;
|
||||
}
|
||||
this.account = notification.account;
|
||||
this.target = notification.target;
|
||||
this.wrapperId = `${this.type}-${notification.id}`;
|
||||
this.notification = notification;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
provider: AccountInfo;
|
||||
notification: Notification;
|
||||
wrapperId: string;
|
||||
account: Account;
|
||||
target: Account;
|
||||
status: StatusWrapper;
|
||||
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll';
|
||||
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move' | 'update';
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
<h3 class="panel__title">search</h3>
|
||||
|
||||
<form class="form-section" (ngSubmit)="onSubmit()">
|
||||
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||
name="searchHandle" placeholder="Search" autocomplete="off" />
|
||||
<input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
|
||||
name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
|
||||
<button class="form-button" type="submit" title="search">GO</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -15,12 +15,17 @@
|
|||
|
||||
<div *ngIf="accounts.length > 0" class="search-results">
|
||||
<h3 class="search-results__title">Accounts</h3>
|
||||
<a href *ngFor="let account of accounts" class="account" title="open account"
|
||||
(click)="browseAccount(account)">
|
||||
|
||||
<app-account class="account" *ngFor="let account of accounts"
|
||||
[account]="account"
|
||||
(accountSelected)="processAndBrowseAccount($event)"></app-account>
|
||||
|
||||
<!-- <a href *ngFor="let account of accounts" class="account" title="open account"
|
||||
(click)="processAndBrowseAccount(account)">
|
||||
<img src="{{account.avatar}}" class="account__avatar" />
|
||||
<div class="account__name">{{ account.username }}</div>
|
||||
<div class="account__fullhandle">@{{ account.acct }}</div>
|
||||
</a>
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<div *ngIf="hashtags.length > 0" class="search-results">
|
||||
|
@ -35,8 +40,10 @@
|
|||
<h3 class="search-results__title">Statuses</h3>
|
||||
|
||||
<div class="search-results__status" *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)">
|
||||
<app-status [statusWrapper]="statusWrapper"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)">
|
||||
</app-status>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
@import "panel";
|
||||
@import "commons";
|
||||
@import "buttons";
|
||||
|
||||
.panel {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
|
@ -11,66 +12,79 @@
|
|||
.form-section {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.form-with-button {
|
||||
width: calc(100% - #{$button-size});
|
||||
float: left;
|
||||
|
||||
background-color: $column-color;
|
||||
border-color: $button-border-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
// background-color: $column-color;
|
||||
// border-color: $button-border-color;
|
||||
// color: #fff;
|
||||
// color: rgb(255, 0, 0);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
font-size: $default-font-size;
|
||||
height: 29px;
|
||||
padding: 0 5px 0 5px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// .form-control {
|
||||
// margin: 0 0 5px 5px;
|
||||
// width: calc(100% - 10px);
|
||||
// background-color: $column-color;
|
||||
// border-color: $status-secondary-color;
|
||||
// color: #fff;
|
||||
// font-size: $default-font-size;
|
||||
// &:focus {
|
||||
// box-shadow: none;
|
||||
// }
|
||||
// // &--privacy {
|
||||
// // display: inline-block;
|
||||
// // width: calc(100% - 15px - #{$btn-send-status-width} - #{$counter-width});
|
||||
// // }
|
||||
// }
|
||||
|
||||
.form-button {
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background-color: $button-background-color;
|
||||
color: $button-color;
|
||||
color: whitesmoke;
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
background-color: $status-editor-footer-background;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
color: $button-color-hover;
|
||||
background-color: lighten($status-editor-footer-background, 20%);
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
|
||||
border: 1px solid $button-border-color;
|
||||
border-width: 1px 1px 1px 0;
|
||||
outline: inherit;
|
||||
&:focus {
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
|
||||
// border: none;
|
||||
// outline: none;
|
||||
// cursor: pointer;
|
||||
// background-color: $button-background-color;
|
||||
// color: $button-color;
|
||||
// color: whitesmoke;
|
||||
// transition: all .2s;
|
||||
|
||||
// &:hover {
|
||||
// background-color: $button-background-color-hover;
|
||||
// color: $button-color-hover;
|
||||
// }
|
||||
|
||||
|
||||
// border: 1px solid $button-border-color;
|
||||
// border-width: 1px 1px 1px 0;
|
||||
}
|
||||
|
||||
$search-form-height: 70px;
|
||||
|
||||
.search-result-form {
|
||||
height: $search-form-height;
|
||||
padding-left: 10px;
|
||||
//padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-bottom: 1px solid #222736;
|
||||
//border-bottom: 1px solid #222736;
|
||||
}
|
||||
|
||||
.search-result-display {
|
||||
|
@ -82,12 +96,14 @@ $search-form-height: 70px;
|
|||
// outline: 1px solid greenyellow;
|
||||
margin-top: 10px; // &:first-of-type{
|
||||
padding-left: 10px; // margin-top: 10px;
|
||||
padding-right: 10px; // margin-top: 10px;
|
||||
//padding-right: 10px; // margin-top: 10px;
|
||||
|
||||
// }
|
||||
&__title {
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__hashtag {
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
|
@ -95,17 +111,22 @@ $search-form-height: 70px;
|
|||
color: white;
|
||||
text-decoration: none;
|
||||
transition: all .3s;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 15px;
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
|
@ -114,36 +135,4 @@ $search-form-height: 70px;
|
|||
|
||||
.account {
|
||||
display: block;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
transition: all .3s; // &:hover &__name {
|
||||
// text-decoration: underline;
|
||||
// }
|
||||
border-top: 1px solid $separator-color;
|
||||
&: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; // &:hover {
|
||||
// color: white;
|
||||
// }
|
||||
}
|
||||
&:hover,
|
||||
&:hover &__fullhandle {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
@include clearfix;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
|
||||
|
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
|
|||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
||||
@ViewChild('search') searchElement: ElementRef;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonWrapperService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchElement.nativeElement.focus();
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
|
@ -54,14 +57,25 @@ export class SearchComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
browseAccount(account: Account): boolean {
|
||||
let accountName = this.toolsService.getAccountFullHandle(account);
|
||||
this.browseAccountEvent.next(accountName);
|
||||
browseAccount(account: string): boolean {
|
||||
if (account) {
|
||||
this.browseAccountEvent.next(account);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
processAndBrowseAccount(account: Account): boolean {
|
||||
if(account){
|
||||
const fullHandle = this.toolsService.getAccountFullHandle(account);
|
||||
this.browseAccountEvent.next(fullHandle);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private lastAccountUsed: AccountInfo;
|
||||
private search(data: string) {
|
||||
if (!data) return;
|
||||
|
||||
this.accounts.length = 0;
|
||||
this.statuses.length = 0;
|
||||
this.hashtags.length = 0;
|
||||
|
@ -77,11 +91,16 @@ export class SearchComponent implements OnInit {
|
|||
})
|
||||
.then((results: Results) => {
|
||||
if (results) {
|
||||
this.accounts = results.accounts.slice(0, 5);
|
||||
this.accounts = results.accounts.slice(0, 7);
|
||||
this.hashtags = results.hashtags;
|
||||
|
||||
if(data && data[0] === '#' && !this.hashtags.map(x => x.toLowerCase()).includes(data.replace('#', '').toLowerCase())){
|
||||
this.hashtags.unshift(data.replace('#', ''));
|
||||
}
|
||||
|
||||
for (let status of results.statuses) {
|
||||
const statusWrapper = new StatusWrapper(status, this.lastAccountUsed);
|
||||
let cwPolicy = this.toolsService.checkContentWarning(status);
|
||||
const statusWrapper = new StatusWrapper(cwPolicy.status, this.lastAccountUsed, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.statuses.push(statusWrapper);
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +110,4 @@ export class SearchComponent implements OnInit {
|
|||
})
|
||||
.then(() => { this.isLoading = false; });
|
||||
}
|
||||
|
||||
private
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<label class="noselect sub-section__label" for="disableSounds">disable sounds</label>
|
||||
<br>
|
||||
|
||||
<span class="sound__title">notification sound:</span><br />
|
||||
<span class="sub-section__title">notification sound:</span><br />
|
||||
<form [formGroup]="notificationForm">
|
||||
<select formControlName="countryControl" (change)="onChange($event.target.value)" class="sound__select">
|
||||
<option [value]="s.id" *ngFor="let s of notificationSounds"> {{s.name}}</option>
|
||||
|
@ -36,8 +36,204 @@
|
|||
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Shortcuts</h4>
|
||||
<div class="sub-section">
|
||||
<span class="sub-section__title">switch column:</span><br />
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="columnShortcutEnabled === 1" (change)="onShortcutChange(1)"
|
||||
type="radio" name="column-ctrl" value="column-ctrl" id="column-ctrl">
|
||||
<label class="noselect sub-section__label" for="column-ctrl">Ctrl + Left | Ctrl + Right</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="columnShortcutEnabled === 2" (change)="onShortcutChange(2)"
|
||||
type="radio" name="colmun-win" value="colmun-win" id="colmun-win">
|
||||
<label class="noselect sub-section__label" for="colmun-win">Win + Alt + Left | Win + Alt + Right</label>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Languages</h4>
|
||||
<div class="sub-section">
|
||||
<div class="sub-section__content">
|
||||
<div *ngIf="!configuredLangs || configuredLangs.length === 0" class="language__warning">
|
||||
No language set.
|
||||
</div>
|
||||
<div *ngFor="let l of configuredLangs" class="language__entry">
|
||||
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
|
||||
<a href (click)="onRemoveLang(l)" class="form-button language__entry__action sound__play">remove</a>
|
||||
</div>
|
||||
<input type="text" (input)="onSearchLang($event.target.value)" [(ngModel)]="searchLang"
|
||||
placeholder="Find Language" autocomplete="off"
|
||||
class="form-control form-control-sm language__search" />
|
||||
<div *ngFor="let l of searchedLangs" class="language__entry">
|
||||
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
|
||||
<a href (click)="onAddLang(l)" class="form-button language__entry__action sound__play">add</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<input class="sub-section__checkbox" [(ngModel)]="disableLangAutodetectEnabled"
|
||||
(change)="onDisableLangAutodetectChanged()" type="checkbox" name="disableLangAutodetec"
|
||||
value="disableLangAutodetec" id="disableLangAutodetec">
|
||||
<label class="noselect sub-section__label" for="disableLangAutodetec">disable language autodetection</label>
|
||||
</div>
|
||||
<h4 class="panel__subtitle">Twitter Bridge</h4>
|
||||
<div class="sub-section">
|
||||
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
|
||||
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
|
||||
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
|
||||
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
|
||||
<br>
|
||||
<div *ngIf="twitterBridgeEnabled">
|
||||
<p>Please provide your bridge instance:
|
||||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||
[(ngModel)]="setTwitterBridgeInstance" placeholder="bridge.tld" />
|
||||
If you don't know any, consider using <a href="https://github.com/NicolasConstant/BirdsiteLive"
|
||||
target="_blank" class="version__link">BirdsiteLIVE</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/NicolasConstant/sengi/wiki/BirdsiteLIVE-integration" target="_blank"
|
||||
class="version__link">What is this?</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Content-Warning Policies</h4>
|
||||
<div class="sub-section">
|
||||
<span class="sub-section__title">global behavior:</span><br />
|
||||
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 1" (change)="onCwPolicyChange(1)"
|
||||
type="radio" name="cw-none" value="cw-none" id="cw-none">
|
||||
<label class="noselect sub-section__label" for="cw-none">None</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 2" (change)="onCwPolicyChange(2)"
|
||||
type="radio" name="cw-hide-all" value="cw-hide-all" id="cw-hide-all">
|
||||
<label class="noselect sub-section__label" for="cw-hide-all">Expand all CWs</label>
|
||||
<br>
|
||||
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
|
||||
<span class="sub-section__title">but add CW on content containing:</span><br />
|
||||
<div class="sub-text-input">
|
||||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||
[(ngModel)]="setAddCwOnContent" placeholder="example;other example" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 3" (change)="onCwPolicyChange(3)"
|
||||
type="radio" name="cw-add-on-all" value="cw-add-on-all" id="cw-add-on-all">
|
||||
<label class="noselect sub-section__label" for="cw-add-on-all">Add CW on all content</label>
|
||||
<br>
|
||||
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 3">
|
||||
<span class="sub-section__title">unless content is containing:</span><br />
|
||||
<div class="sub-text-input">
|
||||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||
[(ngModel)]="setRemoveCwOnContent" placeholder="example;other example" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="sub-section__title">hide completely content containing:</span><br />
|
||||
<div class="sub-text-input">
|
||||
<input type="text" class="form-control form-control-sm sub_section__text-input"
|
||||
[(ngModel)]="setContentHidedCompletely" placeholder="example;other example" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Timelines</h4>
|
||||
<div class="sub-section">
|
||||
<span class="sub-section__title">header display:</span><br />
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 1" (change)="onTimeLineHeaderChange(1)"
|
||||
type="radio" name="timelineheader-1" value="timelineheader-1" id="timelineheader-1">
|
||||
<label class="noselect sub-section__label" for="timelineheader-1">Title - Domain Name</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 2" (change)="onTimeLineHeaderChange(2)"
|
||||
type="radio" name="timelineheader-2" value="timelineheader-2" id="timelineheader-2">
|
||||
<label class="noselect sub-section__label" for="timelineheader-2">Title - Username - Domain Name</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 3" (change)="onTimeLineHeaderChange(3)"
|
||||
type="radio" name="timelineheader-3" value="timelineheader-3" id="timelineheader-3">
|
||||
<label class="noselect sub-section__label" for="timelineheader-3">Title - Account Icon - Domain Name</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 4" (change)="onTimeLineHeaderChange(4)"
|
||||
type="radio" name="timelineheader-4" value="timelineheader-4" id="timelineheader-4">
|
||||
<label class="noselect sub-section__label" for="timelineheader-4">Title - Account Icon</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 5" (change)="onTimeLineHeaderChange(5)"
|
||||
type="radio" name="timelineheader-5" value="timelineheader-5" id="timelineheader-5">
|
||||
<label class="noselect sub-section__label" for="timelineheader-5">Title</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineHeader === 6" (change)="onTimeLineHeaderChange(6)"
|
||||
type="radio" name="timelineheader-6" value="timelineheader-6" id="timelineheader-6">
|
||||
<label class="noselect sub-section__label" for="timelineheader-6">Title - Account Icon - Username - Domain
|
||||
Name</label>
|
||||
<br>
|
||||
|
||||
<span class="sub-section__title">loading behavior:</span><br />
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineMode === 1" (change)="onTimeLineModeChange(1)"
|
||||
type="radio" name="timelinemode-1" value="timelinemode-1" id="timelinemode-1">
|
||||
<label class="noselect sub-section__label" for="timelinemode-1">Load new statuses only on top</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineMode === 2" (change)="onTimeLineModeChange(2)"
|
||||
type="radio" name="timelinemode-2" value="timelinemode-2" id="timelinemode-2">
|
||||
<label class="noselect sub-section__label" for="timelinemode-2">Continuously loading new statuses</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="timeLineMode === 3" (change)="onTimeLineModeChange(3)"
|
||||
type="radio" name="timelinemode-3" value="timelinemode-3" id="timelinemode-3">
|
||||
<label class="noselect sub-section__label" for="timelinemode-3">Slow mode (manual loading)</label>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasPleromaAccount">
|
||||
<h4 class="panel__subtitle">Pleroma</h4>
|
||||
<div class="sub-section">
|
||||
<input class="sub-section__checkbox" [(ngModel)]="autoFollowOnListEnabled"
|
||||
(change)="onAutoFollowOnListChanged()" type="checkbox" name="onAutoFollowOnListChanged"
|
||||
value="onAutoFollowOnListChanged" id="onAutoFollowOnListChanged">
|
||||
<label class="noselect sub-section__label" for="onAutoFollowOnListChanged">autofollow accounts when
|
||||
adding to list</label>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">Other</h4>
|
||||
<div class="sub-section">
|
||||
<input class="sub-section__checkbox" [(ngModel)]="disableRemoteStatusFetchingEnabled"
|
||||
(change)="onDisableRemoteStatusFetchingChanged()" type="checkbox" name="disableRemoteFetching"
|
||||
value="disableRemoteFetching" id="disableRemoteFetching">
|
||||
<label class="noselect sub-section__label" for="disableRemoteFetching">disable remote status
|
||||
fetching</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [(ngModel)]="enableAltLabelEnabled"
|
||||
(change)="onEnableAltLabelChanged()" type="checkbox" name="enableAltLabel"
|
||||
value="enableAltLabel" id="enableAltLabel">
|
||||
<label class="noselect sub-section__label" for="enableAltLabel">enable alt label</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [(ngModel)]="enableFreezeAvatarEnabled"
|
||||
(change)="onEnableFreezeAvatarChanged()" type="checkbox" name="enableFreezeAvatar"
|
||||
value="enableFreezeAvatar" id="enableFreezeAvatar">
|
||||
<label class="noselect sub-section__label" for="enableFreezeAvatar">freeze animated avatar</label>
|
||||
<br>
|
||||
|
||||
reorder account's icons: <a href class="toogle-lock-icon-menu" (click)="toogleLockIconMenu()"><span *ngIf="iconMenuLocked">Unlock Icons</span><span *ngIf="!iconMenuLocked">Lock Icons</span></a>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">About</h4>
|
||||
<p class="version">Sengi version: {{version}}</p>
|
||||
<p class="version">
|
||||
Sengi version: {{version}}<br />
|
||||
<a href class="version__link" (click)="openTutorial()">open tutorial</a><br />
|
||||
<a href="assets/docs/privacy.html" class="version__link" target="_blank">imprint & privacy</a><br />
|
||||
<a href class="version__link" (click)="checkForUpdates()">check for updates</a>
|
||||
<app-waiting-animation *ngIf="isCheckingUpdates" class="waiting-icon"></app-waiting-animation>
|
||||
</p>
|
||||
|
||||
|
||||
<h4 class="panel__subtitle">RESET</h4>
|
||||
<div class="sub-section">
|
||||
|
@ -48,12 +244,15 @@
|
|||
Clear all local data
|
||||
</a>
|
||||
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__red sengi-btn__medium" href (click)="confirmClearAll()">
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__red sengi-btn__medium" href
|
||||
(click)="confirmClearAll()">
|
||||
Confirm Clear All
|
||||
</a>
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__blue sengi-btn__medium" href (click)="cancelClearAll()">
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__blue sengi-btn__medium" href
|
||||
(click)="cancelClearAll()">
|
||||
Cancel
|
||||
</a>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -17,18 +17,33 @@
|
|||
.version {
|
||||
display: block;
|
||||
padding: 0 5px;
|
||||
|
||||
&__link {
|
||||
color: rgb(161, 161, 161);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-section {
|
||||
padding: 0 5px 15px 5px;
|
||||
position: relative;
|
||||
|
||||
&__checkbox{
|
||||
&__content {
|
||||
display: block;
|
||||
padding: 0 0 0 5px;
|
||||
|
||||
// outline: 1px dotted greenyellow;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
position: relative;
|
||||
top:3px;
|
||||
top: 3px;
|
||||
left: 5px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
|
@ -39,14 +54,78 @@
|
|||
&__input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sound {
|
||||
&__title {
|
||||
display: inline-block;
|
||||
margin: 0 0 5px 5px;
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-input {
|
||||
|
||||
}
|
||||
|
||||
&__cw-settings {
|
||||
padding: 0 0 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.language {
|
||||
&__warning {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child){
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
align-items: stretch;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&__action {
|
||||
align-items: stretch;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: block;
|
||||
margin: 5px 0 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 1px solid $settings-text-input-border;
|
||||
color: $settings-text-input-foreground;
|
||||
background-color: $settings-text-input-background;
|
||||
height: 24px;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-text-input {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.sound {
|
||||
&__select {
|
||||
float: left;
|
||||
width: calc(100% - 75px);
|
||||
|
@ -54,7 +133,7 @@
|
|||
margin-left: 5px;
|
||||
|
||||
background-color: #32384d;
|
||||
color: white;
|
||||
color: white;
|
||||
border: 1px solid #32384d;
|
||||
}
|
||||
|
||||
|
@ -76,3 +155,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.toogle-lock-icon-menu {
|
||||
display: block;
|
||||
padding: 3px 40px;
|
||||
width: 170px;
|
||||
|
||||
float: right;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: white;
|
||||
background-color: #1f2330;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #32384d;
|
||||
}
|
||||
}
|
|
@ -1,37 +1,112 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Howl } from 'howler';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { ToolsService, InstanceType } from '../../../services/tools.service';
|
||||
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
|
||||
import { ServiceWorkerService } from '../../../services/service-worker.service';
|
||||
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum, ILanguage } from '../../../states/settings.state';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { SettingsService } from '../../../services/settings.service';
|
||||
import { LanguageService } from '../../../services/language.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
|
||||
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
notificationSounds: NotificationSoundDefinition[];
|
||||
notificationSoundId: string;
|
||||
notificationForm: FormGroup;
|
||||
|
||||
disableAutofocusEnabled: boolean;
|
||||
disableRemoteStatusFetchingEnabled: boolean;
|
||||
disableAvatarNotificationsEnabled: boolean;
|
||||
disableSoundsEnabled: boolean;
|
||||
disableLangAutodetectEnabled: boolean;
|
||||
enableAltLabelEnabled: boolean;
|
||||
enableFreezeAvatarEnabled: boolean;
|
||||
version: string;
|
||||
|
||||
hasPleromaAccount: boolean;
|
||||
autoFollowOnListEnabled: boolean;
|
||||
|
||||
twitterBridgeEnabled: boolean;
|
||||
|
||||
columnShortcutEnabled: ColumnShortcut = ColumnShortcut.Ctrl;
|
||||
timeLineHeader: TimeLineHeaderEnum = TimeLineHeaderEnum.Title_DomainName;
|
||||
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
|
||||
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
|
||||
|
||||
configuredLangs: ILanguage[] = [];
|
||||
searchedLangs: ILanguage[] = [];
|
||||
searchLang: string;
|
||||
|
||||
private addCwOnContent: string;
|
||||
set setAddCwOnContent(value: string) {
|
||||
this.setCwPolicy(null, value, null, null);
|
||||
this.addCwOnContent = value.trim();
|
||||
}
|
||||
get setAddCwOnContent(): string {
|
||||
return this.addCwOnContent;
|
||||
}
|
||||
|
||||
private removeCwOnContent: string;
|
||||
set setRemoveCwOnContent(value: string) {
|
||||
this.setCwPolicy(null, null, value, null);
|
||||
this.removeCwOnContent = value.trim();
|
||||
}
|
||||
get setRemoveCwOnContent(): string {
|
||||
return this.removeCwOnContent;
|
||||
}
|
||||
|
||||
private contentHidedCompletely: string;
|
||||
set setContentHidedCompletely(value: string) {
|
||||
this.setCwPolicy(null, null, null, value);
|
||||
this.contentHidedCompletely = value.trim();
|
||||
}
|
||||
get setContentHidedCompletely(): string {
|
||||
return this.contentHidedCompletely;
|
||||
}
|
||||
|
||||
private twitterBridgeInstance: string;
|
||||
set setTwitterBridgeInstance(value: string) {
|
||||
let instance = value.replace('https://', '').replace('http://', '').replace('/', '').trim();
|
||||
this.setBridgeInstance(instance);
|
||||
this.twitterBridgeInstance = instance;
|
||||
}
|
||||
get setTwitterBridgeInstance(): string {
|
||||
return this.twitterBridgeInstance;
|
||||
}
|
||||
|
||||
private languageSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private formBuilder: FormBuilder,
|
||||
private serviceWorkersService: ServiceWorkerService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly userNotificationsService: UserNotificationService) { }
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationsService: UserNotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.languageSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
|
||||
if(l){
|
||||
this.configuredLangs = l;
|
||||
}
|
||||
});
|
||||
|
||||
this.version = environment.VERSION;
|
||||
|
||||
const settings = this.toolsService.getSettings();
|
||||
const settings = this.settingsService.getSettings();
|
||||
|
||||
this.notificationSounds = this.userNotificationsService.getAllNotificationSounds();
|
||||
this.notificationSoundId = settings.notificationSoundFileId;
|
||||
|
@ -42,13 +117,162 @@ export class SettingsComponent implements OnInit {
|
|||
this.disableAutofocusEnabled = settings.disableAutofocus;
|
||||
this.disableAvatarNotificationsEnabled = settings.disableAvatarNotifications;
|
||||
this.disableSoundsEnabled = settings.disableSounds;
|
||||
this.disableRemoteStatusFetchingEnabled = settings.disableRemoteStatusFetching;
|
||||
|
||||
if (!settings.columnSwitchingWinAlt) {
|
||||
this.columnShortcutEnabled = ColumnShortcut.Ctrl;
|
||||
} else {
|
||||
this.columnShortcutEnabled = ColumnShortcut.Win;
|
||||
}
|
||||
|
||||
this.contentWarningPolicy = settings.contentWarningPolicy.policy;
|
||||
this.addCwOnContent = settings.contentWarningPolicy.addCwOnContent.join(';');
|
||||
this.removeCwOnContent = settings.contentWarningPolicy.removeCwOnContent.join(';');
|
||||
this.contentHidedCompletely = settings.contentWarningPolicy.hideCompletlyContent.join(';');
|
||||
|
||||
this.timeLineHeader = settings.timelineHeader;
|
||||
this.timeLineMode = settings.timelineMode;
|
||||
|
||||
this.autoFollowOnListEnabled = settings.autoFollowOnListEnabled;
|
||||
const accs = this.toolsService.getAllAccounts();
|
||||
accs.forEach(a => {
|
||||
this.toolsService.getInstanceInfo(a)
|
||||
.then(ins => {
|
||||
if(ins.type === InstanceType.Pleroma){
|
||||
this.hasPleromaAccount = true;
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
});
|
||||
|
||||
this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
|
||||
this.twitterBridgeInstance = settings.twitterBridgeInstance;
|
||||
|
||||
this.configuredLangs = this.languageService.getConfiguredLanguages();
|
||||
this.disableLangAutodetectEnabled = settings.disableLangAutodetec;
|
||||
this.enableAltLabelEnabled = settings.enableAltLabel;
|
||||
this.enableFreezeAvatarEnabled = settings.enableFreezeAvatar;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.languageSub) this.languageSub.unsubscribe();
|
||||
}
|
||||
|
||||
iconMenuLocked = true;
|
||||
toogleLockIconMenu(): boolean {
|
||||
this.navigationService.changeIconMenuState(this.iconMenuLocked);
|
||||
this.iconMenuLocked = ! this.iconMenuLocked;
|
||||
return false;
|
||||
}
|
||||
|
||||
onSearchLang(input: string) {
|
||||
this.searchedLangs = this.languageService.searchLanguage(input);
|
||||
}
|
||||
|
||||
onAddLang(lang: ILanguage): boolean {
|
||||
if(this.configuredLangs.findIndex(x => x.iso639 === lang.iso639) >= 0) return false;
|
||||
|
||||
// this.configuredLangs.push(lang);
|
||||
this.languageService.addLanguage(lang);
|
||||
|
||||
this.searchLang = '';
|
||||
this.searchedLangs.length = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onRemoveLang(lang: ILanguage): boolean {
|
||||
// this.configuredLangs = this.configuredLangs.filter(x => x.iso639 !== lang.iso639);
|
||||
this.languageService.removeLanguage(lang);
|
||||
return false;
|
||||
}
|
||||
|
||||
onShortcutChange(id: ColumnShortcut) {
|
||||
this.columnShortcutEnabled = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.columnSwitchingWinAlt = id === ColumnShortcut.Win;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onTimeLineHeaderChange(id: TimeLineHeaderEnum){
|
||||
this.timeLineHeader = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.timelineHeader = id;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onTimeLineModeChange(id: TimeLineModeEnum){
|
||||
this.timeLineMode = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.timelineMode = id;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onCwPolicyChange(id: ContentWarningPolicyEnum) {
|
||||
this.contentWarningPolicy = id;
|
||||
this.notifyRestartNeeded();
|
||||
|
||||
this.setCwPolicy(id);
|
||||
}
|
||||
|
||||
private setCwPolicy(id: ContentWarningPolicyEnum = null, addCw: string = null, removeCw: string = null, hide: string = null){
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
let cwPolicySettings = new ContentWarningPolicy();
|
||||
|
||||
if(id !== null){
|
||||
cwPolicySettings.policy = id;
|
||||
} else {
|
||||
cwPolicySettings.policy = settings.contentWarningPolicy.policy;
|
||||
}
|
||||
|
||||
if(addCw !== null){
|
||||
cwPolicySettings.addCwOnContent = this.splitCwValues(addCw);
|
||||
} else {
|
||||
cwPolicySettings.addCwOnContent = settings.contentWarningPolicy.addCwOnContent;
|
||||
}
|
||||
|
||||
if(removeCw !== null){
|
||||
cwPolicySettings.removeCwOnContent = this.splitCwValues(removeCw);
|
||||
} else {
|
||||
cwPolicySettings.removeCwOnContent = settings.contentWarningPolicy.removeCwOnContent;
|
||||
}
|
||||
|
||||
if(hide !== null){
|
||||
cwPolicySettings.hideCompletlyContent = this.splitCwValues(hide);
|
||||
} else {
|
||||
cwPolicySettings.hideCompletlyContent = settings.contentWarningPolicy.hideCompletlyContent;
|
||||
}
|
||||
|
||||
this.settingsService.saveContentWarningPolicy(cwPolicySettings);
|
||||
}
|
||||
|
||||
private splitCwValues(data: string): string[]{
|
||||
return data.split(';').map(x => x.trim().toLowerCase()).filter((value, index, self) => self.indexOf(value) === index).filter(y => y !== '');
|
||||
}
|
||||
|
||||
private setBridgeInstance(instance: string){
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.twitterBridgeInstance = instance;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
// reload(): boolean {
|
||||
// window.location.reload();
|
||||
// return false;
|
||||
// }
|
||||
|
||||
onChange(soundId: string) {
|
||||
this.notificationSoundId = soundId;
|
||||
let settings = this.toolsService.getSettings()
|
||||
let settings = this.settingsService.getSettings()
|
||||
settings.notificationSoundFileId = soundId;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
playNotificationSound(): boolean {
|
||||
|
@ -62,22 +286,64 @@ export class SettingsComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
onDisableAutofocusChanged(){
|
||||
let settings = this.toolsService.getSettings();
|
||||
onEnableFreezeAvatarChanged(){
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.enableFreezeAvatar = this.enableFreezeAvatarEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onEnableAltLabelChanged(){
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.enableAltLabel = this.enableAltLabelEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableLangAutodetectChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableLangAutodetec = this.disableLangAutodetectEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableAutofocusChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableAutofocus = this.disableAutofocusEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableAvatarNotificationsChanged(){
|
||||
let settings = this.toolsService.getSettings();
|
||||
onDisableRemoteStatusFetchingChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableRemoteStatusFetching = this.disableRemoteStatusFetchingEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableAvatarNotificationsChanged() {
|
||||
this.notifyRestartNeeded();
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableAvatarNotifications = this.disableAvatarNotificationsEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onDisableSoundsEnabledChanged(){
|
||||
let settings = this.toolsService.getSettings();
|
||||
onDisableSoundsEnabledChanged() {
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.disableSounds = this.disableSoundsEnabled;
|
||||
this.toolsService.saveSettings(settings);
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onAutoFollowOnListChanged(){
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.autoFollowOnListEnabled = this.autoFollowOnListEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
onTwitterBridgeEnabledChanged(){
|
||||
let settings = this.settingsService.getSettings();
|
||||
settings.twitterBridgeEnabled = this.twitterBridgeEnabled;
|
||||
this.settingsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
isCleanningAll: boolean = false;
|
||||
|
@ -86,7 +352,7 @@ export class SettingsComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
confirmClearAll(): boolean{
|
||||
confirmClearAll(): boolean {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
return false;
|
||||
|
@ -97,4 +363,31 @@ export class SettingsComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
isCheckingUpdates = false;
|
||||
checkForUpdates(): boolean {
|
||||
this.isCheckingUpdates = true;
|
||||
this.serviceWorkersService.checkForUpdates()
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
this.isCheckingUpdates = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
notifyRestartNeeded(){
|
||||
this.notificationService.notifyRestartNotification('Reload to apply changes');
|
||||
}
|
||||
|
||||
openTutorial(): boolean {
|
||||
localStorage.setItem('tutorial', JSON.stringify(false));
|
||||
this.navigationService.closePanel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum ColumnShortcut {
|
||||
Ctrl = 1,
|
||||
Win = 2
|
||||
}
|
|
@ -8,27 +8,36 @@
|
|||
<fa-icon [icon]="faSearch"></fa-icon>
|
||||
</a>
|
||||
|
||||
<div *ngFor="let account of accounts">
|
||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||
</app-account-icon>
|
||||
<div *ngIf="!iconMenuIsDraggable">
|
||||
<div *ngFor="let account of accounts">
|
||||
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
|
||||
(openMenuNotify)="onOpenMenuNotify($event)">
|
||||
</app-account-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="iconMenuIsDraggable" cdkDropList [cdkDropListData]="accounts" (cdkDropListDropped)="onDrop($event)">
|
||||
<div *ngFor="let account of accounts" cdkDrag class="draggable">
|
||||
<fa-icon class="draggable__icon" [icon]="faArrowsAltV"></fa-icon>
|
||||
<img class="draggable__avatar" src="{{ account.avatar }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }"
|
||||
href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
|
||||
<fa-icon [icon]="faPlus"></fa-icon>
|
||||
</a>
|
||||
|
||||
|
||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
|
||||
*ngIf="hasAccounts && hasScheduledStatuses"
|
||||
(click)="openScheduledStatuses()"
|
||||
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href
|
||||
title="scheduled statuses" *ngIf="hasAccounts && hasScheduledStatuses" (click)="openScheduledStatuses()"
|
||||
(contextmenu)="openScheduledStatuses()">
|
||||
<fa-icon [icon]="faCalendarAlt"></fa-icon>
|
||||
</a>
|
||||
|
||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
|
||||
(contextmenu)="openSettings()">
|
||||
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings"
|
||||
(click)="openSettings()" (contextmenu)="openSettings()">
|
||||
<fa-icon [icon]="faCog"></fa-icon>
|
||||
</a>
|
||||
</div>
|
|
@ -82,4 +82,38 @@ $height-button: 40px;
|
|||
.no-accounts {
|
||||
padding-top: 10px;
|
||||
// color: cornflowerblue;
|
||||
}
|
||||
|
||||
|
||||
$draggable-accent-color: #47e927;
|
||||
// $draggable-accent-color: #a8ff97;
|
||||
.draggable {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
|
||||
border: 2px solid #df0adf;
|
||||
border: 2px solid $draggable-accent-color;
|
||||
border-radius: 2px;
|
||||
|
||||
position: relative;
|
||||
|
||||
&__avatar {
|
||||
width: calc(100%);
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
|
||||
float: left;
|
||||
z-index: 5;
|
||||
color:$draggable-accent-color;
|
||||
|
||||
top: 6px;
|
||||
left: 12px;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
|
||||
import { Subscription, Observable } from "rxjs";
|
||||
import { Store } from "@ngxs/store";
|
||||
import { faPlus, faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faCog, faSearch, faArrowsAltV } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCommentAlt, faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
||||
|
||||
import { AccountWrapper } from "../../models/account.models";
|
||||
import { AccountInfo, SelectAccount } from "../../states/accounts.state";
|
||||
import { AccountInfo, ReorderAccounts, SelectAccount } from "../../states/accounts.state";
|
||||
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
|
||||
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { ScheduledStatusService, ScheduledStatusNotification } from '../../services/scheduled-status.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: "app-left-side-bar",
|
||||
|
@ -23,6 +25,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
faPlus = faPlus;
|
||||
faCog = faCog;
|
||||
faCalendarAlt = faCalendarAlt;
|
||||
faArrowsAltV = faArrowsAltV;
|
||||
|
||||
accounts: AccountWithNotificationWrapper[] = [];
|
||||
hasAccounts: boolean;
|
||||
|
@ -32,8 +35,10 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
private accountSub: Subscription;
|
||||
private scheduledSub: Subscription;
|
||||
private notificationSub: Subscription;
|
||||
private draggableIconMenuSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly hotkeysService: HotkeysService,
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly toolsService: ToolsService,
|
||||
|
@ -101,7 +106,13 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
iconMenuIsDraggable = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.draggableIconMenuSub = this.navigationService.enableDraggableIconMenu.subscribe(x => {
|
||||
this.iconMenuIsDraggable = x;
|
||||
});
|
||||
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
if (accounts) {
|
||||
//Update and Add
|
||||
|
@ -133,7 +144,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.notificationSub = this.userNotificationServiceService.userNotifications.subscribe((notifications: UserNotification[]) => {
|
||||
const settings = this.toolsService.getSettings();
|
||||
const settings = this.settingsService.getSettings();
|
||||
notifications.forEach((notification: UserNotification) => {
|
||||
const acc = this.accounts.find(x => x.info.id === notification.account.id);
|
||||
|
||||
|
@ -162,6 +173,17 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
this.accountSub.unsubscribe();
|
||||
this.notificationSub.unsubscribe();
|
||||
this.scheduledSub.unsubscribe();
|
||||
this.draggableIconMenuSub.unsubscribe();
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<AccountWithNotificationWrapper[]>) {
|
||||
if (event.previousContainer === event.container) {
|
||||
moveItemInArray(event.container.data,
|
||||
event.previousIndex,
|
||||
event.currentIndex);
|
||||
|
||||
this.store.dispatch([new ReorderAccounts(this.accounts.map(x => x.info))])
|
||||
}
|
||||
}
|
||||
|
||||
onToogleAccountNotify(acc: AccountWrapper) {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<img *ngIf="att.type === 'image'" src="{{att.url}}" class="media-viewer-canvas__image" [class.collapsed]="currentIndex !== att.index" />
|
||||
</a>
|
||||
|
||||
<video *ngIf="att.type === 'gifv'" class="media-viewer-canvas__image" role="application" loop autoplay>
|
||||
<video *ngIf="att.type === 'gifv'" class="media-viewer-canvas__image" role="application" loop autoplay [class.collapsed]="currentIndex !== att.index">
|
||||
<source src="{{att.url}}" type="video/mp4">
|
||||
</video>
|
||||
<video *ngIf="att.type === 'video'" class="media-viewer-canvas__image" role="application" loop autoplay
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener, OnDestroy } from '@angular/core';
|
||||
import { SafeHtml } from '@angular/platform-browser';
|
||||
import { faTimes, faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Subject } from 'rxjs';
|
||||
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
||||
|
||||
import { OpenMediaEvent } from '../../models/common.model';
|
||||
import { Attachment, PleromaAttachment } from '../../services/models/mastodon.interfaces';
|
||||
|
@ -12,7 +13,7 @@ import { Attachment, PleromaAttachment } from '../../services/models/mastodon.in
|
|||
templateUrl: './media-viewer.component.html',
|
||||
styleUrls: ['./media-viewer.component.scss']
|
||||
})
|
||||
export class MediaViewerComponent implements OnInit {
|
||||
export class MediaViewerComponent implements OnInit, OnDestroy {
|
||||
private _mediaEvent: OpenMediaEvent;
|
||||
faTimes = faTimes;
|
||||
faAngleLeft = faAngleLeft;
|
||||
|
@ -64,9 +65,19 @@ export class MediaViewerComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
private escapeHotkey = new Hotkey('escape', (event: KeyboardEvent): boolean => {
|
||||
this.close();
|
||||
return false;
|
||||
});
|
||||
|
||||
constructor(private readonly hotkeysService: HotkeysService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.hotkeysService.add(this.escapeHotkey);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.hotkeysService.remove(this.escapeHotkey);
|
||||
}
|
||||
|
||||
private setBrowsing() {
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
<img class="notification-hub__notification--avatar" *ngIf="notification.avatar"
|
||||
src="{{ notification.avatar }}" />
|
||||
|
||||
<div class="notification-hub__notification--message">
|
||||
<div class="notification-hub__notification--message" [class.notification-hub__notification--message--no-avatar]="!notification.avatar">
|
||||
<span *ngIf="!notification.message">Error {{ notification.errorCode }}</span>
|
||||
<span *ngIf="notification.message">{{ notification.message }}</span>
|
||||
<span class="notification-hub__notification--count" *ngIf="notification.count > 1"> ({{ notification.count}})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -27,7 +27,17 @@
|
|||
}
|
||||
|
||||
&--message {
|
||||
padding-top: 4px;
|
||||
margin-left: 37px;
|
||||
|
||||
&--no-avatar {
|
||||
margin-left: 0px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&--count {
|
||||
color: rgba(255, 255, 255, .6);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { NotificationService, NotificatioData } from '../../services/notification.service';
|
||||
import { NotificationService, NotificationData } from '../../services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notification-hub',
|
||||
|
@ -7,17 +7,26 @@ import { NotificationService, NotificatioData } from '../../services/notificatio
|
|||
styleUrls: ['./notification-hub.component.scss']
|
||||
})
|
||||
export class NotificationHubComponent implements OnInit {
|
||||
notifications: NotificatioData[] = [];
|
||||
notifications: NotificationWrapper[] = [];
|
||||
|
||||
constructor(private notificationService: NotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.notificationService.notifactionStream.subscribe((notification: NotificatioData) => {
|
||||
this.notifications.push(notification);
|
||||
this.notificationService.notifactionStream.subscribe((notification: NotificationData) => {
|
||||
let alreadyExistingNotification = this.notifications.find(x => x.avatar === notification.avatar && x.message === notification.message);
|
||||
|
||||
setTimeout(() => {
|
||||
this.notifications = this.notifications.filter(x => x.id !== notification.id);
|
||||
}, 5000);
|
||||
if(alreadyExistingNotification){
|
||||
alreadyExistingNotification.count++;
|
||||
} else{
|
||||
this.notifications.push(new NotificationWrapper(notification));
|
||||
if(this.notifications.length > 3){
|
||||
this.notifications.length = 3;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.notifications = this.notifications.filter(x => x.id !== notification.id);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
//this.autoSubmit();
|
||||
|
@ -31,7 +40,15 @@ export class NotificationHubComponent implements OnInit {
|
|||
// }, 1500);
|
||||
// }
|
||||
|
||||
onClick(notification: NotificatioData): void{
|
||||
onClick(notification: NotificationData): void{
|
||||
this.notifications = this.notifications.filter(x => x.id !== notification.id);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationWrapper extends NotificationData {
|
||||
constructor(data: NotificationData) {
|
||||
super(data.avatar, data.errorCode, data.message, data.isError);
|
||||
}
|
||||
|
||||
count = 1;
|
||||
}
|
|
@ -2,15 +2,16 @@
|
|||
<div class="hashtag-header">
|
||||
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
|
||||
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
|
||||
|
||||
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board">add column</button>
|
||||
<button *ngIf="isHashtagFollowingAvailable && !isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="followThisHashtag($event)" title="follow hashtag" [disabled]="followingLoading">follow</button>
|
||||
<button *ngIf="isHashtagFollowingAvailable && isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="unfollowThisHashtag($event)" title="unfollow hashtag" [disabled]="unfollowingLoading">unfollow</button>
|
||||
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board" [hidden]="columnAdded">add column</button>
|
||||
</a>
|
||||
</div>
|
||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||
[streamElement]="hashtagElement"
|
||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||
[streamElement]="hashtagElement"
|
||||
[goToTop]="goToTopSubject.asObservable()"
|
||||
[userLocked]="false"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
|
||||
</div>
|