sforkowany z mirror/soapbox
Porównaj commity
472 Commity
Autor | SHA1 | Data |
---|---|---|
miklobit | 7dc1cca40c | |
Chewbacca | b277143e3a | |
Chewbacca | eeafb3073e | |
Chewbacca | ccd97e1405 | |
Chewbacca | a994d1c33e | |
Chewbacca | 681eacf827 | |
Chewbacca | 4b3b601659 | |
Chewbacca | 697791fc5d | |
Alex Gleason | a976b542e1 | |
Chewbacca | e32ea32f15 | |
Chewbacca | ff3c0c5cd7 | |
Chewbacca | 01458c3003 | |
Alex Gleason | 453420796b | |
Alex Gleason | e47b9300f0 | |
Alex Gleason | ddf433a5c9 | |
Alex Gleason | 79a33d0f1d | |
Alex Gleason | 9e60d90812 | |
Alex Gleason | f3727440ff | |
Alex Gleason | eb055339d8 | |
Alex Gleason | 9c78a37844 | |
Alex Gleason | 8e6dfe6395 | |
Alex Gleason | 7ec51778f8 | |
Alex Gleason | eb6c82a867 | |
Alex Gleason | bfd40fa373 | |
Chewbacca | 455030ef5b | |
Chewbacca | 4886548889 | |
Chewbacca | 85e5780645 | |
Chewbacca | 20960d7238 | |
Alex Gleason | 319d47b36f | |
Alex Gleason | 609a25fd8d | |
marcin mikołajczak | 55c0f8d6a1 | |
Chewbacca | 1e69812078 | |
Chewbacca | 89ab02224f | |
Chewbacca | af9439f1d3 | |
Chewbacca | a916056367 | |
Chewbacca | 9fe2d4f92c | |
Chewbacca | ca0987dec2 | |
Chewbacca | 6458ebbb11 | |
Chewbacca | 966fcc617a | |
Chewbacca | e12450ee5d | |
Chewbacca | 29c913859e | |
Chewbacca | be4a7e45e9 | |
Chewbacca | 1abcc95a0a | |
Chewbacca | e6252070a6 | |
Alex Gleason | f2744cdf98 | |
Chewbacca | 3a2b4c6efb | |
Alex Gleason | 232e387a50 | |
Alex Gleason | d39e2cc7e0 | |
Alex Gleason | 0522f333c3 | |
Alex Gleason | 22474e3ca9 | |
Alex Gleason | f216b52b36 | |
Alex Gleason | 09ed0bccab | |
Alex Gleason | 63394bb1b4 | |
Alex Gleason | d08a2e215b | |
Alex Gleason | 3b8f43f02d | |
Alex Gleason | 9367b16200 | |
Alex Gleason | 6f705a827e | |
Chewbacca | ff5eb736fc | |
marcin mikołajczak | f6cee79c0e | |
marcin mikołajczak | bf8c454c23 | |
marcin mikołajczak | 12f3b4fbc3 | |
marcin mikołajczak | 4bee42f86d | |
marcin mikołajczak | 96a8bcdf82 | |
marcin mikołajczak | a45be78b97 | |
marcin mikołajczak | 4990e1eaa7 | |
marcin mikołajczak | 52172c923f | |
marcin mikołajczak | 01359ca592 | |
Alex Gleason | 7c1182bfb3 | |
Alex Gleason | 818b10efc3 | |
Alex Gleason | a530ec0202 | |
Alex Gleason | 9d12173b87 | |
Alex Gleason | aa7e2f6965 | |
Alex Gleason | 45c12e9b65 | |
Alex Gleason | 7248331742 | |
Alex Gleason | ac9718e6ed | |
Alex Gleason | 1b569b6c82 | |
Alex Gleason | b4c3248791 | |
Alex Gleason | 1c5a6d8b41 | |
Alex Gleason | 50f65bc7c9 | |
Chewbacca | 30ef70440f | |
Alex Gleason | ad3f8acbe5 | |
Alex Gleason | 948d66bcab | |
Alex Gleason | 4783a41b78 | |
Alex Gleason | 1949651b9a | |
Alex Gleason | 75b0262f9a | |
Alex Gleason | 2674c060ad | |
Alex Gleason | 6929975aaa | |
Alex Gleason | 402daec9c3 | |
Alex Gleason | c5b1f23bda | |
Alex Gleason | c4d0dd568e | |
Alex Gleason | f016ac1e6d | |
Alex Gleason | cb8363d179 | |
Alex Gleason | e2510489c5 | |
Alex Gleason | a256665aad | |
Alex Gleason | 1eed61c386 | |
Alex Gleason | b47cdb368f | |
Alex Gleason | 61fb434a54 | |
Alex Gleason | 8f67d2c76f | |
Alex Gleason | d2fd9e0387 | |
Alex Gleason | b127025167 | |
Alex Gleason | b76559f24a | |
Alex Gleason | 3d72e6305f | |
Alex Gleason | 4049de50aa | |
Alex Gleason | ee1b1b4397 | |
Alex Gleason | 1954848c65 | |
Alex Gleason | 71c7c4adc7 | |
Alex Gleason | cc2fbc0208 | |
Chewbacca | f7a964e6ee | |
Chewbacca | 32ca5f09ee | |
Chewbacca | ad98bf45cc | |
Alex Gleason | 65070f6519 | |
Alex Gleason | ec72ac0db8 | |
Alex Gleason | be32a0c1a0 | |
Alex Gleason | 69d667d6c6 | |
Alex Gleason | be7a462fc4 | |
Alex Gleason | f61e0d889a | |
Alex Gleason | cc3585f319 | |
Alex Gleason | f369a7c765 | |
Alex Gleason | a8be701ea0 | |
Alex Gleason | 2196d9e3e5 | |
Alex Gleason | fb8d543f7c | |
Alex Gleason | b87af6a71c | |
Alex Gleason | 3a12b316d9 | |
Alex Gleason | 9ca384dcd7 | |
Alex Gleason | 4f5866d43f | |
Alex Gleason | 5774516ea0 | |
Alex Gleason | d4e9fddd02 | |
Alex Gleason | 7a06c7f92c | |
Alex Gleason | 28f5a88848 | |
Alex Gleason | 4de8926445 | |
Alex Gleason | f9ab9a45c2 | |
Alex Gleason | fe64f9f84b | |
Alex Gleason | 7c7855e7a1 | |
Alex Gleason | 1d9ed41fec | |
marcin mikołajczak | 282afaa47f | |
marcin mikołajczak | 7329c0bf25 | |
Alex Gleason | ca9a41f102 | |
Chewbacca | babaa979f5 | |
Alex Gleason | 143a9eda44 | |
Alex Gleason | f6b28dd9c3 | |
Alex Gleason | 3c06ba734b | |
Alex Gleason | d08178f5fc | |
Alex Gleason | 28a69ad88b | |
Chewbacca | e46a7e8f4a | |
Chewbacca | 8b478c939a | |
Alex Gleason | 396f6ada1a | |
Chewbacca | e42e0577f4 | |
Chewbacca | 4a6433433f | |
Chewbacca | 4985db7dea | |
Chewbacca | 89bdc9b4a1 | |
Alex Gleason | ea4f707413 | |
Alex Gleason | bc457b61d1 | |
Alex Gleason | f8b20858a3 | |
Alex Gleason | 67ffe9609f | |
Alex Gleason | 47561e5c01 | |
Alex Gleason | 5c7c0ea1dd | |
Alex Gleason | 2b75dcacd2 | |
Alex Gleason | 9dc12f0994 | |
Alex Gleason | f7c7b6ce6c | |
Alex Gleason | da7284212e | |
Alex Gleason | 813fd7f8ee | |
marcin mikołajczak | b62db891fe | |
marcin mikołajczak | 9c80a50b95 | |
marcin mikołajczak | 8634c5f91e | |
marcin mikołajczak | 049554db84 | |
marcin mikołajczak | 4124b85ede | |
marcin mikołajczak | 38c99dbc48 | |
marcin mikołajczak | 09a0a36935 | |
marcin mikołajczak | 179eb7fc99 | |
marcin mikołajczak | 8b81838f2f | |
Alex Gleason | c0a22205f7 | |
Alex Gleason | 3a94736285 | |
Alex Gleason | e6621a802b | |
Alex Gleason | 181bf23c34 | |
Alex Gleason | c51870af6e | |
Alex Gleason | 1518e88904 | |
Alex Gleason | 5871abd786 | |
Alex Gleason | 7fffe59fb9 | |
Alex Gleason | 1ab9b1d75c | |
Chewbacca | 709edaefad | |
Chewbacca | 41ab40485e | |
Alex Gleason | a83cfe7ddd | |
Chewbacca | 0fe48840e1 | |
Alex Gleason | 74ebd560e6 | |
Alex Gleason | 602a670b2e | |
Alex Gleason | 79aa8e210f | |
Chewbacca | 6b30671875 | |
Chewbacca | a99a7b2af5 | |
Chewbacca | 9dde71716f | |
Chewbacca | 20ccd26a6e | |
Chewbacca | 283935e837 | |
gallegonovato | bbda49a31c | |
Poesty Li | 471f275dcb | |
Hosted Weblate | c5a548fa83 | |
Poesty Li | e28e12cd60 | |
gallegonovato | c9bd4291bb | |
Chewbacca | af69c7564e | |
Alex Gleason | 822ab987a1 | |
Alex Gleason | 463dcd2c1e | |
Chewbacca | c6c12fa60f | |
Alex Gleason | ac76af41b2 | |
Chewbacca | c8a4d63fc8 | |
Chewbacca | 7070630eaf | |
Chewbacca | 821b90c372 | |
Chewbacca | 1b542c3ed7 | |
Chewbacca | f4d2f42c01 | |
Chewbacca | 7be8218f0c | |
Alex Gleason | 50dadeb1b8 | |
Chewbacca | 99b7a1bdd7 | |
Chewbacca | 1d53f48904 | |
Chewbacca | 9d1c2df1a2 | |
Chewbacca | 08f97a133e | |
Chewbacca | 8a36561ec8 | |
Alex Gleason | 11d06e6b6e | |
Alex Gleason | 8f8807eb76 | |
Soapbox Bot | 3b1f1cf789 | |
Chewbacca | 7e74e215cc | |
Alex Gleason | fafd27d79a | |
Chewbacca | 3ca168dc8c | |
Alex Gleason | 6ac57910bf | |
Chewbacca | e173418041 | |
Chewbacca | 39d61eabda | |
Alex Gleason | 9df2bb4a86 | |
Alex Gleason | b93a299009 | |
Alex Gleason | 8547aeb517 | |
marcin mikołajczak | 8c6ce74c46 | |
Alex Gleason | a19b1e83a9 | |
Alex Gleason | e9ae8d2c45 | |
Alex Gleason | d0ceac9987 | |
Alex Gleason | d12078a687 | |
marcin mikołajczak | 61ece4d271 | |
Chewbacca | bced3d6632 | |
Chewbacca | 879ac883aa | |
Soapbox Bot | fadceaac45 | |
Alex Gleason | d747e323c6 | |
Alex Gleason | c3728dbddd | |
Alex Gleason | 1922e889f7 | |
Chewbacca | 58527b0656 | |
Chewbacca | 737c43d847 | |
Chewbacca | f21f72461a | |
Chewbacca | 83532aedba | |
Chewbacca | cced90a780 | |
Poesty Li | 466cf91b31 | |
Hosted Weblate | c3dd9515bf | |
Poesty Li | bd7710677f | |
gallegonovato | 8baecde992 | |
Hosted Weblate | 43a204db50 | |
Alex Gleason | a40222c2de | |
Alex Gleason | 607e6b1808 | |
Chewbacca | a9b79f72b4 | |
Chewbacca | a0c67c9b6f | |
Alex Gleason | 487604b15a | |
Alex Gleason | 5278c8eb0f | |
Chewbacca | 55bacbbf05 | |
Chewbacca | a71aaca719 | |
Chewbacca | bd4c99b697 | |
Chewbacca | 7ad2696f85 | |
Chewbacca | 287fda6d6c | |
Chewbacca | e7b3af5260 | |
Chewbacca | 82da9ceeeb | |
marcin mikołajczak | 5148d913a7 | |
marcin mikołajczak | ea6be269bb | |
Alex Gleason | ef10e2a699 | |
Alex Gleason | ccec7f43e5 | |
Alex Gleason | bd49417210 | |
Alex Gleason | 2b137c12cf | |
Alex Gleason | de89a438cc | |
Alex Gleason | 4031e4624c | |
Alex Gleason | 1af67c3a25 | |
Alex Gleason | 6a2c64ae45 | |
Alex Gleason | 3d2331d20b | |
Alex Gleason | 5e8c92ed4d | |
Alex Gleason | 6be8d4d46e | |
Alex Gleason | 37f5b35aab | |
Alex Gleason | a0c1bd84c9 | |
Alex Gleason | cf541e83b3 | |
marcin mikołajczak | b1471be142 | |
marcin mikołajczak | 51524118d4 | |
Alex Gleason | c55154daaf | |
Alex Gleason | 00c00b31fc | |
Alex Gleason | 074c3429cd | |
rurai10 | 29b75a29f0 | |
Alex Gleason | be9d922047 | |
Alex Gleason | 14a84e557c | |
Alex Gleason | fa2884c11b | |
marcin mikołajczak | 1d64f934d9 | |
Alex Gleason | ad583c89f8 | |
marcin mikołajczak | 94d248fb79 | |
Alex Gleason | a3b1f541bc | |
Alex Gleason | d883f2f5bd | |
Alex Gleason | 250b009635 | |
Alex Gleason | 9964491da5 | |
Alex Gleason | 8923e7b5d0 | |
Alex Gleason | 4c6d13e4ef | |
Soapbox Bot | 31351700e0 | |
Alex Gleason | f2983b96fb | |
Alex Gleason | c492af7042 | |
Tassoman | ae1287ae82 | |
Alex Gleason | 68144e4f82 | |
Alex Gleason | bed26727c1 | |
oakes | 332be25784 | |
Alex Gleason | a8b0dc93f2 | |
Alex Gleason | d1531b832d | |
Alex Gleason | c75ce58792 | |
Alex Gleason | 01343bbe0a | |
Alex Gleason | 2a9f05a765 | |
Alex Gleason | d7f5e210d8 | |
Alex Gleason | 925509a985 | |
Alex Gleason | 63df638630 | |
Alex Gleason | 08014a678d | |
gallegonovato | b9ac2084c2 | |
marcin mikołajczak | d969c91c76 | |
Poesty Li | 334f93f2b1 | |
Poesty Li | 1d94d2ec9f | |
Poesty Li | 57da9102cf | |
Hosted Weblate | f9523d7afd | |
Poesty Li | 050f4130bb | |
Isabell De Inschnitzel | 3ef34b1695 | |
Poesty Li | aca7bb63a5 | |
Hosted Weblate | 41a1923ca5 | |
marcin mikołajczak | d2ca2da10a | |
Tassoman | e7e3ef86a3 | |
Simen | f2eaccf0f2 | |
Simen | e87bb3d0ae | |
ruine | 4e9e85a60b | |
ruine | 905c1a47ac | |
marcin mikołajczak | 4e779742fc | |
Tassoman | 3b3a7653b1 | |
Oukiki Saleh | 1e6be8fa0f | |
Oukiki Saleh | d6d7398576 | |
Oukiki Saleh | edf3b093af | |
gallegonovato | 95f4aee356 | |
Mr.Narsus | f8e05013fd | |
Oukiki Saleh | 08b59ea58f | |
Mr.Narsus | 483aa96b33 | |
Oukiki Saleh | fce046df8f | |
Oukiki Saleh | 5ab98da220 | |
Mr.Narsus | 4e8130667a | |
Mr.Narsus | 725750ca7d | |
Oukiki Saleh | 3f13f8e370 | |
Tassoman | 605ea95ee4 | |
Oukiki Saleh | f9541b5c52 | |
Poesty Li | 63a42aac67 | |
Poesty Li | fb1c36b1c8 | |
Alex Gleason | ca9e59b56b | |
Alex Gleason | 65d1c66aad | |
Alex Gleason | d16bce0ecc | |
Chewbacca | 4e2213aba8 | |
Chewbacca | 721b5dafcd | |
Chewbacca | 31653a5a54 | |
Chewbacca | bdaf6e4cd6 | |
Soapbox Bot | 266dd3d110 | |
Alex Gleason | a8c4be01ec | |
marcin mikołajczak | 1d4d9c2732 | |
Soapbox Bot | 3c336cbeb4 | |
marcin mikołajczak | b54a466bfd | |
marcin mikołajczak | af314ee55d | |
marcin mikołajczak | ebe4f9373b | |
marcin mikołajczak | 4c92f581c4 | |
marcin mikołajczak | 4200fa2df4 | |
marcin mikołajczak | c9f2cc0ae2 | |
marcin mikołajczak | 1f6328c9c6 | |
Chewbacca | 3cc4f8b64b | |
Alex Gleason | 55c8887a08 | |
Alex Gleason | d3d363e12f | |
marcin mikołajczak | 2ce98055d8 | |
marcin mikołajczak | eb93cb39fd | |
Chewbacca | 8fd3b99887 | |
Chewbacca | 01dfbad7bc | |
Chewbacca | 0b7a2ac19b | |
Chewbacca | d6d7807807 | |
Alex Gleason | aad7df89a5 | |
oakes | f1a14efc58 | |
oakes | 1b00de14a6 | |
Chewbacca | 5acc231cda | |
marcin mikołajczak | 277045c7a1 | |
marcin mikołajczak | d6732955de | |
marcin mikołajczak | 9aef41eab1 | |
marcin mikołajczak | e6d6ac6d44 | |
Alex Gleason | f450c42048 | |
marcin mikołajczak | dfa5d3ec8e | |
Alex Gleason | 18bcd1c084 | |
Alex Gleason | a6c5d468f4 | |
Chewbacca | e7897228c6 | |
Alex Gleason | 83dab22371 | |
Chewbacca | 0414c36a1e | |
marcin mikołajczak | 01a4e7370f | |
marcin mikołajczak | 2bbbcd625e | |
Alex Gleason | b7c1d7d44a | |
Alex Gleason | 1b020b2a9b | |
marcin mikołajczak | 528acb8ac5 | |
marcin mikołajczak | 5a90bb4456 | |
marcin mikołajczak | 28d84dedff | |
Alex Gleason | 6a3618da9a | |
Alex Gleason | 02cf175e1f | |
Alex Gleason | dc8952ad18 | |
Alex Gleason | 6413bed23f | |
Alex Gleason | 93f873fce9 | |
marcin mikołajczak | eaa060c4bd | |
marcin mikołajczak | 9d6c698d31 | |
miklobit | eb188b0dfa | |
Alex Gleason | d7c9c035a2 | |
Alex Gleason | c6d89fae38 | |
Alex Gleason | 52b24bb013 | |
Alex Gleason | c9285c7abe | |
Alex Gleason | 09baa262f9 | |
Alex Gleason | 10c24f01d9 | |
marcin mikołajczak | b3585bb348 | |
marcin mikołajczak | 81de0014d3 | |
Alex Gleason | 2e27886230 | |
matty | 4b99ab7d35 | |
Soapbox Bot | a6b96c88aa | |
Alex Gleason | ad07305417 | |
Alex Gleason | 445379f180 | |
Alex Gleason | 27500193d8 | |
Alex Gleason | f7bfc40b70 | |
Alex Gleason | 52059f6f37 | |
Alex Gleason | 3b067c6fab | |
Alex Gleason | 8534cabc0a | |
Alex Gleason | a8ebbc15c8 | |
Alex Gleason | 26b5ca8b6e | |
Alex Gleason | 00802b01e9 | |
ewwwwwwww | 3879dad039 | |
ewwwwwwww | a1f0f75f60 | |
ewwwwwwww | b8a335a166 | |
ewwwwwwww | 63ee482982 | |
ewwwwwwww | 3725db9789 | |
ewwwwwwww | 540b28364b | |
ewwwwwwww | 317672e6ec | |
Alex Gleason | b7b0ad06dc | |
ewwwwwwww | 34d77df8d7 | |
ewwwwwwww | d5ca6b58fb | |
ewwwwwwww | 403db1d46e | |
ewwwwwwww | 9770209f00 | |
ewwwwwwww | 2f579839d8 | |
ewwwwwwww | 1249b24ba4 | |
ewwwwwwww | f3c350aeef | |
ewwwwwwww | 8ef60cdf0b | |
ewwwwwwww | 13e76b8022 | |
ewwwwwwww | b1f5902397 | |
ewwwwwwww | a6e56b131c | |
ewwwwwwww | 4a73412142 | |
ewwwwwwww | 16f06cd554 | |
ewwwwwwww | 1962c6544e | |
ewwwwwwww | 84267d3ffe | |
ewwwwwwww | 41eebfdbda | |
ewwwwwwww | 891ce09443 | |
ewwwwwwww | 0de246fcae | |
ewwwwwwww | c183ab0f06 | |
ewwwwwwww | f10001fbfd | |
ewwwwwwww | 9d0a3b7a69 | |
ewwwwwwww | a2fab1285d | |
ewwwwwwww | bd9a33201a | |
ewwwwwwww | 8d8cf53ac4 | |
ewwwwwwww | f08d43ecb3 | |
ewwwwwwww | e2d6a5d41d | |
ewwwwwwww | 485095e502 | |
ewwwwwwww | bfa8331f96 | |
ewwwwwwww | 36d6275107 | |
ewwwwwwww | d98371bf6a | |
ewwwwwwww | 6ded0afc1e | |
ewwwwwwww | ad523574e2 | |
ewwwwwwww | d86b7ba333 | |
ewwwwwwww | 88f5739769 | |
ewwwwwwww | a8e7e10f61 | |
ewwwwwwww | 89bba0e2e3 | |
ewwwwwwww | f8bd30a5f7 | |
ewwwwwwww | eeb30b5492 | |
ewwwwwwww | 4b7876f1a6 | |
ewwwwwwww | 170d3f748e | |
ewwwwwwww | 2727fb8f20 | |
Shevek | 6a3e66bc7e |
|
@ -56,6 +56,7 @@ module.exports = {
|
|||
},
|
||||
polyfills: [
|
||||
'es:all', // core-js
|
||||
'fetch', // not polyfilled, but ignore it
|
||||
'IntersectionObserver', // npm:intersection-observer
|
||||
'Promise', // core-js
|
||||
'ResizeObserver', // npm:resize-observer-polyfill
|
||||
|
@ -260,12 +261,29 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
'@typescript-eslint/no-duplicate-imports': 'error',
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'comma',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
'promise/catch-or-return': 'error',
|
||||
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/classnames-order': [
|
||||
'error',
|
||||
{
|
||||
classRegex: '^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$',
|
||||
config: 'tailwind.config.cjs',
|
||||
},
|
||||
],
|
||||
'tailwindcss/migration-from-tailwind-2': 'error',
|
||||
},
|
||||
overrides: [
|
||||
|
|
|
@ -157,11 +157,11 @@ docker:
|
|||
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
||||
script:
|
||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
||||
- docker build -t $CI_REGISTRY_IMAGE .
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
only:
|
||||
variables:
|
||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
interruptible: false
|
||||
|
||||
release:
|
||||
stage: release
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Posts: Support posts filtering on recent Mastodon versions
|
||||
- Reactions: Support custom emoji reactions
|
||||
- Compatbility: Support Mastodon v2 timeline filters.
|
||||
- Posts: Support dislikes on Friendica.
|
||||
- UI: added a character counter to some textareas.
|
||||
|
||||
### Changed
|
||||
- Posts: truncate Nostr pubkeys in reply mentions.
|
||||
- Posts: upgraded emoji picker component.
|
||||
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
|
||||
|
||||
### Fixed
|
||||
- Posts: fixed emojis being cut off in reactions modal.
|
||||
- Posts: fix audio player progress bar visibility.
|
||||
- Posts: added missing gap in pending status.
|
||||
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
|
||||
- Profile: fix "load more" button height on account gallery page.
|
||||
- 18n: fixed Chinese language being detected from the browser.
|
||||
- Conversations: fixed pagination (Mastodon).
|
||||
- Compatibility: fix version parsing for Friendica.
|
||||
|
||||
## [3.2.0] - 2023-02-15
|
||||
|
||||
### Added
|
||||
|
@ -12,12 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Posts: bot badge on statuses from bot accounts.
|
||||
- Compatibility: improved browser support for older browsers.
|
||||
- Events: allow to repost events in event menu.
|
||||
- Groups: Initial support for groups.
|
||||
- Profile: Add RSS link to user profiles.
|
||||
- Reactions: adds support for reacting to chat messages.
|
||||
- Groups: initial support for groups.
|
||||
- Profile: add RSS link to user profiles.
|
||||
- Posts: fix posts filtering.
|
||||
- Chats: reset chat message field height after sending a message.
|
||||
- Admin: allow to manage announcements.
|
||||
|
||||
|
@ -38,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
|
||||
- Modals: fix media modal automatically switching to video.
|
||||
- Navigation: profile dropdown erratic behavior.
|
||||
- Posts: fix posts filtering.
|
||||
|
||||
### Removed
|
||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
||||
|
|
|
@ -75,7 +75,7 @@ One disadvantage of this approach is that it does not help the software spread.
|
|||
© Alex Gleason & other Soapbox contributors
|
||||
© Eugen Rochko & other Mastodon contributors
|
||||
© Trump Media & Technology Group
|
||||
© Gab AI, Inc.
|
||||
© Gab AI, Inc.
|
||||
|
||||
Soapbox is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
- verified.svg - Created by Alex Gleason. CC0
|
||||
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
||||
|
|
|
@ -1 +1,107 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#ffffff"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
sodipodi:docname="soapbox-logo-white.svg"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
height="100"
|
||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
||||
inkscape:export-xdpi="63.5"
|
||||
inkscape:export-ydpi="63.5"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview12"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="5.0135101"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:current-layer="g1133"
|
||||
inkscape:cx="54.253406"
|
||||
inkscape:cy="42.086282"
|
||||
inkscape:window-width="1920"
|
||||
showgrid="false"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style6"
|
||||
type="text/css">
|
||||
.fil0 {fill:black}
|
||||
</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932"><g
|
||||
id="g936"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
||||
<path
|
||||
id="path934"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
||||
</g></clipPath><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932-3"><g
|
||||
id="g936-6"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
||||
id="path934-7"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
||||
|
||||
|
||||
|
||||
|
||||
</defs>
|
||||
|
||||
<metadata
|
||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1133"
|
||||
inkscape:label="citizen4"
|
||||
style="display:inline;fill:#ffffff"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g1149"
|
||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"
|
||||
style="fill:#ffffff"><path
|
||||
id="path1127"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z M 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z M 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z m 30.07388,182.197911 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z M 151.92079,0.52207567 0.51240486,46.01113 C 4.2261345,158.6288 32.487823,296.01139 151.92079,336.18936 272.66842,297.16072 298.20359,157.43109 303.32917,46.01113 Z" /></g></g></svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 812 B Po Szerokość: | Wysokość: | Rozmiar: 6.7 KiB |
|
@ -1 +1,127 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#0482d8"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
sodipodi:docname="soapbox-logo.svg"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
height="100"
|
||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
||||
inkscape:export-xdpi="63.5"
|
||||
inkscape:export-ydpi="63.5"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview12"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="5.0135101"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:cx="54.253406"
|
||||
inkscape:cy="42.086282"
|
||||
inkscape:window-width="1920"
|
||||
showgrid="false"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style6"
|
||||
type="text/css">
|
||||
.fil0 {fill:black}
|
||||
</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932"><g
|
||||
id="g936"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
||||
<path
|
||||
id="path934"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
||||
</g></clipPath><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932-3"><g
|
||||
id="g936-6"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
||||
id="path934-7"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
||||
|
||||
|
||||
|
||||
|
||||
</defs>
|
||||
|
||||
<metadata
|
||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="shield"
|
||||
style="display:inline"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g912"
|
||||
style="clip-rule:evenodd;fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
transform="matrix(11.344346,0,0,11.344346,-7.3976698,-21.749578)"><path
|
||||
id="path910"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#003399;fill-opacity:1;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1133"
|
||||
inkscape:label="citizen2"
|
||||
style="display:inline"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g1149"
|
||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"><path
|
||||
id="path1119"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 171.79501,236.97095 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z" /><path
|
||||
id="path1121"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z" /><path
|
||||
id="path1125"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z" /><path
|
||||
id="path1127"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z" /></g></g></svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 812 B Po Szerokość: | Wysokość: | Rozmiar: 7.6 KiB |
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"note": "patriots 900000001",
|
||||
"discoverable": true,
|
||||
"id": "109989480368015378",
|
||||
"domain": null,
|
||||
"avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||
"avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||
"header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||
"header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||
"group_visibility": "everyone",
|
||||
"created_at": "2023-03-08T00:00:00.000Z",
|
||||
"display_name": "PATRIOT PATRIOTS",
|
||||
"membership_required": true,
|
||||
"members_count": 1,
|
||||
"tags": []
|
||||
}
|
|
@ -228,7 +228,7 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
|
|||
});
|
||||
|
||||
type FollowAccountOpts = {
|
||||
reblogs?: boolean,
|
||||
reblogs?: boolean
|
||||
notify?: boolean
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ import throttle from 'lodash/throttle';
|
|||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import api from 'soapbox/api';
|
||||
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
|
||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
@ -19,8 +20,8 @@ import { openModal, closeModal } from './modals';
|
|||
import { getSettings } from './settings';
|
||||
import { createStatus } from './statuses';
|
||||
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
@ -277,7 +278,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
|
||||
const idempotencyKey = compose.idempotencyKey;
|
||||
|
||||
const params = {
|
||||
const params: Record<string, any> = {
|
||||
status,
|
||||
in_reply_to_id: compose.in_reply_to,
|
||||
quote_id: compose.quote,
|
||||
|
@ -289,9 +290,10 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
poll: compose.poll,
|
||||
scheduled_at: compose.schedule,
|
||||
to,
|
||||
group_id: compose.privacy === 'group' ? compose.group_id : null,
|
||||
};
|
||||
|
||||
if (compose.privacy === 'group') params.group_id = compose.group_id;
|
||||
|
||||
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
||||
routerHistory.push('/messages');
|
||||
|
@ -515,7 +517,9 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
|||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
|
||||
const state = getState();
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
||||
|
||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
||||
};
|
||||
|
||||
|
@ -560,7 +564,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
|||
let completion, startPosition;
|
||||
|
||||
if (typeof suggestion === 'object' && suggestion.id) {
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||
startPosition = position - 1;
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
|
|
|
@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
|
|||
|
||||
const noOp = () => () => new Promise(f => f(undefined));
|
||||
|
||||
const simpleEmojiReact = (status: Status, emoji: string) =>
|
||||
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
||||
|
||||
|
@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string) =>
|
|||
if (emoji === '👍') {
|
||||
dispatch(favourite(status));
|
||||
} else {
|
||||
dispatch(emojiReact(status, emoji));
|
||||
dispatch(emojiReact(status, emoji, custom));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
|
@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
|
|||
});
|
||||
};
|
||||
|
||||
const emojiReact = (status: Status, emoji: string) =>
|
||||
const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||
|
||||
dispatch(emojiReactRequest(status, emoji));
|
||||
dispatch(emojiReactRequest(status, emoji, custom));
|
||||
|
||||
return api(getState)
|
||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
|
@ -120,10 +120,11 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const emojiReactRequest = (status: Status, emoji: string) => ({
|
||||
const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({
|
||||
type: EMOJI_REACT_REQUEST,
|
||||
status,
|
||||
emoji,
|
||||
custom,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { saveSettings } from './settings';
|
||||
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
|
||||
const EMOJI_USE = 'EMOJI_USE';
|
||||
|
|
|
@ -569,7 +569,7 @@ const rejectEventParticipationRequestFail = (id: string, accountId: string, erro
|
|||
});
|
||||
|
||||
const fetchEventIcs = (id: string) =>
|
||||
(dispatch: any, getState: () => RootState) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
||||
|
||||
const cancelEventCompose = () => ({
|
||||
|
|
|
@ -34,8 +34,8 @@ type ExportDataActions = {
|
|||
| typeof EXPORT_BLOCKS_FAIL
|
||||
| typeof EXPORT_MUTES_REQUEST
|
||||
| typeof EXPORT_MUTES_SUCCESS
|
||||
| typeof EXPORT_MUTES_FAIL,
|
||||
error?: any,
|
||||
| typeof EXPORT_MUTES_FAIL
|
||||
error?: any
|
||||
}
|
||||
|
||||
function fileExport(content: string, fileName: string) {
|
||||
|
|
|
@ -11,25 +11,25 @@ export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCES
|
|||
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
|
||||
|
||||
type FamiliarFollowersFetchRequestAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
|
||||
id: string,
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST
|
||||
id: string
|
||||
}
|
||||
|
||||
type FamiliarFollowersFetchRequestSuccessAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
|
||||
id: string,
|
||||
accounts: Array<APIEntity>,
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS
|
||||
id: string
|
||||
accounts: Array<APIEntity>
|
||||
}
|
||||
|
||||
type FamiliarFollowersFetchRequestFailAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
|
||||
id: string,
|
||||
error: any,
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL
|
||||
id: string
|
||||
error: any
|
||||
}
|
||||
|
||||
type AccountsImportAction = {
|
||||
type: typeof ACCOUNTS_IMPORT,
|
||||
accounts: Array<APIEntity>,
|
||||
type: typeof ACCOUNTS_IMPORT
|
||||
accounts: Array<APIEntity>
|
||||
}
|
||||
|
||||
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
|
||||
|
|
|
@ -12,10 +12,18 @@ const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
|||
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
||||
|
||||
const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST';
|
||||
const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS';
|
||||
const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL';
|
||||
|
||||
const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
||||
const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
||||
const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
||||
|
||||
const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST';
|
||||
const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS';
|
||||
const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL';
|
||||
|
||||
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||
|
@ -25,22 +33,16 @@ const messages = defineMessages({
|
|||
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
||||
});
|
||||
|
||||
const fetchFilters = () =>
|
||||
type FilterKeywords = { keyword: string, whole_word: boolean }[];
|
||||
|
||||
const fetchFiltersV1 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (!features.filters) return;
|
||||
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
api(getState)
|
||||
return api(getState)
|
||||
.get('/api/v1/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
|
@ -55,15 +57,105 @@ const fetchFilters = () =>
|
|||
}));
|
||||
};
|
||||
|
||||
const createFilter = (phrase: string, expires_at: string, context: Array<string>, whole_word: boolean, irreversible: boolean) =>
|
||||
const fetchFiltersV2 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get('/api/v2/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
filters: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTERS_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilters = (fromFiltersPage = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2 && fromFiltersPage) return dispatch(fetchFiltersV2());
|
||||
|
||||
if (features.filters) return dispatch(fetchFiltersV1());
|
||||
};
|
||||
|
||||
const fetchFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v1/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v2/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(fetchFilterV2(id));
|
||||
|
||||
if (features.filters) return dispatch(fetchFilterV1(id));
|
||||
};
|
||||
|
||||
const createFilterV1 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v1/filters', {
|
||||
phrase,
|
||||
phrase: keywords[0].keyword,
|
||||
context,
|
||||
irreversible,
|
||||
whole_word,
|
||||
expires_at,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
|
@ -72,7 +164,80 @@ const createFilter = (phrase: string, expires_at: string, context: Array<string>
|
|||
});
|
||||
};
|
||||
|
||||
const deleteFilter = (id: string) =>
|
||||
const createFilterV2 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v2/filters', {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const createFilter = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords));
|
||||
|
||||
return dispatch(createFilterV1(title, expires_in, context, hide, keywords));
|
||||
};
|
||||
|
||||
const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v1/filters/${id}`, {
|
||||
phrase: keywords[0].keyword,
|
||||
context,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v2/filters/${id}`, {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilter = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords));
|
||||
|
||||
return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords));
|
||||
};
|
||||
|
||||
const deleteFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
|
||||
|
@ -83,17 +248,47 @@ const deleteFilter = (id: string) =>
|
|||
});
|
||||
};
|
||||
|
||||
const deleteFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v2/filters/${id}`).then(response => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.removed);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(deleteFilterV2(id));
|
||||
|
||||
return dispatch(deleteFilterV1(id));
|
||||
};
|
||||
|
||||
export {
|
||||
FILTERS_FETCH_REQUEST,
|
||||
FILTERS_FETCH_SUCCESS,
|
||||
FILTERS_FETCH_FAIL,
|
||||
FILTER_FETCH_REQUEST,
|
||||
FILTER_FETCH_SUCCESS,
|
||||
FILTER_FETCH_FAIL,
|
||||
FILTERS_CREATE_REQUEST,
|
||||
FILTERS_CREATE_SUCCESS,
|
||||
FILTERS_CREATE_FAIL,
|
||||
FILTERS_UPDATE_REQUEST,
|
||||
FILTERS_UPDATE_SUCCESS,
|
||||
FILTERS_UPDATE_FAIL,
|
||||
FILTERS_DELETE_REQUEST,
|
||||
FILTERS_DELETE_SUCCESS,
|
||||
FILTERS_DELETE_FAIL,
|
||||
fetchFilters,
|
||||
fetchFilter,
|
||||
createFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { deleteEntities } from 'soapbox/entity-store/actions';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
@ -40,14 +41,6 @@ const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
|
|||
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
|
||||
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
|
||||
|
||||
const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
|
||||
const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
|
||||
const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
|
||||
|
||||
const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
|
||||
const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
||||
const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
||||
|
||||
const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST';
|
||||
const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS';
|
||||
const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL';
|
||||
|
@ -148,7 +141,8 @@ const createGroup = (params: Record<string, any>, shouldReset?: boolean) =>
|
|||
if (shouldReset) {
|
||||
dispatch(resetGroupEditor());
|
||||
}
|
||||
dispatch(closeModal('MANAGE_GROUP'));
|
||||
|
||||
return data;
|
||||
}).catch(err => dispatch(createGroupFail(err)));
|
||||
};
|
||||
|
||||
|
@ -198,7 +192,7 @@ const updateGroupFail = (error: AxiosError) => ({
|
|||
});
|
||||
|
||||
const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(deleteGroupRequest(id));
|
||||
dispatch(deleteEntities([id], 'Group'));
|
||||
|
||||
return api(getState).delete(`/api/v1/groups/${id}`)
|
||||
.then(() => dispatch(deleteGroupSuccess(id)))
|
||||
|
@ -312,70 +306,6 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({
|
|||
skipNotFound: true,
|
||||
});
|
||||
|
||||
const joinGroup = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const locked = (getState().groups.items.get(id) as any).locked || false;
|
||||
|
||||
dispatch(joinGroupRequest(id, locked));
|
||||
|
||||
return api(getState).post(`/api/v1/groups/${id}/join`).then(response => {
|
||||
dispatch(joinGroupSuccess(response.data));
|
||||
toast.success(locked ? messages.joinRequestSuccess : messages.joinSuccess);
|
||||
}).catch(error => {
|
||||
dispatch(joinGroupFail(error, locked));
|
||||
});
|
||||
};
|
||||
|
||||
const leaveGroup = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(leaveGroupRequest(id));
|
||||
|
||||
return api(getState).post(`/api/v1/groups/${id}/leave`).then(response => {
|
||||
dispatch(leaveGroupSuccess(response.data));
|
||||
toast.success(messages.leaveSuccess);
|
||||
}).catch(error => {
|
||||
dispatch(leaveGroupFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const joinGroupRequest = (id: string, locked: boolean) => ({
|
||||
type: GROUP_JOIN_REQUEST,
|
||||
id,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const joinGroupSuccess = (relationship: APIEntity) => ({
|
||||
type: GROUP_JOIN_SUCCESS,
|
||||
relationship,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const joinGroupFail = (error: AxiosError, locked: boolean) => ({
|
||||
type: GROUP_JOIN_FAIL,
|
||||
error,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const leaveGroupRequest = (id: string) => ({
|
||||
type: GROUP_LEAVE_REQUEST,
|
||||
id,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const leaveGroupSuccess = (relationship: APIEntity) => ({
|
||||
type: GROUP_LEAVE_SUCCESS,
|
||||
relationship,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const leaveGroupFail = (error: AxiosError) => ({
|
||||
type: GROUP_LEAVE_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const groupDeleteStatus = (groupId: string, statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(groupDeleteStatusRequest(groupId, statusId));
|
||||
|
@ -859,9 +789,11 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
|
|||
const note = getState().group_editor.note;
|
||||
const avatar = getState().group_editor.avatar;
|
||||
const header = getState().group_editor.header;
|
||||
const visibility = getState().group_editor.locked ? 'members_only' : 'everyone'; // Truth Social
|
||||
|
||||
const params: Record<string, any> = {
|
||||
display_name: displayName,
|
||||
group_visibility: visibility,
|
||||
note,
|
||||
};
|
||||
|
||||
|
@ -869,9 +801,9 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
|
|||
if (header) params.header = header;
|
||||
|
||||
if (groupId === null) {
|
||||
dispatch(createGroup(params, shouldReset));
|
||||
return dispatch(createGroup(params, shouldReset));
|
||||
} else {
|
||||
dispatch(updateGroup(groupId, params, shouldReset));
|
||||
return dispatch(updateGroup(groupId, params, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -895,12 +827,6 @@ export {
|
|||
GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||
GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||
GROUP_JOIN_REQUEST,
|
||||
GROUP_JOIN_SUCCESS,
|
||||
GROUP_JOIN_FAIL,
|
||||
GROUP_LEAVE_REQUEST,
|
||||
GROUP_LEAVE_SUCCESS,
|
||||
GROUP_LEAVE_FAIL,
|
||||
GROUP_DELETE_STATUS_REQUEST,
|
||||
GROUP_DELETE_STATUS_SUCCESS,
|
||||
GROUP_DELETE_STATUS_FAIL,
|
||||
|
@ -973,14 +899,6 @@ export {
|
|||
fetchGroupRelationshipsRequest,
|
||||
fetchGroupRelationshipsSuccess,
|
||||
fetchGroupRelationshipsFail,
|
||||
joinGroup,
|
||||
leaveGroup,
|
||||
joinGroupRequest,
|
||||
joinGroupSuccess,
|
||||
joinGroupFail,
|
||||
leaveGroupRequest,
|
||||
leaveGroupSuccess,
|
||||
leaveGroupFail,
|
||||
groupDeleteStatus,
|
||||
groupDeleteStatusRequest,
|
||||
groupDeleteStatusSuccess,
|
||||
|
|
|
@ -27,8 +27,8 @@ type ImportDataActions = {
|
|||
| typeof IMPORT_BLOCKS_FAIL
|
||||
| typeof IMPORT_MUTES_REQUEST
|
||||
| typeof IMPORT_MUTES_SUCCESS
|
||||
| typeof IMPORT_MUTES_FAIL,
|
||||
error?: any,
|
||||
| typeof IMPORT_MUTES_FAIL
|
||||
error?: any
|
||||
config?: string
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import { getSettings } from '../settings';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
@ -18,11 +23,11 @@ const importAccount = (account: APIEntity) =>
|
|||
const importAccounts = (accounts: APIEntity[]) =>
|
||||
({ type: ACCOUNTS_IMPORT, accounts });
|
||||
|
||||
const importGroup = (group: APIEntity) =>
|
||||
({ type: GROUP_IMPORT, group });
|
||||
const importGroup = (group: Group) =>
|
||||
importEntities([group], Entities.GROUPS);
|
||||
|
||||
const importGroups = (groups: APIEntity[]) =>
|
||||
({ type: GROUPS_IMPORT, groups });
|
||||
const importGroups = (groups: Group[]) =>
|
||||
importEntities(groups, Entities.GROUPS);
|
||||
|
||||
const importStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
@ -69,17 +74,8 @@ const importFetchedGroup = (group: APIEntity) =>
|
|||
importFetchedGroups([group]);
|
||||
|
||||
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||
const normalGroups: APIEntity[] = [];
|
||||
|
||||
const processGroup = (group: APIEntity) => {
|
||||
if (!group.id) return;
|
||||
|
||||
normalGroups.push(group);
|
||||
};
|
||||
|
||||
groups.forEach(processGroup);
|
||||
|
||||
return importGroups(normalGroups);
|
||||
const entities = filteredArray(groupSchema).catch([]).parse(groups);
|
||||
return importGroups(entities);
|
||||
};
|
||||
|
||||
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||
|
|
|
@ -20,6 +20,10 @@ const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
|||
const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
||||
const DISLIKE_REQUEST = 'DISLIKE_REQUEST';
|
||||
const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS';
|
||||
const DISLIKE_FAIL = 'DISLIKE_FAIL';
|
||||
|
||||
const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
||||
const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
||||
const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
||||
|
@ -28,6 +32,10 @@ const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
|||
const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||
const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||
|
||||
const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST';
|
||||
const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS';
|
||||
const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL';
|
||||
|
||||
const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
||||
const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||
const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
||||
|
@ -36,6 +44,10 @@ const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST';
|
||||
const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS';
|
||||
const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL';
|
||||
|
||||
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||
|
@ -96,7 +108,7 @@ const unreblog = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleReblog = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.reblogged) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
|
@ -169,7 +181,7 @@ const unfavourite = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleFavourite = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.favourited) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
|
@ -215,6 +227,79 @@ const unfavouriteFail = (status: StatusEntity, error: AxiosError) => ({
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(dislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
|
||||
dispatch(dislikeSuccess(status));
|
||||
}).catch(function(error) {
|
||||
dispatch(dislikeFail(status, error));
|
||||
});
|
||||
};
|
||||
|
||||
const undislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(undislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
|
||||
dispatch(undislikeSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(undislikeFail(status, error));
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.disliked) {
|
||||
dispatch(undislike(status));
|
||||
} else {
|
||||
dispatch(dislike(status));
|
||||
}
|
||||
};
|
||||
|
||||
const dislikeRequest = (status: StatusEntity) => ({
|
||||
type: DISLIKE_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislikeSuccess = (status: StatusEntity) => ({
|
||||
type: DISLIKE_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||
type: DISLIKE_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeRequest = (status: StatusEntity) => ({
|
||||
type: UNDISLIKE_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeSuccess = (status: StatusEntity) => ({
|
||||
type: UNDISLIKE_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||
type: UNDISLIKE_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const bookmark = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
@ -351,6 +436,38 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const fetchDislikes = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchDislikesRequest(id));
|
||||
|
||||
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchDislikesSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchDislikesFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDislikesRequest = (id: string) => ({
|
||||
type: DISLIKES_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||
type: DISLIKES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const fetchDislikesFail = (id: string, error: AxiosError) => ({
|
||||
type: DISLIKES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchReactions = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
@ -498,18 +615,27 @@ export {
|
|||
FAVOURITE_REQUEST,
|
||||
FAVOURITE_SUCCESS,
|
||||
FAVOURITE_FAIL,
|
||||
DISLIKE_REQUEST,
|
||||
DISLIKE_SUCCESS,
|
||||
DISLIKE_FAIL,
|
||||
UNREBLOG_REQUEST,
|
||||
UNREBLOG_SUCCESS,
|
||||
UNREBLOG_FAIL,
|
||||
UNFAVOURITE_REQUEST,
|
||||
UNFAVOURITE_SUCCESS,
|
||||
UNFAVOURITE_FAIL,
|
||||
UNDISLIKE_REQUEST,
|
||||
UNDISLIKE_SUCCESS,
|
||||
UNDISLIKE_FAIL,
|
||||
REBLOGS_FETCH_REQUEST,
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
REBLOGS_FETCH_FAIL,
|
||||
FAVOURITES_FETCH_REQUEST,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_FAIL,
|
||||
DISLIKES_FETCH_REQUEST,
|
||||
DISLIKES_FETCH_SUCCESS,
|
||||
DISLIKES_FETCH_FAIL,
|
||||
REACTIONS_FETCH_REQUEST,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_FAIL,
|
||||
|
@ -546,6 +672,15 @@ export {
|
|||
unfavouriteRequest,
|
||||
unfavouriteSuccess,
|
||||
unfavouriteFail,
|
||||
dislike,
|
||||
undislike,
|
||||
toggleDislike,
|
||||
dislikeRequest,
|
||||
dislikeSuccess,
|
||||
dislikeFail,
|
||||
undislikeRequest,
|
||||
undislikeSuccess,
|
||||
undislikeFail,
|
||||
bookmark,
|
||||
unbookmark,
|
||||
toggleBookmark,
|
||||
|
@ -563,6 +698,10 @@ export {
|
|||
fetchFavouritesRequest,
|
||||
fetchFavouritesSuccess,
|
||||
fetchFavouritesFail,
|
||||
fetchDislikes,
|
||||
fetchDislikesRequest,
|
||||
fetchDislikesSuccess,
|
||||
fetchDislikesFail,
|
||||
fetchReactions,
|
||||
fetchReactionsRequest,
|
||||
fetchReactionsSuccess,
|
||||
|
|
|
@ -112,27 +112,6 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
|||
}));
|
||||
};
|
||||
|
||||
const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const name = state.accounts.get(accountId)!.username;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/user-off.svg'),
|
||||
heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
|
||||
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
|
||||
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteUsers([accountId]))
|
||||
.then(() => {
|
||||
afterConfirm();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
@ -178,7 +157,6 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
|
|||
export {
|
||||
deactivateUserModal,
|
||||
deleteUserModal,
|
||||
rejectUserModal,
|
||||
toggleStatusSensitivityModal,
|
||||
deleteStatusModal,
|
||||
};
|
||||
|
|
|
@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root
|
|||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }: {
|
||||
registration: ServiceWorkerRegistration,
|
||||
subscription: PushSubscription | null,
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
}) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
|
||||
|
||||
|
@ -82,8 +82,8 @@ const register = () =>
|
|||
.then(getPushSubscription)
|
||||
// @ts-ignore
|
||||
.then(({ registration, subscription }: {
|
||||
registration: ServiceWorkerRegistration,
|
||||
subscription: PushSubscription | null,
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
}) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
|
|
|
@ -4,7 +4,7 @@ import { openModal } from './modals';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, ChatMessage, Status } from 'soapbox/types/entities';
|
||||
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities';
|
||||
|
||||
const REPORT_INIT = 'REPORT_INIT';
|
||||
const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||
|
@ -20,19 +20,29 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
|
|||
|
||||
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
||||
|
||||
type ReportedEntity = {
|
||||
status?: Status,
|
||||
chatMessage?: ChatMessage
|
||||
enum ReportableEntities {
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
CHAT_MESSAGE = 'CHAT_MESSAGE',
|
||||
GROUP = 'GROUP',
|
||||
STATUS = 'STATUS'
|
||||
}
|
||||
|
||||
const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||
const { status, chatMessage } = entities || {};
|
||||
type ReportedEntity = {
|
||||
status?: Status
|
||||
chatMessage?: ChatMessage
|
||||
group?: Group
|
||||
}
|
||||
|
||||
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||
const { status, chatMessage, group } = entities || {};
|
||||
|
||||
dispatch({
|
||||
type: REPORT_INIT,
|
||||
entityType,
|
||||
account,
|
||||
status,
|
||||
chatMessage,
|
||||
group,
|
||||
});
|
||||
|
||||
return dispatch(openModal('REPORT'));
|
||||
|
@ -56,7 +66,8 @@ const submitReport = () =>
|
|||
return api(getState).post('/api/v1/reports', {
|
||||
account_id: reports.getIn(['new', 'account_id']),
|
||||
status_ids: reports.getIn(['new', 'status_ids']),
|
||||
message_ids: [reports.getIn(['new', 'chat_message', 'id'])],
|
||||
message_ids: [reports.getIn(['new', 'chat_message', 'id'])].filter(Boolean),
|
||||
group_id: reports.getIn(['new', 'group', 'id']),
|
||||
rule_ids: reports.getIn(['new', 'rule_ids']),
|
||||
comment: reports.getIn(['new', 'comment']),
|
||||
forward: reports.getIn(['new', 'forward']),
|
||||
|
@ -97,6 +108,7 @@ const changeReportRule = (ruleId: string) => ({
|
|||
});
|
||||
|
||||
export {
|
||||
ReportableEntities,
|
||||
REPORT_INIT,
|
||||
REPORT_CANCEL,
|
||||
REPORT_SUBMIT_REQUEST,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { defineMessage } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
|
@ -18,12 +19,10 @@ const FE_NAME = 'soapbox_fe';
|
|||
/** Options when changing/saving settings. */
|
||||
type SettingOpts = {
|
||||
/** Whether to display an alert when settings are saved. */
|
||||
showAlert?: boolean,
|
||||
showAlert?: boolean
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
saveSuccess: { id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' },
|
||||
});
|
||||
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
|
||||
|
||||
const defaultSettings = ImmutableMap({
|
||||
onboarded: false,
|
||||
|
@ -40,7 +39,7 @@ const defaultSettings = ImmutableMap({
|
|||
defaultPrivacy: 'public',
|
||||
defaultContentType: 'text/plain',
|
||||
themeMode: 'system',
|
||||
locale: navigator.language.split(/[-_]/)[0] || 'en',
|
||||
locale: navigator.language || 'en',
|
||||
showExplanationBox: true,
|
||||
explanationBox: true,
|
||||
autoloadTimelines: true,
|
||||
|
@ -221,7 +220,7 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
|
|||
dispatch({ type: SETTING_SAVE });
|
||||
|
||||
if (opts?.showAlert) {
|
||||
toast.success(messages.saveSuccess);
|
||||
toast.success(saveSuccessMessage);
|
||||
}
|
||||
}).catch(error => {
|
||||
toast.showAlertForError(error);
|
||||
|
@ -231,6 +230,12 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
|
|||
const saveSettings = (opts?: SettingOpts) =>
|
||||
(dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts));
|
||||
|
||||
const getLocale = (state: RootState, fallback = 'en') => {
|
||||
const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-');
|
||||
const locale = localeWithVariant.split('-')[0];
|
||||
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
|
||||
};
|
||||
|
||||
export {
|
||||
SETTING_CHANGE,
|
||||
SETTING_SAVE,
|
||||
|
@ -242,4 +247,5 @@ export {
|
|||
changeSetting,
|
||||
saveSettingsImmediate,
|
||||
saveSettings,
|
||||
getLocale,
|
||||
};
|
||||
|
|
|
@ -48,6 +48,8 @@ const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
|||
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||
|
||||
const STATUS_UNFILTER = 'STATUS_UNFILTER';
|
||||
|
||||
const statusExists = (getState: () => RootState, statusId: string) => {
|
||||
return (getState().statuses.get(statusId) || null) !== null;
|
||||
};
|
||||
|
@ -335,6 +337,11 @@ const undoStatusTranslation = (id: string) => ({
|
|||
id,
|
||||
});
|
||||
|
||||
const unfilterStatus = (id: string) => ({
|
||||
type: STATUS_UNFILTER,
|
||||
id,
|
||||
});
|
||||
|
||||
export {
|
||||
STATUS_CREATE_REQUEST,
|
||||
STATUS_CREATE_SUCCESS,
|
||||
|
@ -363,6 +370,7 @@ export {
|
|||
STATUS_TRANSLATE_SUCCESS,
|
||||
STATUS_TRANSLATE_FAIL,
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
STATUS_UNFILTER,
|
||||
createStatus,
|
||||
editStatus,
|
||||
fetchStatus,
|
||||
|
@ -381,4 +389,5 @@ export {
|
|||
toggleStatusHidden,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
unfilterStatus,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getLocale, getSettings } from 'soapbox/actions/settings';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
|
@ -34,13 +34,6 @@ import type { APIEntity, Chat } from 'soapbox/types/entities';
|
|||
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
|
||||
const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
|
||||
|
||||
const validLocale = (locale: string) => Object.keys(messages).includes(locale);
|
||||
|
||||
const getLocale = (state: RootState) => {
|
||||
const locale = getSettings(state).get('locale') as string;
|
||||
return validLocale(locale) ? locale : 'en';
|
||||
};
|
||||
|
||||
const updateFollowRelationships = (relationships: APIEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const me = getState().me;
|
||||
|
@ -81,7 +74,7 @@ const updateChatQuery = (chat: IChat) => {
|
|||
};
|
||||
|
||||
interface StreamOpts {
|
||||
statContext?: IStatContext,
|
||||
statContext?: IStatContext
|
||||
}
|
||||
|
||||
const connectTimelineStream = (
|
||||
|
|
|
@ -31,14 +31,14 @@ const AGE: Challenge = 'age';
|
|||
export type Challenge = 'age' | 'sms' | 'email'
|
||||
|
||||
type Challenges = {
|
||||
email?: 0 | 1,
|
||||
sms?: 0 | 1,
|
||||
age?: 0 | 1,
|
||||
email?: 0 | 1
|
||||
sms?: 0 | 1
|
||||
age?: 0 | 1
|
||||
}
|
||||
|
||||
type Verification = {
|
||||
token?: string,
|
||||
challenges?: Challenges,
|
||||
token?: string
|
||||
challenges?: Challenges
|
||||
challengeTypes?: Array<'age' | 'sms' | 'email'>
|
||||
};
|
||||
|
||||
|
|
|
@ -23,7 +23,12 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
|
|||
|
||||
export const getNextLink = (response: AxiosResponse) => {
|
||||
const nextLink = new LinkHeader(response.headers?.link);
|
||||
return nextLink.refs.find((ref) => ref.uri)?.uri;
|
||||
return nextLink.refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse) => {
|
||||
const prevLink = new LinkHeader(response.headers?.link);
|
||||
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
|
||||
export const baseClient = (...params: any[]) => {
|
||||
|
|
|
@ -29,6 +29,10 @@ export const getNextLink = (response: AxiosResponse): string | undefined => {
|
|||
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse): string | undefined => {
|
||||
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
|
||||
const getToken = (state: RootState, authType: string) => {
|
||||
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IInlineSVG {
|
||||
loader?: JSX.Element,
|
||||
loader?: JSX.Element
|
||||
}
|
||||
|
||||
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {
|
||||
|
|
|
@ -12,9 +12,9 @@ const messages = defineMessages({
|
|||
|
||||
interface IAccountSearch {
|
||||
/** Callback when a searched account is chosen. */
|
||||
onSelected: (accountId: string) => void,
|
||||
onSelected: (accountId: string) => void
|
||||
/** Override the default placeholder of the input. */
|
||||
placeholder?: string,
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
/** Input to search for accounts. */
|
||||
|
|
|
@ -14,11 +14,12 @@ import RelativeTimestamp from './relative-timestamp';
|
|||
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
||||
|
||||
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
|
||||
import type { Account as AccountSchema } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IInstanceFavicon {
|
||||
account: AccountEntity,
|
||||
disabled?: boolean,
|
||||
account: AccountEntity | AccountSchema
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -53,7 +54,7 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
|||
};
|
||||
|
||||
interface IProfilePopper {
|
||||
condition: boolean,
|
||||
condition: boolean
|
||||
wrapper: (children: React.ReactNode) => React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
@ -67,30 +68,31 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
|||
};
|
||||
|
||||
export interface IAccount {
|
||||
account: AccountEntity,
|
||||
action?: React.ReactElement,
|
||||
actionAlignment?: 'center' | 'top',
|
||||
actionIcon?: string,
|
||||
actionTitle?: string,
|
||||
account: AccountEntity | AccountSchema
|
||||
action?: React.ReactElement
|
||||
actionAlignment?: 'center' | 'top'
|
||||
actionIcon?: string
|
||||
actionTitle?: string
|
||||
/** Override other actions for specificity like mute/unmute. */
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request',
|
||||
avatarSize?: number,
|
||||
hidden?: boolean,
|
||||
hideActions?: boolean,
|
||||
id?: string,
|
||||
onActionClick?: (account: any) => void,
|
||||
showProfileHoverCard?: boolean,
|
||||
timestamp?: string,
|
||||
timestampUrl?: string,
|
||||
futureTimestamp?: boolean,
|
||||
withAccountNote?: boolean,
|
||||
withDate?: boolean,
|
||||
withLinkToProfile?: boolean,
|
||||
withRelationship?: boolean,
|
||||
showEdit?: boolean,
|
||||
approvalStatus?: StatusApprovalStatus,
|
||||
emoji?: string,
|
||||
note?: string,
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request'
|
||||
avatarSize?: number
|
||||
hidden?: boolean
|
||||
hideActions?: boolean
|
||||
id?: string
|
||||
onActionClick?: (account: any) => void
|
||||
showProfileHoverCard?: boolean
|
||||
timestamp?: string
|
||||
timestampUrl?: string
|
||||
futureTimestamp?: boolean
|
||||
withAccountNote?: boolean
|
||||
withDate?: boolean
|
||||
withLinkToProfile?: boolean
|
||||
withRelationship?: boolean
|
||||
showEdit?: boolean
|
||||
approvalStatus?: StatusApprovalStatus
|
||||
emoji?: string
|
||||
emojiUrl?: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
const Account = ({
|
||||
|
@ -115,6 +117,7 @@ const Account = ({
|
|||
showEdit = false,
|
||||
approvalStatus,
|
||||
emoji,
|
||||
emojiUrl,
|
||||
note,
|
||||
}: IAccount) => {
|
||||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -143,7 +146,7 @@ const Account = ({
|
|||
title={actionTitle}
|
||||
onClick={handleAction}
|
||||
className='bg-transparent text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
iconClassName='w-4 h-4'
|
||||
iconClassName='h-4 w-4'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -190,8 +193,9 @@ const Account = ({
|
|||
<Avatar src={account.avatar} size={avatarSize} />
|
||||
{emoji && (
|
||||
<Emoji
|
||||
className='absolute -bottom-1.5 -right-1.5 h-5 w-5'
|
||||
className='absolute bottom-0 -right-1.5 h-5 w-5'
|
||||
emoji={emoji}
|
||||
src={emojiUrl}
|
||||
/>
|
||||
)}
|
||||
</LinkEl>
|
||||
|
|
|
@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => {
|
|||
};
|
||||
|
||||
interface IAnimatedNumber {
|
||||
value: number;
|
||||
obfuscate?: boolean;
|
||||
value: number
|
||||
obfuscate?: boolean
|
||||
}
|
||||
|
||||
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: AnnouncementEntity;
|
||||
announcement: AnnouncementEntity
|
||||
}
|
||||
|
||||
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {
|
||||
|
|
|
@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable';
|
|||
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
announcement: AnnouncementEntity
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
}
|
||||
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
interface IEmoji {
|
||||
emoji: string;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
hovered: boolean;
|
||||
emoji: string
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
hovered: boolean
|
||||
}
|
||||
|
||||
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
|
||||
import Emoji from './emoji';
|
||||
|
||||
|
@ -10,12 +10,12 @@ import type { Map as ImmutableMap } from 'immutable';
|
|||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||
|
||||
interface IReaction {
|
||||
announcementId: string;
|
||||
reaction: AnnouncementReaction;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
style: React.CSSProperties;
|
||||
announcementId: string
|
||||
reaction: AnnouncementReaction
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
style: React.CSSProperties
|
||||
}
|
||||
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
|
||||
|
|
|
@ -2,29 +2,28 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
import { Icon } from 'soapbox/components/ui';
|
||||
import EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown';
|
||||
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Reaction from './reaction';
|
||||
|
||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||
|
||||
interface IReactionsBar {
|
||||
announcementId: string;
|
||||
reactions: ImmutableList<AnnouncementReaction>;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
announcementId: string
|
||||
reactions: ImmutableList<AnnouncementReaction>
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
}
|
||||
|
||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
|
||||
const reduceMotion = useSettings().get('reduceMotion');
|
||||
|
||||
const handleEmojiPick = (data: Emoji) => {
|
||||
addReaction(announcementId, data.native.replace(/:/g, ''));
|
||||
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
|
||||
};
|
||||
|
||||
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
||||
|
@ -55,7 +54,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
|||
/>
|
||||
))}
|
||||
|
||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} button={<Icon className='h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-white' src={require('@tabler/icons/plus.svg')} />} />}
|
||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||
</div>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { HStack, IconButton, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IAuthorizeRejectButtons {
|
||||
onAuthorize(): Promise<unknown> | unknown
|
||||
onReject(): Promise<unknown> | unknown
|
||||
countdown?: number
|
||||
}
|
||||
|
||||
/** Buttons to approve or reject a pending item, usually an account. */
|
||||
const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize, onReject, countdown }) => {
|
||||
const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending');
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
function handleAction(
|
||||
present: 'authorizing' | 'rejecting',
|
||||
past: 'authorized' | 'rejected',
|
||||
action: () => Promise<unknown> | unknown,
|
||||
): void {
|
||||
if (state === present) {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
setState('pending');
|
||||
} else {
|
||||
const doAction = async () => {
|
||||
try {
|
||||
await action();
|
||||
setState(past);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
if (typeof countdown === 'number') {
|
||||
setState(present);
|
||||
timeout.current = setTimeout(doAction, countdown);
|
||||
} else {
|
||||
doAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthorize = async () => handleAction('authorizing', 'authorized', onAuthorize);
|
||||
const handleReject = async () => handleAction('rejecting', 'rejected', onReject);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
switch (state) {
|
||||
case 'authorized':
|
||||
return (
|
||||
<ActionEmblem text={<FormattedMessage id='authorize.success' defaultMessage='Approved' />} />
|
||||
);
|
||||
case 'rejected':
|
||||
return (
|
||||
<ActionEmblem text={<FormattedMessage id='reject.success' defaultMessage='Rejected' />} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<HStack space={3} alignItems='center'>
|
||||
<AuthorizeRejectButton
|
||||
theme='danger'
|
||||
icon={require('@tabler/icons/x.svg')}
|
||||
action={handleReject}
|
||||
isLoading={state === 'rejecting'}
|
||||
disabled={state === 'authorizing'}
|
||||
/>
|
||||
<AuthorizeRejectButton
|
||||
theme='primary'
|
||||
icon={require('@tabler/icons/check.svg')}
|
||||
action={handleAuthorize}
|
||||
isLoading={state === 'authorizing'}
|
||||
disabled={state === 'rejecting'}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface IActionEmblem {
|
||||
text: React.ReactNode
|
||||
}
|
||||
|
||||
const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
|
||||
return (
|
||||
<div className='rounded-full bg-gray-100 px-4 py-2 dark:bg-gray-800'>
|
||||
<Text theme='muted' size='sm'>
|
||||
{text}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IAuthorizeRejectButton {
|
||||
theme: 'primary' | 'danger'
|
||||
icon: string
|
||||
action(): void
|
||||
isLoading?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, disabled }) => {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<IconButton
|
||||
src={isLoading ? require('@tabler/icons/player-stop-filled.svg') : icon}
|
||||
onClick={action}
|
||||
theme='seamless'
|
||||
className={clsx('h-10 w-10 items-center justify-center border-2', {
|
||||
'border-primary-500/10 hover:border-primary-500': theme === 'primary',
|
||||
'border-danger-600/10 hover:border-danger-600': theme === 'danger',
|
||||
})}
|
||||
iconClassName={clsx('h-6 w-6', {
|
||||
'text-primary-500': theme === 'primary',
|
||||
'text-danger-600': theme === 'danger',
|
||||
})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{(isLoading) && (
|
||||
<div
|
||||
className={clsx('pointer-events-none absolute inset-0 h-10 w-10 animate-spin rounded-full border-2 border-transparent', {
|
||||
'border-t-primary-500': theme === 'primary',
|
||||
'border-t-danger-600': theme === 'danger',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { AuthorizeRejectButtons };
|
|
@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input';
|
|||
const noOp = () => { };
|
||||
|
||||
interface IAutosuggestAccountInput {
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>,
|
||||
onSelected: (accountId: string) => void,
|
||||
autoFocus?: boolean,
|
||||
value: string,
|
||||
limit?: number,
|
||||
className?: string,
|
||||
autoSelect?: boolean,
|
||||
menu?: Menu,
|
||||
onKeyDown?: React.KeyboardEventHandler,
|
||||
theme?: InputThemes,
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
onSelected: (accountId: string) => void
|
||||
autoFocus?: boolean
|
||||
value: string
|
||||
limit?: number
|
||||
className?: string
|
||||
autoSelect?: boolean
|
||||
menu?: Menu
|
||||
onKeyDown?: React.KeyboardEventHandler
|
||||
theme?: InputThemes
|
||||
}
|
||||
|
||||
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
||||
|
|
|
@ -1,38 +1,30 @@
|
|||
import React from 'react';
|
||||
|
||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
||||
import { isCustomEmoji } from 'soapbox/features/emoji';
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
export type Emoji = {
|
||||
id: string,
|
||||
custom: boolean,
|
||||
imageUrl: string,
|
||||
native: string,
|
||||
colons: string,
|
||||
}
|
||||
|
||||
type UnicodeMapping = {
|
||||
filename: string,
|
||||
}
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
interface IAutosuggestEmoji {
|
||||
emoji: Emoji,
|
||||
emoji: Emoji
|
||||
}
|
||||
|
||||
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||
let url;
|
||||
let url, alt;
|
||||
|
||||
if (emoji.custom) {
|
||||
if (isCustomEmoji(emoji)) {
|
||||
url = emoji.imageUrl;
|
||||
alt = emoji.colons;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const mapping: UnicodeMapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||
|
||||
if (!mapping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
|
||||
url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`);
|
||||
alt = emoji.native;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -40,7 +32,7 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
|||
<img
|
||||
className='emojione'
|
||||
src={url}
|
||||
alt={emoji.native || emoji.colons}
|
||||
alt={alt}
|
||||
/>
|
||||
|
||||
{emoji.colons}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { List as ImmutableList } from 'immutable';
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import AutosuggestEmoji, { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Input, Portal } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
|
@ -12,27 +12,28 @@ import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
|||
|
||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||
import type { InputThemes } from 'soapbox/components/ui/input/input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
export type AutoSuggestion = string | Emoji;
|
||||
|
||||
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
||||
value: string,
|
||||
suggestions: ImmutableList<any>,
|
||||
disabled?: boolean,
|
||||
placeholder?: string,
|
||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void,
|
||||
onSuggestionsClearRequested: () => void,
|
||||
onSuggestionsFetchRequested: (token: string) => void,
|
||||
autoFocus: boolean,
|
||||
autoSelect: boolean,
|
||||
className?: string,
|
||||
id?: string,
|
||||
searchTokens: string[],
|
||||
maxLength?: number,
|
||||
menu?: Menu,
|
||||
renderSuggestion?: React.FC<{ id: string }>,
|
||||
hidePortal?: boolean,
|
||||
theme?: InputThemes,
|
||||
value: string
|
||||
suggestions: ImmutableList<any>
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void
|
||||
onSuggestionsClearRequested: () => void
|
||||
onSuggestionsFetchRequested: (token: string) => void
|
||||
autoFocus: boolean
|
||||
autoSelect: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
searchTokens: string[]
|
||||
maxLength?: number
|
||||
menu?: Menu
|
||||
renderSuggestion?: React.FC<{ id: string }>
|
||||
hidePortal?: boolean
|
||||
theme?: InputThemes
|
||||
}
|
||||
|
||||
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
|
||||
|
|
|
@ -19,7 +19,7 @@ export const ADDRESS_ICONS: Record<string, string> = {
|
|||
};
|
||||
|
||||
interface IAutosuggestLocation {
|
||||
id: string,
|
||||
id: string
|
||||
}
|
||||
|
||||
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
|
||||
|
|
|
@ -4,33 +4,33 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import { Portal } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
import { isRtl } from 'soapbox/rtl';
|
||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||
|
||||
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
|
||||
import { isRtl } from '../rtl';
|
||||
|
||||
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
|
||||
import AutosuggestEmoji from './autosuggest-emoji';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
interface IAutosuggesteTextarea {
|
||||
id?: string,
|
||||
value: string,
|
||||
suggestions: ImmutableList<string>,
|
||||
disabled: boolean,
|
||||
placeholder: string,
|
||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void,
|
||||
onSuggestionsClearRequested: () => void,
|
||||
onSuggestionsFetchRequested: (token: string | number) => void,
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>,
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onPaste: (files: FileList) => void,
|
||||
autoFocus: boolean,
|
||||
onFocus: () => void,
|
||||
onBlur?: () => void,
|
||||
condensed?: boolean,
|
||||
children: React.ReactNode,
|
||||
id?: string
|
||||
value: string
|
||||
suggestions: ImmutableList<string>
|
||||
disabled: boolean
|
||||
placeholder: string
|
||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void
|
||||
onSuggestionsClearRequested: () => void
|
||||
onSuggestionsFetchRequested: (token: string | number) => void
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>
|
||||
onPaste: (files: FileList) => void
|
||||
autoFocus: boolean
|
||||
onFocus: () => void
|
||||
onBlur?: () => void
|
||||
condensed?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
||||
|
|
|
@ -2,8 +2,8 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
interface IBadge {
|
||||
title: React.ReactNode,
|
||||
slug: string,
|
||||
title: React.ReactNode
|
||||
slug: string
|
||||
}
|
||||
/** Badge to display on a user's profile. */
|
||||
const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
||||
|
|
|
@ -15,9 +15,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IBirthdayInput {
|
||||
value?: string,
|
||||
onChange: (value: string) => void,
|
||||
required?: boolean,
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
|
||||
|
@ -56,15 +56,15 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
nextYearButtonDisabled,
|
||||
date,
|
||||
}: {
|
||||
decreaseMonth(): void,
|
||||
increaseMonth(): void,
|
||||
prevMonthButtonDisabled: boolean,
|
||||
nextMonthButtonDisabled: boolean,
|
||||
decreaseYear(): void,
|
||||
increaseYear(): void,
|
||||
prevYearButtonDisabled: boolean,
|
||||
nextYearButtonDisabled: boolean,
|
||||
date: Date,
|
||||
decreaseMonth(): void
|
||||
increaseMonth(): void
|
||||
prevMonthButtonDisabled: boolean
|
||||
nextMonthButtonDisabled: boolean
|
||||
decreaseYear(): void
|
||||
increaseYear(): void
|
||||
prevYearButtonDisabled: boolean
|
||||
nextYearButtonDisabled: boolean
|
||||
date: Date
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-2'>
|
||||
|
|
|
@ -3,18 +3,18 @@ import React, { useRef, useEffect } from 'react';
|
|||
|
||||
interface IBlurhash {
|
||||
/** Hash to render */
|
||||
hash: string | null | undefined,
|
||||
hash: string | null | undefined
|
||||
/** Width of the blurred region in pixels. Defaults to 32. */
|
||||
width?: number,
|
||||
width?: number
|
||||
/** Height of the blurred region in pixels. Defaults to width. */
|
||||
height?: number,
|
||||
height?: number
|
||||
/**
|
||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||
* and canvas left untouched.
|
||||
*/
|
||||
dummy?: boolean,
|
||||
dummy?: boolean
|
||||
/** className of the canvas element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Button, HStack, Input } from './ui';
|
|||
|
||||
interface ICopyableInput {
|
||||
/** Text to be copied. */
|
||||
value: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
/** An input with copy abilities. */
|
||||
|
|
|
@ -12,7 +12,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IDomain {
|
||||
domain: string,
|
||||
domain: string
|
||||
}
|
||||
|
||||
const Domain: React.FC<IDomain> = ({ domain }) => {
|
||||
|
|
|
@ -73,7 +73,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<li className='truncate focus-within:ring-2 focus-within:ring-primary-500'>
|
||||
<li className='truncate focus-visible:ring-2 focus-visible:ring-primary-500'>
|
||||
<a
|
||||
href={item.href || item.to || '#'}
|
||||
role='button'
|
||||
|
|
|
@ -271,6 +271,10 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
|||
};
|
||||
}, [refs.floating.current]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
|
|
|
@ -31,10 +31,10 @@ interface Props extends ReturnType<typeof mapStateToProps> {
|
|||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean,
|
||||
error: any,
|
||||
componentStack: any,
|
||||
browser?: Bowser.Parser.Parser,
|
||||
hasError: boolean
|
||||
error: any
|
||||
componentStack: any
|
||||
browser?: Bowser.Parser.Parser
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
|
|
|
@ -3,14 +3,14 @@ import React, { useEffect, useRef } from 'react';
|
|||
import { isIOS } from 'soapbox/is-mobile';
|
||||
|
||||
interface IExtendedVideoPlayer {
|
||||
src: string,
|
||||
alt?: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
time?: number,
|
||||
controls?: boolean,
|
||||
muted?: boolean,
|
||||
onClick?: () => void,
|
||||
src: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
time?: number
|
||||
controls?: boolean
|
||||
muted?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {
|
||||
|
|
|
@ -9,9 +9,9 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
||||
id: string,
|
||||
className?: string,
|
||||
fixedWidth?: boolean,
|
||||
id: string
|
||||
className?: string
|
||||
fixedWidth?: boolean
|
||||
}
|
||||
|
||||
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Avatar, HStack, Icon, Stack, Text } from './ui';
|
||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||
import GroupRelationship from 'soapbox/features/group/components/group-relationship';
|
||||
|
||||
import GroupAvatar from './groups/group-avatar';
|
||||
import { HStack, Stack, Text } from './ui';
|
||||
|
||||
import type { Group as GroupEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -17,43 +22,42 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
|||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden'>
|
||||
<Stack className='rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900 sm:rounded-xl'>
|
||||
<div className='relative -m-[1px] mb-0 h-[120px] rounded-t-lg bg-primary-100 dark:bg-gray-800 sm:rounded-t-xl'>
|
||||
{group.header && <img className='h-full w-full rounded-t-lg object-cover sm:rounded-t-xl' src={group.header} alt={intl.formatMessage(messages.groupHeader)} />}
|
||||
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
|
||||
<Avatar className='ring-2 ring-white dark:ring-primary-900' src={group.avatar} size={64} />
|
||||
</div>
|
||||
</div>
|
||||
<Stack className='p-3 pt-9' alignItems='center' space={3}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
|
||||
{group.relationship?.role === 'admin' ? (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
||||
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
|
||||
</HStack>
|
||||
) : group.relationship?.role === 'moderator' && (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/gavel.svg')} />
|
||||
<span><FormattedMessage id='group.role.moderator' defaultMessage='Moderator' /></span>
|
||||
</HStack>
|
||||
)}
|
||||
{group.locked ? (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/lock.svg')} />
|
||||
<span><FormattedMessage id='group.privacy.locked' defaultMessage='Private' /></span>
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/world.svg')} />
|
||||
<span><FormattedMessage id='group.privacy.public' defaultMessage='Public' /></span>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'
|
||||
data-testid='group-card'
|
||||
>
|
||||
{/* Group Cover Image */}
|
||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||
{group.header && (
|
||||
<img
|
||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
||||
src={group.header} alt={intl.formatMessage(messages.groupHeader)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{/* Group Avatar */}
|
||||
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||
<GroupAvatar group={group} size={64} withRing />
|
||||
</div>
|
||||
|
||||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<HStack alignItems='center' space={1.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
|
||||
{group.relationship?.pending_requests && (
|
||||
<div className='h-2 w-2 rounded-full bg-secondary-500' />
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupRelationship group={group} />
|
||||
<GroupPrivacy group={group} />
|
||||
<GroupMemberCount group={group} />
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import { Avatar } from '../ui';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
interface IGroupAvatar {
|
||||
group: Group
|
||||
size: number
|
||||
withRing?: boolean
|
||||
}
|
||||
|
||||
const GroupAvatar = (props: IGroupAvatar) => {
|
||||
const { group, size, withRing = false } = props;
|
||||
|
||||
const isOwner = group.relationship?.role === GroupRoles.OWNER;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
className={
|
||||
clsx('relative rounded-full', {
|
||||
'shadow-[0_0_0_2px_theme(colors.primary.600),0_0_0_4px_theme(colors.white)]': isOwner && withRing,
|
||||
'dark:shadow-[0_0_0_2px_theme(colors.primary.600),0_0_0_4px_theme(colors.gray.800)]': isOwner && withRing,
|
||||
'shadow-[0_0_0_2px_theme(colors.primary.600)]': isOwner && !withRing,
|
||||
'shadow-[0_0_0_2px_theme(colors.white)] dark:shadow-[0_0_0_2px_theme(colors.gray.800)]': !isOwner && withRing,
|
||||
})
|
||||
}
|
||||
src={group.avatar}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupAvatar;
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, Divider, HStack, Popover, Stack, Text } from 'soapbox/components/ui';
|
||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||
|
||||
import GroupAvatar from '../group-avatar';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
interface IGroupPopoverContainer {
|
||||
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||
isEnabled: boolean
|
||||
group: Group
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'group.popover.title', defaultMessage: 'Membership required' },
|
||||
summary: { id: 'group.popover.summary', defaultMessage: 'You must be a member of the group in order to reply to this status.' },
|
||||
action: { id: 'group.popover.action', defaultMessage: 'View Group' },
|
||||
});
|
||||
|
||||
const GroupPopover = (props: IGroupPopoverContainer) => {
|
||||
const { children, group, isEnabled } = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
if (!isEnabled) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
interaction='click'
|
||||
referenceElementClassName='cursor-pointer'
|
||||
content={
|
||||
<Stack space={4} className='w-80'>
|
||||
<Stack
|
||||
className='relative h-60 rounded-lg bg-white dark:border-primary-800 dark:bg-primary-900'
|
||||
data-testid='group-card'
|
||||
>
|
||||
{/* Group Cover Image */}
|
||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||
{group.header && (
|
||||
<img
|
||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
||||
src={group.header}
|
||||
alt=''
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Group Avatar */}
|
||||
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||
<GroupAvatar group={group} size={64} withRing />
|
||||
</div>
|
||||
|
||||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupPrivacy group={group} />
|
||||
<GroupMemberCount group={group} />
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack space={0.5} className='px-4'>
|
||||
<Text weight='semibold'>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</Text>
|
||||
<Text theme='muted'>
|
||||
{intl.formatMessage(messages.summary)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<div className='px-4 pb-4'>
|
||||
<Link to={`/groups/${group.id}`}>
|
||||
<Button type='button' theme='secondary' block>
|
||||
{intl.formatMessage(messages.action)}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Stack>
|
||||
}
|
||||
isFlush
|
||||
children={
|
||||
<div className='inline-block'>{children}</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupPopover;
|
|
@ -10,7 +10,7 @@ import { HStack, Stack, Text } from './ui';
|
|||
import type { Tag } from 'soapbox/types/entities';
|
||||
|
||||
interface IHashtag {
|
||||
hashtag: Tag,
|
||||
hashtag: Tag
|
||||
}
|
||||
|
||||
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
||||
|
|
|
@ -15,10 +15,10 @@ const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
|
|||
}, 600);
|
||||
|
||||
interface IHoverRefWrapper {
|
||||
accountId: string,
|
||||
inline?: boolean,
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
accountId: string
|
||||
inline?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Makes a profile hover card appear when the wrapped element is hovered. */
|
||||
|
|
|
@ -14,10 +14,10 @@ const showStatusHoverCard = debounce((dispatch, ref, statusId) => {
|
|||
}, 300);
|
||||
|
||||
interface IHoverStatusWrapper {
|
||||
statusId: any,
|
||||
inline: boolean,
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
statusId: any
|
||||
inline: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Makes a status hover card appear when the wrapped element is hovered. */
|
||||
|
|
|
@ -4,10 +4,10 @@ import Icon, { IIcon } from 'soapbox/components/icon';
|
|||
import { Counter } from 'soapbox/components/ui';
|
||||
|
||||
interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
|
||||
count: number,
|
||||
count: number
|
||||
countMax?: number
|
||||
icon?: string;
|
||||
src?: string;
|
||||
icon?: string
|
||||
src?: string
|
||||
}
|
||||
|
||||
const IconWithCounter: React.FC<IIconWithCounter> = ({ icon, count, countMax, ...rest }) => {
|
||||
|
|
|
@ -8,12 +8,15 @@ import React from 'react';
|
|||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
export interface IIcon extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string,
|
||||
id?: string,
|
||||
alt?: string,
|
||||
className?: string,
|
||||
src: string
|
||||
id?: string
|
||||
alt?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the UI Icon component directly.
|
||||
*/
|
||||
const Icon: React.FC<IIcon> = ({ src, alt, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -4,8 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
|
||||
import { SelectDropdown } from '../features/forms';
|
||||
|
||||
import Icon from './icon';
|
||||
import { HStack, Select } from './ui';
|
||||
import { Icon, HStack, Select } from './ui';
|
||||
|
||||
interface IList {
|
||||
children: React.ReactNode
|
||||
|
@ -16,9 +15,9 @@ const List: React.FC<IList> = ({ children }) => (
|
|||
);
|
||||
|
||||
interface IListItem {
|
||||
label: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
onClick?(): void,
|
||||
label: React.ReactNode
|
||||
hint?: React.ReactNode
|
||||
onClick?(): void
|
||||
onSelect?(): void
|
||||
isSelected?: boolean
|
||||
children?: React.ReactNode
|
||||
|
@ -58,13 +57,13 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
return (
|
||||
<Comp
|
||||
className={clsx({
|
||||
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10': true,
|
||||
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
'flex items-center justify-between px-4 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 dark:from-gradient-start/10 dark:to-gradient-end/10': true,
|
||||
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
})}
|
||||
{...linkProps}
|
||||
>
|
||||
<div className='flex flex-col py-1.5 pr-4 rtl:pl-4 rtl:pr-0'>
|
||||
<LabelComp className='text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
|
||||
<LabelComp className='font-medium text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
|
||||
|
||||
{hint ? (
|
||||
<span className='text-sm text-gray-700 dark:text-gray-600'>{hint}</span>
|
||||
|
@ -83,9 +82,26 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
<div className='flex flex-row items-center text-gray-700 dark:text-gray-600'>
|
||||
{children}
|
||||
|
||||
{isSelected ? (
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='ml-1 text-primary-500 dark:text-primary-400' />
|
||||
) : null}
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'flex h-6 w-6 items-center justify-center rounded-full border-2 border-solid border-primary-500 dark:border-primary-400 transition': true,
|
||||
'bg-primary-500 dark:bg-primary-400': isSelected,
|
||||
'bg-transparent': !isSelected,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
src={require('@tabler/icons/check.svg')}
|
||||
className={
|
||||
clsx({
|
||||
'h-4 w-4 text-white dark:text-white transition-all duration-500': true,
|
||||
'opacity-0 scale-50': !isSelected,
|
||||
'opacity-100 scale-100': isSelected,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ILoadGap {
|
||||
disabled?: boolean,
|
||||
maxId: string,
|
||||
onClick: (id: string) => void,
|
||||
disabled?: boolean
|
||||
maxId: string
|
||||
onClick: (id: string) => void
|
||||
}
|
||||
|
||||
const LoadGap: React.FC<ILoadGap> = ({ disabled, maxId, onClick }) => {
|
||||
|
|
|
@ -4,18 +4,19 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Button } from 'soapbox/components/ui';
|
||||
|
||||
interface ILoadMore {
|
||||
onClick: React.MouseEventHandler,
|
||||
disabled?: boolean,
|
||||
visible?: Boolean,
|
||||
onClick: React.MouseEventHandler
|
||||
disabled?: boolean
|
||||
visible?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true }) => {
|
||||
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true, className }) => {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button theme='primary' block disabled={disabled || !visible} onClick={onClick}>
|
||||
<Button className={className} theme='primary' block disabled={disabled || !visible} onClick={onClick}>
|
||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ILocationSearch {
|
||||
onSelected: (locationId: string) => void,
|
||||
onSelected: (locationId: string) => void
|
||||
}
|
||||
|
||||
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
||||
|
|
|
@ -19,21 +19,21 @@ const ATTACHMENT_LIMIT = 4;
|
|||
const MAX_FILENAME_LENGTH = 45;
|
||||
|
||||
interface Dimensions {
|
||||
w: Property.Width | number,
|
||||
h: Property.Height | number,
|
||||
t?: Property.Top,
|
||||
r?: Property.Right,
|
||||
b?: Property.Bottom,
|
||||
l?: Property.Left,
|
||||
float?: Property.Float,
|
||||
pos?: Property.Position,
|
||||
w: Property.Width | number
|
||||
h: Property.Height | number
|
||||
t?: Property.Top
|
||||
r?: Property.Right
|
||||
b?: Property.Bottom
|
||||
l?: Property.Left
|
||||
float?: Property.Float
|
||||
pos?: Property.Position
|
||||
}
|
||||
|
||||
interface SizeData {
|
||||
style: React.CSSProperties,
|
||||
itemsDimensions: Dimensions[],
|
||||
size: number,
|
||||
width: number,
|
||||
style: React.CSSProperties
|
||||
itemsDimensions: Dimensions[]
|
||||
size: number
|
||||
width: number
|
||||
}
|
||||
|
||||
const withinLimits = (aspectRatio: number) => {
|
||||
|
@ -48,16 +48,16 @@ const shouldLetterbox = (attachment: Attachment): boolean => {
|
|||
};
|
||||
|
||||
interface IItem {
|
||||
attachment: Attachment,
|
||||
standalone?: boolean,
|
||||
index: number,
|
||||
size: number,
|
||||
onClick: (index: number) => void,
|
||||
displayWidth?: number,
|
||||
visible: boolean,
|
||||
dimensions: Dimensions,
|
||||
last?: boolean,
|
||||
total: number,
|
||||
attachment: Attachment
|
||||
standalone?: boolean
|
||||
index: number
|
||||
size: number
|
||||
onClick: (index: number) => void
|
||||
displayWidth?: number
|
||||
visible: boolean
|
||||
dimensions: Dimensions
|
||||
last?: boolean
|
||||
total: number
|
||||
}
|
||||
|
||||
const Item: React.FC<IItem> = ({
|
||||
|
@ -152,7 +152,14 @@ const Item: React.FC<IItem> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={clsx('media-gallery__item', { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<div
|
||||
className={clsx('media-gallery__item', {
|
||||
standalone,
|
||||
'rounded-md': total > 1,
|
||||
})}
|
||||
key={attachment.id}
|
||||
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
|
||||
>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.url} target='_blank' style={{ cursor: 'pointer' }}>
|
||||
<Blurhash hash={attachment.blurhash} className='media-gallery__preview' />
|
||||
<span className='media-gallery__item__icons'>{attachmentIcon}</span>
|
||||
|
@ -245,7 +252,14 @@ const Item: React.FC<IItem> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('media-gallery__item', `media-gallery__item--${attachment.type}`, { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<div
|
||||
className={clsx('media-gallery__item', `media-gallery__item--${attachment.type}`, {
|
||||
standalone,
|
||||
'rounded-md': total > 1,
|
||||
})}
|
||||
key={attachment.id}
|
||||
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
|
||||
>
|
||||
{last && total > ATTACHMENT_LIMIT && (
|
||||
<div className='media-gallery__item-overflow'>
|
||||
+{total - ATTACHMENT_LIMIT + 1}
|
||||
|
@ -260,23 +274,25 @@ const Item: React.FC<IItem> = ({
|
|||
);
|
||||
};
|
||||
|
||||
interface IMediaGallery {
|
||||
sensitive?: boolean,
|
||||
media: ImmutableList<Attachment>,
|
||||
height?: number,
|
||||
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void,
|
||||
defaultWidth?: number,
|
||||
cacheWidth?: (width: number) => void,
|
||||
visible?: boolean,
|
||||
onToggleVisibility?: () => void,
|
||||
displayMedia?: string,
|
||||
compact: boolean,
|
||||
export interface IMediaGallery {
|
||||
sensitive?: boolean
|
||||
media: ImmutableList<Attachment>
|
||||
height?: number
|
||||
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void
|
||||
defaultWidth?: number
|
||||
cacheWidth?: (width: number) => void
|
||||
visible?: boolean
|
||||
onToggleVisibility?: () => void
|
||||
displayMedia?: string
|
||||
compact?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
||||
const {
|
||||
media,
|
||||
defaultWidth = 0,
|
||||
className,
|
||||
onOpenMedia,
|
||||
cacheWidth,
|
||||
compact,
|
||||
|
@ -546,7 +562,11 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
|||
}, [node.current]);
|
||||
|
||||
return (
|
||||
<div className={clsx('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
|
||||
<div
|
||||
className={clsx(className, 'media-gallery', { 'media-gallery--compact': compact })}
|
||||
style={sizeData.style}
|
||||
ref={node}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -39,10 +39,10 @@ export const checkEventComposeContent = (compose?: ReturnType<typeof ReducerComp
|
|||
};
|
||||
|
||||
interface IModalRoot {
|
||||
onCancel?: () => void,
|
||||
onClose: (type?: ModalType) => void,
|
||||
type: ModalType,
|
||||
children: React.ReactNode,
|
||||
onCancel?: () => void
|
||||
onClose: (type?: ModalType) => void
|
||||
type: ModalType
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type }) => {
|
||||
|
|
|
@ -2,8 +2,8 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
interface IOutlineBox extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode,
|
||||
className?: string,
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** Wraps children in a container with an outline. */
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IPendingItemsRow {
|
||||
/** Path to navigate the user when clicked. */
|
||||
to: string
|
||||
/** Number of pending items. */
|
||||
count: number
|
||||
/** Size of the icon. */
|
||||
size?: 'md' | 'lg'
|
||||
}
|
||||
|
||||
const PendingItemsRow: React.FC<IPendingItemsRow> = ({ to, count, size = 'md' }) => {
|
||||
return (
|
||||
<Link to={to} className='group' data-testid='pending-items-row'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<div className={clsx('rounded-full bg-primary-200 text-primary-500 dark:bg-primary-800 dark:text-primary-200', {
|
||||
'p-3': size === 'lg',
|
||||
'p-2.5': size === 'md',
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
src={require('@tabler/icons/exclamation-circle.svg')}
|
||||
className={clsx({
|
||||
'h-5 w-5': size === 'md',
|
||||
'h-7 w-7': size === 'lg',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text weight='bold' size='md'>
|
||||
<FormattedMessage
|
||||
id='groups.pending.count'
|
||||
defaultMessage='{number, plural, one {# pending request} other {# pending requests}}'
|
||||
values={{ number: count }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Icon
|
||||
src={require('@tabler/icons/chevron-right.svg')}
|
||||
className='h-5 w-5 text-gray-600 transition-colors group-hover:text-gray-700 dark:text-gray-600 dark:group-hover:text-gray-500'
|
||||
/>
|
||||
</HStack>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export { PendingItemsRow };
|
|
@ -16,9 +16,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IPollFooter {
|
||||
poll: PollEntity,
|
||||
showResults: boolean,
|
||||
selected: Selected,
|
||||
poll: PollEntity
|
||||
showResults: boolean
|
||||
selected: Selected
|
||||
}
|
||||
|
||||
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {
|
||||
|
|
|
@ -29,7 +29,7 @@ const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ pe
|
|||
};
|
||||
|
||||
interface IPollOptionText extends IPollOption {
|
||||
percent: number,
|
||||
percent: number
|
||||
}
|
||||
|
||||
const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active, onToggle }) => {
|
||||
|
@ -95,12 +95,12 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
};
|
||||
|
||||
interface IPollOption {
|
||||
poll: PollEntity,
|
||||
option: PollOptionEntity,
|
||||
index: number,
|
||||
showResults?: boolean,
|
||||
active: boolean,
|
||||
onToggle: (value: number) => void,
|
||||
poll: PollEntity
|
||||
option: PollOptionEntity
|
||||
index: number
|
||||
showResults?: boolean
|
||||
active: boolean
|
||||
onToggle: (value: number) => void
|
||||
}
|
||||
|
||||
const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||
|
|
|
@ -13,8 +13,8 @@ import PollOption from './poll-option';
|
|||
export type Selected = Record<number, boolean>;
|
||||
|
||||
interface IPoll {
|
||||
id: string,
|
||||
status?: string,
|
||||
id: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -54,7 +54,7 @@ const handleMouseLeave = (dispatch: AppDispatch): React.MouseEventHandler => {
|
|||
};
|
||||
|
||||
interface IProfileHoverCard {
|
||||
visible: boolean,
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/** Popup profile preview that appears when hovering avatars and display names. */
|
||||
|
|
|
@ -2,10 +2,10 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
interface IProgressCircle {
|
||||
progress: number,
|
||||
radius?: number,
|
||||
stroke?: number,
|
||||
title?: string,
|
||||
progress: number
|
||||
radius?: number
|
||||
stroke?: number
|
||||
title?: string
|
||||
}
|
||||
|
||||
const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stroke = 4, title }) => {
|
||||
|
|
|
@ -4,10 +4,10 @@ import PTRComponent from 'react-simple-pull-to-refresh';
|
|||
import { Spinner } from 'soapbox/components/ui';
|
||||
|
||||
interface IPullToRefresh {
|
||||
onRefresh?: () => Promise<any>;
|
||||
refreshingContent?: JSX.Element | string;
|
||||
pullingContent?: JSX.Element | string;
|
||||
children: React.ReactNode;
|
||||
onRefresh?: () => Promise<any>
|
||||
refreshingContent?: JSX.Element | string
|
||||
pullingContent?: JSX.Element | string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import PullToRefresh from './pull-to-refresh';
|
||||
|
||||
interface IPullable {
|
||||
children: React.ReactNode,
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,11 +23,11 @@ const messages = defineMessages({
|
|||
|
||||
interface IQuotedStatus {
|
||||
/** The quoted status entity. */
|
||||
status?: StatusEntity,
|
||||
status?: StatusEntity
|
||||
/** Callback when cancelled (during compose). */
|
||||
onCancel?: Function,
|
||||
onCancel?: Function
|
||||
/** Whether the status is shown in the post composer. */
|
||||
compose?: boolean,
|
||||
compose?: boolean
|
||||
}
|
||||
|
||||
/** Status embedded in a quote post. */
|
||||
|
|
|
@ -16,11 +16,11 @@ const RadioGroup = ({ onChange, children }: IRadioGroup) => {
|
|||
};
|
||||
|
||||
interface IRadioItem {
|
||||
label: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
value: string,
|
||||
checked: boolean,
|
||||
onChange?: React.ChangeEventHandler,
|
||||
label: React.ReactNode
|
||||
hint?: React.ReactNode
|
||||
value: string
|
||||
checked: boolean
|
||||
onChange?: React.ChangeEventHandler
|
||||
}
|
||||
|
||||
const RadioItem: React.FC<IRadioItem> = ({ label, hint, checked = false, onChange, value }) => {
|
||||
|
|
|
@ -113,14 +113,14 @@ const timeRemainingString = (intl: IntlShape, date: Date, now: number) => {
|
|||
};
|
||||
|
||||
interface RelativeTimestampProps extends IText {
|
||||
intl: IntlShape,
|
||||
timestamp: string,
|
||||
year?: number,
|
||||
futureDate?: boolean,
|
||||
intl: IntlShape
|
||||
timestamp: string
|
||||
year?: number
|
||||
futureDate?: boolean
|
||||
}
|
||||
|
||||
interface RelativeTimestampState {
|
||||
now: number,
|
||||
now: number
|
||||
}
|
||||
|
||||
/** Displays a timestamp compared to the current time, eg "1m" for one minute ago. */
|
||||
|
|
|
@ -2,13 +2,13 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||
|
||||
interface ISafeEmbed {
|
||||
/** Styles for the outer frame element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** Space-separate list of restrictions to ALLOW for the iframe. */
|
||||
sandbox?: string,
|
||||
sandbox?: string
|
||||
/** Unique title for the iframe. */
|
||||
title: string,
|
||||
title: string
|
||||
/** HTML body to embed. */
|
||||
html?: string,
|
||||
html?: string
|
||||
}
|
||||
|
||||
/** Safely embeds arbitrary HTML content on the page (by putting it in an iframe). */
|
||||
|
|
|
@ -9,15 +9,15 @@ import { useSettings } from 'soapbox/hooks';
|
|||
|
||||
interface IScrollTopButton {
|
||||
/** Callback when clicked, and also when scrolled to the top. */
|
||||
onClick: () => void,
|
||||
onClick: () => void
|
||||
/** Number of unread items. */
|
||||
count: number,
|
||||
count: number
|
||||
/** Message to display in the button (should contain a `{count}` value). */
|
||||
message: MessageDescriptor,
|
||||
message: MessageDescriptor
|
||||
/** Distance from the top of the screen (scrolling down) before the button appears. */
|
||||
threshold?: number,
|
||||
threshold?: number
|
||||
/** Distance from the top of the screen (scrolling up) before the action is triggered. */
|
||||
autoloadThreshold?: number,
|
||||
autoloadThreshold?: number
|
||||
}
|
||||
|
||||
/** Floating new post counter above timelines, clicked to scroll to top. */
|
||||
|
|
|
@ -10,14 +10,14 @@ import { Card, Spinner } from './ui';
|
|||
|
||||
/** Custom Viruoso component context. */
|
||||
type Context = {
|
||||
itemClassName?: string,
|
||||
listClassName?: string,
|
||||
itemClassName?: string
|
||||
listClassName?: string
|
||||
}
|
||||
|
||||
/** Scroll position saved in sessionStorage. */
|
||||
type SavedScrollPosition = {
|
||||
index: number,
|
||||
offset: number,
|
||||
index: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
/** Custom Virtuoso Item component representing a single scrollable item. */
|
||||
|
@ -37,44 +37,46 @@ const List: Components<JSX.Element, Context>['List'] = React.forwardRef((props,
|
|||
|
||||
interface IScrollableList extends VirtuosoProps<any, any> {
|
||||
/** Unique key to preserve the scroll position when navigating back. */
|
||||
scrollKey?: string,
|
||||
scrollKey?: string
|
||||
/** Pagination callback when the end of the list is reached. */
|
||||
onLoadMore?: () => void,
|
||||
onLoadMore?: () => void
|
||||
/** Whether the data is currently being fetched. */
|
||||
isLoading?: boolean,
|
||||
isLoading?: boolean
|
||||
/** Whether to actually display the loading state. */
|
||||
showLoading?: boolean,
|
||||
showLoading?: boolean
|
||||
/** Whether we expect an additional page of data. */
|
||||
hasMore?: boolean,
|
||||
hasMore?: boolean
|
||||
/** Additional element to display at the top of the list. */
|
||||
prepend?: React.ReactNode,
|
||||
prepend?: React.ReactNode
|
||||
/** Whether to display the prepended element. */
|
||||
alwaysPrepend?: boolean,
|
||||
alwaysPrepend?: boolean
|
||||
/** Message to display when the list is loaded but empty. */
|
||||
emptyMessage?: React.ReactNode,
|
||||
emptyMessage?: React.ReactNode
|
||||
/** Should the empty message be displayed in a Card */
|
||||
emptyMessageCard?: boolean
|
||||
/** Scrollable content. */
|
||||
children: Iterable<React.ReactNode>,
|
||||
children: Iterable<React.ReactNode>
|
||||
/** Callback when the list is scrolled to the top. */
|
||||
onScrollToTop?: () => void,
|
||||
onScrollToTop?: () => void
|
||||
/** Callback when the list is scrolled. */
|
||||
onScroll?: () => void,
|
||||
onScroll?: () => void
|
||||
/** Placeholder component to render while loading. */
|
||||
placeholderComponent?: React.ComponentType | React.NamedExoticComponent,
|
||||
placeholderComponent?: React.ComponentType | React.NamedExoticComponent
|
||||
/** Number of placeholders to render while loading. */
|
||||
placeholderCount?: number,
|
||||
placeholderCount?: number
|
||||
/**
|
||||
* Pull to refresh callback.
|
||||
* @deprecated Put a PTR around the component instead.
|
||||
*/
|
||||
onRefresh?: () => Promise<any>,
|
||||
onRefresh?: () => Promise<any>
|
||||
/** Extra class names on the Virtuoso element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** Class names on each item container. */
|
||||
itemClassName?: string,
|
||||
itemClassName?: string
|
||||
/** `id` attribute on the Virtuoso element. */
|
||||
id?: string,
|
||||
id?: string
|
||||
/** CSS styles on the Virtuoso element. */
|
||||
style?: React.CSSProperties,
|
||||
style?: React.CSSProperties
|
||||
/** Whether to use the window to scroll the content instead of Virtuoso's container. */
|
||||
useWindowScroll?: boolean
|
||||
}
|
||||
|
@ -87,6 +89,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
|||
children,
|
||||
isLoading,
|
||||
emptyMessage,
|
||||
emptyMessageCard = true,
|
||||
showLoading,
|
||||
onRefresh,
|
||||
onScroll,
|
||||
|
@ -158,13 +161,17 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
|||
<div className='mt-2'>
|
||||
{alwaysPrepend && prepend}
|
||||
|
||||
<Card variant='rounded' size='lg'>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
emptyMessage
|
||||
)}
|
||||
</Card>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
{emptyMessageCard ? (
|
||||
<Card variant='rounded' size='lg'>
|
||||
{emptyMessage}
|
||||
</Card>
|
||||
) : emptyMessage}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import { closeSidebar } from 'soapbox/actions/sidebar';
|
|||
import Account from 'soapbox/components/account';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures } from 'soapbox/hooks';
|
||||
import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors';
|
||||
|
||||
import { Divider, HStack, Icon, IconButton, Text } from './ui';
|
||||
|
@ -43,11 +43,11 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ISidebarLink {
|
||||
href?: string,
|
||||
to?: string,
|
||||
icon: string,
|
||||
text: string | JSX.Element,
|
||||
onClick: React.EventHandler<React.MouseEvent>,
|
||||
href?: string
|
||||
to?: string
|
||||
icon: string
|
||||
text: string | JSX.Element
|
||||
onClick: React.EventHandler<React.MouseEvent>
|
||||
}
|
||||
|
||||
const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick }) => {
|
||||
|
@ -90,6 +90,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
|
||||
const settings = useAppSelector((state) => getSettings(state));
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const groupsPath = useGroupsPath();
|
||||
|
||||
const closeButtonRef = React.useRef(null);
|
||||
|
||||
|
@ -210,7 +211,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
|
||||
{features.groups && (
|
||||
<SidebarLink
|
||||
to='/groups'
|
||||
to={groupsPath}
|
||||
icon={require('@tabler/icons/circles.svg')}
|
||||
text={intl.formatMessage(messages.groups)}
|
||||
onClick={onClose}
|
||||
|
@ -296,7 +297,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{features.filters && (
|
||||
{(features.filters || features.filtersV2) && (
|
||||
<SidebarLink
|
||||
to='/filters'
|
||||
icon={require('@tabler/icons/filter.svg')}
|
||||
|
|
|
@ -6,17 +6,17 @@ import { Icon, Text } from './ui';
|
|||
|
||||
interface ISidebarNavigationLink {
|
||||
/** Notification count, if any. */
|
||||
count?: number,
|
||||
count?: number
|
||||
/** Optional max to cap count (ie: N+) */
|
||||
countMax?: number
|
||||
/** URL to an SVG icon. */
|
||||
icon: string,
|
||||
icon: string
|
||||
/** Link label. */
|
||||
text: React.ReactNode,
|
||||
text: React.ReactNode
|
||||
/** Route to an internal page. */
|
||||
to?: string,
|
||||
to?: string
|
||||
/** Callback when the link is clicked. */
|
||||
onClick?: React.EventHandler<React.MouseEvent>,
|
||||
onClick?: React.EventHandler<React.MouseEvent>
|
||||
}
|
||||
|
||||
/** Desktop sidebar navigation link. */
|
||||
|
|
|
@ -4,7 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import { Stack } from 'soapbox/components/ui';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
||||
import { useAppSelector, useFeatures, useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||
import { useAppSelector, useGroupsPath, useFeatures, useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import DropdownMenu, { Menu } from './dropdown-menu';
|
||||
import SidebarNavigationLink from './sidebar-navigation-link';
|
||||
|
@ -25,6 +25,8 @@ const SidebarNavigation = () => {
|
|||
const features = useFeatures();
|
||||
const settings = useSettings();
|
||||
const account = useOwnAccount();
|
||||
const groupsPath = useGroupsPath();
|
||||
|
||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||
|
@ -135,7 +137,7 @@ const SidebarNavigation = () => {
|
|||
|
||||
{features.groups && (
|
||||
<SidebarNavigationLink
|
||||
to='/groups'
|
||||
to={groupsPath}
|
||||
icon={require('@tabler/icons/circles.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
||||
/>
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useSoapboxConfig, useSettings, useTheme } from 'soapbox/hooks';
|
|||
|
||||
interface ISiteLogo extends React.ComponentProps<'img'> {
|
||||
/** Extra class names for the <img> element. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** Override theme setting for <SitePreview /> */
|
||||
theme?: 'dark' | 'light',
|
||||
theme?: 'dark' | 'light'
|
||||
}
|
||||
|
||||
/** Display the most appropriate site logo based on the theme and configuration. */
|
||||
|
|
|
@ -8,11 +8,11 @@ import { launchChat } from 'soapbox/actions/chats';
|
|||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
|
||||
import { editEvent } from 'soapbox/actions/events';
|
||||
import { groupBlock, groupDeleteStatus, groupKick } from 'soapbox/actions/groups';
|
||||
import { toggleBookmark, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions';
|
||||
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||
import { initReport } from 'soapbox/actions/reports';
|
||||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
|
@ -24,6 +24,8 @@ import { isLocal, isRemote } from 'soapbox/utils/accounts';
|
|||
import copy from 'soapbox/utils/copy';
|
||||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
import GroupPopover from './groups/popover/group-popover';
|
||||
|
||||
import type { Menu } from 'soapbox/components/dropdown-menu';
|
||||
import type { Account, Group, Status } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -45,6 +47,7 @@ const messages = defineMessages({
|
|||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Like' },
|
||||
disfavourite: { id: 'status.disfavourite', defaultMessage: 'Disike' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
|
@ -97,10 +100,10 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IStatusActionBar {
|
||||
status: Status,
|
||||
withLabels?: boolean,
|
||||
expandable?: boolean,
|
||||
space?: 'expand' | 'compact',
|
||||
status: Status
|
||||
withLabels?: boolean
|
||||
expandable?: boolean
|
||||
space?: 'expand' | 'compact'
|
||||
}
|
||||
|
||||
const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
|
@ -161,6 +164,14 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleDislikeClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (me) {
|
||||
dispatch(toggleDislike(status));
|
||||
} else {
|
||||
onOpenUnauthorizedModal('DISLIKE');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
@ -254,7 +265,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.id));
|
||||
dispatch(initReport(account, { status }));
|
||||
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
@ -271,7 +282,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(initReport(status.account as Account, { status }));
|
||||
dispatch(initReport(ReportableEntities.STATUS, status.account as Account, { status }));
|
||||
};
|
||||
|
||||
const handleConversationMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
|
@ -538,7 +549,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
allowedEmoji,
|
||||
).reduce((acc, cur) => acc + cur.get('count'), 0);
|
||||
|
||||
const meEmojiReact = getReactForStatus(status, allowedEmoji) as keyof typeof reactMessages | undefined;
|
||||
const meEmojiReact = getReactForStatus(status, allowedEmoji);
|
||||
const meEmojiName = meEmojiReact?.get('name') as keyof typeof reactMessages | undefined;
|
||||
|
||||
const reactMessages = {
|
||||
'👍': messages.reactionLike,
|
||||
|
@ -550,7 +562,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
'': messages.favourite,
|
||||
};
|
||||
|
||||
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
|
||||
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiName || ''] || messages.favourite);
|
||||
|
||||
const menu = _makeMenu(publicStatus);
|
||||
let reblogIcon = require('@tabler/icons/repeat.svg');
|
||||
|
@ -607,14 +619,19 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
grow={space === 'expand'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<StatusActionButton
|
||||
title={replyTitle}
|
||||
icon={require('@tabler/icons/message-circle-2.svg')}
|
||||
onClick={handleReplyClick}
|
||||
count={replyCount}
|
||||
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
||||
disabled={replyDisabled}
|
||||
/>
|
||||
<GroupPopover
|
||||
group={status.group as any}
|
||||
isEnabled={replyDisabled}
|
||||
>
|
||||
<StatusActionButton
|
||||
title={replyTitle}
|
||||
icon={require('@tabler/icons/message-circle-2.svg')}
|
||||
onClick={handleReplyClick}
|
||||
count={replyCount}
|
||||
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
||||
disabled={replyDisabled}
|
||||
/>
|
||||
</GroupPopover>
|
||||
|
||||
{(features.quotePosts && me) ? (
|
||||
<DropdownMenu
|
||||
|
@ -635,7 +652,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon={require('@tabler/icons/heart.svg')}
|
||||
filled
|
||||
color='accent'
|
||||
active={Boolean(meEmojiReact)}
|
||||
active={Boolean(meEmojiName)}
|
||||
count={emojiReactCount}
|
||||
emoji={meEmojiReact}
|
||||
text={withLabels ? meEmojiTitle : undefined}
|
||||
|
@ -644,16 +661,29 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
) : (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.favourite)}
|
||||
icon={require('@tabler/icons/heart.svg')}
|
||||
icon={features.dislikes ? require('@tabler/icons/thumb-up.svg') : require('@tabler/icons/heart.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleFavouriteClick}
|
||||
active={Boolean(meEmojiReact)}
|
||||
active={Boolean(meEmojiName)}
|
||||
count={favouriteCount}
|
||||
text={withLabels ? meEmojiTitle : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.dislikes && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.disfavourite)}
|
||||
icon={require('@tabler/icons/thumb-down.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleDislikeClick}
|
||||
active={status.disliked}
|
||||
count={status.dislikes_count}
|
||||
text={withLabels ? intl.formatMessage(messages.disfavourite) : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canShare && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.share)}
|
||||
|
|
|
@ -4,6 +4,8 @@ import React from 'react';
|
|||
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const COLORS = {
|
||||
accent: 'accent',
|
||||
success: 'success',
|
||||
|
@ -12,7 +14,7 @@ const COLORS = {
|
|||
type Color = keyof typeof COLORS;
|
||||
|
||||
interface IStatusActionCounter {
|
||||
count: number,
|
||||
count: number
|
||||
}
|
||||
|
||||
/** Action button numerical counter, eg "5" likes. */
|
||||
|
@ -25,14 +27,14 @@ const StatusActionCounter: React.FC<IStatusActionCounter> = ({ count = 0 }): JSX
|
|||
};
|
||||
|
||||
interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
iconClassName?: string,
|
||||
icon: string,
|
||||
count?: number,
|
||||
active?: boolean,
|
||||
color?: Color,
|
||||
filled?: boolean,
|
||||
emoji?: string,
|
||||
text?: React.ReactNode,
|
||||
iconClassName?: string
|
||||
icon: string
|
||||
count?: number
|
||||
active?: boolean
|
||||
color?: Color
|
||||
filled?: boolean
|
||||
emoji?: ImmutableMap<string, any>
|
||||
text?: React.ReactNode
|
||||
}
|
||||
|
||||
const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButton>((props, ref): JSX.Element => {
|
||||
|
@ -42,7 +44,7 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
|||
if (emoji) {
|
||||
return (
|
||||
<span className='flex h-6 w-6 items-center justify-center'>
|
||||
<Emoji className='h-full w-full p-0.5' emoji={emoji} />
|
||||
<Emoji className='h-full w-full p-0.5' emoji={emoji.get('name')} src={emoji.get('url')} />
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -20,7 +20,7 @@ const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
|||
const BIG_EMOJI_LIMIT = 10;
|
||||
|
||||
interface IReadMoreButton {
|
||||
onClick: React.MouseEventHandler,
|
||||
onClick: React.MouseEventHandler
|
||||
}
|
||||
|
||||
/** Button to expand a truncated status (due to too much content) */
|
||||
|
@ -32,11 +32,11 @@ const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
|||
);
|
||||
|
||||
interface IStatusContent {
|
||||
status: Status,
|
||||
onClick?: () => void,
|
||||
collapsable?: boolean,
|
||||
translatable?: boolean,
|
||||
textSize?: Sizes,
|
||||
status: Status
|
||||
onClick?: () => void
|
||||
collapsable?: boolean
|
||||
translatable?: boolean
|
||||
textSize?: Sizes
|
||||
}
|
||||
|
||||
/** Renders the text content of a status */
|
||||
|
|
|
@ -15,7 +15,7 @@ import { showStatusHoverCard } from './hover-status-wrapper';
|
|||
import { Card, CardBody } from './ui';
|
||||
|
||||
interface IStatusHoverCard {
|
||||
visible: boolean,
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/** Popup status preview that appears when hovering reply to */
|
||||
|
|
|
@ -23,31 +23,31 @@ import type { Ad as AdEntity } from 'soapbox/types/soapbox';
|
|||
|
||||
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||
/** Unique key to preserve the scroll position when navigating back. */
|
||||
scrollKey: string,
|
||||
scrollKey: string
|
||||
/** List of status IDs to display. */
|
||||
statusIds: ImmutableOrderedSet<string>,
|
||||
statusIds: ImmutableOrderedSet<string>
|
||||
/** Last _unfiltered_ status ID (maxId) for pagination. */
|
||||
lastStatusId?: string,
|
||||
lastStatusId?: string
|
||||
/** Pinned statuses to show at the top of the feed. */
|
||||
featuredStatusIds?: ImmutableOrderedSet<string>,
|
||||
featuredStatusIds?: ImmutableOrderedSet<string>
|
||||
/** Pagination callback when the end of the list is reached. */
|
||||
onLoadMore?: (lastStatusId: string) => void,
|
||||
onLoadMore?: (lastStatusId: string) => void
|
||||
/** Whether the data is currently being fetched. */
|
||||
isLoading: boolean,
|
||||
isLoading: boolean
|
||||
/** Whether the server did not return a complete page. */
|
||||
isPartial?: boolean,
|
||||
isPartial?: boolean
|
||||
/** Whether we expect an additional page of data. */
|
||||
hasMore: boolean,
|
||||
hasMore: boolean
|
||||
/** Message to display when the list is loaded but empty. */
|
||||
emptyMessage: React.ReactNode,
|
||||
emptyMessage: React.ReactNode
|
||||
/** ID of the timeline in Redux. */
|
||||
timelineId?: string,
|
||||
timelineId?: string
|
||||
/** Whether to display a gap or border between statuses in the list. */
|
||||
divideType?: 'space' | 'border',
|
||||
divideType?: 'space' | 'border'
|
||||
/** Whether to display ads. */
|
||||
showAds?: boolean,
|
||||
showAds?: boolean
|
||||
/** Whether to show group information. */
|
||||
showGroup?: boolean,
|
||||
showGroup?: boolean
|
||||
}
|
||||
|
||||
/** Feed of statuses, built atop ScrollableList. */
|
||||
|
|
|
@ -15,15 +15,15 @@ import type { Status, Attachment } from 'soapbox/types/entities';
|
|||
|
||||
interface IStatusMedia {
|
||||
/** Status entity to render media for. */
|
||||
status: Status,
|
||||
status: Status
|
||||
/** Whether to display compact media. */
|
||||
muted?: boolean,
|
||||
muted?: boolean
|
||||
/** Callback when compact media is clicked. */
|
||||
onClick?: () => void,
|
||||
onClick?: () => void
|
||||
/** Whether or not the media is concealed behind a NSFW banner. */
|
||||
showMedia?: boolean,
|
||||
showMedia?: boolean
|
||||
/** Callback when visibility is toggled (eg clicked through NSFW). */
|
||||
onToggleVisibility?: () => void,
|
||||
onToggleVisibility?: () => void
|
||||
}
|
||||
|
||||
/** Render media attachments for a status. */
|
||||
|
|
|
@ -8,8 +8,8 @@ import { isUserTouching } from 'soapbox/is-mobile';
|
|||
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
interface IStatusReactionWrapper {
|
||||
statusId: string,
|
||||
children: JSX.Element,
|
||||
statusId: string
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
/** Provides emoji reaction functionality to the underlying button component */
|
||||
|
@ -60,9 +60,9 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
}
|
||||
};
|
||||
|
||||
const handleReact = (emoji: string): void => {
|
||||
const handleReact = (emoji: string, custom?: string): void => {
|
||||
if (ownAccount) {
|
||||
dispatch(simpleEmojiReact(status, emoji));
|
||||
dispatch(simpleEmojiReact(status, emoji, custom));
|
||||
} else {
|
||||
handleUnauthorized();
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
};
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji) || '👍';
|
||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
|
||||
|
||||
if (isUserTouching()) {
|
||||
if (ownAccount) {
|
||||
|
@ -112,6 +112,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
referenceElement={referenceElement}
|
||||
onReact={handleReact}
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
|
|
|
@ -6,12 +6,13 @@ import { openModal } from 'soapbox/actions/modals';
|
|||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { isPubkey } from 'soapbox/utils/nostr';
|
||||
|
||||
import type { Account, Status } from 'soapbox/types/entities';
|
||||
|
||||
interface IStatusReplyMentions {
|
||||
status: Status,
|
||||
hoverable?: boolean,
|
||||
status: Status
|
||||
hoverable?: boolean
|
||||
}
|
||||
|
||||
const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable = true }) => {
|
||||
|
@ -56,7 +57,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
className='reply-mentions__account'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{account.username}
|
||||
@{isPubkey(account.username) ? account.username.slice(0, 8) : account.username}
|
||||
</Link>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
|
||||
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
||||
import { toggleStatusHidden, unfilterStatus } from 'soapbox/actions/statuses';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import TranslateButton from 'soapbox/components/translate-button';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
|
@ -38,22 +38,22 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
export interface IStatus {
|
||||
id?: string,
|
||||
avatarSize?: number,
|
||||
status: StatusEntity,
|
||||
onClick?: () => void,
|
||||
muted?: boolean,
|
||||
hidden?: boolean,
|
||||
unread?: boolean,
|
||||
onMoveUp?: (statusId: string, featured?: boolean) => void,
|
||||
onMoveDown?: (statusId: string, featured?: boolean) => void,
|
||||
focusable?: boolean,
|
||||
featured?: boolean,
|
||||
hideActionBar?: boolean,
|
||||
hoverable?: boolean,
|
||||
variant?: 'default' | 'rounded',
|
||||
showGroup?: boolean,
|
||||
accountAction?: React.ReactElement,
|
||||
id?: string
|
||||
avatarSize?: number
|
||||
status: StatusEntity
|
||||
onClick?: () => void
|
||||
muted?: boolean
|
||||
hidden?: boolean
|
||||
unread?: boolean
|
||||
onMoveUp?: (statusId: string, featured?: boolean) => void
|
||||
onMoveDown?: (statusId: string, featured?: boolean) => void
|
||||
focusable?: boolean
|
||||
featured?: boolean
|
||||
hideActionBar?: boolean
|
||||
hoverable?: boolean
|
||||
variant?: 'default' | 'rounded'
|
||||
showGroup?: boolean
|
||||
accountAction?: React.ReactElement
|
||||
}
|
||||
|
||||
const Status: React.FC<IStatus> = (props) => {
|
||||
|
@ -93,6 +93,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
||||
const group = actualStatus.group as GroupEntity | null;
|
||||
|
||||
const filtered = (status.filtered.size || actualStatus.filtered.size) > 0;
|
||||
|
||||
// Track height changes we know about to compensate scrolling.
|
||||
useEffect(() => {
|
||||
didShowCard.current = Boolean(!muted && !hidden && status?.card);
|
||||
|
@ -202,6 +204,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
_expandEmojiSelector();
|
||||
};
|
||||
|
||||
const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.size ? status.id : actualStatus.id));
|
||||
|
||||
const _expandEmojiSelector = (): void => {
|
||||
const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji');
|
||||
firstEmoji?.focus();
|
||||
|
@ -281,7 +285,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (status.filtered || actualStatus.filtered) {
|
||||
if (filtered && status.showFiltered) {
|
||||
const minHandlers = muted ? undefined : {
|
||||
moveUp: handleHotkeyMoveUp,
|
||||
moveDown: handleHotkeyMoveDown,
|
||||
|
@ -291,7 +295,11 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
<HotKeys handlers={minHandlers}>
|
||||
<div className={clsx('status__wrapper text-center', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {status.filtered.join(', ')}.
|
||||
{' '}
|
||||
<button className='text-primary-600 hover:underline dark:text-accent-blue' onClick={handleUnfilter}>
|
||||
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
|
||||
</button>
|
||||
</Text>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
|
|
@ -5,17 +5,17 @@ import { useSettings } from 'soapbox/hooks';
|
|||
|
||||
interface IStillImage {
|
||||
/** Image alt text. */
|
||||
alt?: string,
|
||||
alt?: string
|
||||
/** Extra class names for the outer <div> container. */
|
||||
className?: string,
|
||||
className?: string
|
||||
/** URL to the image */
|
||||
src: string,
|
||||
src: string
|
||||
/** Extra CSS styles on the outer <div> element. */
|
||||
style?: React.CSSProperties,
|
||||
style?: React.CSSProperties
|
||||
/** Whether to display the image contained vs filled in its container. */
|
||||
letterboxed?: boolean,
|
||||
letterboxed?: boolean
|
||||
/** Whether to show the file extension in the corner. */
|
||||
showExt?: boolean,
|
||||
showExt?: boolean
|
||||
}
|
||||
|
||||
/** Renders images on a canvas, only playing GIFs if autoPlayGif is enabled. */
|
||||
|
@ -80,7 +80,7 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
|
|||
|
||||
interface IExtensionBadge {
|
||||
/** File extension. */
|
||||
ext: string,
|
||||
ext: string
|
||||
}
|
||||
|
||||
/** Badge displaying a file extension. */
|
||||
|
|
|
@ -6,13 +6,13 @@ import IconWithCounter from 'soapbox/components/icon-with-counter';
|
|||
import { Icon, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IThumbNavigationLink {
|
||||
count?: number,
|
||||
countMax?: number,
|
||||
src: string,
|
||||
text: string | React.ReactElement,
|
||||
to: string,
|
||||
exact?: boolean,
|
||||
paths?: Array<string>,
|
||||
count?: number
|
||||
countMax?: number
|
||||
src: string
|
||||
text: string | React.ReactElement
|
||||
to: string
|
||||
exact?: boolean
|
||||
paths?: Array<string>
|
||||
}
|
||||
|
||||
const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax, src, text, to, exact, paths }): JSX.Element => {
|
||||
|
|
|
@ -5,9 +5,9 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Text } from 'soapbox/components/ui';
|
||||
|
||||
interface ITombstone {
|
||||
id: string,
|
||||
onMoveUp: (statusId: string) => void,
|
||||
onMoveDown: (statusId: string) => void,
|
||||
id: string
|
||||
onMoveUp: (statusId: string) => void
|
||||
onMoveDown: (statusId: string) => void
|
||||
}
|
||||
|
||||
/** Represents a deleted item. */
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Stack, Button, Text } from './ui';
|
|||
import type { Account, Status } from 'soapbox/types/entities';
|
||||
|
||||
interface ITranslateButton {
|
||||
status: Status,
|
||||
status: Status
|
||||
}
|
||||
|
||||
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue