Compare commits
669 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ef1f9d47 | ||
|
|
0182763b58 | ||
|
|
761cadbcc7 | ||
|
|
ff7b6e4564 | ||
|
|
db84317f7f | ||
|
|
72df72228f | ||
|
|
3e26bd2f52 | ||
|
|
f241d74e27 | ||
|
|
ff0f3b414b | ||
|
|
188da6dbf7 | ||
|
|
f9fd62db09 | ||
|
|
4795a5ff6e | ||
|
|
4da52e4797 | ||
|
|
46bc0ccbeb | ||
|
|
75f1caf022 | ||
|
|
6f6d2e1e08 | ||
|
|
027b873f13 | ||
|
|
482b7d5e7d | ||
|
|
1e729d97a0 | ||
|
|
357e348681 | ||
|
|
65c1b4def0 | ||
|
|
461eac8932 | ||
|
|
9a679de9c9 | ||
|
|
12d4ec2129 | ||
|
|
26f9737e5e | ||
|
|
771740ca93 | ||
|
|
ed070118d6 | ||
|
|
88c0985908 | ||
|
|
e7094beab9 | ||
|
|
501a2715be | ||
|
|
bc2380dfb1 | ||
|
|
4b584af0a9 | ||
|
|
4bc1083939 | ||
|
|
b34b5032ee | ||
|
|
d10b70cc62 | ||
|
|
a8ba45c14b | ||
|
|
a50a2446a7 | ||
|
|
4e2e3fb4e2 | ||
|
|
9d34f0896f | ||
|
|
e492aca885 | ||
|
|
12208b849a | ||
|
|
7f2af61244 | ||
|
|
f745f95286 | ||
|
|
cf1c8eb4e3 | ||
|
|
6d5ab6b4ba | ||
|
|
ab27e36560 | ||
|
|
f368026124 | ||
|
|
bb4c68c6bd | ||
|
|
09d68ec197 | ||
|
|
b675c14767 | ||
|
|
5e1b63ba21 | ||
|
|
5a462df797 | ||
|
|
6d96ab5b7c | ||
|
|
1f051f3fd8 | ||
|
|
e4e91dd283 | ||
|
|
aca0773874 | ||
|
|
2e5d73162d | ||
|
|
0c04701750 | ||
|
|
591ee62cc9 | ||
|
|
1ff3ec5431 | ||
|
|
c8277624d2 | ||
|
|
a7ab35a6b0 | ||
|
|
89d1498956 | ||
|
|
cc585c9b43 | ||
|
|
43a500c150 | ||
|
|
5ac37a38c5 | ||
|
|
960e54f4e6 | ||
|
|
b107523d65 | ||
|
|
1c6da783ad | ||
|
|
b2e01188ee | ||
|
|
bfd3387fa1 | ||
|
|
433161b08d | ||
|
|
b3ab900e47 | ||
|
|
bc6b3e1186 | ||
|
|
15d400c758 | ||
|
|
2023e55bfd | ||
|
|
d0352b86e0 | ||
|
|
ecdc6c05b0 | ||
|
|
c4e3566ff0 | ||
|
|
109d103cc8 | ||
|
|
f96237b6f1 | ||
|
|
d3344e3e03 | ||
|
|
115a6378d3 | ||
|
|
264f9a933c | ||
|
|
cc42110d49 | ||
|
|
5e9900378e | ||
|
|
16d41fd1fc | ||
|
|
bacd11f4f3 | ||
|
|
dd7aa01b3f | ||
|
|
6270a3dfdc | ||
|
|
3cfed238fb | ||
|
|
4e1bf80e12 | ||
|
|
04129920eb | ||
|
|
8725ed9d3c | ||
|
|
30cfd883f5 | ||
|
|
03e0b2af51 | ||
|
|
da155fa5b1 | ||
|
|
fe2adb28e2 | ||
|
|
95a9d44fcf | ||
|
|
4a712b1f26 | ||
|
|
ee8ed127f6 | ||
|
|
c0ba23af3c | ||
|
|
c5197975c5 | ||
|
|
e66751dc06 | ||
|
|
8a219ab58d | ||
|
|
f80f240d4e | ||
|
|
92de40dd23 | ||
|
|
513b29f57d | ||
|
|
a8f4efa929 | ||
|
|
767e414c94 | ||
|
|
7e1f63348a | ||
|
|
0e5c09626f | ||
|
|
0dabe89bcd | ||
|
|
c3e48d20f3 | ||
|
|
1352f884cb | ||
|
|
f1700c9573 | ||
|
|
256eb472c2 | ||
|
|
5d0ebac464 | ||
|
|
b2956a901a | ||
|
|
bc592a97f2 | ||
|
|
414ad1df9c | ||
|
|
868e60db22 | ||
|
|
8d16c31e75 | ||
|
|
83b4a5b222 | ||
|
|
c6483f2de2 | ||
|
|
2884c6a8dd | ||
|
|
8e01c18484 | ||
|
|
f39b204e45 | ||
|
|
121708ca55 | ||
|
|
49c5ae3730 | ||
|
|
6b10ffd71a | ||
|
|
bcdbe6e6e8 | ||
|
|
1df9fbdf21 | ||
|
|
15091f2a15 | ||
|
|
34b719178e | ||
|
|
790ef36b1b | ||
|
|
1cbe0f10c4 | ||
|
|
0b806bfee1 | ||
|
|
bd737e97f1 | ||
|
|
1b8d21a3e0 | ||
|
|
4e0552e978 | ||
|
|
de2280dd2c | ||
|
|
04b4f05642 | ||
|
|
ef05232e4e | ||
|
|
21dda592b4 | ||
|
|
4e42a97b4b | ||
|
|
3a029bd242 | ||
|
|
055bbab36c | ||
|
|
576d37633c | ||
|
|
cbccc231f2 | ||
|
|
2928301a9f | ||
|
|
47e105228a | ||
|
|
3228deec4c | ||
|
|
62a4393e03 | ||
|
|
9fd47f4825 | ||
|
|
1823919e28 | ||
|
|
1037d39168 | ||
|
|
7da0974ef0 | ||
|
|
aa7a307cc1 | ||
|
|
bf44f7ef13 | ||
|
|
1b4afe7ba9 | ||
|
|
832d371b2e | ||
|
|
15984eabdf | ||
|
|
1f72ff3ed7 | ||
|
|
9456a0045a | ||
|
|
0035a41fe9 | ||
|
|
8c3fe2ff52 | ||
|
|
7678d7b809 | ||
|
|
9be424fa77 | ||
|
|
e65dfe16ac | ||
|
|
84c555e3bc | ||
|
|
c4d7018531 | ||
|
|
1dd40a77de | ||
|
|
b2c797fb46 | ||
|
|
c94e1f939a | ||
|
|
2dae00800e | ||
|
|
b888f71d2a | ||
|
|
6ffdbb7b71 | ||
|
|
7cb40f1556 | ||
|
|
975999df96 | ||
|
|
a6f1943e05 | ||
|
|
91ff898ac7 | ||
|
|
5c705b0f22 | ||
|
|
77a07ffbb1 | ||
|
|
559ec3ecb3 | ||
|
|
17bdd54f63 | ||
|
|
cfdf6ac27e | ||
|
|
4fbf9aa1f8 | ||
|
|
d66463c7c6 | ||
|
|
e2d5d4c30b | ||
|
|
5db90188b2 | ||
|
|
cfe6332005 | ||
|
|
d1a706860f | ||
|
|
29ab502d2e | ||
|
|
629e65edba | ||
|
|
f4c2c5b7f3 | ||
|
|
2b334e973a | ||
|
|
3212d3ce04 | ||
|
|
094acf86c3 | ||
|
|
51e91efb12 | ||
|
|
fe48a9ece2 | ||
|
|
97152ffcdf | ||
|
|
25f8649ee5 | ||
|
|
359b9d04be | ||
|
|
359392d77d | ||
|
|
755b56a8db | ||
|
|
9a99223d19 | ||
|
|
b77ae93290 | ||
|
|
688b6b3f82 | ||
|
|
ff8d2e7010 | ||
|
|
a29672d758 | ||
|
|
7b2c1c1f32 | ||
|
|
7f4a51d1cd | ||
|
|
7486bf4c06 | ||
|
|
97e16b9f73 | ||
|
|
16b3352e65 | ||
|
|
4936127655 | ||
|
|
aa7453855e | ||
|
|
c789c71768 | ||
|
|
37de5b79a8 | ||
|
|
11b24a1821 | ||
|
|
5963b5ab1e | ||
|
|
b4f738cd98 | ||
|
|
d44107d64d | ||
|
|
ad2006c853 | ||
|
|
296c3bbcc8 | ||
|
|
c349293e24 | ||
|
|
c624bb69b6 | ||
|
|
db58e8f214 | ||
|
|
4379f5cd12 | ||
|
|
bc78c61009 | ||
|
|
51b2fc7dc5 | ||
|
|
2bd2dbe624 | ||
|
|
4a1b1e19e8 | ||
|
|
2456f07128 | ||
|
|
e517db3002 | ||
|
|
c7b8cc72fc | ||
|
|
06e832848b | ||
|
|
7848bef09b | ||
|
|
943a7fcff3 | ||
|
|
c67194606f | ||
|
|
2c823b9eb5 | ||
|
|
238abc20b5 | ||
|
|
657e1b9c36 | ||
|
|
7164bca3c8 | ||
|
|
7b7e0e1f8d | ||
|
|
4058a844b7 | ||
|
|
2f8b2e4069 | ||
|
|
21323fd396 | ||
|
|
35efc6a1bc | ||
|
|
8bbef30a1d | ||
|
|
70c61dcbf5 | ||
|
|
6375850da6 | ||
|
|
8a18e2f167 | ||
|
|
61dd972ee5 | ||
|
|
30efd8c45b | ||
|
|
6b31a3e2d6 | ||
|
|
0e868f0be0 | ||
|
|
67847d90da | ||
|
|
730804409a | ||
|
|
567347e66a | ||
|
|
b3f033e30b | ||
|
|
c44dbc82c8 | ||
|
|
00cffd24fe | ||
|
|
51469e1198 | ||
|
|
78f75d4cc1 | ||
|
|
9fbc4ad851 | ||
|
|
bcc62ed222 | ||
|
|
9cc8769d45 | ||
|
|
9ed3e2f897 | ||
|
|
9bf399070a | ||
|
|
dd72723198 | ||
|
|
3f4567fa5f | ||
|
|
065d6398e6 | ||
|
|
68e3e54ae0 | ||
|
|
cae5622d7c | ||
|
|
5e8230291e | ||
|
|
e2c9115d63 | ||
|
|
cae67ad3a9 | ||
|
|
1f7939c90d | ||
|
|
b6b2d873a0 | ||
|
|
f1393ee02e | ||
|
|
316593e32c | ||
|
|
78af4bfb59 | ||
|
|
ea53200e23 | ||
|
|
b0c77d42c0 | ||
|
|
2ec8ddbebe | ||
|
|
bfc0076429 | ||
|
|
1b1a1e4449 | ||
|
|
5fa6ad31a8 | ||
|
|
21e59d9a68 | ||
|
|
5739d8a5ae | ||
|
|
1f7ad8e535 | ||
|
|
ced0112561 | ||
|
|
e0f11440d9 | ||
|
|
75d92cdbbe | ||
|
|
2c6ecb7d42 | ||
|
|
e8391c2463 | ||
|
|
c0d631b3c3 | ||
|
|
89806139a8 | ||
|
|
210b0b3be2 | ||
|
|
49d4421635 | ||
|
|
bf3cae3da7 | ||
|
|
e93730a5dc | ||
|
|
7e544749d5 | ||
|
|
1e23d2f510 | ||
|
|
5bcda74066 | ||
|
|
b599b4d6cc | ||
|
|
3f3610102f | ||
|
|
e4e6ebf91c | ||
|
|
589d011d4c | ||
|
|
325d40c1bf | ||
|
|
f2dc402869 | ||
|
|
c19a0be8f8 | ||
|
|
445fea5237 | ||
|
|
d1b38b926b | ||
|
|
fcd128d8d9 | ||
|
|
0025f812b8 | ||
|
|
d7c7e05ec1 | ||
|
|
c932bafc12 | ||
|
|
a0376b344a | ||
|
|
1cbc117878 | ||
|
|
ea8c1abec9 | ||
|
|
a410eb424f | ||
|
|
8624de5705 | ||
|
|
358d5820f6 | ||
|
|
7c06aecf31 | ||
|
|
5d7544e58c | ||
|
|
17423efb96 | ||
|
|
a28793edd2 | ||
|
|
79087e3c86 | ||
|
|
50760471d5 | ||
|
|
a6df8cb62d | ||
|
|
873711939d | ||
|
|
2bd13eb3ba | ||
|
|
d423f17342 | ||
|
|
a02c99452d | ||
|
|
fd854912fc | ||
|
|
5999991209 | ||
|
|
50ff46465f | ||
|
|
c1040907d9 | ||
|
|
2cae19a75d | ||
|
|
c302473798 | ||
|
|
4107636f00 | ||
|
|
ee07ea9426 | ||
|
|
c806f20f2c | ||
|
|
98b5121a72 | ||
|
|
fb842f72c8 | ||
|
|
982a043342 | ||
|
|
09e1baf19f | ||
|
|
fd2dbcf120 | ||
|
|
9ff0d76c3a | ||
|
|
48f0186151 | ||
|
|
45a945e983 | ||
|
|
971c07a7c2 | ||
|
|
eeb85ae3db | ||
|
|
fe13376a56 | ||
|
|
0d55c1fc37 | ||
|
|
d5ebe761d5 | ||
|
|
9774b2a215 | ||
|
|
ba442511b7 | ||
|
|
e831a01a28 | ||
|
|
884ab72ac6 | ||
|
|
6edb483a5b | ||
|
|
d1c788c639 | ||
|
|
b17680d897 | ||
|
|
707d9d2332 | ||
|
|
9c67d68d05 | ||
|
|
a37ae88d3b | ||
|
|
b8489b0379 | ||
|
|
87e2bdd39d | ||
|
|
52b95ef5f9 | ||
|
|
81375f501e | ||
|
|
d37d977c79 | ||
|
|
654967b6f0 | ||
|
|
8ad48a04a0 | ||
|
|
a4c04bb279 | ||
|
|
d4e0797586 | ||
|
|
8c6009cd63 | ||
|
|
09be5b3f97 | ||
|
|
31af0251ea | ||
|
|
d0d899c73d | ||
|
|
b5aa1a4598 | ||
|
|
b9956950b6 | ||
|
|
dee5e3b365 | ||
|
|
8224801024 | ||
|
|
f5377dc276 | ||
|
|
94f6b49487 | ||
|
|
acaacf063e | ||
|
|
881705fa9c | ||
|
|
5440674498 | ||
|
|
c1770c428d | ||
|
|
db3cb118e1 | ||
|
|
085cce211f | ||
|
|
671318a810 | ||
|
|
8accae1105 | ||
|
|
104896ae1b | ||
|
|
977fc2483f | ||
|
|
658177538b | ||
|
|
b2d49c3143 | ||
|
|
cbf36eb999 | ||
|
|
26e25268f7 | ||
|
|
2d93e98dc7 | ||
|
|
7764ff22dc | ||
|
|
a5e0b96af5 | ||
|
|
f30d58446f | ||
|
|
a4041d3bcd | ||
|
|
e72b7f3d29 | ||
|
|
2bfe5cdfc9 | ||
|
|
1124bc48c2 | ||
|
|
69b7484a4a | ||
|
|
19939e457b | ||
|
|
92603217e7 | ||
|
|
3e256d41d2 | ||
|
|
b6a9c1b879 | ||
|
|
bf839b8d52 | ||
|
|
a13c98e6c9 | ||
|
|
65806d2491 | ||
|
|
0450701548 | ||
|
|
9f1b329a46 | ||
|
|
22a71dd2d2 | ||
|
|
595690aea9 | ||
|
|
deae7d100c | ||
|
|
0397fcb9be | ||
|
|
6df27566d2 | ||
|
|
0b919e3815 | ||
|
|
1d5f07a658 | ||
|
|
571a09364c | ||
|
|
92f94d7a82 | ||
|
|
87cb7dbc39 | ||
|
|
2c3dd3960d | ||
|
|
d1a16a3202 | ||
|
|
3bfb665a76 | ||
|
|
1ebf6eb8f9 | ||
|
|
1af6ce2ce6 | ||
|
|
f0040620c8 | ||
|
|
41e91371d5 | ||
|
|
c2a00f88d9 | ||
|
|
4a0ac11883 | ||
|
|
a50082f738 | ||
|
|
19dc448a08 | ||
|
|
ad06dc14b0 | ||
|
|
c1aa2775a5 | ||
|
|
ec58a7e369 | ||
|
|
973ffb0fe5 | ||
|
|
5e4c993525 | ||
|
|
3ffc427cd6 | ||
|
|
24b065ba03 | ||
|
|
47bd910727 | ||
|
|
a02f654b14 | ||
|
|
d1c6afcebd | ||
|
|
75d64c3c31 | ||
|
|
97b16e879f | ||
|
|
00db01d163 | ||
|
|
56610538ce | ||
|
|
d555f5d3e4 | ||
|
|
89d5b8fb69 | ||
|
|
69e36b0f71 | ||
|
|
9b5ef3ad33 | ||
|
|
ec5dd81018 | ||
|
|
72f546ed15 | ||
|
|
0b48414715 | ||
|
|
ca67c1eaca | ||
|
|
4dd5a80ef2 | ||
|
|
c9f211807a | ||
|
|
440c15d9fa | ||
|
|
86a443c39f | ||
|
|
a8d86db57f | ||
|
|
8359b48285 | ||
|
|
36f4770cae | ||
|
|
932cf91800 | ||
|
|
5cf02e66b7 | ||
|
|
b7251972a8 | ||
|
|
006a423d5c | ||
|
|
374b1edc81 | ||
|
|
0894549687 | ||
|
|
cc5963cc34 | ||
|
|
fffe77501d | ||
|
|
3443c2ae82 | ||
|
|
01324af544 | ||
|
|
d56b1fe89b | ||
|
|
92457d54df | ||
|
|
4d2e30ff85 | ||
|
|
1d0c279956 | ||
|
|
0202ca5b23 | ||
|
|
5e8ebeadc3 | ||
|
|
8bfb0c45a8 | ||
|
|
2c1ecf32ad | ||
|
|
9cc4bd722d | ||
|
|
d5f1e091b8 | ||
|
|
439f3b44cb | ||
|
|
e07bd39e95 | ||
|
|
11fe2ba2a4 | ||
|
|
2b0e507f45 | ||
|
|
84bad0aa6c | ||
|
|
b41cda84e4 | ||
|
|
0ae6fb2833 | ||
|
|
21b6a1f4ef | ||
|
|
69f9da4be4 | ||
|
|
032db0921d | ||
|
|
eef4601ce2 | ||
|
|
71701048f5 | ||
|
|
2361bb7682 | ||
|
|
0b1b8c5c5a | ||
|
|
fb885b0e00 | ||
|
|
a2dec4f7cf | ||
|
|
521797d070 | ||
|
|
d2922dc226 | ||
|
|
ecc2b675d5 | ||
|
|
5a49b650b0 | ||
|
|
5fd57caabf | ||
|
|
bad6afc543 | ||
|
|
ffaa036115 | ||
|
|
65133e969e | ||
|
|
267ee4e03e | ||
|
|
db972ae421 | ||
|
|
ea93dd5b2d | ||
|
|
c4d738844e | ||
|
|
f124d2cabc | ||
|
|
022038878b | ||
|
|
c6a846c602 | ||
|
|
31986a1ce5 | ||
|
|
04eea3b6e4 | ||
|
|
09a5482df5 | ||
|
|
ba8e5a03ea | ||
|
|
ddb3c34078 | ||
|
|
8d4daa5d00 | ||
|
|
65e1787987 | ||
|
|
202f41b34b | ||
|
|
10497f358e | ||
|
|
ffe0dafbdc | ||
|
|
5828be28e8 | ||
|
|
b4e0605016 | ||
|
|
da7686b9b3 | ||
|
|
651d5ae56a | ||
|
|
570d8ce7eb | ||
|
|
a9491e22e4 | ||
|
|
1ba31afa23 | ||
|
|
8f30d0d468 | ||
|
|
4a8cea262b | ||
|
|
e25574ce9a | ||
|
|
9773c1cb98 | ||
|
|
6d3eafe9e8 | ||
|
|
19acab9d18 | ||
|
|
bfcda1d73c | ||
|
|
600fc7939e | ||
|
|
1189aaae4f | ||
|
|
e9c8e8d764 | ||
|
|
3e90620fcc | ||
|
|
62a364a110 | ||
|
|
fbbbe99bf4 | ||
|
|
d078ccc78c | ||
|
|
cbacb6568e | ||
|
|
65f220b570 | ||
|
|
22d83d831d | ||
|
|
ac564a67ca | ||
|
|
d27a8dc29c | ||
|
|
568dfe911e | ||
|
|
64a647ca84 | ||
|
|
6b3c3ac9b0 | ||
|
|
6438df92c6 | ||
|
|
5f98fdfafc | ||
|
|
4a1c8aadf8 | ||
|
|
441567f9d2 | ||
|
|
381fd434ad | ||
|
|
89f713899b | ||
|
|
a82c61791e | ||
|
|
734c1ddab6 | ||
|
|
1d0b31e9de | ||
|
|
1441036475 | ||
|
|
7a84352723 | ||
|
|
6f153f3879 | ||
|
|
f888091e22 | ||
|
|
e59cf2afca | ||
|
|
7516b8e662 | ||
|
|
bb4a480250 | ||
|
|
c6df18c456 | ||
|
|
cb945998d3 | ||
|
|
5622c93bd9 | ||
|
|
7552227da0 | ||
|
|
1904fce32d | ||
|
|
f1e5e572f4 | ||
|
|
d8f83170be | ||
|
|
95c135b270 | ||
|
|
8408daf070 | ||
|
|
bd8eb6a034 | ||
|
|
9c4d0ef85e | ||
|
|
1ee441314f | ||
|
|
d5d1e51bbc | ||
|
|
84aa99ba88 | ||
|
|
23e02d2c24 | ||
|
|
3ea66c6c4c | ||
|
|
50cf737db6 | ||
|
|
bf55b5a802 | ||
|
|
49bf04c6c6 | ||
|
|
f5d64f3882 | ||
|
|
48d7de53c0 | ||
|
|
d53bace4ce | ||
|
|
02c800496c | ||
|
|
0ce39946cb | ||
|
|
52b573d20f | ||
|
|
5be6faa07c | ||
|
|
ee05e818d9 | ||
|
|
29d5e4fa13 | ||
|
|
9bd830b368 | ||
|
|
7d70f816d1 | ||
|
|
0d0cf04b57 | ||
|
|
4a6e514b81 | ||
|
|
b74f9092e7 | ||
|
|
a061347d76 | ||
|
|
71586b1100 | ||
|
|
809aa6afd2 | ||
|
|
0067a036ae | ||
|
|
794d3329fe | ||
|
|
f96ed6c56f | ||
|
|
92d44eebe6 | ||
|
|
93fe734636 | ||
|
|
71a5c132f4 | ||
|
|
15a514aca5 | ||
|
|
71f74ced7d | ||
|
|
5c86c911c1 | ||
|
|
7aebc44062 | ||
|
|
6f6e1f1009 | ||
|
|
2e7f17b823 | ||
|
|
5194dae9a6 | ||
|
|
2db03669ce | ||
|
|
a6c4f83973 | ||
|
|
f625cea183 | ||
|
|
d6ee9db6ff | ||
|
|
9ea94ce177 | ||
|
|
8102163c5b | ||
|
|
1034c16bfb | ||
|
|
956f9547e7 | ||
|
|
95fb241da1 | ||
|
|
7b06af4c8b | ||
|
|
1ca0ff53c8 | ||
|
|
1e0bdf44c2 | ||
|
|
c611d6386a | ||
|
|
b326dc3bc2 | ||
|
|
7f6e21450a | ||
|
|
ee911a15c6 | ||
|
|
aa0e05f085 | ||
|
|
f1fe078cf2 | ||
|
|
e53dcf27ec | ||
|
|
6e9ce8d5a5 | ||
|
|
686d88557b | ||
|
|
b07f14d01b | ||
|
|
c200a72031 | ||
|
|
2a8ff3e50a | ||
|
|
06cde138c1 | ||
|
|
9fae62f289 | ||
|
|
a1333929e9 | ||
|
|
6a05fafe04 | ||
|
|
13c6fc60f8 | ||
|
|
ff7948ad83 | ||
|
|
3972ab207c | ||
|
|
33a8f1dab4 | ||
|
|
27f261ae4a | ||
|
|
ef59331dd3 | ||
|
|
b019731249 | ||
|
|
47aa7fc191 | ||
|
|
9356b26dfd | ||
|
|
41b626ddbd | ||
|
|
f76b41581b | ||
|
|
14f08b7759 | ||
|
|
8c1cec09d6 | ||
|
|
15c10cb14c | ||
|
|
3b2f68a400 | ||
|
|
2a9e4e0b82 |
2
.github/workflows/build_and_deploy.yml
vendored
2
.github/workflows/build_and_deploy.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7.2
|
||||
ruby-version: 3.3.0
|
||||
bundler-cache: true
|
||||
|
||||
- name: Set up Android SDK
|
||||
|
||||
218
Gemfile.lock
Normal file
218
Gemfile.lock
Normal file
@@ -0,0 +1,218 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.913.0)
|
||||
aws-sdk-core (3.191.6)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.78.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.146.1)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.110.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.220.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.31.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.1)
|
||||
base64
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
optparse (0.4.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
public_suffix (5.0.5)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.6)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.24.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.4
|
||||
2
fastlane/metadata/android/en-US/changelogs/102.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/102.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Updated look for link previews and the News tab
|
||||
- Bug fixes and improvements
|
||||
1
fastlane/metadata/android/en-US/changelogs/110.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/110.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Added option to donate to Mastodon in-app when connected to our flagship server. This option will not appear when using a 3rd party server to avoid confusion. Please check if the 3rd party server you're using accepts donations through their website.
|
||||
8
fastlane/metadata/android/en-US/changelogs/93.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/93.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
- Adjust filters in the Notifications tab to silence unwanted alerts*
|
||||
- Opt into push notifications when a user posts by tapping the bell 🔔 in the top corner of a user's profile.
|
||||
- Mute overly active conversation notifications via the More button ⋮ on your posts
|
||||
- When writing a post, choose Quiet public 🌙 visibility to avoid appearing in feeds or searches
|
||||
- Improved post legibility with adjusted line height
|
||||
- Bug fixes
|
||||
|
||||
*Your server must support filtered notifications to see this option.
|
||||
1
fastlane/metadata/android/en-US/changelogs/default.txt
Symbolic link
1
fastlane/metadata/android/en-US/changelogs/default.txt
Symbolic link
@@ -0,0 +1 @@
|
||||
110.txt
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 46 KiB |
@@ -1,4 +1,4 @@
|
||||
Mastodon est le plus grand réseau social décentralisé sur Internet. Au lieu d’un site Web unique, c’est un réseau de millions d’utilisateurs dans des communautés indépendantes qui peuvent tous interagir les uns avec les autres, de manière transparente. Peu importe ce que vous êtes, vous pouvez rencontrer des gens passionnés qui publient à ce sujet sur Mastodon !
|
||||
Mastodon est le plus grand réseau social décentralisé sur Internet. Au lieu d’un site Web unique, c’est un réseau de millions d’utilisateurs dans des communautés indépendantes qui peuvent tous interagir les uns avec les autres, de manière transparente. Peu importe ce que vous aimez, vous pouvez rencontrer des gens passionnés qui en parlent sur Mastodon !
|
||||
|
||||
Rejoignez une communauté et créez votre profil. Trouvez et suivez des personnes fascinantes et lisez leurs messages chronologiquement et sans publicité. Exprimez-vous avec des émojis personnalisés, des images, des GIFs, des vidéos et de l’audio dans des messages de 500 caractères. Répondez aux sujets de discussions et aux reblogues de n’importe qui pour partager des choses géniales. Trouvez de nouveaux comptes à suivre et des hashtags tendance pour étendre votre réseau.
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
A Mastodon a legnagyobb decentralizált közösségi hálózat az interneten. Egyetlen weboldal helyett, ez több millió felhasználóból álló, független közösségek hálózata, amelyek egymással kapcsolatba tudnak lépni, zökkenőmentesen. Nem számít, mi az érdeklődésed, a Mastodonon találkozhatsz róla posztoló lelkes emberekkel!
|
||||
|
||||
Csatlakozz egy közösséghez és készítsd el a profilodat. Találj meg és kövess be fantasztikus embereket, olvasd el a bejegyzéseiket reklámmentesen, időrendben. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú bejegyzésekben. Válaszolj szálakban, told meg bárki bejegyzését, hogy megoszthass szuper dolgokat. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat.
|
||||
Csatlakozz egy közösséghez és készítsd el a profilodat. Találj meg és kövess fantasztikus embereket, olvasd el a bejegyzéseiket reklámmentesen, időrendben. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú bejegyzésekben. Válaszolj szálakban, told meg bárki bejegyzését, hogy megoszthass szuper dolgokat. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat.
|
||||
|
||||
A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a bejegyzéseidet csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. A tartalomfigyelmeztetések elrejthetővé teszik az érzékeny vagy ingerlő tartalmakat addig, amíg nem vagy kész azok megtekintésére. Minden közösségnek saját irényelvei és moderátorai vannak arra, hogy biztonságban tudják a tagjaikat. Erőteljes letiltási és bejelentési eszközök segítik a visszaélések megelőzését.
|
||||
A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a bejegyzéseidet csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. A tartalmi figyelmeztetések elrejthetővé teszik az érzékeny vagy ingerlő tartalmakat addig, amíg nem vagy kész azok megtekintésére. Minden közösségnek saját irányelvei és moderátorai vannak arra, hogy biztonságban tudják a tagjaikat. Robusztus letiltási és jelentési eszközök segítik a visszaélések megelőzését.
|
||||
|
||||
További funkciók:
|
||||
|
||||
• Sötét mód: Olvasd a bejegyzéseket világos, sötét vagy teljesen fekete módban
|
||||
• Sötét mód: olvasd a bejegyzéseket világos, sötét vagy valódi fekete módban
|
||||
• Szavazás: Kérd ki a követőid véleményét és összesítsd a szavazataikat
|
||||
• Felfedezés: A felkapott hashtagek és fiókok egyetlen kattintásra vannak
|
||||
• Értesítések: Értesülj az új követőidről, válaszokról, megtolásokról
|
||||
• Megosztás: Írj bejegyzést Mastodonra bármely app megosztási funkciójával
|
||||
• Cukiság: A kabalánk egy cuki elefánt, mely fel fog bukkanni időről időre
|
||||
|
||||
A Mastodon egy bejegyzett non-profit szervezet, a fejlesztés közvetlenül a te adományaidból történik. Nincs hirdetés, se monetizáció, se kockázati tőke, és ez így is fog maradni.
|
||||
A Mastodon egy bejegyzett nonprofit szervezet, a fejlesztés közvetlenül a felhasználók adományaiból történik. Nincs hirdetés, se monetizáció, se kockázati tőke, és ez így is fog maradni.
|
||||
|
||||
16
fastlane/metadata/android/ia/full_description.txt
Normal file
16
fastlane/metadata/android/ia/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon es le rete social decentralisate le plus grande sur internet. In vice que un sol sito web, illo es un rete de milliones de usatores in communitates independente que pote interager le unes con le alteres, transparentemente. Non importa lo que te interessa, tu pote incontrar personas passionate que parla de illo sur Mastodon!
|
||||
|
||||
Inscribe te a un communitate e crea tu profilo. Trova e seque personas fascinante e lege lor messages in un chronologia libere de publicitate. Exprime te con emojis personalisate, imagines, GIFs, videos e audio in messages de 500 characteres. Responde a discussiones e impulsa messages de quicunque pro compartir cosas genial. Trova nove contos a sequer e hashtags de tendentia pro expander tu rete.
|
||||
|
||||
Mastodon es producite con attention a confidentialitate e securitate. Decide si tu messages es compartite con tu sequitores, con solo le gente que tu mentiona, o con tote le mundo. Le advertimentos de contento te permitte celar messages que contine material sensibile o provocatori usque tu es preste a interager con illos. Cata communitate ha su proprie directivas e moderatores pro mantener su membros secur, e robuste utensiles de blocage e de reportage pro adjutar a impedir abusos.
|
||||
|
||||
Altere functiones:
|
||||
|
||||
• Modo obscur: lege messages in modo clar, obscur, o vermente nigre
|
||||
• Sondages: Demanda al sequitores lor opinion e conta le votos
|
||||
• Explorar: le hashtags e contos popular es a portata de mano
|
||||
• Notificationes: sia notificate de nove sequitores, responsas e impulsos
|
||||
• Compartir: publica directemente sur Mastodon desde le function Compartir de qualcunque app
|
||||
• Pachydermo: nostre mascotte es un adorabile elephante, e tu lo videra apparer de tempore in tempore
|
||||
|
||||
Mastodon es un organisation registrate sin scopo lucrative e le disveloppamento es sustenite directemente per tu donationes. Il non ha publicitate, ni monetisation, ni capital de risco, e nos intende mantener lo assi.
|
||||
1
fastlane/metadata/android/ia/short_description.txt
Normal file
1
fastlane/metadata/android/ia/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Rete social decentralisate
|
||||
1
fastlane/metadata/android/ia/title.txt
Normal file
1
fastlane/metadata/android/ia/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon dalah jaringan sosial terdesentralisasi terbesar di internet. Bukan hanya satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat saling interaksi antar sesama, tanpa batasan. Apapun yang kamu minati, kamu dapat bertemu orang-orang baru yang mengirimkan apa yang mereka minati di Mastodon!
|
||||
|
||||
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam linimasa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
||||
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam lini masa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
||||
|
||||
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon – didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
|
||||
„Mastodon“ – tai didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, „Mastodon“ platformoje gali sutikti aistringų žmonių, skelbiančių apie tai!
|
||||
|
||||
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
|
||||
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perdalyk bet kurio asmens įrašus, kad galėtum bendrinti puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
|
||||
|
||||
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
|
||||
„Mastodon“ sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
|
||||
|
||||
Daugiau funkcijų:
|
||||
|
||||
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
|
||||
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
|
||||
• Naršyti: tendencingos saitažodžiai ir paskyros – vos nuo vieno prisilietimo
|
||||
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
|
||||
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
|
||||
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
|
||||
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu.
|
||||
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus.
|
||||
• Naršyti: tendencingos saitažodžiai ir paskyros – vos nuo vieno prisilietimo.
|
||||
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir pasidalinimus.
|
||||
• Bendrinti: skelbk tiesiogiai į „Mastodon“ iš bet kurio bendrinimo lapo bet kurioje programėlėje
|
||||
• Mielumas: mūsų talismanas – žavus drambliukas, kurį retkarčiais pamatysi.
|
||||
|
||||
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.
|
||||
„Mastodon“ registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokių reklamų, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
|
||||
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个单一的网站,而是由一个个独立社区中的数百万用户组成的网络,所有这些用户都可以无缝交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
|
||||
|
||||
加入一个社区节点并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频,在最多 500 字的帖文中表达自我。 通过回复或转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
|
||||
加入一个社区并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频,在最多 500 字的帖文中表达自我。 通过回复或转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
|
||||
|
||||
Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分享分享对象,可以是你的关注者、你提到的人或是整个世界。 在你做好充足的互动准备之前,内容警告可以隐藏包含敏感或刺激内容的帖文。 每个社区都有自己的规则和管理员来保证其成员安全,同时还有强力的屏蔽和举报工具来避免滥用。
|
||||
|
||||
@@ -10,7 +10,7 @@ Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分
|
||||
• 投票:询问关注者的意见并统计他们的投票
|
||||
• 探索:热门的话题标签及账号只有一触之遥
|
||||
• 通知:获取关注、回复和转发相关的通知提醒
|
||||
• 分享:从其他应用中的分享菜单中直接发布到 Mastodon
|
||||
• 分享:从任意应用的分享菜单直接发布到 Mastodon
|
||||
• 吉祥物:你会不时地看到我们可爱的长毛象
|
||||
|
||||
Mastodon 是一个直接由用户捐赠支持、已注册非营利开发项目。 它没有广告、没有商业化,也没有风险资本,并且我们也计划保持这种方式。
|
||||
|
||||
@@ -4,15 +4,18 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
compileSdk 34
|
||||
defaultConfig {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 89
|
||||
versionName "2.4.1"
|
||||
targetSdk 34
|
||||
versionCode 111
|
||||
versionName "2.6.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -87,7 +90,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||
implementation 'me.grishka.appkit:appkit:1.3.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
@@ -98,7 +101,7 @@ dependencies {
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
def appCenterSdkVersion = "4.4.2"
|
||||
def appCenterSdkVersion = "5.0.4"
|
||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||
@@ -31,10 +32,11 @@
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/Theme.Mastodon.AutoLightDark"
|
||||
android:largeHeap="true">
|
||||
android:largeHeap="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
@@ -78,8 +80,10 @@
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".DonationFragmentActivity" android:exported="false" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
|
||||
<service android:name=".NotificationActionHandlerService" android:exported="false"/>
|
||||
|
||||
<receiver android:name=".PushNotificationReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
|
||||
<intent-filter>
|
||||
|
||||
@@ -88,8 +88,13 @@ public class AudioPlayerService extends Service{
|
||||
nm=getSystemService(NotificationManager.class);
|
||||
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
|
||||
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE), RECEIVER_EXPORTED);
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP), RECEIVER_EXPORTED);
|
||||
}else{
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
|
||||
}
|
||||
instance=this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.fragments.DonationWebViewFragment;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
// This exists because our designer wanted to avoid extra sheet showing/hiding animations.
|
||||
// This is the only way to show a fragment on top of a sheet without having to rewrite way too many things.
|
||||
public class DonationFragmentActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
DonationWebViewFragment fragment=new DonationWebViewFragment();
|
||||
fragment.setArguments(getIntent().getBundleExtra("fragmentArgs"));
|
||||
showFragment(fragment);
|
||||
overridePendingTransition(R.anim.fragment_enter, R.anim.no_op_300ms);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish(){
|
||||
super.finish();
|
||||
overridePendingTransition(0, R.anim.fragment_exit);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||
public static ThemePreference theme;
|
||||
public static ThemePreference theme=ThemePreference.AUTO;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
|
||||
@@ -61,6 +61,7 @@ public class MainActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent){
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
if(intent.getBooleanExtra("fromNotification", false)){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
AccountSession accountSession;
|
||||
@@ -85,6 +86,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
showCompose();
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}else if(intent.getBooleanExtra("explore", false)){
|
||||
restartHomeFragment();
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||
}*/
|
||||
@@ -152,6 +155,11 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
fragment.setArguments(args);
|
||||
showFragment(fragment);
|
||||
Intent intent=getIntent();
|
||||
intent.removeExtra("fromNotification");
|
||||
intent.removeExtra("notification");
|
||||
intent.removeExtra("accountID");
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
private void showCompose(){
|
||||
@@ -206,6 +214,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else if(intent.getBooleanExtra("explore", false) && fragment instanceof HomeFragment hf){
|
||||
getWindow().getDecorView().post(()->hf.setCurrentTab(R.id.tab_search));
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}else{
|
||||
@@ -213,4 +223,10 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Fragment getTopmostFragment(){
|
||||
if(fragmentContainers.isEmpty())
|
||||
return null;
|
||||
return getFragmentManager().findFragmentById(fragmentContainers.get(fragmentContainers.size()-1).getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteInput;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class NotificationActionHandlerService extends Service{
|
||||
private static final String TAG="NotificationActionHandl";
|
||||
private int runningRequestCount=0;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId){
|
||||
String action=intent.getStringExtra("action");
|
||||
String account=intent.getStringExtra("account");
|
||||
String postID=intent.getStringExtra("post");
|
||||
String notificationTag=intent.getStringExtra("notificationTag");
|
||||
if(action==null || account==null || postID==null || notificationTag==null){
|
||||
maybeStopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
NotificationManager nm=getSystemService(NotificationManager.class);
|
||||
StatusBarNotification notification=findNotification(notificationTag);
|
||||
if("reply".equals(action)){
|
||||
Bundle remoteInputResults=RemoteInput.getResultsFromIntent(intent);
|
||||
if(remoteInputResults==null){
|
||||
maybeStopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
CharSequence replyText=remoteInputResults.getCharSequence("replyText");
|
||||
if(replyText==null){
|
||||
maybeStopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
req.inReplyToId=postID;
|
||||
req.status=intent.getStringExtra("replyPrefix")+replyText;
|
||||
req.visibility=StatusPrivacy.valueOf(intent.getStringExtra("visibility"));
|
||||
runningRequestCount++;
|
||||
new CreateStatus(req, UUID.randomUUID().toString())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
E.post(new StatusCreatedEvent(result, account));
|
||||
if(notification!=null){
|
||||
Notification n=notification.getNotification();
|
||||
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
|
||||
}
|
||||
runningRequestCount--;
|
||||
maybeStopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(NotificationActionHandlerService.this);
|
||||
if(notification!=null){
|
||||
Notification n=notification.getNotification();
|
||||
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
|
||||
}
|
||||
runningRequestCount--;
|
||||
maybeStopSelf();
|
||||
}
|
||||
})
|
||||
.exec(account);
|
||||
}else if("favorite".equals(action)){
|
||||
PendingIntent prevActionIntent;
|
||||
if(notification!=null){
|
||||
Notification n=notification.getNotification();
|
||||
prevActionIntent=n.actions[1].actionIntent;
|
||||
n.actions[1].actionIntent=null;
|
||||
n.actions[1].title=getString(R.string.button_favorited);
|
||||
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
|
||||
}else{
|
||||
prevActionIntent=null;
|
||||
}
|
||||
runningRequestCount++;
|
||||
new SetStatusFavorited(postID, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
runningRequestCount--;
|
||||
maybeStopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(notification!=null){
|
||||
Notification n=notification.getNotification();
|
||||
n.actions[1].actionIntent=prevActionIntent;
|
||||
n.actions[1].title=getString(R.string.button_favorite);
|
||||
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
|
||||
}
|
||||
error.showToast(NotificationActionHandlerService.this);
|
||||
runningRequestCount--;
|
||||
maybeStopSelf();
|
||||
}
|
||||
})
|
||||
.exec(account);
|
||||
}else if("boost".equals(action)){
|
||||
PendingIntent prevActionIntent;
|
||||
if(notification!=null){
|
||||
Notification n=notification.getNotification();
|
||||
prevActionIntent=n.actions[2].actionIntent;
|
||||
n.actions[2].actionIntent=null;
|
||||
n.actions[2].title=getString(R.string.button_reblogged);
|
||||
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
|
||||
}else{
|
||||
prevActionIntent=null;
|
||||
}
|
||||
runningRequestCount++;
|
||||
new SetStatusReblogged(postID, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
runningRequestCount--;
|
||||
maybeStopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(notification!=null){
|
||||
Notification n=notification.getNotification();
|
||||
n.actions[2].actionIntent=prevActionIntent;
|
||||
n.actions[2].title=getString(R.string.button_reblog);
|
||||
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
|
||||
}
|
||||
error.showToast(NotificationActionHandlerService.this);
|
||||
runningRequestCount--;
|
||||
maybeStopSelf();
|
||||
}
|
||||
})
|
||||
.exec(account);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void maybeStopSelf(){
|
||||
if(runningRequestCount==0)
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private StatusBarNotification findNotification(String tag){
|
||||
for(StatusBarNotification sbn:getSystemService(NotificationManager.class).getActiveNotifications()){
|
||||
if(tag.equals(sbn.getTag())){
|
||||
return sbn;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,15 @@ import android.app.NotificationChannel;
|
||||
import android.app.NotificationChannelGroup;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -21,10 +22,13 @@ import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -75,7 +79,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
}
|
||||
String accountID=account.getID();
|
||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||
new GetNotificationByID(pn.notificationId+"")
|
||||
new GetNotificationByID(pn.notificationId)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(org.joinmastodon.android.model.Notification result){
|
||||
@@ -144,19 +148,114 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
.setContentText(pn.body)
|
||||
.setStyle(new Notification.BigTextStyle().bigText(pn.body))
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(context, (accountID+pn.notificationId).hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
|
||||
.setShowWhen(true)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setLights(context.getColor(R.color.primary_700), 500, 1000)
|
||||
.setColor(context.getColor(R.color.primary_700));
|
||||
.setColor(context.getColor(R.color.primary_700))
|
||||
.setGroup(accountID);
|
||||
if(avatar!=null){
|
||||
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
|
||||
}
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){
|
||||
builder.setSubText(accountName);
|
||||
}
|
||||
nm.notify(accountID, NOTIFICATION_ID, builder.build());
|
||||
String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id);
|
||||
if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!notification.status.account.id.equals(ownID))
|
||||
mentions.add('@'+notification.status.account.acct);
|
||||
for(Mention mention:notification.status.mentions){
|
||||
if(mention.id.equals(ownID))
|
||||
continue;
|
||||
String m='@'+mention.acct;
|
||||
if(!mentions.contains(m))
|
||||
mentions.add(m);
|
||||
}
|
||||
String replyPrefix=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
Intent replyIntent=new Intent(context, NotificationActionHandlerService.class);
|
||||
replyIntent.putExtra("action", "reply");
|
||||
replyIntent.putExtra("account", accountID);
|
||||
replyIntent.putExtra("post", notification.status.id);
|
||||
replyIntent.putExtra("notificationTag", notificationTag);
|
||||
replyIntent.putExtra("visibility", notification.status.visibility.toString());
|
||||
replyIntent.putExtra("replyPrefix", replyPrefix);
|
||||
builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px),
|
||||
context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE))
|
||||
.addRemoteInput(new RemoteInput.Builder("replyText").setLabel(context.getString(R.string.button_reply)).build())
|
||||
.build());
|
||||
}
|
||||
|
||||
Intent favIntent=new Intent(context, NotificationActionHandlerService.class);
|
||||
favIntent.putExtra("action", "favorite");
|
||||
favIntent.putExtra("account", accountID);
|
||||
favIntent.putExtra("post", notification.status.id);
|
||||
favIntent.putExtra("notificationTag", notificationTag);
|
||||
builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_star_24px),
|
||||
context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)).build());
|
||||
|
||||
PendingIntent boostActionIntent;
|
||||
if(notification.status.visibility!=StatusPrivacy.DIRECT){
|
||||
Intent boostIntent=new Intent(context, NotificationActionHandlerService.class);
|
||||
boostIntent.putExtra("action", "boost");
|
||||
boostIntent.putExtra("account", accountID);
|
||||
boostIntent.putExtra("post", notification.status.id);
|
||||
boostIntent.putExtra("notificationTag", notificationTag);
|
||||
boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}else{
|
||||
boostActionIntent=null;
|
||||
}
|
||||
builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_boost_24px),
|
||||
context.getString(R.string.button_reblog), boostActionIntent).build());
|
||||
}
|
||||
nm.notify(notificationTag, NOTIFICATION_ID, builder.build());
|
||||
|
||||
StatusBarNotification[] activeNotifications=nm.getActiveNotifications();
|
||||
ArrayList<String> summaryLines=new ArrayList<>();
|
||||
int notificationCount=0;
|
||||
for(StatusBarNotification sbn:activeNotifications){
|
||||
String tag=sbn.getTag();
|
||||
if(tag!=null && tag.startsWith(accountID+"_")){
|
||||
if((sbn.getNotification().flags & Notification.FLAG_GROUP_SUMMARY)==0){
|
||||
if(summaryLines.size()<5){
|
||||
summaryLines.add(sbn.getNotification().extras.getString("android.title"));
|
||||
}
|
||||
notificationCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(summaryLines.size()>1){
|
||||
Notification.Builder summaryBuilder;
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
summaryBuilder=new Notification.Builder(context, accountID+"_"+pn.notificationType);
|
||||
}else{
|
||||
summaryBuilder=new Notification.Builder(context)
|
||||
.setPriority(Notification.PRIORITY_DEFAULT);
|
||||
}
|
||||
Notification.InboxStyle inboxStyle=new Notification.InboxStyle();
|
||||
for(String line:summaryLines){
|
||||
inboxStyle.addLine(line);
|
||||
}
|
||||
summaryBuilder.setContentTitle(context.getString(R.string.app_name))
|
||||
.setContentText(context.getResources().getQuantityString(R.plurals.x_new_notifications, notificationCount, notificationCount))
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setColor(context.getColor(R.color.primary_700))
|
||||
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
|
||||
.setShowWhen(true)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setAutoCancel(true)
|
||||
.setGroup(accountID)
|
||||
.setGroupSummary(true)
|
||||
.setStyle(inboxStyle.setSummaryText(accountName));
|
||||
nm.notify(accountID+"_summary", NOTIFICATION_ID, summaryBuilder.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -45,8 +44,8 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=3;
|
||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
public static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
private final String accountID;
|
||||
private DatabaseHelper db;
|
||||
@@ -467,9 +466,4 @@ public class CacheController{
|
||||
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface DatabaseRunnable{
|
||||
void run(SQLiteDatabase db) throws IOException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DatabaseRunnable{
|
||||
void run(SQLiteDatabase db) throws IOException;
|
||||
}
|
||||
@@ -12,10 +12,12 @@ import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.time.Instant;
|
||||
@@ -29,6 +31,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.WorkerThread;
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -49,8 +53,11 @@ public class MastodonAPIController{
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.cache(new Cache(new File(MastodonApp.context.getCacheDir(), "http"), 10*1024*1024))
|
||||
.build();
|
||||
|
||||
private static final CacheControl NO_CACHE_WHATSOEVER=new CacheControl.Builder().noCache().noStore().build();
|
||||
|
||||
private AccountSession session;
|
||||
|
||||
static{
|
||||
@@ -80,6 +87,9 @@ public class MastodonAPIController{
|
||||
if(token!=null)
|
||||
builder.header("Authorization", "Bearer "+token);
|
||||
|
||||
if(!req.cacheable)
|
||||
builder.cacheControl(NO_CACHE_WHATSOEVER);
|
||||
|
||||
if(req.headers!=null){
|
||||
for(Map.Entry<String, String> header:req.headers.entrySet()){
|
||||
builder.header(header.getKey(), header.getValue());
|
||||
@@ -113,8 +123,10 @@ public class MastodonAPIController{
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
if(req.canceled)
|
||||
if(req.canceled){
|
||||
response.close();
|
||||
return;
|
||||
}
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||
synchronized(req){
|
||||
|
||||
@@ -46,6 +46,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
boolean canceled;
|
||||
Map<String, String> headers;
|
||||
long timeout;
|
||||
boolean cacheable;
|
||||
private ProgressDialog progressDialog;
|
||||
protected boolean removeUnsupportedItems;
|
||||
|
||||
@@ -132,6 +133,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
this.timeout=timeout;
|
||||
}
|
||||
|
||||
protected void setCacheable(){
|
||||
cacheable=true;
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v1";
|
||||
}
|
||||
|
||||
@@ -5,31 +5,62 @@ import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.JsonIOException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MastodonErrorResponse extends ErrorResponse{
|
||||
public final String error;
|
||||
public final int httpStatus;
|
||||
public final Throwable underlyingException;
|
||||
public final int messageResource;
|
||||
|
||||
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
|
||||
this.error=error;
|
||||
this.httpStatus=httpStatus;
|
||||
this.underlyingException=exception;
|
||||
|
||||
if(exception instanceof UnknownHostException){
|
||||
this.messageResource=R.string.could_not_reach_server;
|
||||
}else if(exception instanceof SocketTimeoutException){
|
||||
this.messageResource=R.string.connection_timed_out;
|
||||
}else if(exception instanceof JsonSyntaxException || exception instanceof JsonIOException || httpStatus>=500){
|
||||
this.messageResource=R.string.server_error;
|
||||
}else if(httpStatus==404){
|
||||
this.messageResource=R.string.not_found;
|
||||
}else{
|
||||
this.messageResource=0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindErrorView(View view){
|
||||
TextView text=view.findViewById(R.id.error_text);
|
||||
text.setText(error);
|
||||
String message;
|
||||
if(messageResource>0){
|
||||
message=view.getContext().getString(messageResource, error);
|
||||
}else{
|
||||
message=error;
|
||||
}
|
||||
text.setText(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showToast(Context context){
|
||||
if(context==null)
|
||||
return;
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
|
||||
String message;
|
||||
if(messageResource>0){
|
||||
message=context.getString(messageResource, error);
|
||||
}else{
|
||||
message=error;
|
||||
}
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
|
||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
|
||||
if(followed)
|
||||
setRequestBody(new Request(showReblogs, null));
|
||||
setRequestBody(new Request(showReblogs, notify));
|
||||
else
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import java.util.List;
|
||||
public class GetCatalogInstances extends MastodonAPIRequest<List<CatalogInstance>>{
|
||||
|
||||
private String lang, category;
|
||||
private boolean includeClosedSignups;
|
||||
|
||||
public GetCatalogInstances(String lang, String category){
|
||||
public GetCatalogInstances(String lang, String category, boolean includeClosedSignups){
|
||||
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||
this.lang=lang;
|
||||
this.category=category;
|
||||
this.includeClosedSignups=includeClosedSignups;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,6 +32,8 @@ public class GetCatalogInstances extends MastodonAPIRequest<List<CatalogInstance
|
||||
builder.appendQueryParameter("language", lang);
|
||||
if(!TextUtils.isEmpty(category))
|
||||
builder.appendQueryParameter("category", category);
|
||||
if(includeClosedSignups)
|
||||
builder.appendQueryParameter("registrations", "all");
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
|
||||
public class GetDonationCampaigns extends MastodonAPIRequest<DonationCampaign>{
|
||||
private final String locale, seed, source;
|
||||
private boolean staging;
|
||||
|
||||
public GetDonationCampaigns(String locale, String seed, String source){
|
||||
super(HttpMethod.GET, null, DonationCampaign.class);
|
||||
this.locale=locale;
|
||||
this.seed=seed;
|
||||
this.source=source;
|
||||
setCacheable();
|
||||
}
|
||||
|
||||
public void setStaging(boolean staging){
|
||||
this.staging=staging;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
Uri.Builder builder=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority("api.joinmastodon.org")
|
||||
.path("/v1/donations/campaigns/active")
|
||||
.appendQueryParameter("platform", "android")
|
||||
.appendQueryParameter("locale", locale)
|
||||
.appendQueryParameter("seed", seed);
|
||||
if(staging)
|
||||
builder.appendQueryParameter("environment", "staging");
|
||||
if(!TextUtils.isEmpty(source))
|
||||
builder.appendQueryParameter("source", source);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.NotificationRequest;
|
||||
|
||||
public class GetNotificationRequests extends HeaderPaginationRequest<NotificationRequest>{
|
||||
public GetNotificationRequests(String maxID){
|
||||
super(HttpMethod.GET, "/notifications/requests", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
@@ -12,6 +13,10 @@ import java.util.List;
|
||||
|
||||
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
|
||||
this(maxID, limit, includeTypes, null);
|
||||
}
|
||||
|
||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
|
||||
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
@@ -25,6 +30,8 @@ public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
||||
addQueryParameter("exclude_types[]", type);
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(onlyAccountID))
|
||||
addQueryParameter("account_id", onlyAccountID);
|
||||
removeUnsupportedItems=true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||
|
||||
public class GetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
|
||||
public GetNotificationsPolicy(){
|
||||
super(HttpMethod.GET, "/notifications/policy", NotificationsPolicy.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class RespondToNotificationRequest extends ResultlessMastodonAPIRequest{
|
||||
public RespondToNotificationRequest(String id, boolean allow){
|
||||
super(HttpMethod.POST, "/notifications/requests/"+id+(allow ? "/accept" : "/dismiss"));
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||
|
||||
public class SetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
|
||||
public SetNotificationsPolicy(NotificationsPolicy policy){
|
||||
super(HttpMethod.PUT, "/notifications/policy", NotificationsPolicy.class);
|
||||
setRequestBody(policy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class SetStatusConversationMuted extends MastodonAPIRequest<Status>{
|
||||
public SetStatusConversationMuted(String id, boolean muted){
|
||||
super(HttpMethod.POST, "/statuses/"+id+(muted ? "/mute" : "/unmute"), Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,8 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -44,6 +45,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class AccountSession{
|
||||
private static final String TAG="AccountSession";
|
||||
private static final int MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS=28;
|
||||
|
||||
public Token token;
|
||||
public Account self;
|
||||
@@ -276,4 +278,12 @@ public class AccountSession{
|
||||
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||
}
|
||||
|
||||
public boolean isEligibleForDonations(){
|
||||
return ("mastodon.social".equalsIgnoreCase(domain) || "mastodon.online".equalsIgnoreCase(domain)) && self.createdAt.isBefore(Instant.now().minus(MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS, ChronoUnit.DAYS));
|
||||
}
|
||||
|
||||
public int getDonationSeed(){
|
||||
return Math.abs(getFullUsername().hashCode())%100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ package org.joinmastodon.android.api.session;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -18,11 +23,13 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.DatabaseRunnable;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
||||
@@ -30,9 +37,10 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -60,6 +68,7 @@ public class AccountSessionManager{
|
||||
private static final String TAG="AccountSessionManager";
|
||||
public static final String SCOPE="read write follow push";
|
||||
public static final String REDIRECT_URI="mastodon-android-auth://callback";
|
||||
private static final int DB_VERSION=1;
|
||||
|
||||
private static final AccountSessionManager instance=new AccountSessionManager();
|
||||
|
||||
@@ -73,6 +82,8 @@ public class AccountSessionManager{
|
||||
private String lastActiveAccountID;
|
||||
private SharedPreferences prefs;
|
||||
private boolean loadedInstances;
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
|
||||
public static AccountSessionManager getInstance(){
|
||||
return instance;
|
||||
@@ -94,7 +105,7 @@ public class AccountSessionManager{
|
||||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
|
||||
readInstanceInfo(domains);
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
@@ -270,11 +281,11 @@ public class AccountSessionManager{
|
||||
}
|
||||
}
|
||||
if(loadedInstances){
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
maybeUpdateInstanceInfo(domains);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateCustomEmojis(Set<String> domains){
|
||||
private void maybeUpdateInstanceInfo(Set<String> domains){
|
||||
long now=System.currentTimeMillis();
|
||||
for(String domain:domains){
|
||||
Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
@@ -388,7 +399,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
if(!loadedInstances){
|
||||
loadedInstances=true;
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
MastodonAPIController.runInBackground(()->maybeUpdateInstanceInfo(domains));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +436,7 @@ public class AccountSessionManager{
|
||||
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
|
||||
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
|
||||
// There are no shortcuts, but there are accounts. Add a compose shortcut.
|
||||
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
||||
ShortcutInfo compose=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
||||
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
||||
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
|
||||
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
|
||||
@@ -433,15 +444,85 @@ public class AccountSessionManager{
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.putExtra("compose", true))
|
||||
.build();
|
||||
sm.setDynamicShortcuts(Collections.singletonList(info));
|
||||
ShortcutInfo explore=new ShortcutInfo.Builder(MastodonApp.context, "explore")
|
||||
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
||||
.setShortLabel(MastodonApp.context.getString(R.string.tab_search))
|
||||
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_explore))
|
||||
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.putExtra("explore", true))
|
||||
.build();
|
||||
sm.setDynamicShortcuts(List.of(compose, explore));
|
||||
}else if(sessions.isEmpty()){
|
||||
// There are shortcuts, but no accounts. Disable existing shortcuts.
|
||||
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
|
||||
sm.disableShortcuts(List.of("compose", "explore"), MastodonApp.context.getString(R.string.err_not_logged_in));
|
||||
}else{
|
||||
sm.enableShortcuts(Collections.singletonList("compose"));
|
||||
sm.enableShortcuts(List.of("compose", "explore"));
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDelayed(){
|
||||
CacheController.databaseThread.postRunnable(databaseCloseRunnable, 10_000);
|
||||
}
|
||||
|
||||
public void closeDatabase(){
|
||||
if(db!=null){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "closeDatabase");
|
||||
db.close();
|
||||
db=null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelDelayedClose(){
|
||||
if(db!=null){
|
||||
CacheController.databaseThread.handler.removeCallbacks(databaseCloseRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
private SQLiteDatabase getOrOpenDatabase(){
|
||||
if(db==null)
|
||||
db=new DatabaseHelper();
|
||||
return db.getWritableDatabase();
|
||||
}
|
||||
|
||||
private void runOnDbThread(DatabaseRunnable r){
|
||||
cancelDelayedClose();
|
||||
CacheController.databaseThread.postRunnable(()->{
|
||||
try{
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
r.run(db);
|
||||
}catch(SQLiteException|IOException x){
|
||||
Log.w(TAG, x);
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public void runIfDonationCampaignNotDismissed(String id, Runnable action){
|
||||
runOnDbThread(db->{
|
||||
try(Cursor cursor=db.query("dismissed_donation_campaigns", null, "id=?", new String[]{id}, null, null, null)){
|
||||
if(!cursor.moveToFirst()){
|
||||
UiUtils.runOnUiThread(action);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void markDonationCampaignAsDismissed(String id){
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("id", id);
|
||||
values.put("dismissed_at", System.currentTimeMillis());
|
||||
db.insert("dismissed_donation_campaigns", null, values);
|
||||
});
|
||||
}
|
||||
|
||||
public void clearDismissedDonationCampaigns(){
|
||||
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
|
||||
}
|
||||
|
||||
private static class SessionsStorageWrapper{
|
||||
public List<AccountSession> accounts;
|
||||
}
|
||||
@@ -451,4 +532,24 @@ public class AccountSessionManager{
|
||||
public List<Emoji> emojis;
|
||||
public long lastUpdated;
|
||||
}
|
||||
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper{
|
||||
public DatabaseHelper(){
|
||||
super(MastodonApp.context, "accounts.db", null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db){
|
||||
db.execSQL("""
|
||||
CREATE TABLE `dismissed_donation_campaigns` (
|
||||
`id` text PRIMARY KEY,
|
||||
`dismissed_at` bigint
|
||||
)""");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class DismissDonationCampaignBannerEvent{
|
||||
public final String campaignID;
|
||||
|
||||
public DismissDonationCampaignBannerEvent(String campaignID){
|
||||
this.campaignID=campaignID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationRequestRespondedEvent{
|
||||
public final String accountID, requestID;
|
||||
|
||||
public NotificationRequestRespondedEvent(String accountID, String requestID){
|
||||
this.accountID=accountID;
|
||||
this.requestID=requestID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
|
||||
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
public class AccountNotificationsListFragment extends BaseNotificationsListFragment{
|
||||
private Account account;
|
||||
private String requestID;
|
||||
private TextView expandedTitle;
|
||||
private boolean choiceMade, allowed;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
requestID=getArguments().getString("requestID");
|
||||
setTitleMarqueeEnabled(false);
|
||||
loadData();
|
||||
setTitle(getString(R.string.notifications_from_user, account.displayName));
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(!refreshing && endMark!=null)
|
||||
endMark.setVisibility(View.GONE);
|
||||
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=list.getAdapter().getItemCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
|
||||
expandedTitle=(TextView) LayoutInflater.from(getActivity()).inflate(R.layout.expanded_title_medium, list, false);
|
||||
expandedTitle.setText(getTitle());
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(expandedTitle));
|
||||
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
if(recyclerView.getChildCount()==0)
|
||||
return;
|
||||
float fraction;
|
||||
View topChild=recyclerView.getChildAt(0);
|
||||
if(recyclerView.getChildAdapterPosition(topChild)>0){
|
||||
fraction=1;
|
||||
}else{
|
||||
fraction=(-topChild.getTop())/(float)(topChild.getHeight()-topChild.getPaddingBottom());
|
||||
}
|
||||
expandedTitle.setAlpha(1f-fraction);
|
||||
toolbarTitleView.setAlpha(fraction);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notification_request, menu);
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
MenuItem allow=menu.findItem(R.id.allow);
|
||||
if(choiceMade && allowed){
|
||||
allow.setIcon(R.drawable.ic_check_wght700_24px);
|
||||
tintMenuIcon(allow, R.attr.colorM3Primary);
|
||||
}else{
|
||||
tintMenuIcon(allow, R.attr.colorM3OnSurfaceVariant);
|
||||
}
|
||||
if(choiceMade && !allowed){
|
||||
mute.setIcon(R.drawable.ic_delete_wght700_24px);
|
||||
tintMenuIcon(mute, R.attr.colorM3Primary);
|
||||
}else{
|
||||
tintMenuIcon(mute, R.attr.colorM3OnSurfaceVariant);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(choiceMade)
|
||||
return true;
|
||||
allowed=item.getItemId()==R.id.allow;
|
||||
new RespondToNotificationRequest(requestID, allowed)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
choiceMade=true;
|
||||
invalidateOptionsMenu();
|
||||
E.post(new NotificationRequestRespondedEvent(accountID, requestID));
|
||||
new Snackbar.Builder(getActivity())
|
||||
.setText(getString(allowed ? R.string.notifications_allowed : R.string.notifications_muted, account.displayName))
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
}
|
||||
return super.buildDisplayItems(n);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted(){
|
||||
return false;
|
||||
}
|
||||
|
||||
private void tintMenuIcon(MenuItem item, int color){
|
||||
int tintColor=UiUtils.getThemeColor(getActivity(), color);
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
|
||||
Drawable icon=item.getIcon();
|
||||
if(icon!=null && icon.getColorFilter()==null){
|
||||
icon=icon.mutate();
|
||||
icon.setTintList(ColorStateList.valueOf(tintColor));
|
||||
item.setIcon(icon);
|
||||
}
|
||||
}else{
|
||||
item.setIconTintList(ColorStateList.valueOf(tintColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,11 +140,6 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMainAdapterOffset(){
|
||||
return super.getMainAdapterOffset()+1;
|
||||
}
|
||||
|
||||
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
|
||||
return switch(filter){
|
||||
case DEFAULT -> defaultFilter;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
protected String maxID;
|
||||
protected View endMark;
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
NotificationHeaderStatusDisplayItem titleItem;
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
titleItem=null;
|
||||
}else{
|
||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||
if(n.status!=null){
|
||||
n.status.card=null;
|
||||
n.status.spoilerText=null;
|
||||
}
|
||||
}
|
||||
if(n.status!=null){
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else if(titleItem!=null){
|
||||
return Collections.singletonList(titleItem);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Notification s){
|
||||
if(!knownAccounts.containsKey(s.account.id))
|
||||
knownAccounts.put(s.account.id, s.account);
|
||||
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
||||
knownAccounts.put(s.status.account.id, s.status.account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Notification n=getNotificationByID(id);
|
||||
if(n.status!=null){
|
||||
Status status=n.status;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}else{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
for(Notification n : data){
|
||||
if(n.id.equals(id))
|
||||
return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void removeNotification(Notification n){
|
||||
data.remove(n);
|
||||
preloadedData.remove(n);
|
||||
int index=-1;
|
||||
for(int i=0; i<displayItems.size(); i++){
|
||||
if(n.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(index==-1)
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateFooterView(LayoutInflater inflater){
|
||||
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||
endMark=v.findViewById(R.id.end_mark);
|
||||
endMark.setVisibility(View.GONE);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -79,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected Rect tmpRect=new Rect();
|
||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||
private SpringAnimation listShakeAnimation;
|
||||
|
||||
public BaseStatusListFragment(){
|
||||
super(20);
|
||||
@@ -283,8 +287,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
|
||||
}
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sih){
|
||||
if(sih.getItem() instanceof StatusDisplayItem sdi && sdi.getType()==StatusDisplayItem.Type.GAP){
|
||||
outRect.setEmpty();
|
||||
return;
|
||||
}
|
||||
@@ -292,8 +296,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
View child=list.getChildAt(i);
|
||||
holder=list.getChildViewHolder(child);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sih2){
|
||||
String otherID=sih2.getItemID();
|
||||
if(otherID.equals(id)){
|
||||
list.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
outRect.left=Math.min(outRect.left, tmpRect.left);
|
||||
@@ -325,7 +329,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
}
|
||||
|
||||
protected int getMainAdapterOffset(){
|
||||
public int getMainAdapterOffset(){
|
||||
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
|
||||
return mergeAdapter.getPositionForAdapter(adapter);
|
||||
}
|
||||
@@ -662,7 +666,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
proceed.run();
|
||||
}, status.account, accountID).show();
|
||||
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
|
||||
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
|
||||
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS)) && !status.account.id.equals(AccountSessionManager.get(accountID).self.id)){
|
||||
new OldPostPreReplySheet(getActivity(), notAgain->{
|
||||
if(notAgain)
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
|
||||
@@ -675,6 +679,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
public void shakeListView(){
|
||||
if(listShakeAnimation!=null)
|
||||
listShakeAnimation.cancel();
|
||||
SpringAnimation anim=new SpringAnimation(list, DynamicAnimation.TRANSLATION_X, 0);
|
||||
anim.setStartVelocity(V.dp(-500));
|
||||
anim.getSpring().setStiffness(500).setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
|
||||
listShakeAnimation=anim;
|
||||
anim.addEndListener((animation, canceled, value, velocity)->listShakeAnimation=null);
|
||||
anim.start();
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
@@ -745,7 +760,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
// Do not draw dividers between hashtag and/or account rows
|
||||
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
|
||||
return false;
|
||||
return !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
|
||||
return !ih.getItemID().equals(sh.getItemID()) && ih.getItem() instanceof StatusDisplayItem sdi && sdi.getType()!=StatusDisplayItem.Type.GAP;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.transition.ChangeBounds;
|
||||
import android.transition.Fade;
|
||||
import android.transition.TransitionManager;
|
||||
import android.transition.TransitionSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -32,15 +36,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -66,7 +69,9 @@ import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
|
||||
import org.joinmastodon.android.ui.ExtendedPopupMenu;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.PopupKeyboard;
|
||||
@@ -87,21 +92,22 @@ import org.parceler.Parcels;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements ComposeEditText.SelectionListener, CustomTransitionsFragment{
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
@@ -131,7 +137,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, languageBtn;
|
||||
private TextView replyText;
|
||||
private Button visibilityBtn;
|
||||
private LinearLayout visibilityBtn;
|
||||
private TextView visibilityText1, visibilityText2, visibilityCurrentText;
|
||||
private LinearLayout bottomBar;
|
||||
private View autocompleteDivider;
|
||||
|
||||
@@ -165,6 +172,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private BackgroundColorSpan overLimitBG;
|
||||
private ForegroundColorSpan overLimitFG;
|
||||
|
||||
private Runnable emojiKeyboardHider;
|
||||
private Runnable sendingBackButtonBlocker=()->{};
|
||||
private Runnable discardConfirmationCallback=this::confirmDiscardDraftAndFinish;
|
||||
private boolean prevHadDraft;
|
||||
|
||||
public ComposeFragment(){
|
||||
super(R.layout.toolbar_fragment_with_progressbar);
|
||||
}
|
||||
@@ -241,6 +253,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
|
||||
}
|
||||
});
|
||||
emojiKeyboardHider=emojiKeyboard::hide;
|
||||
|
||||
View view=inflater.inflate(R.layout.fragment_compose, container, false);
|
||||
mainLayout=view.findViewById(R.id.compose_main_ll);
|
||||
@@ -271,6 +284,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
emojiBtn=view.findViewById(R.id.btn_emoji);
|
||||
spoilerBtn=view.findViewById(R.id.btn_spoiler);
|
||||
visibilityBtn=view.findViewById(R.id.btn_visibility);
|
||||
visibilityText1=view.findViewById(R.id.visibility_text1);
|
||||
visibilityText2=view.findViewById(R.id.visibility_text2);
|
||||
visibilityCurrentText=visibilityText1;
|
||||
languageBtn=view.findViewById(R.id.btn_language);
|
||||
replyText=view.findViewById(R.id.reply_text);
|
||||
|
||||
@@ -280,14 +296,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||
languageBtn.setOnClickListener(v->showLanguageAlert());
|
||||
visibilityBtn.setOnClickListener(this::onVisibilityClick);
|
||||
visibilityBtn.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName("android.widget.Spinner");
|
||||
}
|
||||
});
|
||||
Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate();
|
||||
arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||
visibilityBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrow, null);
|
||||
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
|
||||
@Override
|
||||
public void onIconChanged(int icon){
|
||||
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
|
||||
updateNavigationBarColor(icon!=PopupKeyboard.ICON_HIDDEN);
|
||||
if(icon!=PopupKeyboard.ICON_HIDDEN)
|
||||
addBackCallback(emojiKeyboardHider);
|
||||
else
|
||||
removeBackCallback(emojiKeyboardHider);
|
||||
if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
|
||||
contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
|
||||
if(icon==PopupKeyboard.ICON_HIDDEN)
|
||||
@@ -323,7 +349,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(editingStatus!=null && editingStatus.visibility!=null) {
|
||||
statusVisibility=editingStatus.visibility;
|
||||
}
|
||||
updateVisibilityIcon();
|
||||
updateVisibilityIcon(false);
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
|
||||
@@ -378,6 +404,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(prevHadDraft){
|
||||
prevHadDraft=false;
|
||||
removeBackCallback(discardConfirmationCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
updateDraftState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -463,6 +504,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
updateCharCounter();
|
||||
updateDraftState();
|
||||
}
|
||||
});
|
||||
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
|
||||
@@ -502,7 +544,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
ignoreSelectionChanges=true;
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
ignoreSelectionChanges=false;
|
||||
mediaViewController.onViewCreated(savedInstanceState);;
|
||||
mediaViewController.onViewCreated(savedInstanceState);
|
||||
}else{
|
||||
String prefilledText=getArguments().getString("prefilledText");
|
||||
if(!TextUtils.isEmpty(prefilledText)){
|
||||
@@ -604,6 +646,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(publishButton==null)
|
||||
return;
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
|
||||
updateDraftState();
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
@@ -679,6 +722,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
|
||||
overlayParams.token=mainEditText.getWindowToken();
|
||||
wm.addView(sendingOverlay, overlayParams);
|
||||
addBackCallback(sendingBackButtonBlocker);
|
||||
|
||||
publishButton.setEnabled(false);
|
||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||
@@ -703,8 +747,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(!pollViewController.isEmpty()){
|
||||
req.poll=pollViewController.getPollForRequest();
|
||||
}
|
||||
if(hasSpoiler && spoilerEdit.length()>0){
|
||||
req.spoilerText=spoilerEdit.getText().toString();
|
||||
if(hasSpoiler){
|
||||
if(spoilerEdit.length()>0)
|
||||
req.spoilerText=spoilerEdit.getText().toString();
|
||||
else
|
||||
req.sensitive=true;
|
||||
}
|
||||
if(postLang!=null){
|
||||
req.language=postLang.locale.toLanguageTag();
|
||||
@@ -717,6 +764,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onSuccess(Status result){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
removeBackCallback(sendingBackButtonBlocker);
|
||||
removeBackCallback(discardConfirmationCallback);
|
||||
removeBackCallback(emojiKeyboardHider);
|
||||
if(editingStatus==null){
|
||||
E.post(new StatusCreatedEvent(result, accountID));
|
||||
if(replyTo!=null){
|
||||
@@ -749,6 +799,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private void handlePublishError(ErrorResponse error){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
removeBackCallback(sendingBackButtonBlocker);
|
||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||
publishButton.setEnabled(true);
|
||||
if(error instanceof MastodonErrorResponse me){
|
||||
@@ -776,19 +827,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !mediaViewController.isEmpty() || pollFieldsHaveContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(emojiKeyboard.isVisible()){
|
||||
emojiKeyboard.hide();
|
||||
return true;
|
||||
private void updateDraftState(){
|
||||
boolean hasDraft=hasDraft();
|
||||
if(hasDraft!=prevHadDraft){
|
||||
prevHadDraft=hasDraft;
|
||||
if(hasDraft){
|
||||
addBackCallback(discardConfirmationCallback);
|
||||
}else{
|
||||
removeBackCallback(discardConfirmationCallback);
|
||||
}
|
||||
}
|
||||
if(hasDraft()){
|
||||
confirmDiscardDraftAndFinish();
|
||||
return true;
|
||||
}
|
||||
if(sendingOverlay!=null)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -822,7 +870,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private void confirmDiscardDraftAndFinish(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
|
||||
.setPositiveButton(R.string.discard, (dialog, which)->{
|
||||
removeBackCallback(discardConfirmationCallback);
|
||||
Nav.finish(this);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
@@ -909,22 +960,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void onVisibilityClick(View v){
|
||||
PopupMenu menu=new PopupMenu(getActivity(), v);
|
||||
menu.inflate(R.menu.compose_visibility);
|
||||
menu.setOnMenuItemClickListener(item->{
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.vis_public){
|
||||
statusVisibility=StatusPrivacy.PUBLIC;
|
||||
}else if(id==R.id.vis_followers){
|
||||
statusVisibility=StatusPrivacy.PRIVATE;
|
||||
}else if(id==R.id.vis_private){
|
||||
statusVisibility=StatusPrivacy.DIRECT;
|
||||
ArrayList<ListItem<StatusPrivacy>> items=new ArrayList<>();
|
||||
ExtendedPopupMenu menu=new ExtendedPopupMenu(getActivity(), items);
|
||||
Consumer<ListItem<StatusPrivacy>> onClick=i->{
|
||||
if(statusVisibility!=i.parentObject){
|
||||
statusVisibility=i.parentObject;
|
||||
updateVisibilityIcon(true);
|
||||
}
|
||||
item.setChecked(true);
|
||||
updateVisibilityIcon();
|
||||
return true;
|
||||
});
|
||||
menu.show();
|
||||
menu.dismiss();
|
||||
};
|
||||
items.add(new ListItem<>(R.string.visibility_public, R.string.visibility_subtitle_public, R.drawable.ic_public_24px, StatusPrivacy.PUBLIC, onClick));
|
||||
items.add(new ListItem<>(R.string.visibility_unlisted, R.string.visibility_subtitle_unlisted, R.drawable.ic_clear_night_24px, StatusPrivacy.UNLISTED, onClick));
|
||||
items.add(new ListItem<>(R.string.visibility_followers_only, R.string.visibility_subtitle_followers, R.drawable.ic_lock_24px, StatusPrivacy.PRIVATE, onClick));
|
||||
items.add(new ListItem<>(R.string.visibility_private, R.string.visibility_subtitle_private, R.drawable.ic_alternate_email_24px, StatusPrivacy.DIRECT, onClick));
|
||||
menu.showAsDropDown(v);
|
||||
}
|
||||
|
||||
private void loadDefaultStatusVisibility(Bundle savedInstanceState){
|
||||
@@ -950,12 +999,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private void applyPreferencesForPostVisibility(Preferences prefs, Bundle savedInstanceState){
|
||||
// Only override the reply visibility if our preference is more private
|
||||
if(prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)){
|
||||
// Map unlisted from the API onto public, because we don't have unlisted in the UI
|
||||
statusVisibility=switch(prefs.postingDefaultVisibility){
|
||||
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
|
||||
case PRIVATE -> StatusPrivacy.PRIVATE;
|
||||
case DIRECT -> StatusPrivacy.DIRECT;
|
||||
};
|
||||
statusVisibility=prefs.postingDefaultVisibility;
|
||||
}
|
||||
|
||||
// A saved privacy setting from a previous compose session wins over all
|
||||
@@ -963,28 +1007,45 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
|
||||
updateVisibilityIcon();
|
||||
updateVisibilityIcon(false);
|
||||
}
|
||||
|
||||
private void updateVisibilityIcon(){
|
||||
private void updateVisibilityIcon(boolean animated){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(statusVisibility==null){ // TODO find out why this happens
|
||||
statusVisibility=StatusPrivacy.PUBLIC;
|
||||
}
|
||||
visibilityBtn.setText(switch(statusVisibility){
|
||||
case PUBLIC, UNLISTED -> R.string.visibility_public;
|
||||
TextView visibilityText;
|
||||
if(!animated){
|
||||
visibilityText=visibilityCurrentText;
|
||||
}else{
|
||||
TransitionManager.beginDelayedTransition(visibilityBtn, new TransitionSet()
|
||||
.addTransition(new Fade(Fade.IN | Fade.OUT))
|
||||
.addTransition(new ChangeBounds().excludeTarget(TextView.class, true))
|
||||
.setDuration(250)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
);
|
||||
visibilityText=visibilityCurrentText==visibilityText1 ? visibilityText2 : visibilityText1;
|
||||
visibilityText.setVisibility(View.VISIBLE);
|
||||
visibilityCurrentText.setVisibility(View.GONE);
|
||||
visibilityCurrentText=visibilityText;
|
||||
}
|
||||
visibilityText.setText(switch(statusVisibility){
|
||||
case PUBLIC -> R.string.visibility_public;
|
||||
case UNLISTED -> R.string.visibility_unlisted;
|
||||
case PRIVATE -> R.string.visibility_followers_only;
|
||||
case DIRECT -> R.string.visibility_private;
|
||||
});
|
||||
Drawable icon=getResources().getDrawable(switch(statusVisibility){
|
||||
case PUBLIC, UNLISTED -> R.drawable.ic_public_20px;
|
||||
case PUBLIC -> R.drawable.ic_public_20px;
|
||||
case UNLISTED -> R.drawable.ic_clear_night_20px;
|
||||
case PRIVATE -> R.drawable.ic_group_20px;
|
||||
case DIRECT -> R.drawable.ic_alternate_email_20px;
|
||||
}, getActivity().getTheme()).mutate();
|
||||
icon.setBounds(0, 0, V.dp(18), V.dp(18));
|
||||
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
visibilityBtn.setCompoundDrawablesRelative(icon, null, visibilityBtn.getCompoundDrawablesRelative()[2], null);
|
||||
visibilityText.setCompoundDrawablesRelative(icon, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,12 +32,11 @@ import java.util.Collections;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
|
||||
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
|
||||
private static final String TAG="ComposeImageDescription";
|
||||
|
||||
private String accountID, attachmentID;
|
||||
@@ -138,9 +137,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
public void onStop(){
|
||||
super.onStop();
|
||||
deliverResult();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,13 +42,11 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
|
||||
public class CreateListAddMembersFragment extends BaseAccountListFragment implements AddNewListMembersFragment.Listener{
|
||||
private FollowList followList;
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
@@ -59,6 +57,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
|
||||
private WindowInsets lastInsets;
|
||||
private boolean dismissingSearchFragment;
|
||||
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||
private Runnable searchFragmentDismisser=this::dismissSearchFragment;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -156,6 +155,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
|
||||
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
rootView.setVisibility(View.GONE);
|
||||
}).start();
|
||||
addBackCallback(searchFragmentDismisser);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
|
||||
private void dismissSearchFragment(){
|
||||
if(searchFragment==null || dismissingSearchFragment)
|
||||
return;
|
||||
removeBackCallback(searchFragmentDismisser);
|
||||
dismissingSearchFragment=true;
|
||||
rootView.setVisibility(View.VISIBLE);
|
||||
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
@@ -201,15 +202,6 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchFragment!=null){
|
||||
dismissSearchFragment();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountInList(AccountViewModel account){
|
||||
return accountIDsInList.contains(account.account.id);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.webkit.WebResourceRequest;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class DonationWebViewFragment extends WebViewFragment{
|
||||
public static final String SUCCESS_URL="https://sponsor.joinmastodon.org/donate/success";
|
||||
public static final String FAILURE_URL="https://sponsor.joinmastodon.org/donate/failure";
|
||||
public static final String CANCEL_URL="https://sponsor.joinmastodon.org/donate/cancel";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(BuildConfig.DEBUG){
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
webView.loadUrl(Objects.requireNonNull(getArguments().getString("url")));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldOverrideUrlLoading(WebResourceRequest req){
|
||||
String url=req.getUrl().buildUpon().clearQuery().fragment(null).build().toString();
|
||||
if(url.equalsIgnoreCase(SUCCESS_URL)){
|
||||
onSuccess();
|
||||
return true;
|
||||
}else if(url.equalsIgnoreCase(FAILURE_URL)){
|
||||
onFailure();
|
||||
return true;
|
||||
}else if(url.equalsIgnoreCase(CANCEL_URL)){
|
||||
onCancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if(BuildConfig.DEBUG){
|
||||
menu.add(0, 0, 0, "Simulate success");
|
||||
menu.add(0, 1, 0, "Simulate failure");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==0)
|
||||
onSuccess();
|
||||
else if(item.getItemId()==1)
|
||||
onFailure();
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void onFailure(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.donation_server_error)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dlg->Nav.finish(this))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onSuccess(){
|
||||
String campaignID=getArguments().getString("campaignID");
|
||||
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(campaignID);
|
||||
E.post(new DismissDonationCampaignBannerEvent(campaignID));
|
||||
getActivity().setResult(Activity.RESULT_OK, new Intent().putExtra("postText", getArguments().getString("successPostText")));
|
||||
getActivity().finish();
|
||||
}
|
||||
|
||||
private void onCancel(){
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -155,7 +154,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMainAdapterOffset(){
|
||||
public int getMainAdapterOffset(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -276,4 +275,8 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
public String getHashtagName(){
|
||||
return hashtagName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
@@ -48,13 +48,12 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
public class HomeFragment extends AppKitFragment{
|
||||
private FragmentRootLinearLayout content;
|
||||
private HomeTimelineFragment homeTimelineFragment;
|
||||
private NotificationsListFragment notificationsFragment;
|
||||
@@ -272,15 +271,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(currentTab==R.id.tab_profile)
|
||||
return profileFragment.onBackPressed();
|
||||
if(currentTab==R.id.tab_search)
|
||||
return searchFragment.onBackPressed();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
@@ -5,15 +5,23 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
@@ -26,14 +34,17 @@ import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
@@ -41,8 +52,11 @@ import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSheet;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
|
||||
@@ -53,6 +67,7 @@ import org.parceler.Parcels;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -64,8 +79,11 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{
|
||||
private static final int DONATION_RESULT=211;
|
||||
|
||||
private ImageButton fab;
|
||||
private LinearLayout listsDropdown;
|
||||
private FixedAspectRatioImageView listsDropdownArrow;
|
||||
@@ -81,9 +99,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
private FollowList currentList;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
private DiscoverInfoBannerHelper localTimelineBannerHelper;
|
||||
private View donationBanner;
|
||||
private boolean donationBannerDismissing;
|
||||
|
||||
private String maxID;
|
||||
private String lastSavedMarkerID;
|
||||
private DonationCampaign currentDonationCampaign;
|
||||
private BottomSheet donationSheet;
|
||||
|
||||
public HomeTimelineFragment(){
|
||||
setListLayoutId(R.layout.fragment_timeline);
|
||||
@@ -93,6 +115,32 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
|
||||
|
||||
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
|
||||
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), null);
|
||||
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
|
||||
req.setStaging(true);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(DonationCampaign result){
|
||||
if(result==null)
|
||||
return;
|
||||
AccountSessionManager.getInstance().runIfDonationCampaignNotDismissed(result.id, ()->showDonationBanner(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.execNoAuth("");
|
||||
}
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,9 +278,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
});
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.register(this);
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
if(currentDonationCampaign!=null)
|
||||
showDonationBanner(currentDonationCampaign);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -584,9 +633,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.unregister(this);
|
||||
}
|
||||
donationBanner=null;
|
||||
donationBannerDismissing=false;
|
||||
}
|
||||
|
||||
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||
@@ -599,6 +647,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onDismissDonationCampaignBanner(DismissDonationCampaignBannerEvent ev){
|
||||
if(currentDonationCampaign!=null && ev.campaignID.equals(currentDonationCampaign.id)){
|
||||
dismissDonationBanner();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
@@ -653,6 +708,17 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
super.onDataLoaded(d, more);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(requestCode==DONATION_RESULT){
|
||||
if(donationSheet!=null)
|
||||
donationSheet.dismissWithoutAnimation();
|
||||
if(resultCode==Activity.RESULT_OK){
|
||||
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getCurrentListTitle(){
|
||||
return switch(listMode){
|
||||
case FOLLOWING -> getString(R.string.timeline_following);
|
||||
@@ -661,6 +727,77 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
};
|
||||
}
|
||||
|
||||
private void showDonationBanner(DonationCampaign campaign){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
currentDonationCampaign=campaign;
|
||||
if(donationBanner==null){
|
||||
ViewStub stub=contentView.findViewById(R.id.donation_banner);
|
||||
donationBanner=stub.inflate();
|
||||
donationBanner.findViewById(R.id.banner_dismiss).setOnClickListener(v->{
|
||||
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(currentDonationCampaign.id);
|
||||
dismissDonationBanner();
|
||||
});
|
||||
donationBanner.setOnClickListener(v->openDonationSheet());
|
||||
}else{
|
||||
donationBanner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
TextView text=donationBanner.findViewById(R.id.banner_text);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage);
|
||||
ssb.append(' ');
|
||||
int start=ssb.length();
|
||||
ssb.append(campaign.bannerButtonText);
|
||||
ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0);
|
||||
ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0);
|
||||
text.setText(ssb);
|
||||
donationBanner.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
donationBanner.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight(), 0),
|
||||
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, -donationBanner.getHeight())
|
||||
);
|
||||
set.setDuration(250);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void dismissDonationBanner(){
|
||||
if(donationBanner==null || donationBannerDismissing)
|
||||
return;
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight()),
|
||||
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, 0)
|
||||
);
|
||||
set.setDuration(250);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
donationBanner.setVisibility(View.GONE);
|
||||
donationBannerDismissing=false;
|
||||
}
|
||||
});
|
||||
donationBannerDismissing=true;
|
||||
set.start();
|
||||
currentDonationCampaign=null;
|
||||
}
|
||||
|
||||
private void openDonationSheet(){
|
||||
donationSheet=new DonationSheet(getActivity(), currentDonationCampaign, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
|
||||
donationSheet.setOnDismissListener(dialog->donationSheet=null);
|
||||
donationSheet.show();
|
||||
}
|
||||
|
||||
private enum ListMode{
|
||||
FOLLOWING,
|
||||
LOCAL,
|
||||
|
||||
@@ -44,12 +44,11 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{
|
||||
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener{
|
||||
private ImageButton fab;
|
||||
private FollowList followList;
|
||||
private boolean inSelectionMode;
|
||||
@@ -63,6 +62,8 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
private WindowInsets lastInsets;
|
||||
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||
private boolean dismissingSearchFragment;
|
||||
private Runnable searchFragmentDismisser=this::dismissSearchFragment;;
|
||||
private Runnable actionModeDismisser=()->actionMode.finish();
|
||||
|
||||
public ListMembersFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -214,6 +215,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
rootView.setVisibility(View.GONE);
|
||||
}).start();
|
||||
addBackCallback(searchFragmentDismisser);
|
||||
}
|
||||
|
||||
private void onItemClick(AccountViewHolder holder){
|
||||
@@ -293,9 +295,11 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
selectedAccounts.clear();
|
||||
updateItemsForSelectionModeTransition();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
removeBackCallback(actionModeDismisser);
|
||||
}
|
||||
});
|
||||
updateActionModeTitle();
|
||||
addBackCallback(actionModeDismisser);
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
@@ -371,15 +375,6 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
removeAccounts(Set.of(account.account.id), onDone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchFragment!=null){
|
||||
dismissSearchFragment();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void dismissSearchFragment(){
|
||||
if(searchFragment==null || dismissingSearchFragment)
|
||||
return;
|
||||
@@ -393,6 +388,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
searchFragment=null;
|
||||
dismissingSearchFragment=false;
|
||||
}).start();
|
||||
removeBackCallback(searchFragmentDismisser);
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationRequests;
|
||||
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
|
||||
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.NotificationRequest;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class NotificationRequestsFragment extends MastodonRecyclerFragment<NotificationRequest>{
|
||||
private String accountID;
|
||||
private String maxID;
|
||||
private HashMap<String, AccountViewModel> accountViewModels=new HashMap<>();
|
||||
private View endMark;
|
||||
private NotificationRequestsAdapter adapter;
|
||||
|
||||
public NotificationRequestsFragment(){
|
||||
super(50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.filtered_notifications);
|
||||
loadData();
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
E.unregister(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(!refreshing && endMark!=null)
|
||||
endMark.setVisibility(View.GONE);
|
||||
currentRequest=new GetNotificationRequests(offset==0 ? null : maxID)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<NotificationRequest> result){
|
||||
if(data.isEmpty() || refreshing)
|
||||
accountViewModels.clear();
|
||||
maxID=result.getNextPageMaxID();
|
||||
for(NotificationRequest req:result){
|
||||
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
|
||||
}
|
||||
onDataLoaded(result, !TextUtils.isEmpty(maxID));
|
||||
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return adapter=new NotificationRequestsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof NotificationRequestViewHolder).setDrawBelowLastItem(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateFooterView(LayoutInflater inflater){
|
||||
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||
endMark=v.findViewById(R.id.end_mark);
|
||||
endMark.setVisibility(View.GONE);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onNotificationRequestResponded(NotificationRequestRespondedEvent ev){
|
||||
if(adapter==null || !ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).id.equals(ev.requestID)){
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for(NotificationRequest nr:preloadedData){
|
||||
if(nr.id.equals(ev.requestID)){
|
||||
preloadedData.remove(nr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationRequestsAdapter extends UsableRecyclerView.Adapter<NotificationRequestViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public NotificationRequestsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public NotificationRequestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new NotificationRequestViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(NotificationRequestViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return Objects.requireNonNull(accountViewModels.get(data.get(position).account.id)).emojiHelper.getImageCount()+1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(data.get(position).account.id));
|
||||
return switch(image){
|
||||
case 0 -> model.avaRequest;
|
||||
default -> model.emojiHelper.getImageRequest(image-1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationRequestViewHolder extends BindableViewHolder<NotificationRequest> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name, username, badge;
|
||||
private final ImageView ava;
|
||||
private final ImageButton allow, mute;
|
||||
|
||||
public NotificationRequestViewHolder(){
|
||||
super(getActivity(), R.layout.item_notification_request, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
badge=findViewById(R.id.badge);
|
||||
ava=findViewById(R.id.ava);
|
||||
allow=findViewById(R.id.btn_allow);
|
||||
mute=findViewById(R.id.btn_mute);
|
||||
ava.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
ava.setClipToOutline(true);
|
||||
allow.setOnClickListener(this::onAllowClick);
|
||||
mute.setOnClickListener(this::onMuteClick);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(NotificationRequest item){
|
||||
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
|
||||
name.setText(model.parsedName);
|
||||
username.setText(item.account.getDisplayUsername());
|
||||
badge.setText(item.notificationsCount>99 ? String.format("%d+", 99) : String.format("%d", item.notificationsCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
if(image==null)
|
||||
ava.setImageResource(R.drawable.image_placeholder);
|
||||
else
|
||||
ava.setImageDrawable(image);
|
||||
}else{
|
||||
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
|
||||
model.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(item.account));
|
||||
args.putString("requestID", item.id);
|
||||
Nav.go(getActivity(), AccountNotificationsListFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAllowClick(View v){
|
||||
acceptOrDecline(true);
|
||||
}
|
||||
|
||||
private void onMuteClick(View v){
|
||||
acceptOrDecline(false);
|
||||
}
|
||||
|
||||
private void acceptOrDecline(boolean accept){
|
||||
new RespondToNotificationRequest(item.id, accept)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
int pos=data.indexOf(item);
|
||||
data.remove(pos);
|
||||
adapter.notifyItemRemoved(pos);
|
||||
new Snackbar.Builder(getActivity())
|
||||
.setText(getString(accept ? R.string.notifications_allowed : R.string.notifications_muted, item.account.displayName))
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,70 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationsPolicy;
|
||||
import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolicy;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.GenericListItemsViewController;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
private boolean onlyMentions;
|
||||
private String maxID;
|
||||
private View tabBar;
|
||||
private View mentionsTab, allTab;
|
||||
private View endMark;
|
||||
private String unreadMarker, realUnreadMarker;
|
||||
private MenuItem markAllReadItem;
|
||||
private boolean reloadingFromCache;
|
||||
private ListItem<Void> requestsItem=new ListItem<>(R.string.filtered_notifications, 0, R.drawable.ic_inventory_2_24px, i->openNotificationRequests());
|
||||
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
|
||||
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
|
||||
private NotificationsPolicy lastPolicy;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -74,43 +87,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
NotificationHeaderStatusDisplayItem titleItem;
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
titleItem=null;
|
||||
}else{
|
||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||
if(n.status!=null){
|
||||
n.status.card=null;
|
||||
n.status.spoilerText=null;
|
||||
}
|
||||
}
|
||||
if(n.status!=null){
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else if(titleItem!=null){
|
||||
return Collections.singletonList(titleItem);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Notification s){
|
||||
if(!knownAccounts.containsKey(s.account.id))
|
||||
knownAccounts.put(s.account.id, s.account);
|
||||
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
||||
knownAccounts.put(s.status.account.id, s.status.account);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(!refreshing && !reloadingFromCache)
|
||||
endMark.setVisibility(View.GONE);
|
||||
if(offset==0)
|
||||
reloadPolicy();
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||
@@ -142,30 +124,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
resetUnreadBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Notification n=getNotificationByID(id);
|
||||
if(n.status!=null){
|
||||
Status status=n.status;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}else{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
tabBar=view.findViewById(R.id.tabbar);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
|
||||
View tabBarItself=view.findViewById(R.id.tabbar_inner);
|
||||
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
@@ -215,14 +177,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
return views;
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
for(Notification n:data){
|
||||
if(n.id.equals(id))
|
||||
return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPollUpdated(PollUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
@@ -249,25 +203,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
|
||||
private void removeNotification(Notification n){
|
||||
data.remove(n);
|
||||
preloadedData.remove(n);
|
||||
int index=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
if(n.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(index==-1)
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount()) || holder.getAbsoluteAdapterPosition()<requestsItems.size();
|
||||
}
|
||||
|
||||
private void onTabClick(View v){
|
||||
@@ -285,34 +223,34 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateFooterView(LayoutInflater inflater){
|
||||
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||
endMark=v.findViewById(R.id.end_mark);
|
||||
endMark.setVisibility(View.GONE);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
MenuItem filters=menu.findItem(R.id.filters);
|
||||
filters.setVisible(lastPolicy!=null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.mark_all_read){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.mark_all_read){
|
||||
markAsRead();
|
||||
resetUnreadBackground();
|
||||
}else if(id==R.id.filters){
|
||||
showFiltersAlert();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(requestsRowAdapter);
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void markAsRead(){
|
||||
if(data.isEmpty())
|
||||
return;
|
||||
@@ -366,4 +304,93 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updatePolicy(NotificationsPolicy policy){
|
||||
int count=policy.summary==null ? 0 : policy.summary.pendingRequestsCount;
|
||||
boolean isShown=!requestsItems.isEmpty();
|
||||
boolean needShow=count>0;
|
||||
if(isShown && !needShow){
|
||||
requestsItems.clear();
|
||||
requestsRowAdapter.notifyItemRemoved(0);
|
||||
}else if(!isShown && needShow){
|
||||
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
|
||||
requestsItems.add(requestsItem);
|
||||
requestsRowAdapter.notifyItemInserted(0);
|
||||
}else if(isShown){
|
||||
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
|
||||
requestsRowAdapter.notifyItemChanged(0);
|
||||
}
|
||||
lastPolicy=policy;
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private void reloadPolicy(){
|
||||
new GetNotificationsPolicy()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(NotificationsPolicy policy){
|
||||
updatePolicy(policy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse errorResponse){
|
||||
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void showFiltersAlert(){
|
||||
GenericListItemsViewController<Void> controller=new GenericListItemsViewController<>(getActivity());
|
||||
Consumer<CheckableListItem<Void>> toggler=item->{
|
||||
item.toggle();
|
||||
controller.rebindItem(item);
|
||||
};
|
||||
CheckableListItem<Void> followingItem, followersItem, newAccountsItem, mentionsItem;
|
||||
List<ListItem<Void>> items=List.of(
|
||||
followingItem=new CheckableListItem<>(R.string.notification_filter_following, R.string.notification_filter_following_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowing, toggler, true),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_filter_followers, R.string.notification_filter_followers_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowers, toggler, true),
|
||||
newAccountsItem=new CheckableListItem<>(R.string.notification_filter_new_accounts, R.string.notification_filter_new_accounts_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNewAccounts, toggler, true),
|
||||
mentionsItem=new CheckableListItem<>(R.string.notification_filter_mentions, R.string.notification_filter_mentions_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterPrivateMentions, toggler, true)
|
||||
);
|
||||
controller.setItems(items);
|
||||
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.filter_notifications)
|
||||
.setView(controller.getView())
|
||||
.setPositiveButton(R.string.save, null)
|
||||
.show();
|
||||
Button btn=dlg.getButton(Dialog.BUTTON_POSITIVE);
|
||||
btn.setOnClickListener(v->{
|
||||
UiUtils.showProgressForAlertButton(btn, true);
|
||||
NotificationsPolicy newPolicy=new NotificationsPolicy();
|
||||
newPolicy.filterNotFollowing=followingItem.checked;
|
||||
newPolicy.filterNotFollowers=followersItem.checked;
|
||||
newPolicy.filterNewAccounts=newAccountsItem.checked;
|
||||
newPolicy.filterPrivateMentions=mentionsItem.checked;
|
||||
new SetNotificationsPolicy(newPolicy)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(NotificationsPolicy policy){
|
||||
updatePolicy(policy);
|
||||
dlg.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse errorResponse){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
UiUtils.showProgressForAlertButton(btn, false);
|
||||
errorResponse.showToast(activity);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
private void openNotificationRequests(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), NotificationRequestsFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
|
||||
private void showAllFeaturedHashtags(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
|
||||
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
|
||||
args.putParcelableArrayList("hashtags", tags);
|
||||
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.SpannedString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.ImageSpan;
|
||||
@@ -29,7 +28,6 @@ import android.transition.Fade;
|
||||
import android.transition.Transition;
|
||||
import android.transition.TransitionManager;
|
||||
import android.transition.TransitionSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -67,6 +65,7 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
@@ -101,14 +100,13 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
|
||||
public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
private static final int AVATAR_RESULT=722;
|
||||
private static final int COVER_RESULT=343;
|
||||
|
||||
@@ -136,6 +134,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private View actionButtonWrap;
|
||||
private CustomDrawingOrderLinearLayout scrollableContent;
|
||||
private ImageButton qrCodeButton;
|
||||
private ProgressBar innerProgress;
|
||||
private View actions;
|
||||
|
||||
private Account account;
|
||||
private String accountID;
|
||||
@@ -157,6 +157,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private Animator tabBarColorAnim;
|
||||
private MenuItem editSaveMenuItem;
|
||||
private boolean savingEdits;
|
||||
private Runnable editModeBackCallback=this::onEditModeBackCallback;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -219,6 +220,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
|
||||
scrollableContent=content.findViewById(R.id.scrollable_content);
|
||||
qrCodeButton=content.findViewById(R.id.qr_code);
|
||||
innerProgress=content.findViewById(R.id.profile_progress);
|
||||
actions=content.findViewById(R.id.profile_actions);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avatar.setClipToOutline(true);
|
||||
@@ -306,6 +309,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
|
||||
username.setOnLongClickListener(v->{
|
||||
if(account==null)
|
||||
return true;
|
||||
String username=account.acct;
|
||||
if(!username.contains("@")){
|
||||
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
@@ -331,7 +336,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
|
||||
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
|
||||
usernameDomain.setOnClickListener(v->{
|
||||
if(account==null)
|
||||
return;
|
||||
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
|
||||
});
|
||||
qrCodeButton.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -462,6 +471,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(!loaded)
|
||||
bindHeaderViewForPreviewMaybe();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -506,7 +517,41 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
}
|
||||
|
||||
private void bindHeaderViewForPreviewMaybe(){
|
||||
if(loaded)
|
||||
return;
|
||||
String username=getArguments().getString("accountUsername");
|
||||
String domain=getArguments().getString("accountDomain");
|
||||
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(domain))
|
||||
return;
|
||||
content.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.GONE);
|
||||
errorView.setVisibility(View.GONE);
|
||||
innerProgress.setVisibility(View.VISIBLE);
|
||||
this.username.setText(username);
|
||||
name.setText(username);
|
||||
usernameDomain.setText(domain);
|
||||
avatar.setImageResource(R.drawable.image_placeholder);
|
||||
cover.setImageResource(R.drawable.image_placeholder);
|
||||
actions.setVisibility(View.GONE);
|
||||
bio.setVisibility(View.GONE);
|
||||
countersLayout.setVisibility(View.GONE);
|
||||
tabsDivider.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void bindHeaderView(){
|
||||
if(innerProgress.getVisibility()==View.VISIBLE){
|
||||
TransitionManager.beginDelayedTransition(contentView, new TransitionSet()
|
||||
.addTransition(new Fade(Fade.IN | Fade.OUT))
|
||||
.excludeChildren(actions, true)
|
||||
.setDuration(250)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
);
|
||||
innerProgress.setVisibility(View.GONE);
|
||||
countersLayout.setVisibility(View.VISIBLE);
|
||||
actions.setVisibility(View.VISIBLE);
|
||||
tabsDivider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
setTitle(account.displayName);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
@@ -635,7 +680,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){
|
||||
if(relationship.following){
|
||||
MenuItem notifications=menu.findItem(R.id.notifications);
|
||||
notifications.setVisible(true);
|
||||
notifications.setIcon(relationship.notifying ? R.drawable.ic_notifications_fill1_24px : R.drawable.ic_notifications_24px);
|
||||
notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername()));
|
||||
}
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()){
|
||||
menu.setGroupDividerEnabled(true);
|
||||
}
|
||||
}
|
||||
@@ -663,7 +715,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
updateRelationship();
|
||||
}, this::updateRelationship);
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
@@ -693,6 +745,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
|
||||
}else if(id==R.id.notifications){
|
||||
new SetAccountFollowed(account.id, true, relationship.showingReblogs, !relationship.notifying)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
updateRelationship(result);
|
||||
new Snackbar.Builder(getActivity())
|
||||
.setText(result.notifying ? R.string.new_post_notifications_enabled : R.string.new_post_notifications_disabled)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -913,12 +983,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
refreshLayout.setEnabled(false);
|
||||
editDirty=false;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
addBackCallback(editModeBackCallback);
|
||||
}
|
||||
|
||||
private void exitEditMode(){
|
||||
if(!isInEditMode)
|
||||
throw new IllegalStateException();
|
||||
isInEditMode=false;
|
||||
removeBackCallback(editModeBackCallback);
|
||||
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
@@ -1028,23 +1100,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
updateRelationship();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isInEditMode){
|
||||
if(savingEdits)
|
||||
return true;
|
||||
if(editDirty || aboutFragment.isEditDirty()){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}else{
|
||||
exitEditMode();
|
||||
}
|
||||
return true;
|
||||
private void onEditModeBackCallback(){
|
||||
if(savingEdits)
|
||||
return;
|
||||
if(editDirty || aboutFragment.isEditDirty()){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}else{
|
||||
exitEditMode();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<Attachment> createFakeAttachments(String url, Drawable drawable){
|
||||
@@ -1058,6 +1125,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
private void onAvatarClick(View v){
|
||||
if(account==null)
|
||||
return;
|
||||
if(isInEditMode){
|
||||
startImagePicker(AVATAR_RESULT);
|
||||
}else{
|
||||
@@ -1071,11 +1140,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
private void onCoverClick(View v){
|
||||
if(account==null)
|
||||
return;
|
||||
if(isInEditMode){
|
||||
startImagePicker(COVER_RESULT);
|
||||
}else{
|
||||
Drawable drawable=cover.getDrawable();
|
||||
if(drawable==null || drawable instanceof ColorDrawable)
|
||||
if(drawable==null || drawable instanceof ColorDrawable || account.headerStatic.endsWith("/missing.png"))
|
||||
return;
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
|
||||
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
||||
|
||||
@@ -53,6 +53,8 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.window.OnBackInvokedCallback;
|
||||
import android.window.OnBackInvokedDispatcher;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
@@ -144,12 +146,16 @@ public class ProfileQrCodeFragment extends AppKitFragment{
|
||||
if(!isTablet){
|
||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
dlg.setOnKeyListener((dialog, keyCode, event)->{
|
||||
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
dlg.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this::dismiss);
|
||||
}else{
|
||||
dlg.setOnKeyListener((dialog, keyCode, event)->{
|
||||
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceError;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
|
||||
public abstract class WebViewFragment extends LoaderFragment{
|
||||
private static final String TAG="WebViewFragment";
|
||||
|
||||
protected WebView webView;
|
||||
private Runnable backCallback=this::onGoBack;
|
||||
private boolean backCallbackSet;
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
webView=new WebView(getActivity());
|
||||
webView.setWebChromeClient(new WebChromeClient(){
|
||||
@Override
|
||||
public void onReceivedTitle(WebView view, String title){
|
||||
setTitle(title);
|
||||
}
|
||||
});
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url){
|
||||
if(BuildConfig.DEBUG){
|
||||
Log.d(TAG, "onPageFinished: "+url);
|
||||
}
|
||||
dataLoaded();
|
||||
updateBackCallback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error){
|
||||
if(!loaded){
|
||||
onError(new MastodonErrorResponse(error.getDescription().toString(), -1, null));
|
||||
updateBackCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
|
||||
return WebViewFragment.this.shouldOverrideUrlLoading(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload){
|
||||
updateBackCallback();
|
||||
}
|
||||
});
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
return webView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
webView.reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
private void updateBackCallback(){
|
||||
boolean canGoBack=webView.canGoBack();
|
||||
if(canGoBack!=backCallbackSet){
|
||||
if(canGoBack){
|
||||
addBackCallback(backCallback);
|
||||
backCallbackSet=true;
|
||||
}else{
|
||||
removeBackCallback(backCallback);
|
||||
backCallbackSet=false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onGoBack(){
|
||||
if(webView.canGoBack())
|
||||
webView.goBack();
|
||||
}
|
||||
|
||||
protected abstract boolean shouldOverrideUrlLoading(WebResourceRequest req);
|
||||
}
|
||||
@@ -36,10 +36,9 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
private static final int QUERY_RESULT=937;
|
||||
private static final int SCAN_RESULT=456;
|
||||
|
||||
@@ -62,6 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private String accountID;
|
||||
private String currentQuery;
|
||||
private Intent scannerIntent;
|
||||
private Runnable searchExitCallback=this::exitSearch;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -232,6 +232,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
searchBack.setEnabled(true);
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
tabsDivider.setVisibility(View.GONE);
|
||||
addBackCallback(searchExitCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +249,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
tabsDivider.setVisibility(View.VISIBLE);
|
||||
currentQuery=null;
|
||||
removeBackCallback(searchExitCallback);
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
@@ -260,15 +262,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchActive){
|
||||
exitSearch();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(reqCode==QUERY_RESULT && success){
|
||||
|
||||
@@ -2,56 +2,35 @@ package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.model.viewmodel.CardViewModel;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.HorizontalScrollingTouchListener;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.LinkCardHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
|
||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<DiscoverNewsFragment.CardItem> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
private UsableRecyclerView cardsList;
|
||||
private ArrayList<CardViewModel> top3=new ArrayList<>();
|
||||
private CardLinksAdapter cardsAdapter;
|
||||
|
||||
public DiscoverNewsFragment(){
|
||||
super(10);
|
||||
@@ -70,12 +49,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Card> result){
|
||||
top3.clear();
|
||||
top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
|
||||
cardsAdapter.notifyDataSetChanged();
|
||||
|
||||
onDataLoaded(result.subList(top3.size(), result.size()).stream()
|
||||
.map(card->new CardViewModel(card, 56, 56))
|
||||
int[] index={0};
|
||||
onDataLoaded(result.stream()
|
||||
.map(card->{
|
||||
int actualIndex=index[0]+(refreshing ? 0 : (data.size()+preloadedData.size()));
|
||||
index[0]++;
|
||||
int size=actualIndex==0 ? 1000 : 192;
|
||||
return new CardItem(new CardViewModel(card, size, size, card, accountID));
|
||||
})
|
||||
.collect(Collectors.toList()), false);
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
@@ -86,27 +67,9 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
cardsList=new UsableRecyclerView(getActivity());
|
||||
cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
|
||||
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
|
||||
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
|
||||
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
|
||||
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
|
||||
cardsList.setClipToPadding(false);
|
||||
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
outRect.right=V.dp(16);
|
||||
}
|
||||
});
|
||||
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
|
||||
cardsList.setDrawSelectorOnTop(true);
|
||||
cardsList.setOnTouchListener(new HorizontalScrollingTouchListener(getActivity()));
|
||||
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, mergeAdapter);
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
|
||||
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
|
||||
mergeAdapter.addAdapter(new LinksAdapter(imgLoader));
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@@ -115,18 +78,46 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
private final List<CardViewModel> data;
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildAdapterPosition(view)==0 && !bannerHelper.isBannerShown()){
|
||||
outRect.top=V.dp(16);
|
||||
}
|
||||
if(parent.getChildViewHolder(view) instanceof LinkCardHolder<?>){
|
||||
outRect.bottom=V.dp(8);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
|
||||
public static class CardItem implements LinkCardHolder.LinkCardProvider{
|
||||
public final CardViewModel card;
|
||||
|
||||
private CardItem(CardViewModel card){
|
||||
this.card=card;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardViewModel getCard(){
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkCardHolder<CardItem>> implements ImageLoaderRecyclerAdapter{
|
||||
public LinksAdapter(ListImageLoaderWrapper imgLoader){
|
||||
super(imgLoader);
|
||||
this.data=data;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new LinkViewHolder();
|
||||
public LinkCardHolder<CardItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
LinkCardHolder<CardItem> vh=new LinkCardHolder<>(getActivity(), list, viewType==1, accountID);
|
||||
vh.setTryResolving(false);
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,91 +126,24 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(BaseLinkViewHolder holder, int position){
|
||||
holder.bind(data.get(position).card);
|
||||
public void onBindViewHolder(LinkCardHolder<CardItem> holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return data.get(position).imageRequest==null ? 0 : 1;
|
||||
return data.get(position).card.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return data.get(position).imageRequest;
|
||||
}
|
||||
}
|
||||
|
||||
private class CardLinksAdapter extends LinksAdapter{
|
||||
public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
|
||||
super(imgLoader, data);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new LinkCardViewHolder();
|
||||
}
|
||||
}
|
||||
|
||||
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
|
||||
protected final TextView name, title;
|
||||
protected final ImageView photo;
|
||||
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||
private boolean didClear;
|
||||
|
||||
public BaseLinkViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
name=findViewById(R.id.name);
|
||||
title=findViewById(R.id.title);
|
||||
photo=findViewById(R.id.photo);
|
||||
return data.get(position).card.getImageRequest(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Card item){
|
||||
name.setText(item.providerName);
|
||||
title.setText(item.title);
|
||||
crossfadeDrawable.setSize(item.width, item.height);
|
||||
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
didClear=false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
crossfadeDrawable.setImageDrawable(drawable);
|
||||
if(didClear)
|
||||
crossfadeDrawable.animateAlpha(0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
crossfadeDrawable.setCrossfadeAlpha(1f);
|
||||
didClear=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
UiUtils.launchWebBrowser(getActivity(), item.url);
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkViewHolder extends BaseLinkViewHolder{
|
||||
public LinkViewHolder(){
|
||||
super(R.layout.item_trending_link);
|
||||
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
photo.setClipToOutline(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkCardViewHolder extends BaseLinkViewHolder{
|
||||
public LinkCardViewHolder(){
|
||||
super(R.layout.item_trending_link_card);
|
||||
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
itemView.setClipToOutline(true);
|
||||
public int getItemViewType(int position){
|
||||
return position==0 ? 1 : 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +57,13 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
|
||||
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment{
|
||||
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@@ -371,6 +370,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
container.invalidateOutline();
|
||||
navigationIcon.invalidateSelf();
|
||||
});
|
||||
if(!enter){
|
||||
String initialQuery=getArguments().getString("query");
|
||||
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
|
||||
currentQuery=initialQuery;
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
@@ -437,14 +441,6 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
String initialQuery=getArguments().getString("query");
|
||||
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
|
||||
currentQuery=initialQuery;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
|
||||
private float boundsFraction, radius;
|
||||
private final Rect boundsFrom, boundsTo;
|
||||
|
||||
@@ -73,8 +73,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
protected boolean isSignup;
|
||||
protected CatalogInstance fakeInstance=new CatalogInstance();
|
||||
|
||||
private static final double DUNBAR=Math.log(800);
|
||||
|
||||
public InstanceCatalogFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
@@ -155,7 +153,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
}
|
||||
|
||||
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
|
||||
if(TextUtils.isEmpty(_domain))
|
||||
if(TextUtils.isEmpty(_domain) || _domain.indexOf('.')==-1)
|
||||
return;
|
||||
String domain=normalizeInstanceDomain(_domain);
|
||||
Instance cachedInstance=instancesCache.get(domain);
|
||||
@@ -316,8 +314,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
loadingInstanceRedirectRequest=null;
|
||||
loadingInstanceDomain=null;
|
||||
Activity a=getActivity();
|
||||
if(a==null)
|
||||
if(a==null) {
|
||||
response.close();
|
||||
return;
|
||||
}
|
||||
try(response){
|
||||
if(!response.isSuccessful()){
|
||||
a.runOnUiThread(()->{
|
||||
|
||||
@@ -5,8 +5,9 @@ import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
@@ -26,16 +27,12 @@ import android.widget.PopupMenu;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.catalog.CatalogCategory;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
@@ -49,11 +46,9 @@ import org.parceler.Parcels;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -63,17 +58,11 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{
|
||||
private MastodonAPIRequest<?> getCategoriesRequest;
|
||||
private String currentCategory="all";
|
||||
private List<CatalogCategory> categories=new ArrayList<>();
|
||||
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||
private View topBar;
|
||||
|
||||
private List<String> languages=Collections.emptyList();
|
||||
@@ -94,6 +83,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
private String inviteCode, inviteCodeHost;
|
||||
private AlertDialog currentInviteLinkAlert;
|
||||
|
||||
private Runnable exitQueryModeCallback=()->setSearchQueryMode(false);
|
||||
|
||||
public InstanceCatalogSignupFragment(){
|
||||
super(R.layout.fragment_onboarding_common, 10);
|
||||
}
|
||||
@@ -113,7 +104,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetCatalogInstances(null, null)
|
||||
currentRequest=new GetCatalogInstances(null, null, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<CatalogInstance> result){
|
||||
@@ -149,58 +140,17 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
}
|
||||
})
|
||||
.execNoAuth("");
|
||||
getCategoriesRequest=new GetCatalogCategories(null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<CatalogCategory> result){
|
||||
getCategoriesRequest=null;
|
||||
CatalogCategory all=new CatalogCategory();
|
||||
all.category="all";
|
||||
categories.add(all);
|
||||
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
|
||||
updateCategories();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getCategoriesRequest=null;
|
||||
error.showToast(getActivity());
|
||||
CatalogCategory all=new CatalogCategory();
|
||||
all.category="all";
|
||||
categories.add(all);
|
||||
updateCategories();
|
||||
}
|
||||
})
|
||||
.execNoAuth("");
|
||||
}
|
||||
|
||||
private void updateCategories(){
|
||||
// categoriesList.removeAllTabs();
|
||||
// for(CatalogCategory cat:categories){
|
||||
// int titleRes=getTitleForCategory(cat.category);
|
||||
// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
|
||||
// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
|
||||
// emoji.setImageResource(getEmojiForCategory(cat.category));
|
||||
// categoriesList.addTab(tab);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
removeBackCallback(exitQueryModeCallback);
|
||||
super.onDestroy();
|
||||
if(getCategoriesRequest!=null)
|
||||
getCategoriesRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
View headerView=new View(getActivity());
|
||||
headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
|
||||
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
|
||||
return mergeAdapter;
|
||||
return adapter=new InstancesAdapter();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -222,7 +172,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
setStatusBarColor(0);
|
||||
topBar=view.findViewById(R.id.top_bar);
|
||||
|
||||
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar));
|
||||
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar));
|
||||
if(buttonBar.getBackground() instanceof LayerDrawable ld){
|
||||
ld=(LayerDrawable) ld.mutate();
|
||||
buttonBar.setBackground(ld);
|
||||
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
|
||||
if(overlay!=null){
|
||||
overlay.setAlpha(20);
|
||||
}
|
||||
}
|
||||
buttonBar.setElevation(V.dp(3));
|
||||
|
||||
searchEdit=view.findViewById(R.id.search_edit);
|
||||
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
||||
@@ -572,6 +531,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
filteredData.add(instance);
|
||||
}
|
||||
}
|
||||
setEmptyText(getString(R.string.no_servers_found, currentSearchQuery));
|
||||
}else{
|
||||
setEmptyText("");
|
||||
}
|
||||
}else{
|
||||
for(CatalogInstance instance:data){
|
||||
@@ -591,27 +553,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
}
|
||||
}
|
||||
}
|
||||
DiffUtil.calculateDiff(new DiffUtil.Callback(){
|
||||
@Override
|
||||
public int getOldListSize(){
|
||||
return prevData.size();
|
||||
}
|
||||
UiUtils.updateRecyclerViewKeepingAbsoluteScrollPosition(list, ()->{
|
||||
DiffUtil.calculateDiff(new DiffUtil.Callback(){
|
||||
@Override
|
||||
public int getOldListSize(){
|
||||
return prevData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize(){
|
||||
return filteredData.size();
|
||||
}
|
||||
@Override
|
||||
public int getNewListSize(){
|
||||
return filteredData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
|
||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||
}
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
|
||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
|
||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||
}
|
||||
}).dispatchUpdatesTo(adapter);
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
|
||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||
}
|
||||
}).dispatchUpdatesTo(adapter);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -620,19 +584,13 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchQueryMode){
|
||||
setSearchQueryMode(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setSearchQueryMode(boolean enabled){
|
||||
if(searchQueryMode==enabled)
|
||||
return;
|
||||
searchQueryMode=enabled;
|
||||
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams();
|
||||
if(searchQueryMode){
|
||||
addBackCallback(exitQueryModeCallback);
|
||||
filtersScroll.setVisibility(View.GONE);
|
||||
lp.removeRule(RelativeLayout.END_OF);
|
||||
backBtn.setScaleX(0.83333333f);
|
||||
@@ -640,6 +598,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
backBtn.setTranslationX(V.dp(8));
|
||||
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0));
|
||||
}else{
|
||||
removeBackCallback(exitQueryModeCallback);
|
||||
filtersScroll.setVisibility(View.VISIBLE);
|
||||
focusThing.requestFocus();
|
||||
searchEdit.setText("");
|
||||
@@ -732,7 +691,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
boolean found=false;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
|
||||
if(holder instanceof InstanceViewHolder ivh && holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx){
|
||||
ivh.radioButton.setChecked(false);
|
||||
found=true;
|
||||
break;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -65,7 +64,7 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
|
||||
protected void updateFilteredList(){
|
||||
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
|
||||
filteredData.clear();
|
||||
if(currentSearchQuery.length()>0){
|
||||
if(!TextUtils.isEmpty(currentSearchQuery)){
|
||||
boolean foundExactMatch=false;
|
||||
for(CatalogInstance inst:data){
|
||||
if(inst.normalizedDomain.contains(currentSearchQuery)){
|
||||
@@ -74,9 +73,16 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
|
||||
foundExactMatch=true;
|
||||
}
|
||||
}
|
||||
if(!foundExactMatch)
|
||||
if(!foundExactMatch && currentSearchQuery.indexOf('.')!=-1)
|
||||
filteredData.add(0, fakeInstance);
|
||||
}
|
||||
if(filteredData.isEmpty()){
|
||||
for(CatalogInstance inst:data){
|
||||
if(inst.normalizedDomain.equals("mastodon.social") || inst.normalizedDomain.equals("mastodon.online")){
|
||||
filteredData.add(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
list.getChildAt(i).invalidateOutline();
|
||||
@@ -90,12 +96,15 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
|
||||
|
||||
private void loadAutocompleteServers(){
|
||||
loadedAutocomplete=true;
|
||||
new GetCatalogInstances(null, null)
|
||||
new GetCatalogInstances(null, null, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<CatalogInstance> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
data.clear();
|
||||
data.addAll(sortInstances(result));
|
||||
updateFilteredList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,6 +121,9 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
|
||||
Toolbar toolbar=getToolbar();
|
||||
toolbar.setElevation(0);
|
||||
toolbar.setBackground(null);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
toolbar.setContentInsetStartWithNavigation(V.dp(80));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -133,7 +133,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
}
|
||||
numRunningFollowRequests++;
|
||||
String id=accountIdsToFollow.remove(0);
|
||||
new SetAccountFollowed(id, true, true)
|
||||
new SetAccountFollowed(id, true, true, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
|
||||
@@ -50,6 +50,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -62,6 +63,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class SignupFragment extends ToolbarFragment{
|
||||
private static final String TAG="SignupFragment";
|
||||
private final Pattern emailRegex=Pattern.compile("^[^@]+@[^@]+\\.[^@]{2,}$");
|
||||
|
||||
private Instance instance;
|
||||
|
||||
@@ -97,6 +99,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false);
|
||||
|
||||
TextView domain=view.findViewById(R.id.domain);
|
||||
TextView atSign=view.findViewById(R.id.at_sign);
|
||||
displayName=view.findViewById(R.id.display_name);
|
||||
username=view.findViewById(R.id.username);
|
||||
email=view.findViewById(R.id.email);
|
||||
@@ -118,7 +121,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
username.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom());
|
||||
username.setPadding(atSign.getWidth(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -145,6 +148,10 @@ public class SignupFragment extends ToolbarFragment{
|
||||
reasonExplain.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
password.setOnFocusChangeListener(this::onPasswordFieldFocusChange);
|
||||
passwordConfirm.setOnFocusChangeListener(this::onPasswordFieldFocusChange);
|
||||
email.setOnFocusChangeListener(this::onEmailFieldFocusChange);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -281,34 +288,44 @@ public class SignupFragment extends ToolbarFragment{
|
||||
.exec(instance.uri, apiToken);
|
||||
}
|
||||
|
||||
private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
|
||||
private int spanStart;
|
||||
@Override
|
||||
public void head(Node node, int depth){
|
||||
if(node instanceof TextNode tn){
|
||||
ssb.append(tn.text());
|
||||
}else if(node instanceof Element){
|
||||
spanStart=ssb.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tail(Node node, int depth){
|
||||
if(node instanceof Element){
|
||||
ssb.setSpan(new LinkSpan("", onClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
return ssb;
|
||||
}
|
||||
|
||||
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
|
||||
return switch(fieldName){
|
||||
case "email" -> switch(error.error){
|
||||
case "ERR_BLOCKED" -> {
|
||||
String emailAddr=email.getText().toString();
|
||||
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
|
||||
private int spanStart;
|
||||
@Override
|
||||
public void head(Node node, int depth){
|
||||
if(node instanceof TextNode tn){
|
||||
ssb.append(tn.text());
|
||||
}else if(node instanceof Element){
|
||||
spanStart=ssb.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tail(Node node, int depth){
|
||||
if(node instanceof Element){
|
||||
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
yield ssb;
|
||||
yield makeLinkInErrorMessage(s, this::onGoBackLinkClick);
|
||||
}
|
||||
case "ERR_INVALID" -> getString(R.string.signup_email_invalid);
|
||||
case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_email_taken), this::onForgotPasswordLinkClick);
|
||||
default -> error.description;
|
||||
};
|
||||
case "username" -> switch(error.error){
|
||||
case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_username_taken), this::onGoBackLinkClick);
|
||||
default -> error.description;
|
||||
};
|
||||
default -> error.description;
|
||||
@@ -345,7 +362,9 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
|
||||
private void updateButtonState(){
|
||||
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0));
|
||||
btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find()
|
||||
&& password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString())
|
||||
&& (!instance.approvalRequired || reason.length()>0));
|
||||
}
|
||||
|
||||
private void createAppAndGetToken(){
|
||||
@@ -406,6 +425,24 @@ public class SignupFragment extends ToolbarFragment{
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
private void onForgotPasswordLinkClick(LinkSpan span){
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new");
|
||||
}
|
||||
|
||||
private void onPasswordFieldFocusChange(View v, boolean hasFocus){
|
||||
if(hasFocus || password.length()==0 || passwordConfirm.length()==0)
|
||||
return;
|
||||
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
|
||||
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
|
||||
}
|
||||
}
|
||||
|
||||
private void onEmailFieldFocusChange(View v, boolean hasFocus){
|
||||
if(!hasFocus && email.length()>0 && !emailRegex.matcher(email.getText()).find()){
|
||||
emailWrap.setErrorState(getString(R.string.signup_email_invalid));
|
||||
}
|
||||
}
|
||||
|
||||
private class ErrorClearingListener implements TextWatcher{
|
||||
public final EditText editText;
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMainAdapterOffset(){
|
||||
public int getMainAdapterOffset(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
@@ -177,7 +176,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private void onUnfollowClick(){
|
||||
new SetAccountFollowed(reportAccount.id, false, false)
|
||||
new SetAccountFollowed(reportAccount.id, false, false, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
@@ -44,11 +45,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
|
||||
public class EditFilterFragment extends BaseSettingsFragment<Void>{
|
||||
private static final int WORDS_RESULT=370;
|
||||
private static final int CONTEXT_RESULT=651;
|
||||
|
||||
@@ -63,6 +63,13 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
private ArrayList<String> deletedWordIDs=new ArrayList<>();
|
||||
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
|
||||
private boolean dirty;
|
||||
private boolean wasDirty;
|
||||
|
||||
private Runnable confirmCallback=()->{
|
||||
if(isDirty()){
|
||||
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -90,6 +97,12 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
removeBackCallback(confirmCallback);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@@ -101,6 +114,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
titleEditLayout.updateHint();
|
||||
if(filter!=null)
|
||||
titleEdit.setText(filter.title);
|
||||
titleEdit.addTextChangedListener(new SimpleTextWatcher(e->updateBackCallback()));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
|
||||
@@ -158,6 +172,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
}
|
||||
a.dismiss();
|
||||
}
|
||||
updateBackCallback();
|
||||
})
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
@@ -309,6 +324,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
}
|
||||
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
|
||||
}
|
||||
updateBackCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,11 +333,19 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isDirty()){
|
||||
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
|
||||
return true;
|
||||
protected void toggleCheckableItem(ListItem<?> item){
|
||||
super.toggleCheckableItem(item);
|
||||
updateBackCallback();
|
||||
}
|
||||
|
||||
private void updateBackCallback(){
|
||||
boolean dirty=isDirty();
|
||||
if(dirty!=wasDirty){
|
||||
wasDirty=dirty;
|
||||
if(dirty)
|
||||
addBackCallback(confirmCallback);
|
||||
else
|
||||
removeBackCallback(confirmCallback);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
||||
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
|
||||
public class FilterContextFragment extends BaseSettingsFragment<FilterContext>{
|
||||
private EnumSet<FilterContext> context;
|
||||
|
||||
@Override
|
||||
@@ -33,7 +31,8 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
public void onStop(){
|
||||
super.onStop();
|
||||
context=EnumSet.noneOf(FilterContext.class);
|
||||
for(ListItem<FilterContext> item:data){
|
||||
if(((CheckableListItem<FilterContext>) item).checked)
|
||||
@@ -42,6 +41,5 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
|
||||
Bundle args=new Bundle();
|
||||
args.putSerializable("context", context);
|
||||
setResult(true, args);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
@@ -11,11 +10,9 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
@@ -33,15 +30,15 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
|
||||
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword>{
|
||||
private Button fab;
|
||||
private ActionMode actionMode;
|
||||
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
|
||||
private ArrayList<String> deletedItemIDs=new ArrayList<>();
|
||||
private MenuItem deleteItem;
|
||||
private Runnable actionModeDismisser=()->actionMode.finish();
|
||||
|
||||
public FilterWordsFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
|
||||
@@ -80,12 +77,12 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
public void onStop(){
|
||||
super.onStop();
|
||||
Bundle result=new Bundle();
|
||||
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
result.putStringArrayList("deleted", deletedItemIDs);
|
||||
setResult(true, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -259,6 +256,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
updateActionModeTitle();
|
||||
addBackCallback(actionModeDismisser);
|
||||
}
|
||||
|
||||
private void leaveSelectionMode(boolean fromActionMode){
|
||||
@@ -280,6 +278,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
removeBackCallback(actionModeDismisser);
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -9,6 +11,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
@@ -18,6 +21,8 @@ import java.util.List;
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
private CheckableListItem<Void> donationsStagingItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -28,7 +33,9 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
|
||||
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
|
||||
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
|
||||
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick),
|
||||
new ListItem<>("Clear dismissed donation campaigns", null, this::onClearDismissedCampaignsClick),
|
||||
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", null, CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
@@ -39,6 +46,12 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public void onStop(){
|
||||
super.onStop();
|
||||
getPrefs().edit().putBoolean("donationsStaging", donationsStagingItem.checked).apply();
|
||||
}
|
||||
|
||||
private void onTestEmailConfirmClick(ListItem<?> item){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
@@ -70,9 +83,18 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void onClearDismissedCampaignsClick(ListItem<?> item){
|
||||
AccountSessionManager.getInstance().clearDismissedDonationCampaigns();
|
||||
Toast.makeText(getActivity(), "Dismissed campaigns cleared. Restart app to see your current campaign, if any", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void restartUI(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
|
||||
private SharedPreferences getPrefs(){
|
||||
return getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
@@ -12,27 +16,38 @@ import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSheet;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
private static final int DONATION_RESULT=433;
|
||||
|
||||
private boolean loggedOut;
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private Button updateButton1, updateButton2;
|
||||
private TextView updateText;
|
||||
private DonationSheet donationSheet;
|
||||
private Runnable updateDownloadProgressUpdater=new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
@@ -49,21 +64,26 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings);
|
||||
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
|
||||
onDataLoaded(List.of(
|
||||
ArrayList<ListItem<Void>> items=new ArrayList<>();
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
items.add(new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
}
|
||||
items.addAll(List.of(
|
||||
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
|
||||
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
|
||||
new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick),
|
||||
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
|
||||
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
|
||||
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
|
||||
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
|
||||
new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick),
|
||||
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
|
||||
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true)
|
||||
));
|
||||
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
|
||||
items.add(new ListItem<>(R.string.settings_donate, 0, R.drawable.ic_volunteer_activism_24px, this::onDonateClick));
|
||||
items.add(new ListItem<>(R.string.settings_manage_donations, 0, R.drawable.ic_settings_heart_24px, this::onManageDonationClick, 0, true));
|
||||
}
|
||||
items.add(new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick));
|
||||
items.add(new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false));
|
||||
onDataLoaded(items);
|
||||
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
session.reloadPreferences(null);
|
||||
@@ -117,6 +137,17 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(requestCode==DONATION_RESULT){
|
||||
if(donationSheet!=null)
|
||||
donationSheet.dismissWithoutAnimation();
|
||||
if(resultCode==Activity.RESULT_OK){
|
||||
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle makeFragmentArgs(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -167,6 +198,39 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onDonateClick(ListItem<?> item){
|
||||
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), "menu");
|
||||
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
|
||||
req.setStaging(true);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(DonationCampaign result){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
if(result==null){
|
||||
Toast.makeText(activity, "No campaign available (server misconfiguration?)", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
donationSheet=new DonationSheet(getActivity(), result, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
|
||||
donationSheet.setOnDismissListener(dialog->donationSheet=null);
|
||||
donationSheet.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.execNoAuth("");
|
||||
}
|
||||
|
||||
private void onManageDonationClick(ListItem<?> item){
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://sponsor.staging.joinmastodon.org/donate/manage");
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateBanner();
|
||||
|
||||
@@ -100,15 +100,19 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
scroller.setClipToPadding(false);
|
||||
scroller.addView(scrollingLayout);
|
||||
|
||||
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
|
||||
banner.setAspectRatio(1.914893617f);
|
||||
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
|
||||
banner.setClipToOutline(true);
|
||||
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
|
||||
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
blp.bottomMargin=V.dp(24);
|
||||
scrollingLayout.addView(banner, blp);
|
||||
if(!TextUtils.isEmpty(instance.thumbnail)){
|
||||
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
|
||||
banner.setAspectRatio(1.914893617f);
|
||||
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
|
||||
banner.setClipToOutline(true);
|
||||
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
|
||||
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
blp.bottomMargin=V.dp(24);
|
||||
scrollingLayout.addView(banner, blp);
|
||||
}else{
|
||||
scrollingLayout.setPadding(0, V.dp(24), 0, 0);
|
||||
}
|
||||
|
||||
boolean needDivider=false;
|
||||
if(instance.contactAccount!=null){
|
||||
|
||||
@@ -36,6 +36,8 @@ public class Card extends BaseModel{
|
||||
public String blurhash;
|
||||
public List<History> history;
|
||||
public Instant publishedAt;
|
||||
public Account authorAccount;
|
||||
public List<Author> authors;
|
||||
|
||||
public transient Drawable blurhashPlaceholder;
|
||||
|
||||
@@ -49,6 +51,13 @@ public class Card extends BaseModel{
|
||||
if(placeholder!=null)
|
||||
blurhashPlaceholder=new BlurHashDrawable(placeholder, width, height);
|
||||
}
|
||||
if(authorAccount!=null)
|
||||
authorAccount.postprocess();
|
||||
if(authors!=null){
|
||||
for(Author a:authors){
|
||||
a.postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,4 +91,19 @@ public class Card extends BaseModel{
|
||||
@SerializedName("rich")
|
||||
RICH
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class Author extends BaseModel{
|
||||
@RequiredField
|
||||
public String name;
|
||||
public String url;
|
||||
public Account account;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
if(account!=null)
|
||||
account.postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class NotificationRequest extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public Instant createdAt;
|
||||
@RequiredField
|
||||
public Instant updatedAt;
|
||||
public int notificationsCount;
|
||||
@RequiredField
|
||||
public Account account;
|
||||
public Status lastStatus;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
account.postprocess();
|
||||
if(lastStatus!=null)
|
||||
lastStatus.postprocess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
public class NotificationsPolicy extends BaseModel{
|
||||
public boolean filterNewAccounts;
|
||||
public boolean filterNotFollowers;
|
||||
public boolean filterNotFollowing;
|
||||
public boolean filterPrivateMentions;
|
||||
public Summary summary;
|
||||
|
||||
public static class Summary{
|
||||
public int pendingNotificationsCount;
|
||||
public int pendingRequestsCount;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import androidx.annotation.StringRes;
|
||||
public class PushNotification extends BaseModel{
|
||||
public String accessToken;
|
||||
public String preferredLocale;
|
||||
public long notificationId;
|
||||
public String notificationId;
|
||||
@RequiredField
|
||||
public Type notificationType;
|
||||
@RequiredField
|
||||
|
||||
@@ -58,7 +58,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
|
||||
public boolean favourited;
|
||||
public boolean reblogged;
|
||||
public boolean muted;
|
||||
public Boolean muted;
|
||||
public boolean bookmarked;
|
||||
public Boolean pinned;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
import java.net.IDN;
|
||||
@@ -15,14 +16,18 @@ import java.util.List;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class CatalogInstance extends BaseModel{
|
||||
@RequiredField
|
||||
public String domain;
|
||||
@RequiredField
|
||||
public String version;
|
||||
@RequiredField
|
||||
public String description;
|
||||
@RequiredField
|
||||
public List<String> languages;
|
||||
@SerializedName("region")
|
||||
private String _region;
|
||||
@RequiredField
|
||||
public List<String> categories;
|
||||
public String proxiedThumbnail;
|
||||
public int totalUsers;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.model.donations;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class DonationCampaign extends BaseModel{
|
||||
public String id;
|
||||
public String bannerMessage;
|
||||
public String bannerButtonText;
|
||||
public String donationMessage;
|
||||
public String donationButtonText;
|
||||
public Amounts amounts;
|
||||
public String defaultCurrency;
|
||||
public String donationUrl;
|
||||
public String donationSuccessPost;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
amounts.postprocess();
|
||||
}
|
||||
|
||||
public static class Amounts extends BaseModel{
|
||||
public Map<String, long[]> oneTime;
|
||||
@RequiredField
|
||||
public Map<String, long[]> monthly;
|
||||
public Map<String, long[]> yearly;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,10 @@ public class AccountViewModel{
|
||||
public final String verifiedLink;
|
||||
|
||||
public AccountViewModel(Account account, String accountID){
|
||||
this(account, accountID, true);
|
||||
}
|
||||
|
||||
public AccountViewModel(Account account, String accountID, boolean needBio){
|
||||
this.account=account;
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
@@ -32,9 +36,13 @@ public class AccountViewModel{
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
else
|
||||
parsedName=account.displayName;
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
|
||||
ssb.append(parsedBio);
|
||||
if(needBio){
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
ssb.append(parsedBio);
|
||||
}else{
|
||||
parsedBio=null;
|
||||
}
|
||||
emojiHelper.setText(ssb);
|
||||
String verifiedLink=null;
|
||||
for(AccountField fld:account.fields){
|
||||
|
||||
@@ -1,19 +1,62 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class CardViewModel{
|
||||
public final Object parentObject;
|
||||
public final Card card;
|
||||
public final ImageLoaderRequest imageRequest;
|
||||
public final UrlImageLoaderRequest authorAvaRequest;
|
||||
public final SpannableStringBuilder parsedAuthorName;
|
||||
public final CustomEmojiHelper authorNameEmojiHelper=new CustomEmojiHelper();
|
||||
|
||||
public CardViewModel(Card card, int width, int height){
|
||||
public CardViewModel(Card card, int width, int height, Object parentObject, String accountID){
|
||||
this.card=card;
|
||||
this.parentObject=parentObject;
|
||||
this.imageRequest=TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(width), V.dp(height));
|
||||
|
||||
Account authorAccount=getAuthorAccount();
|
||||
|
||||
if(authorAccount!=null){
|
||||
parsedAuthorName=new SpannableStringBuilder(authorAccount.displayName);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(parsedAuthorName, authorAccount.emojis);
|
||||
authorNameEmojiHelper.setText(parsedAuthorName);
|
||||
authorAvaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? authorAccount.avatar : authorAccount.avatarStatic, V.dp(50), V.dp(50));
|
||||
}else{
|
||||
parsedAuthorName=null;
|
||||
authorAvaRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getImageCount(){
|
||||
return 1+(getAuthorAccount()!=null ? (1+authorNameEmojiHelper.getImageCount()) : 0);
|
||||
}
|
||||
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return switch(index){
|
||||
case 0 -> imageRequest;
|
||||
case 1 -> authorAvaRequest;
|
||||
default -> authorNameEmojiHelper.getImageRequest(index-2);
|
||||
};
|
||||
}
|
||||
|
||||
public Account getAuthorAccount(){
|
||||
if(card.authors!=null && !card.authors.isEmpty() && card.authors.get(0).account!=null)
|
||||
return card.authors.get(0).account;
|
||||
else
|
||||
return card.authorAccount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,12 @@ public class ListItem<T>{
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, T parentObject, Consumer<ListItem<T>> onClick){
|
||||
this(null, null, iconRes, onClick, parentObject, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
|
||||
@@ -35,8 +35,9 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
|
||||
this.drawDividerPredicate=drawDividerPredicate;
|
||||
}
|
||||
|
||||
public void setDrawBelowLastItem(boolean drawBelowLastItem){
|
||||
public DividerItemDecoration setDrawBelowLastItem(boolean drawBelowLastItem){
|
||||
this.drawBelowLastItem=drawBelowLastItem;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ExtendedPopupMenu extends PopupWindow{
|
||||
private UsableRecyclerView list;
|
||||
|
||||
public <T> ExtendedPopupMenu(Context context, List<ListItem<T>> items){
|
||||
super(context, null, 0, R.style.Widget_Mastodon_PopupMenu);
|
||||
setWidth(V.dp(200));
|
||||
setElevation(V.dp(3));
|
||||
setOutsideTouchable(true);
|
||||
setFocusable(true);
|
||||
setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
|
||||
list=new UsableRecyclerView(context);
|
||||
list.setLayoutManager(new LinearLayoutManager(context));
|
||||
list.setAdapter(new ReducedPaddingItemsAdapter<>(items));
|
||||
list.setClipToPadding(false);
|
||||
setContentView(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity){
|
||||
super.showAsDropDown(anchor, xoff, yoff, gravity);
|
||||
View bgView=(View) list.getParent();
|
||||
list.setPadding(0, bgView.getPaddingTop(), 0, bgView.getPaddingBottom());
|
||||
bgView.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private static class ReducedPaddingItemsAdapter<T> extends GenericListItemsAdapter<T>{
|
||||
public ReducedPaddingItemsAdapter(List<ListItem<T>> listItems){
|
||||
super(listItems);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListItemViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
ListItemViewHolder<?> holder=super.onCreateViewHolder(parent, viewType);
|
||||
int padH=V.dp(12), padV=V.dp(8);
|
||||
holder.itemView.setPadding(padH, padV, padH, padV);
|
||||
View icon=holder.itemView.findViewById(R.id.icon);
|
||||
((ViewGroup.MarginLayoutParams)icon.getLayoutParams()).setMarginEnd(padH);
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,11 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
helpButton.setSelected(helpText.getVisibility()==View.VISIBLE);
|
||||
});
|
||||
setCustomTitle(titleLayout);
|
||||
}else if(!TextUtils.isEmpty(title)){
|
||||
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title, null);
|
||||
TextView title=titleLayout.findViewById(R.id.title);
|
||||
title.setText(this.title);
|
||||
setCustomTitle(titleLayout);
|
||||
}
|
||||
|
||||
alert=super.create();
|
||||
|
||||
@@ -58,7 +58,7 @@ public class PhotoLayoutHelper{
|
||||
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
|
||||
|
||||
if(cnt==2){
|
||||
if(allAreWide && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
|
||||
if(allAreWide && avgRatio>1.4*maxRatio && Math.abs(ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
|
||||
float h=Math.max(Math.min(MAX_WIDTH/ratios.get(0), Math.min(MAX_WIDTH/ratios.get(1), (MAX_HEIGHT-GAP)/2.0f)), MIN_HEIGHT/2f);
|
||||
|
||||
result.width=MAX_WIDTH;
|
||||
@@ -69,7 +69,23 @@ public class PhotoLayoutHelper{
|
||||
new TiledLayoutResult.Tile(1, 1, 0, 0),
|
||||
new TiledLayoutResult.Tile(1, 1, 0, 1)
|
||||
};
|
||||
}else if(allAreWide || allAreSquare){ // next to each other, same ratio
|
||||
}else if(allAreWide){ // two wide photos, one above the other, different ratios
|
||||
result.width=MAX_WIDTH;
|
||||
float h0=MAX_WIDTH/ratios.get(0);
|
||||
float h1=MAX_WIDTH/ratios.get(1);
|
||||
if(h0+h1<MIN_HEIGHT){
|
||||
float prevTotalHeight=h0+h1;
|
||||
h0=MIN_HEIGHT*(h0/prevTotalHeight);
|
||||
h1=MIN_HEIGHT*(h1/prevTotalHeight);
|
||||
}
|
||||
result.height=Math.round(h0+h1+GAP);
|
||||
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
|
||||
result.columnSizes=new int[]{MAX_WIDTH};
|
||||
result.tiles=new TiledLayoutResult.Tile[]{
|
||||
new TiledLayoutResult.Tile(1, 1, 0, 0),
|
||||
new TiledLayoutResult.Tile(1, 1, 0, 1)
|
||||
};
|
||||
}else if(allAreSquare){ // next to each other, same ratio
|
||||
float w=((MAX_WIDTH-GAP)/2);
|
||||
float h=Math.max(Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), MAX_HEIGHT)), MIN_HEIGHT);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
@@ -19,6 +20,9 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -48,6 +52,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
private final ImageView share;
|
||||
private final ColorStateList buttonColors;
|
||||
private final View replyBtn, boostBtn, favoriteBtn, shareBtn;
|
||||
private final PopupMenu boostLongTapMenu, favoriteLongTapMenu;
|
||||
|
||||
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
@@ -97,11 +102,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
replyBtn.setOnClickListener(this::onReplyClick);
|
||||
replyBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
boostBtn.setOnClickListener(this::onBoostClick);
|
||||
boostBtn.setOnLongClickListener(this::onBoostLongClick);
|
||||
boostBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
favoriteBtn.setOnClickListener(this::onFavoriteClick);
|
||||
favoriteBtn.setOnLongClickListener(this::onFavoriteLongClick);
|
||||
favoriteBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
shareBtn.setOnClickListener(this::onShareClick);
|
||||
shareBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
|
||||
favoriteLongTapMenu=new PopupMenu(activity, favoriteBtn);
|
||||
favoriteLongTapMenu.inflate(R.menu.favorite_longtap);
|
||||
favoriteLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
|
||||
boostLongTapMenu=new PopupMenu(activity, boostBtn);
|
||||
boostLongTapMenu.inflate(R.menu.boost_longtap);
|
||||
boostLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -172,6 +186,45 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
UiUtils.openSystemShareSheet(v.getContext(), item.status);
|
||||
}
|
||||
|
||||
private boolean onBoostLongClick(View v){
|
||||
MenuItem boost=boostLongTapMenu.getMenu().findItem(R.id.boost);
|
||||
boost.setTitle(item.status.reblogged ? R.string.undo_reblog : R.string.button_reblog);
|
||||
boostLongTapMenu.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onFavoriteLongClick(View v){
|
||||
MenuItem favorite=favoriteLongTapMenu.getMenu().findItem(R.id.favorite);
|
||||
MenuItem bookmark=favoriteLongTapMenu.getMenu().findItem(R.id.bookmark);
|
||||
favorite.setTitle(item.status.favourited ? R.string.undo_favorite : R.string.button_favorite);
|
||||
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
|
||||
favoriteLongTapMenu.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onLongTapMenuItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.favorite){
|
||||
onFavoriteClick(null);
|
||||
}else if(id==R.id.boost){
|
||||
onBoostClick(null);
|
||||
}else if(id==R.id.bookmark){
|
||||
AccountSessionManager.getInstance().getAccount(this.item.accountID).getStatusInteractionController().setBookmarked(this.item.status, !this.item.status.bookmarked);
|
||||
}else if(id==R.id.view_favorites){
|
||||
startAccountListFragment(StatusFavoritesListFragment.class);
|
||||
}else if(id==R.id.view_boosts){
|
||||
startAccountListFragment(StatusReblogsListFragment.class);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
Nav.go(item.parentFragment.getActivity(), cls, args);
|
||||
}
|
||||
|
||||
private int descriptionForId(int id){
|
||||
if(id==R.id.reply_btn)
|
||||
return R.string.button_reply;
|
||||
|
||||
@@ -25,11 +25,13 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusConversationMuted;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -117,6 +119,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
private final TextView name, timeAndUsername, extraText;
|
||||
private final ImageView avatar, more;
|
||||
private final PopupMenu optionsMenu;
|
||||
private final View clickableThing;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
this(activity, R.layout.display_item_header, parent);
|
||||
@@ -129,14 +132,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
clickableThing=findViewById(R.id.clickable_thing);
|
||||
if(clickableThing!=null)
|
||||
clickableThing.setOnClickListener(this::onAvaClick);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
|
||||
optionsMenu=new PopupMenu(activity, more);
|
||||
optionsMenu.inflate(R.menu.post);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic())
|
||||
optionsMenu.getMenu().setGroupDividerEnabled(true);
|
||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||
Account account=item.user;
|
||||
@@ -228,6 +233,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, true)
|
||||
.exec(item.accountID);
|
||||
}else if(id==R.id.mute_conversation){
|
||||
new SetStatusConversationMuted(item.status.id, !item.status.muted)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
// TODO snackbar?
|
||||
item.status.muted=result.muted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, true)
|
||||
.exec(item.accountID);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -244,7 +265,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
|
||||
|
||||
timeAndUsername.setText(time+" · @"+item.user.acct);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : V.dp(4));
|
||||
if(TextUtils.isEmpty(item.extraText)){
|
||||
extraText.setVisibility(View.GONE);
|
||||
}else{
|
||||
@@ -252,8 +273,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
extraText.setText(item.extraText);
|
||||
}
|
||||
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
|
||||
avatar.setClickable(!item.inset);
|
||||
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
|
||||
if(clickableThing!=null){
|
||||
clickableThing.setClickable(!item.inset);
|
||||
clickableThing.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -314,6 +337,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
MenuItem follow=menu.findItem(R.id.follow);
|
||||
MenuItem bookmark=menu.findItem(R.id.bookmark);
|
||||
MenuItem pin=menu.findItem(R.id.pin);
|
||||
MenuItem muteConversation=menu.findItem(R.id.mute_conversation);
|
||||
if(item.status!=null){
|
||||
bookmark.setVisible(true);
|
||||
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
|
||||
@@ -340,6 +364,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
|
||||
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
|
||||
}
|
||||
if(item.status.muted!=null){
|
||||
muteConversation.setVisible(isOwnPost || item.parentFragment instanceof NotificationsListFragment);
|
||||
muteConversation.setTitle(item.status.muted ? R.string.unmute_conversation : R.string.mute_conversation);
|
||||
}else{
|
||||
muteConversation.setVisible(false);
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +1,55 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.app.Activity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.model.viewmodel.CardViewModel;
|
||||
import org.joinmastodon.android.ui.viewholders.LinkCardHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
|
||||
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
public class LinkCardStatusDisplayItem extends StatusDisplayItem implements LinkCardHolder.LinkCardProvider{
|
||||
private final Status status;
|
||||
private final UrlImageLoaderRequest imgRequest;
|
||||
private final CardViewModel cardViewModel;
|
||||
|
||||
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
if(status.card.image!=null)
|
||||
imgRequest=new UrlImageLoaderRequest(status.card.image, 1000, 1000);
|
||||
else
|
||||
imgRequest=null;
|
||||
int size=shouldUseLargeCard() ? 1000 : 192;
|
||||
cardViewModel=new CardViewModel(status.card, size, size, status, parentFragment.getAccountID());
|
||||
}
|
||||
|
||||
private boolean shouldUseLargeCard(){
|
||||
return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height) ? Type.CARD_LARGE : Type.CARD_COMPACT;
|
||||
return shouldUseLargeCard() ? Type.CARD_LARGE : Type.CARD_COMPACT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return imgRequest==null ? 0 : 1;
|
||||
return cardViewModel.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return imgRequest;
|
||||
return cardViewModel.getImageRequest(index);
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView title, description, domain, timestamp;
|
||||
private final ImageView photo;
|
||||
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||
private boolean didClear;
|
||||
private final View inner;
|
||||
private final boolean isLarge;
|
||||
@Override
|
||||
public CardViewModel getCard(){
|
||||
return cardViewModel;
|
||||
}
|
||||
|
||||
public Holder(Context context, ViewGroup parent, boolean isLarge){
|
||||
super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent);
|
||||
this.isLarge=isLarge;
|
||||
title=findViewById(R.id.title);
|
||||
description=findViewById(R.id.description);
|
||||
domain=findViewById(R.id.domain);
|
||||
timestamp=findViewById(R.id.timestamp);
|
||||
photo=findViewById(R.id.photo);
|
||||
inner=findViewById(R.id.inner);
|
||||
inner.setOnClickListener(this::onClick);
|
||||
inner.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
inner.setClipToOutline(true);
|
||||
}
|
||||
public static class Holder extends LinkCardHolder<LinkCardStatusDisplayItem>{
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(LinkCardStatusDisplayItem item){
|
||||
Card card=item.status.card;
|
||||
title.setText(card.title);
|
||||
if(description!=null){
|
||||
description.setText(card.description);
|
||||
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
String cardDomain=HtmlParser.normalizeDomain(Objects.requireNonNull(Uri.parse(card.url).getHost()));
|
||||
if(isLarge && !TextUtils.isEmpty(card.authorName)){
|
||||
domain.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)+" · "+cardDomain);
|
||||
}else{
|
||||
domain.setText(cardDomain);
|
||||
}
|
||||
if(card.publishedAt!=null){
|
||||
timestamp.setVisibility(View.VISIBLE);
|
||||
timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt));
|
||||
}else{
|
||||
timestamp.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
photo.setImageDrawable(null);
|
||||
if(item.imgRequest!=null){
|
||||
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
photo.setBackground(null);
|
||||
photo.setImageTintList(null);
|
||||
crossfadeDrawable.setSize(card.width, card.height);
|
||||
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
didClear=false;
|
||||
}else{
|
||||
photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant));
|
||||
photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
|
||||
photo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
photo.setImageResource(R.drawable.ic_feed_48px);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
crossfadeDrawable.setImageDrawable(drawable);
|
||||
if(didClear)
|
||||
crossfadeDrawable.animateAlpha(0f);
|
||||
Card card=item.status.card;
|
||||
// Make sure the image is not stretched if the server returned wrong dimensions
|
||||
if(drawable!=null && (drawable.getIntrinsicWidth()!=card.width || drawable.getIntrinsicHeight()!=card.height)){
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
crossfadeDrawable.setCrossfadeAlpha(1f);
|
||||
didClear=true;
|
||||
}
|
||||
|
||||
private void onClick(View v){
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url, item.status);
|
||||
public Holder(Activity context, ViewGroup parent, boolean isLarge, String accountID){
|
||||
super(context, parent, isLarge, accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
controllers.add(c);
|
||||
|
||||
if (item.status.translation != null){
|
||||
if (item.status.translation!=null && item.status.translation.mediaAttachments!=null){
|
||||
if(item.status.translationState==Status.TranslationState.SHOWN){
|
||||
if(!item.translatedAttachments.containsKey(att.id)){
|
||||
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();
|
||||
|
||||
@@ -125,7 +125,8 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(NotificationHeaderStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
|
||||
// TODO use real icons
|
||||
if(item.notification.type!=Notification.Type.POLL)
|
||||
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.notification.account.acct));
|
||||
icon.setImageResource(switch(item.notification.type){
|
||||
case FAVORITE -> R.drawable.ic_star_fill1_24px;
|
||||
case REBLOG -> R.drawable.ic_repeat_fill1_24px;
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.ViewGroup;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
@@ -68,8 +69,8 @@ public abstract class StatusDisplayItem{
|
||||
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
|
||||
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
|
||||
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true);
|
||||
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false);
|
||||
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true, ((BaseStatusListFragment<?>)parentFragment).getAccountID());
|
||||
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false, ((BaseStatusListFragment<?>)parentFragment).getAccountID());
|
||||
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
|
||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||
@@ -151,10 +152,12 @@ public abstract class StatusDisplayItem{
|
||||
if(!imageAttachments.isEmpty()){
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
mediaGrid.sensitiveRevealed=false;
|
||||
}else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia){
|
||||
mediaGrid.sensitiveRevealed=true;
|
||||
}
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
@@ -224,7 +227,7 @@ public abstract class StatusDisplayItem{
|
||||
FILTER_SPOILER
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
public static abstract class Holder<T> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
public Holder(View itemView){
|
||||
super(itemView);
|
||||
}
|
||||
@@ -234,17 +237,18 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
|
||||
public String getItemID(){
|
||||
return item.parentID;
|
||||
return item instanceof StatusDisplayItem sdi ? sdi.parentID : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.parentFragment.onItemClick(item.parentID);
|
||||
if(item instanceof StatusDisplayItem sdi)
|
||||
sdi.parentFragment.onItemClick(sdi.parentID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.parentFragment.isItemEnabled(item.parentID);
|
||||
return item instanceof StatusDisplayItem sdi && sdi.parentFragment.isItemEnabled(sdi.parentID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
text.setTextIsSelectable(item.textSelectable);
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
itemView.setClickable(false);
|
||||
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom());
|
||||
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(12), text.getPaddingRight(), text.getPaddingBottom());
|
||||
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
|
||||
updateTranslation(false);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
import android.window.OnBackInvokedDispatcher;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
@@ -169,7 +170,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
windowView=new FrameLayout(activity){
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
onStartSwipeToDismissTransition(0f);
|
||||
}
|
||||
@@ -257,6 +258,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
wm.addView(windowView, wlp);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
// TODO make use of the progress callback for nicer animation
|
||||
windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->onStartSwipeToDismissTransition(0));
|
||||
}
|
||||
|
||||
windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
|
||||
@@ -69,6 +69,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
|
||||
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
|
||||
backButton.setElevation(V.dp(2));
|
||||
backButton.setAlpha(0f);
|
||||
backButton.setContentDescription(context.getString(R.string.back));
|
||||
backButton.setOnClickListener(v->{
|
||||
listener.onDismissEntireViewer();
|
||||
dismiss();
|
||||
@@ -82,6 +83,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
|
||||
infoButton.setElevation(V.dp(2));
|
||||
infoButton.setAlpha(0f);
|
||||
infoButton.setSelected(true);
|
||||
infoButton.setContentDescription(context.getString(R.string.info));
|
||||
infoButton.setOnClickListener(v->dismiss());
|
||||
|
||||
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import org.joinmastodon.android.DonationFragmentActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.DonationWebViewFragment;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CurrencyAmountInput;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
|
||||
public class DonationSheet extends BottomSheet{
|
||||
private final DonationCampaign campaign;
|
||||
private final String accountID;
|
||||
private final Consumer<Intent> startCallback;
|
||||
private DonationFrequency frequency=DonationFrequency.MONTHLY;
|
||||
|
||||
private View onceTab, monthlyTab, yearlyTab;
|
||||
private int currentTab;
|
||||
private CurrencyAmountInput amountField;
|
||||
private ToggleButton[] suggestedAmountButtons=new ToggleButton[6];
|
||||
private View button;
|
||||
private TextView buttonText;
|
||||
private Activity activity;
|
||||
|
||||
public DonationSheet(@NonNull Activity activity, DonationCampaign campaign, String accountID, Consumer<Intent> startCallback){
|
||||
super(activity);
|
||||
this.campaign=campaign;
|
||||
this.accountID=accountID;
|
||||
this.activity=activity;
|
||||
this.startCallback=startCallback;
|
||||
Context context=activity;
|
||||
|
||||
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation, null);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
|
||||
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||
|
||||
TextView text=findViewById(R.id.text);
|
||||
text.setText(campaign.donationMessage);
|
||||
|
||||
onceTab=findViewById(R.id.once);
|
||||
monthlyTab=findViewById(R.id.monthly);
|
||||
yearlyTab=findViewById(R.id.yearly);
|
||||
onceTab.setOnClickListener(this::onTabClick);
|
||||
monthlyTab.setOnClickListener(this::onTabClick);
|
||||
yearlyTab.setOnClickListener(this::onTabClick);
|
||||
|
||||
if(campaign.amounts.yearly==null)
|
||||
yearlyTab.setVisibility(View.GONE);
|
||||
if(campaign.amounts.oneTime==null)
|
||||
onceTab.setVisibility(View.GONE);
|
||||
if(campaign.amounts.monthly==null){
|
||||
monthlyTab.setVisibility(View.GONE);
|
||||
if(campaign.amounts.oneTime!=null){
|
||||
onceTab.setSelected(true);
|
||||
currentTab=R.id.once;
|
||||
frequency=DonationFrequency.ONCE;
|
||||
}else if(campaign.amounts.yearly!=null){
|
||||
yearlyTab.setSelected(true);
|
||||
currentTab=R.id.yearly;
|
||||
frequency=DonationFrequency.YEARLY;
|
||||
}else{
|
||||
Toast.makeText(context, "Amounts object is empty", Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
monthlyTab.setSelected(true);
|
||||
currentTab=R.id.monthly;
|
||||
}
|
||||
|
||||
|
||||
View tabBarItself=findViewById(R.id.tabbar_inner);
|
||||
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
tabBarItself.setClipToOutline(true);
|
||||
|
||||
amountField=findViewById(R.id.amount);
|
||||
List<String> availableCurrencies=campaign.amounts.monthly.keySet().stream().sorted().collect(Collectors.toList());
|
||||
amountField.setCurrencies(availableCurrencies);
|
||||
try{
|
||||
amountField.setSelectedCurrency(campaign.defaultCurrency);
|
||||
}catch(IllegalArgumentException x){
|
||||
new M3AlertDialogBuilder(context)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage("Default currency "+campaign.defaultCurrency+" not in list of available currencies "+availableCurrencies)
|
||||
.show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
amountField.setChangeListener(new CurrencyAmountInput.ChangeListener(){
|
||||
@Override
|
||||
public void onCurrencyChanged(String code){
|
||||
updateSuggestedAmounts(code);
|
||||
button.setEnabled(amountField.getAmount()>=getMinimumChargeAmount(code));
|
||||
updateSuggestedButtonsState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAmountChanged(long amount){
|
||||
button.setEnabled(amount>=getMinimumChargeAmount(amountField.getCurrency()));
|
||||
updateSuggestedButtonsState();
|
||||
}
|
||||
});
|
||||
button=findViewById(R.id.button);
|
||||
buttonText=findViewById(R.id.button_text);
|
||||
|
||||
ViewGroup suggestedAmounts=findViewById(R.id.suggested_amounts);
|
||||
for(int i=0;i<suggestedAmountButtons.length;i++){
|
||||
ToggleButton btn=new ToggleButton(context);
|
||||
btn.setBackgroundResource(R.drawable.bg_filter_chip);
|
||||
btn.setTextAppearance(R.style.m3_label_large);
|
||||
btn.setTextColor(context.getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
|
||||
btn.setMinWidth(V.dp(64));
|
||||
btn.setMinimumWidth(0);
|
||||
btn.setPadding(0, 0, 0, 0);
|
||||
btn.setStateListAnimator(null);
|
||||
btn.setTextOff(null);
|
||||
btn.setTextOn(null);
|
||||
btn.setOnClickListener(this::onSuggestedAmountClick);
|
||||
btn.setTag(i);
|
||||
btn.setSingleLine();
|
||||
suggestedAmountButtons[i]=btn;
|
||||
suggestedAmounts.addView(btn);
|
||||
}
|
||||
updateSuggestedAmounts(campaign.defaultCurrency);
|
||||
button.setEnabled(false);
|
||||
buttonText.setText(campaign.donationButtonText);
|
||||
button.setOnClickListener(v->openWebView());
|
||||
|
||||
Arrays.stream(getCurrentSuggestedAmounts(campaign.defaultCurrency)).min().ifPresent(amountField::setAmount);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
Window window=getWindow();
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
|
||||
private void onTabClick(View v){
|
||||
if(v.getId()==currentTab)
|
||||
return;
|
||||
findViewById(currentTab).setSelected(false);
|
||||
v.setSelected(true);
|
||||
currentTab=v.getId();
|
||||
if(currentTab==R.id.once)
|
||||
frequency=DonationFrequency.ONCE;
|
||||
else if(currentTab==R.id.monthly)
|
||||
frequency=DonationFrequency.MONTHLY;
|
||||
else if(currentTab==R.id.yearly)
|
||||
frequency=DonationFrequency.YEARLY;
|
||||
updateSuggestedAmounts(amountField.getCurrency());
|
||||
}
|
||||
|
||||
private long[] getCurrentSuggestedAmounts(String currency){
|
||||
long[] amounts=(switch(frequency){
|
||||
case ONCE -> campaign.amounts.oneTime;
|
||||
case MONTHLY -> campaign.amounts.monthly;
|
||||
case YEARLY -> campaign.amounts.yearly;
|
||||
}).get(currency);
|
||||
if(amounts==null){
|
||||
amounts=new long[0];
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private void updateSuggestedAmounts(String currency){
|
||||
NumberFormat format=NumberFormat.getCurrencyInstance();
|
||||
try{
|
||||
format.setCurrency(Currency.getInstance(currency));
|
||||
}catch(IllegalArgumentException ignore){}
|
||||
int defaultFractionDigits=format.getMinimumFractionDigits();
|
||||
long[] amounts=getCurrentSuggestedAmounts(currency);
|
||||
for(int i=0;i<suggestedAmountButtons.length;i++){
|
||||
ToggleButton btn=suggestedAmountButtons[i];
|
||||
if(i>=amounts.length){
|
||||
btn.setVisibility(View.GONE);
|
||||
continue;
|
||||
}
|
||||
btn.setVisibility(View.VISIBLE);
|
||||
long amount=amounts[i];
|
||||
format.setMinimumFractionDigits(amount%100==0 ? 0 : defaultFractionDigits);
|
||||
btn.setText(format.format(amount/100.0));
|
||||
}
|
||||
updateSuggestedButtonsState();
|
||||
}
|
||||
|
||||
private void onSuggestedAmountClick(View v){
|
||||
int index=(int) v.getTag();
|
||||
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
|
||||
amountField.setAmount(amounts[index]);
|
||||
}
|
||||
|
||||
private void updateSuggestedButtonsState(){
|
||||
long amount=amountField.getAmount();
|
||||
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
|
||||
for(int i=0;i<Math.min(amounts.length, suggestedAmountButtons.length);i++){
|
||||
ToggleButton btn=suggestedAmountButtons[i];
|
||||
btn.setChecked(amounts[i]==amount);
|
||||
}
|
||||
}
|
||||
|
||||
private void openWebView(){
|
||||
Uri.Builder builder=Uri.parse(campaign.donationUrl).buildUpon();
|
||||
builder.appendQueryParameter("locale", Locale.getDefault().toLanguageTag().replace('-', '_'))
|
||||
.appendQueryParameter("platform", "android")
|
||||
.appendQueryParameter("currency", amountField.getCurrency())
|
||||
.appendQueryParameter("amount", String.valueOf(amountField.getAmount()))
|
||||
.appendQueryParameter("source", "campaign")
|
||||
.appendQueryParameter("campaign_id", campaign.id)
|
||||
.appendQueryParameter("frequency", switch(frequency){
|
||||
case ONCE -> "one_time";
|
||||
case MONTHLY -> "monthly";
|
||||
case YEARLY -> "yearly";
|
||||
})
|
||||
.appendQueryParameter("success_callback_url", DonationWebViewFragment.SUCCESS_URL)
|
||||
.appendQueryParameter("cancel_callback_url", DonationWebViewFragment.CANCEL_URL)
|
||||
.appendQueryParameter("failure_callback_url", DonationWebViewFragment.FAILURE_URL);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("url", builder.build().toString());
|
||||
args.putString("account", accountID);
|
||||
args.putString("campaignID", campaign.id);
|
||||
args.putString("successPostText", campaign.donationSuccessPost);
|
||||
args.putBoolean("_can_go_back", true);
|
||||
startCallback.accept(new Intent(activity, DonationFragmentActivity.class).putExtra("fragmentArgs", args));
|
||||
}
|
||||
|
||||
private static long getMinimumChargeAmount(String currency){
|
||||
// https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts
|
||||
// values are in cents
|
||||
return switch(currency){
|
||||
case "USD" -> 50;
|
||||
case "AED" -> 2_00;
|
||||
case "AUD" -> 50;
|
||||
case "BGN" -> 1_00;
|
||||
case "BRL" -> 50;
|
||||
case "CAD" -> 50;
|
||||
case "CHF" -> 50;
|
||||
case "CZK" -> 15_00;
|
||||
case "DKK" -> 2_50;
|
||||
case "EUR" -> 50;
|
||||
case "GBP" -> 30;
|
||||
case "HKD" -> 4_00;
|
||||
case "HUF" -> 175_00;
|
||||
case "INR" -> 50;
|
||||
case "JPY" -> 50_00;
|
||||
case "MXN" -> 10_00;
|
||||
case "MYR" -> 2_00;
|
||||
case "NOK" -> 3_00;
|
||||
case "NZD" -> 50;
|
||||
case "PLN" -> 2_00;
|
||||
case "RON" -> 2_00;
|
||||
case "SEK" -> 3_00;
|
||||
case "SGD" -> 50;
|
||||
case "THB" -> 10_00;
|
||||
|
||||
default -> 50;
|
||||
};
|
||||
}
|
||||
|
||||
private enum DonationFrequency{
|
||||
ONCE,
|
||||
MONTHLY,
|
||||
YEARLY
|
||||
}
|
||||
|
||||
public static class SuggestedAmountsLayout extends ViewGroup implements CustomViewHelper{
|
||||
private int visibleChildCount;
|
||||
private static final int H_GAP=24;
|
||||
private static final int V_GAP=8;
|
||||
private static final int ROW_HEIGHT=32;
|
||||
|
||||
public SuggestedAmountsLayout(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SuggestedAmountsLayout(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SuggestedAmountsLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
visibleChildCount=0;
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
if(child.getVisibility()==GONE)
|
||||
continue;
|
||||
visibleChildCount++;
|
||||
}
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec);
|
||||
setMeasuredDimension(width, visibleChildCount>4 ? dp(ROW_HEIGHT*2+V_GAP) : dp(ROW_HEIGHT));
|
||||
int buttonsPerRow=visibleChildCount>4 ? 3 : visibleChildCount;
|
||||
int buttonWidth=(width-dp(H_GAP)*(buttonsPerRow-1))/buttonsPerRow;
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
if(child.getVisibility()==GONE)
|
||||
continue;
|
||||
child.measure(buttonWidth | MeasureSpec.EXACTLY, dp(ROW_HEIGHT) | MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b){
|
||||
int width=r-l;
|
||||
int buttonsPerRow=visibleChildCount>4 ? 3 : visibleChildCount;
|
||||
int buttonWidth=(width-dp(H_GAP)*(buttonsPerRow-1))/buttonsPerRow;
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
if(child.getVisibility()==GONE)
|
||||
continue;
|
||||
int column=i%buttonsPerRow;
|
||||
int row=i/buttonsPerRow;
|
||||
int left=(buttonWidth+dp(H_GAP))*column;
|
||||
int top=dp(ROW_HEIGHT+V_GAP)*row;
|
||||
child.layout(left, top, left+buttonWidth, top+dp(ROW_HEIGHT));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
|
||||
public class DonationSuccessfulSheet extends BottomSheet{
|
||||
|
||||
public DonationSuccessfulSheet(@NonNull Context context, @NonNull String accountID, String postText){
|
||||
super(context);
|
||||
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation_success, null);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
|
||||
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||
|
||||
content.findViewById(R.id.btn_done).setOnClickListener(v->dismiss());
|
||||
View shareButton=content.findViewById(R.id.btn_share);
|
||||
if(postText==null){
|
||||
shareButton.setEnabled(false);
|
||||
}
|
||||
shareButton.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("prefilledText", postText);
|
||||
Nav.go((Activity) context, ComposeFragment.class, args);
|
||||
dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user